From efa408ab2cc94e2a7a3ff42a4471271a2733fd9c Mon Sep 17 00:00:00 2001 From: Aevann1 Date: Fri, 15 Oct 2021 16:08:27 +0200 Subject: [PATCH] fd --- .gitattributes | 8 +- .gitignore | 12 +- Dockerfile | 30 +- LICENSE | 746 +-- appspec.yml | 24 +- buildspec.yml | 14 +- dependabot.yml | 10 +- disablesignups | 0 docker-compose.yml | 130 +- env | 64 +- files/__main__.py | 260 +- files/classes/__init__.py | 28 +- files/classes/alts.py | 30 +- files/classes/award.py | 174 +- files/classes/badges.py | 187 +- files/classes/clients.py | 162 +- files/classes/comment.py | 794 +-- files/classes/domains.py | 16 +- files/classes/flags.py | 110 +- files/classes/mod_logs.py | 472 +- files/classes/submission.py | 812 +-- files/classes/subscriptions.py | 66 +- files/classes/user.py | 1240 ++--- files/classes/userblock.py | 30 +- files/classes/votes.py | 166 +- files/helpers/alerts.py | 274 +- files/helpers/const.py | 461 +- files/helpers/discord.py | 126 +- files/helpers/filters.py | 66 +- files/helpers/get.py | 540 +- files/helpers/images.py | 52 +- files/helpers/jinja2.py | 70 +- files/helpers/lazy.py | 36 +- files/helpers/markdown.py | 270 +- files/helpers/sanitize.py | 456 +- files/helpers/security.py | 46 +- files/helpers/session.py | 40 +- files/helpers/sqla_values.py | 66 +- files/helpers/wrappers.py | 378 +- files/mail/__init__.py | 176 +- files/routes/__init__.py | 32 +- files/routes/admin.py | 2484 ++++----- files/routes/awards.py | 742 +-- files/routes/comments.py | 1722 +++---- files/routes/discord.py | 290 +- files/routes/errors.py | 168 +- files/routes/feeds.py | 130 +- files/routes/front.py | 790 +-- files/routes/giphy.py | 44 +- files/routes/login.py | 1126 ++--- files/routes/oauth.py | 496 +- files/routes/posts.py | 2006 ++++---- files/routes/reporting.py | 178 +- files/routes/search.py | 572 +-- files/routes/settings.py | 1740 +++---- files/routes/static.py | 682 +-- files/routes/users.py | 1490 +++--- files/routes/votes.py | 396 +- files/templates/admin/admin_home.html | 120 +- files/templates/admin/alt_votes.html | 174 +- files/templates/admin/app.html | 142 +- files/templates/admin/apps.html | 142 +- files/templates/admin/badge_grant.html | 148 +- files/templates/admin/banned_domains.html | 72 +- files/templates/admin/content_stats.html | 48 +- files/templates/admin/image_posts.html | 154 +- files/templates/admin/new_users.html | 16 +- files/templates/admin/removed_posts.html | 118 +- files/templates/admin/reported_comments.html | 48 +- files/templates/admin/reported_posts.html | 208 +- files/templates/admin/rules.html | 60 +- files/templates/admin/user_award.html | 140 +- files/templates/admins.html | 44 +- files/templates/api.html | 166 +- files/templates/authforms.html | 206 +- files/templates/award_modal.html | 138 +- files/templates/badges.html | 142 +- files/templates/ban_modal.html | 120 +- files/templates/banned.html | 46 +- files/templates/blocks.html | 44 +- files/templates/changelog.html | 214 +- files/templates/comment_failed.html | 86 +- files/templates/comments.html | 1368 ++--- files/templates/contact.html | 136 +- files/templates/default.html | 720 +-- files/templates/delete_post_modal.html | 124 +- files/templates/email/2fa_remove.html | 72 +- files/templates/email/default.html | 98 +- files/templates/email/email_change.html | 112 +- files/templates/email/email_verify.html | 108 +- files/templates/email/password_reset.html | 108 +- files/templates/embeds/twitter.html | 10 +- files/templates/embeds/twitterlight.html | 10 +- files/templates/embeds/youtube.html | 4 +- files/templates/emoji_modal.html | 196 +- files/templates/errors/400.html | 40 +- files/templates/errors/401.html | 46 +- files/templates/errors/403.html | 42 +- files/templates/errors/404.html | 42 +- files/templates/errors/405.html | 42 +- files/templates/errors/429.html | 40 +- files/templates/errors/500.html | 44 +- files/templates/errors/nsfw.html | 54 +- files/templates/errors/patron.html | 38 +- files/templates/expanded_image_modal.html | 54 +- files/templates/followers.html | 70 +- files/templates/forgot_password.html | 60 +- files/templates/formatting.html | 990 ++-- files/templates/gif_modal.html | 58 +- files/templates/header.html | 410 +- files/templates/home.html | 338 +- files/templates/home_comments.html | 174 +- files/templates/leaderboard.html | 306 +- files/templates/log.html | 186 +- files/templates/login.html | 240 +- files/templates/login_2fa.html | 210 +- files/templates/lost_2fa.html | 70 +- files/templates/message.html | 54 +- files/templates/message_success.html | 34 +- files/templates/mine.html | 76 +- files/templates/mobile_navigation_bar.html | 152 +- files/templates/norules.html | 50 +- files/templates/notifications.html | 234 +- files/templates/oauth.html | 84 +- files/templates/patrons.html | 64 +- files/templates/rentoids.html | 40 +- files/templates/report_post_modal.html | 72 +- files/templates/reset_password.html | 64 +- files/templates/rules.html | 38 +- files/templates/search.html | 324 +- files/templates/search_comments.html | 24 +- files/templates/search_users.html | 16 +- files/templates/settings.html | 542 +- files/templates/settings2.html | 352 +- files/templates/settings_apps.html | 312 +- files/templates/settings_blocks.html | 316 +- files/templates/settings_css.html | 100 +- files/templates/settings_filters.html | 642 +-- files/templates/settings_profile.html | 1152 ++--- files/templates/settings_profilecss.html | 80 +- files/templates/settings_security.html | 566 +-- files/templates/settings_shop.html | 170 +- files/templates/sign_up.html | 332 +- files/templates/sign_up_failed_ref.html | 204 +- files/templates/submission.html | 1494 +++--- files/templates/submission_banned.html | 282 +- files/templates/submission_listing.html | 1104 ++-- files/templates/submit.html | 348 +- files/templates/thiefs.html | 112 +- files/templates/truescore.html | 48 +- files/templates/user_listing.html | 62 +- files/templates/userpage.html | 1420 +++--- files/templates/userpage_blocked.html | 88 +- files/templates/userpage_blocking.html | 112 +- files/templates/userpage_comments.html | 202 +- files/templates/userpage_private.html | 68 +- files/templates/userpage_reserved.html | 114 +- files/templates/viewers.html | 40 +- files/templates/votes.html | 96 +- pg_hba.conf | 86 +- push.sh | 12 +- pushforce.sh | 4 +- readme.md | 120 +- redis.conf | 2744 +++++----- requirements.txt | 48 +- restart | 4 +- schema.sql | 3684 +++++++------- seed-db.sql | 110 +- setup | 38 +- snappy.txt | 4724 +++++++++--------- supervisord.conf | 24 +- 171 files changed, 28159 insertions(+), 28269 deletions(-) mode change 100644 => 100755 .gitattributes mode change 100644 => 100755 .gitignore mode change 100644 => 100755 Dockerfile mode change 100644 => 100755 LICENSE mode change 100644 => 100755 appspec.yml mode change 100644 => 100755 buildspec.yml mode change 100644 => 100755 dependabot.yml mode change 100644 => 100755 disablesignups mode change 100644 => 100755 docker-compose.yml mode change 100644 => 100755 env mode change 100644 => 100755 files/__main__.py mode change 100644 => 100755 files/classes/__init__.py mode change 100644 => 100755 files/classes/alts.py mode change 100644 => 100755 files/classes/award.py mode change 100644 => 100755 files/classes/badges.py mode change 100644 => 100755 files/classes/clients.py mode change 100644 => 100755 files/classes/comment.py mode change 100644 => 100755 files/classes/domains.py mode change 100644 => 100755 files/classes/flags.py mode change 100644 => 100755 files/classes/mod_logs.py mode change 100644 => 100755 files/classes/submission.py mode change 100644 => 100755 files/classes/subscriptions.py mode change 100644 => 100755 files/classes/user.py mode change 100644 => 100755 files/classes/userblock.py mode change 100644 => 100755 files/classes/votes.py mode change 100644 => 100755 files/helpers/alerts.py mode change 100644 => 100755 files/helpers/const.py mode change 100644 => 100755 files/helpers/discord.py mode change 100644 => 100755 files/helpers/filters.py mode change 100644 => 100755 files/helpers/get.py mode change 100644 => 100755 files/helpers/images.py mode change 100644 => 100755 files/helpers/jinja2.py mode change 100644 => 100755 files/helpers/lazy.py mode change 100644 => 100755 files/helpers/markdown.py mode change 100644 => 100755 files/helpers/sanitize.py mode change 100644 => 100755 files/helpers/security.py mode change 100644 => 100755 files/helpers/session.py mode change 100644 => 100755 files/helpers/sqla_values.py mode change 100644 => 100755 files/helpers/wrappers.py mode change 100644 => 100755 files/mail/__init__.py mode change 100644 => 100755 files/routes/__init__.py mode change 100644 => 100755 files/routes/admin.py mode change 100644 => 100755 files/routes/awards.py mode change 100644 => 100755 files/routes/comments.py mode change 100644 => 100755 files/routes/discord.py mode change 100644 => 100755 files/routes/errors.py mode change 100644 => 100755 files/routes/feeds.py mode change 100644 => 100755 files/routes/front.py mode change 100644 => 100755 files/routes/giphy.py mode change 100644 => 100755 files/routes/login.py mode change 100644 => 100755 files/routes/oauth.py mode change 100644 => 100755 files/routes/posts.py mode change 100644 => 100755 files/routes/reporting.py mode change 100644 => 100755 files/routes/search.py mode change 100644 => 100755 files/routes/settings.py mode change 100644 => 100755 files/routes/static.py mode change 100644 => 100755 files/routes/users.py mode change 100644 => 100755 files/routes/votes.py mode change 100644 => 100755 files/templates/admin/admin_home.html mode change 100644 => 100755 files/templates/admin/alt_votes.html mode change 100644 => 100755 files/templates/admin/app.html mode change 100644 => 100755 files/templates/admin/apps.html mode change 100644 => 100755 files/templates/admin/badge_grant.html mode change 100644 => 100755 files/templates/admin/banned_domains.html mode change 100644 => 100755 files/templates/admin/content_stats.html mode change 100644 => 100755 files/templates/admin/image_posts.html mode change 100644 => 100755 files/templates/admin/new_users.html mode change 100644 => 100755 files/templates/admin/removed_posts.html mode change 100644 => 100755 files/templates/admin/reported_comments.html mode change 100644 => 100755 files/templates/admin/reported_posts.html mode change 100644 => 100755 files/templates/admin/rules.html mode change 100644 => 100755 files/templates/admin/user_award.html mode change 100644 => 100755 files/templates/admins.html mode change 100644 => 100755 files/templates/api.html mode change 100644 => 100755 files/templates/authforms.html mode change 100644 => 100755 files/templates/award_modal.html mode change 100644 => 100755 files/templates/badges.html mode change 100644 => 100755 files/templates/ban_modal.html mode change 100644 => 100755 files/templates/banned.html mode change 100644 => 100755 files/templates/blocks.html mode change 100644 => 100755 files/templates/changelog.html mode change 100644 => 100755 files/templates/comment_failed.html mode change 100644 => 100755 files/templates/comments.html mode change 100644 => 100755 files/templates/contact.html mode change 100644 => 100755 files/templates/default.html mode change 100644 => 100755 files/templates/delete_post_modal.html mode change 100644 => 100755 files/templates/email/2fa_remove.html mode change 100644 => 100755 files/templates/email/default.html mode change 100644 => 100755 files/templates/email/email_change.html mode change 100644 => 100755 files/templates/email/email_verify.html mode change 100644 => 100755 files/templates/email/password_reset.html mode change 100644 => 100755 files/templates/embeds/twitter.html mode change 100644 => 100755 files/templates/embeds/twitterlight.html mode change 100644 => 100755 files/templates/embeds/youtube.html mode change 100644 => 100755 files/templates/emoji_modal.html mode change 100644 => 100755 files/templates/errors/400.html mode change 100644 => 100755 files/templates/errors/401.html mode change 100644 => 100755 files/templates/errors/403.html mode change 100644 => 100755 files/templates/errors/404.html mode change 100644 => 100755 files/templates/errors/405.html mode change 100644 => 100755 files/templates/errors/429.html mode change 100644 => 100755 files/templates/errors/500.html mode change 100644 => 100755 files/templates/errors/nsfw.html mode change 100644 => 100755 files/templates/errors/patron.html mode change 100644 => 100755 files/templates/expanded_image_modal.html mode change 100644 => 100755 files/templates/followers.html mode change 100644 => 100755 files/templates/forgot_password.html mode change 100644 => 100755 files/templates/formatting.html mode change 100644 => 100755 files/templates/gif_modal.html mode change 100644 => 100755 files/templates/header.html mode change 100644 => 100755 files/templates/home.html mode change 100644 => 100755 files/templates/home_comments.html mode change 100644 => 100755 files/templates/leaderboard.html mode change 100644 => 100755 files/templates/log.html mode change 100644 => 100755 files/templates/login.html mode change 100644 => 100755 files/templates/login_2fa.html mode change 100644 => 100755 files/templates/lost_2fa.html mode change 100644 => 100755 files/templates/message.html mode change 100644 => 100755 files/templates/message_success.html mode change 100644 => 100755 files/templates/mine.html mode change 100644 => 100755 files/templates/mobile_navigation_bar.html mode change 100644 => 100755 files/templates/norules.html mode change 100644 => 100755 files/templates/notifications.html mode change 100644 => 100755 files/templates/oauth.html mode change 100644 => 100755 files/templates/patrons.html mode change 100644 => 100755 files/templates/rentoids.html mode change 100644 => 100755 files/templates/report_post_modal.html mode change 100644 => 100755 files/templates/reset_password.html mode change 100644 => 100755 files/templates/rules.html mode change 100644 => 100755 files/templates/search.html mode change 100644 => 100755 files/templates/search_comments.html mode change 100644 => 100755 files/templates/search_users.html mode change 100644 => 100755 files/templates/settings.html mode change 100644 => 100755 files/templates/settings2.html mode change 100644 => 100755 files/templates/settings_apps.html mode change 100644 => 100755 files/templates/settings_blocks.html mode change 100644 => 100755 files/templates/settings_css.html mode change 100644 => 100755 files/templates/settings_filters.html mode change 100644 => 100755 files/templates/settings_profile.html mode change 100644 => 100755 files/templates/settings_profilecss.html mode change 100644 => 100755 files/templates/settings_security.html mode change 100644 => 100755 files/templates/settings_shop.html mode change 100644 => 100755 files/templates/sign_up.html mode change 100644 => 100755 files/templates/sign_up_failed_ref.html mode change 100644 => 100755 files/templates/submission.html mode change 100644 => 100755 files/templates/submission_banned.html mode change 100644 => 100755 files/templates/submission_listing.html mode change 100644 => 100755 files/templates/submit.html mode change 100644 => 100755 files/templates/thiefs.html mode change 100644 => 100755 files/templates/truescore.html mode change 100644 => 100755 files/templates/user_listing.html mode change 100644 => 100755 files/templates/userpage.html mode change 100644 => 100755 files/templates/userpage_blocked.html mode change 100644 => 100755 files/templates/userpage_blocking.html mode change 100644 => 100755 files/templates/userpage_comments.html mode change 100644 => 100755 files/templates/userpage_private.html mode change 100644 => 100755 files/templates/userpage_reserved.html mode change 100644 => 100755 files/templates/viewers.html mode change 100644 => 100755 files/templates/votes.html mode change 100644 => 100755 pg_hba.conf mode change 100644 => 100755 push.sh mode change 100644 => 100755 pushforce.sh mode change 100644 => 100755 readme.md mode change 100644 => 100755 redis.conf mode change 100644 => 100755 requirements.txt mode change 100644 => 100755 restart mode change 100644 => 100755 schema.sql mode change 100644 => 100755 seed-db.sql mode change 100644 => 100755 setup mode change 100644 => 100755 snappy.txt mode change 100644 => 100755 supervisord.conf diff --git a/.gitattributes b/.gitattributes old mode 100644 new mode 100755 index 8dc698e6b..cfef720ed --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -*.css linguist-detectable=false -*.js linguist-detectable=true -*.html linguist-detectable=false -*.py linguist-detectable=true +*.css linguist-detectable=false +*.js linguist-detectable=true +*.html linguist-detectable=false +*.py linguist-detectable=true diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 1eb768ab0..368d17215 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -image.* -chart.png -video.mp4 -cache/ -__pycache__/ -disablesignups +image.* +chart.png +video.mp4 +cache/ +__pycache__/ +disablesignups *rules.html \ No newline at end of file diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 index 3a717c91f..c417faf1a --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,15 @@ -FROM ubuntu:20.04 - -COPY supervisord.conf /etc/supervisord.conf - -RUN apt update && apt install -y python3.8 python3-pip supervisor - -RUN mkdir -p ./service - -COPY requirements.txt ./service/requirements.txt - -RUN cd ./service && pip3 install -r requirements.txt - -EXPOSE 80/tcp - -CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ] +FROM ubuntu:20.04 + +COPY supervisord.conf /etc/supervisord.conf + +RUN apt update && apt install -y python3.8 python3-pip supervisor + +RUN mkdir -p ./service + +COPY requirements.txt ./service/requirements.txt + +RUN cd ./service && pip3 install -r requirements.txt + +EXPOSE 80/tcp + +CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ] diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 index 611bbef65..14c04dfc0 --- a/LICENSE +++ b/LICENSE @@ -1,373 +1,373 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at https://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/appspec.yml b/appspec.yml old mode 100644 new mode 100755 index b5f03014d..e74241949 --- a/appspec.yml +++ b/appspec.yml @@ -1,13 +1,13 @@ -version: 0.0 -os: linux -files: - - source: / - destination: files -permissions: - - object: files/* - mode: 4755 -hooks: - AfterInstall: - - location: scripts/install_pip - ApplicationStart: +version: 0.0 +os: linux +files: + - source: / + destination: files +permissions: + - object: files/* + mode: 4755 +hooks: + AfterInstall: + - location: scripts/install_pip + ApplicationStart: - location: scripts/start_files \ No newline at end of file diff --git a/buildspec.yml b/buildspec.yml old mode 100644 new mode 100755 index 85e540179..1caacd859 --- a/buildspec.yml +++ b/buildspec.yml @@ -1,8 +1,8 @@ -version: 0.2 -phases: - install: - runtime-versions: - python: 3.7 -artifacts: - files: +version: 0.2 +phases: + install: + runtime-versions: + python: 3.7 +artifacts: + files: - '**/*' \ No newline at end of file diff --git a/dependabot.yml b/dependabot.yml old mode 100644 new mode 100755 index fa05fac46..b4c618375 --- a/dependabot.yml +++ b/dependabot.yml @@ -1,6 +1,6 @@ -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: interval: "daily" \ No newline at end of file diff --git a/disablesignups b/disablesignups old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 index 1dee2234f..4dffc1221 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,66 +1,66 @@ -version: '2.3' - -services: - files: - build: - context: . - volumes: - - "./:/service" - environment: - - DATABASE_URL=postgresql://postgres@127.0.0.1:5432/postgres - - MASTER_KEY=${MASTER_KEY:-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA=} - - DOMAIN=localhost - - SITE_NAME=Drama - - GIPHY_KEY=3435tdfsdudebussylmaoxxt43 - - FORCE_HTTPS=0 - - DISCORD_SERVER_ID=3435tdfsdudebussylmaoxxt43 - - DISCORD_CLIENT_ID=3435tdfsdudebussylmaoxxt43 - - DISCORD_CLIENT_SECRET=3435tdfsdudebussylmaoxxt43 - - DISCORD_BOT_TOKEN=3435tdfsdudebussylmaoxxt43 - #- HCAPTCHA_SITEKEY=3435tdfsdudebussylmaoxxt43 - - HCAPTCHA_SECRET=3435tdfsdudebussylmaoxxt43 - - YOUTUBE_KEY=3435tdfsdudebussylmaoxxt43 - - PUSHER_KEY=3435tdfsdudebussylmaoxxt43 - - CATBOX_KEY=3435tdfsdudebussylmaoxxt43 - - SPAM_SIMILARITY_THRESHOLD=0.5 - - SPAM_SIMILAR_COUNT_THRESHOLD=5 - - SPAM_URL_SIMILARITY_THRESHOLD=0.1 - - COMMENT_SPAM_SIMILAR_THRESHOLD=0.5 - - COMMENT_SPAM_COUNT_THRESHOLD=5 - - READ_ONLY=0 - - BOT_DISABLE=0 - - COINS_NAME=Dramacoins - - DEFAULT_TIME_FILTER=all - - DEFAULT_THEME=midnight - - DEFAULT_COLOR=ff66ac #YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58 - - SLOGAN=Dude bussy lmao - - GUMROAD_TOKEN=3435tdfsdudebussylmaoxxt43 - - GUMROAD_LINK=https://marsey1.gumroad.com/l/tfcvri - - CARD_VIEW=1 - - DISABLE_DOWNVOTES=0 - - DUES=0 - - MAIL_USERNAME=blahblahblah@gmail.com - - MAIL_PASSWORD=3435tdfsdudebussylmaoxxt43 - links: - - "redis" - - "postgres" - ports: - - "80:80" - depends_on: - - redis - - postgres - - redis: - image: redis - ports: - - "6379:6379" - - postgres: - image: postgres:12.3 - volumes: - - "./schema.sql:/docker-entrypoint-initdb.d/00-schema.sql" - - "./seed-db.sql:/docker-entrypoint-initdb.d/01-schema.sql" - environment: - - POSTGRES_HOST_AUTH_METHOD=trust - #ports: +version: '2.3' + +services: + files: + build: + context: . + volumes: + - "./:/service" + environment: + - DATABASE_URL=postgresql://postgres@127.0.0.1:5432/postgres + - MASTER_KEY=${MASTER_KEY:-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA=} + - DOMAIN=localhost + - SITE_NAME=Drama + - GIPHY_KEY=3435tdfsdudebussylmaoxxt43 + - FORCE_HTTPS=0 + - DISCORD_SERVER_ID=3435tdfsdudebussylmaoxxt43 + - DISCORD_CLIENT_ID=3435tdfsdudebussylmaoxxt43 + - DISCORD_CLIENT_SECRET=3435tdfsdudebussylmaoxxt43 + - DISCORD_BOT_TOKEN=3435tdfsdudebussylmaoxxt43 + #- HCAPTCHA_SITEKEY=3435tdfsdudebussylmaoxxt43 + - HCAPTCHA_SECRET=3435tdfsdudebussylmaoxxt43 + - YOUTUBE_KEY=3435tdfsdudebussylmaoxxt43 + - PUSHER_KEY=3435tdfsdudebussylmaoxxt43 + - CATBOX_KEY=3435tdfsdudebussylmaoxxt43 + - SPAM_SIMILARITY_THRESHOLD=0.5 + - SPAM_SIMILAR_COUNT_THRESHOLD=5 + - SPAM_URL_SIMILARITY_THRESHOLD=0.1 + - COMMENT_SPAM_SIMILAR_THRESHOLD=0.5 + - COMMENT_SPAM_COUNT_THRESHOLD=5 + - READ_ONLY=0 + - BOT_DISABLE=0 + - COINS_NAME=Dramacoins + - DEFAULT_TIME_FILTER=all + - DEFAULT_THEME=midnight + - DEFAULT_COLOR=ff66ac #YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58 + - SLOGAN=Dude bussy lmao + - GUMROAD_TOKEN=3435tdfsdudebussylmaoxxt43 + - GUMROAD_LINK=https://marsey1.gumroad.com/l/tfcvri + - CARD_VIEW=1 + - DISABLE_DOWNVOTES=0 + - DUES=0 + - MAIL_USERNAME=blahblahblah@gmail.com + - MAIL_PASSWORD=3435tdfsdudebussylmaoxxt43 + links: + - "redis" + - "postgres" + ports: + - "80:80" + depends_on: + - redis + - postgres + + redis: + image: redis + ports: + - "6379:6379" + + postgres: + image: postgres:12.3 + volumes: + - "./schema.sql:/docker-entrypoint-initdb.d/00-schema.sql" + - "./seed-db.sql:/docker-entrypoint-initdb.d/01-schema.sql" + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + #ports: #- "5432:5432" \ No newline at end of file diff --git a/env b/env old mode 100644 new mode 100755 index cc8a63e76..ae56dfce0 --- a/env +++ b/env @@ -1,33 +1,33 @@ -export DATABASE_URL="postgresql://postgres@127.0.0.1:5432/postgres" -export MASTER_KEY="-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA=" -export DOMAIN="localhost" -export SITE_NAME="Drama" -export GIPHY_KEY="3435tdfsdudebussylmaoxxt43" -export FORCE_HTTPS="0" -export DISCORD_SERVER_ID="3435tdfsdudebussylmaoxxt43" -export DISCORD_CLIENT_ID="3435tdfsdudebussylmaoxxt43" -export DISCORD_CLIENT_SECRET="3435tdfsdudebussylmaoxxt43" -export DISCORD_BOT_TOKEN="3435tdfsdudebussylmaoxxt43" -export HCAPTCHA_SECRET="3435tdfsdudebussylmaoxxt43" -export YOUTUBE_KEY="3435tdfsdudebussylmaoxxt43" -export PUSHER_KEY="3435tdfsdudebussylmaoxxt43" -export CATBOX_KEY="3435tdfsdudebussylmaoxxt43" -export SPAM_SIMILARITY_THRESHOLD="0.5" -export SPAM_SIMILAR_COUNT_THRESHOLD="5" -export SPAM_URL_SIMILARITY_THRESHOLD="0.1" -export COMMENT_SPAM_SIMILAR_THRESHOLD="0.5" -export COMMENT_SPAM_COUNT_THRESHOLD="5" -export READ_ONLY="0" -export BOT_DISABLE="0" -export COINS_NAME="Dramacoins" -export DEFAULT_TIME_FILTER="all" -export SLOGAN="Dude bussy lmao" -export GUMROAD_TOKEN="3435tdfsdudebussylmaoxxt43" -export GUMROAD_LINK="https://marsey1.gumroad.com/l/tfcvri" -export CARD_VIEW="1" -export DISABLE_DOWNVOTES="0" -export DUES="0" -export DEFAULT_THEME="midnight" -export DEFAULT_COLOR="ff66ac" # YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58 -export MAIL_USERNAME="blahblahblah@gmail.com" +export DATABASE_URL="postgresql://postgres@127.0.0.1:5432/postgres" +export MASTER_KEY="-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA=" +export DOMAIN="localhost" +export SITE_NAME="Drama" +export GIPHY_KEY="3435tdfsdudebussylmaoxxt43" +export FORCE_HTTPS="0" +export DISCORD_SERVER_ID="3435tdfsdudebussylmaoxxt43" +export DISCORD_CLIENT_ID="3435tdfsdudebussylmaoxxt43" +export DISCORD_CLIENT_SECRET="3435tdfsdudebussylmaoxxt43" +export DISCORD_BOT_TOKEN="3435tdfsdudebussylmaoxxt43" +export HCAPTCHA_SECRET="3435tdfsdudebussylmaoxxt43" +export YOUTUBE_KEY="3435tdfsdudebussylmaoxxt43" +export PUSHER_KEY="3435tdfsdudebussylmaoxxt43" +export CATBOX_KEY="3435tdfsdudebussylmaoxxt43" +export SPAM_SIMILARITY_THRESHOLD="0.5" +export SPAM_SIMILAR_COUNT_THRESHOLD="5" +export SPAM_URL_SIMILARITY_THRESHOLD="0.1" +export COMMENT_SPAM_SIMILAR_THRESHOLD="0.5" +export COMMENT_SPAM_COUNT_THRESHOLD="5" +export READ_ONLY="0" +export BOT_DISABLE="0" +export COINS_NAME="Dramacoins" +export DEFAULT_TIME_FILTER="all" +export SLOGAN="Dude bussy lmao" +export GUMROAD_TOKEN="3435tdfsdudebussylmaoxxt43" +export GUMROAD_LINK="https://marsey1.gumroad.com/l/tfcvri" +export CARD_VIEW="1" +export DISABLE_DOWNVOTES="0" +export DUES="0" +export DEFAULT_THEME="midnight" +export DEFAULT_COLOR="ff66ac" # YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58 +export MAIL_USERNAME="blahblahblah@gmail.com" export MAIL_PASSWORD="3435tdfsdudebussylmaoxxt43" \ No newline at end of file diff --git a/files/__main__.py b/files/__main__.py old mode 100644 new mode 100755 index 57025e296..ff25aff56 --- a/files/__main__.py +++ b/files/__main__.py @@ -1,131 +1,131 @@ -import gevent.monkey -gevent.monkey.patch_all() -from os import environ -import secrets -from flask import * -from flask_caching import Cache -from flask_limiter import Limiter -from flask_compress import Compress -from flask_limiter.util import get_ipaddr -from flask_mail import Mail - -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, scoped_session -from sqlalchemy import * -import gevent -from werkzeug.middleware.proxy_fix import ProxyFix -import redis - -app = Flask(__name__, template_folder='./templates') -app.wsgi_app = ProxyFix(app.wsgi_app, x_for=3) -app.url_map.strict_slashes = False -app.jinja_env.cache = {} -import faulthandler -faulthandler.enable() - -app.config["SITE_NAME"]=environ.get("SITE_NAME").strip() -app.config["COINS_NAME"]=environ.get("COINS_NAME").strip() -app.config["GUMROAD_LINK"]=environ.get("GUMROAD_LINK", "https://marsey1.gumroad.com/l/tfcvri").strip() -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.config['DATABASE_URL'] = environ.get("DATABASE_URL") -app.config['SECRET_KEY'] = environ.get('MASTER_KEY') -app.config["SERVER_NAME"] = environ.get("DOMAIN").strip() -app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 86400 -app.config["SESSION_COOKIE_NAME"] = "session_" + environ.get("SITE_NAME").strip().lower() -app.config["VERSION"] = "1.0.0" -app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 -app.config["SESSION_COOKIE_SECURE"] = bool(int(environ.get("FORCE_HTTPS", 1))) -app.config["SESSION_COOKIE_SAMESITE"] = "Lax" -app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 365 -app.config["SESSION_REFRESH_EACH_REQUEST"] = True -app.config["SLOGAN"] = environ.get("SLOGAN", "").strip() -app.config["DEFAULT_COLOR"] = environ.get("DEFAULT_COLOR", "ff0000").strip() -app.config["DEFAULT_THEME"] = environ.get("DEFAULT_THEME", "midnight").strip() -app.config["FORCE_HTTPS"] = int(environ.get("FORCE_HTTPS", 1)) if ("localhost" not in app.config["SERVER_NAME"] and "127.0.0.1" not in app.config["SERVER_NAME"]) else 0 -app.config["UserAgent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36" -app.config["HCAPTCHA_SITEKEY"] = environ.get("HCAPTCHA_SITEKEY","").strip() -app.config["HCAPTCHA_SECRET"] = environ.get("HCAPTCHA_SECRET","").strip() -app.config["SPAM_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_SIMILARITY_THRESHOLD", 0.5)) -app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", 5)) -app.config["SPAM_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", 0.5)) -app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"] = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD", 0.5)) -app.config["COMMENT_SPAM_COUNT_THRESHOLD"] = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD", 0.5)) -app.config["VIDEO_COIN_REQUIREMENT"] = int(environ.get("VIDEO_COIN_REQUIREMENT", 0)) -app.config["READ_ONLY"]=bool(int(environ.get("READ_ONLY", "0"))) -app.config["BOT_DISABLE"]=bool(int(environ.get("BOT_DISABLE", False))) -app.config["RATELIMIT_KEY_PREFIX"] = "flask_limiting_" -app.config["RATELIMIT_ENABLED"] = True -app.config["RATELIMIT_DEFAULTS_DEDUCT_WHEN"]=lambda:True -app.config["RATELIMIT_DEFAULTS_EXEMPT_WHEN"]=lambda:False -app.config["RATELIMIT_HEADERS_ENABLED"]=True -app.config["CACHE_TYPE"] = "filesystem" -app.config["CACHE_DIR"] = "cache" -app.config["RATELIMIT_STORAGE_URL"] = environ.get("REDIS_URL", "redis://127.0.0.1") -app.config['MAIL_SERVER'] = 'smtp.gmail.com' -app.config['MAIL_PORT'] = 587 -app.config['MAIL_USE_TLS'] = True -app.config['MAIL_USERNAME'] = environ.get("MAIL_USERNAME", "").strip() -app.config['MAIL_PASSWORD'] = environ.get("MAIL_PASSWORD", "").strip() - -r=redis.Redis(host=environ.get("REDIS_URL", "redis://127.0.0.1"), decode_responses=True, ssl_cert_reqs=None) - -limiter = Limiter( - app, - key_func=get_ipaddr, - default_limits=["50/minute"], - headers_enabled=True, - strategy="fixed-window" -) - -Base = declarative_base() - -engine = create_engine(app.config['DATABASE_URL']) - -db_session = scoped_session(sessionmaker(bind=engine, autoflush=False)) - -cache = Cache(app) -Compress(app) -mail = Mail(app) - -@app.before_request -def before_request(): - - if request.method.lower() != "get" and app.config["READ_ONLY"]: return {"error":f"{app.config['SITE_NAME']} is currently in read-only mode."}, 500 - - if app.config["BOT_DISABLE"] and request.headers.get("X-User-Type")=="Bot": abort(503) - - g.db = db_session() - - g.timestamp = int(time.time()) - - if not request.path.startswith("/assets") and not request.path.startswith("/images") and not request.path.startswith("/hostedimages"): - session.permanent = True - if not session.get("session_id"): session["session_id"] = secrets.token_hex(16) - - if app.config["FORCE_HTTPS"] and request.url.startswith("http://") and "localhost" not in app.config["SERVER_NAME"]: - url = request.url.replace("http://", "https://", 1) - return redirect(url, code=301) - - ua=request.headers.get("User-Agent","") - if "CriOS/" in ua: g.system="ios/chrome" - elif "Version/" in ua: g.system="android/webview" - elif "Mobile Safari/" in ua: g.system="android/chrome" - elif "Safari/" in ua: g.system="ios/safari" - elif "Mobile/" in ua: g.system="ios/webview" - else: g.system="other/other" - -@app.teardown_appcontext -def teardown_request(error): - if hasattr(g, 'db') and g.db: - g.db.close() - -@app.after_request -def after_request(response): - - response.headers.add("Strict-Transport-Security", "max-age=31536000") - response.headers.add("Referrer-Policy", "same-origin") - response.headers.add("X-Frame-Options", "deny") - return response - - +import gevent.monkey +gevent.monkey.patch_all() +from os import environ +import secrets +from flask import * +from flask_caching import Cache +from flask_limiter import Limiter +from flask_compress import Compress +from flask_limiter.util import get_ipaddr +from flask_mail import Mail + +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy import * +import gevent +from werkzeug.middleware.proxy_fix import ProxyFix +import redis + +app = Flask(__name__, template_folder='./templates') +app.wsgi_app = ProxyFix(app.wsgi_app, x_for=3) +app.url_map.strict_slashes = False +app.jinja_env.cache = {} +import faulthandler +faulthandler.enable() + +app.config["SITE_NAME"]=environ.get("SITE_NAME").strip() +app.config["COINS_NAME"]=environ.get("COINS_NAME").strip() +app.config["GUMROAD_LINK"]=environ.get("GUMROAD_LINK", "https://marsey1.gumroad.com/l/tfcvri").strip() +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['DATABASE_URL'] = environ.get("DATABASE_URL") +app.config['SECRET_KEY'] = environ.get('MASTER_KEY') +app.config["SERVER_NAME"] = environ.get("DOMAIN").strip() +app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 86400 +app.config["SESSION_COOKIE_NAME"] = "session_" + environ.get("SITE_NAME").strip().lower() +app.config["VERSION"] = "1.0.0" +app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 +app.config["SESSION_COOKIE_SECURE"] = bool(int(environ.get("FORCE_HTTPS", 1))) +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" +app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 365 +app.config["SESSION_REFRESH_EACH_REQUEST"] = True +app.config["SLOGAN"] = environ.get("SLOGAN", "").strip() +app.config["DEFAULT_COLOR"] = environ.get("DEFAULT_COLOR", "ff0000").strip() +app.config["DEFAULT_THEME"] = environ.get("DEFAULT_THEME", "midnight").strip() +app.config["FORCE_HTTPS"] = int(environ.get("FORCE_HTTPS", 1)) if ("localhost" not in app.config["SERVER_NAME"] and "127.0.0.1" not in app.config["SERVER_NAME"]) else 0 +app.config["UserAgent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36" +app.config["HCAPTCHA_SITEKEY"] = environ.get("HCAPTCHA_SITEKEY","").strip() +app.config["HCAPTCHA_SECRET"] = environ.get("HCAPTCHA_SECRET","").strip() +app.config["SPAM_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_SIMILARITY_THRESHOLD", 0.5)) +app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", 5)) +app.config["SPAM_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", 0.5)) +app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"] = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD", 0.5)) +app.config["COMMENT_SPAM_COUNT_THRESHOLD"] = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD", 0.5)) +app.config["VIDEO_COIN_REQUIREMENT"] = int(environ.get("VIDEO_COIN_REQUIREMENT", 0)) +app.config["READ_ONLY"]=bool(int(environ.get("READ_ONLY", "0"))) +app.config["BOT_DISABLE"]=bool(int(environ.get("BOT_DISABLE", False))) +app.config["RATELIMIT_KEY_PREFIX"] = "flask_limiting_" +app.config["RATELIMIT_ENABLED"] = True +app.config["RATELIMIT_DEFAULTS_DEDUCT_WHEN"]=lambda:True +app.config["RATELIMIT_DEFAULTS_EXEMPT_WHEN"]=lambda:False +app.config["RATELIMIT_HEADERS_ENABLED"]=True +app.config["CACHE_TYPE"] = "filesystem" +app.config["CACHE_DIR"] = "cache" +app.config["RATELIMIT_STORAGE_URL"] = environ.get("REDIS_URL", "redis://127.0.0.1") +app.config['MAIL_SERVER'] = 'smtp.gmail.com' +app.config['MAIL_PORT'] = 587 +app.config['MAIL_USE_TLS'] = True +app.config['MAIL_USERNAME'] = environ.get("MAIL_USERNAME", "").strip() +app.config['MAIL_PASSWORD'] = environ.get("MAIL_PASSWORD", "").strip() + +r=redis.Redis(host=environ.get("REDIS_URL", "redis://127.0.0.1"), decode_responses=True, ssl_cert_reqs=None) + +limiter = Limiter( + app, + key_func=get_ipaddr, + default_limits=["50/minute"], + headers_enabled=True, + strategy="fixed-window" +) + +Base = declarative_base() + +engine = create_engine(app.config['DATABASE_URL']) + +db_session = scoped_session(sessionmaker(bind=engine, autoflush=False)) + +cache = Cache(app) +Compress(app) +mail = Mail(app) + +@app.before_request +def before_request(): + + if request.method.lower() != "get" and app.config["READ_ONLY"]: return {"error":f"{app.config['SITE_NAME']} is currently in read-only mode."}, 500 + + if app.config["BOT_DISABLE"] and request.headers.get("X-User-Type")=="Bot": abort(503) + + g.db = db_session() + + g.timestamp = int(time.time()) + + if not request.path.startswith("/assets") and not request.path.startswith("/images") and not request.path.startswith("/hostedimages"): + session.permanent = True + if not session.get("session_id"): session["session_id"] = secrets.token_hex(16) + + if app.config["FORCE_HTTPS"] and request.url.startswith("http://") and "localhost" not in app.config["SERVER_NAME"]: + url = request.url.replace("http://", "https://", 1) + return redirect(url, code=301) + + ua=request.headers.get("User-Agent","") + if "CriOS/" in ua: g.system="ios/chrome" + elif "Version/" in ua: g.system="android/webview" + elif "Mobile Safari/" in ua: g.system="android/chrome" + elif "Safari/" in ua: g.system="ios/safari" + elif "Mobile/" in ua: g.system="ios/webview" + else: g.system="other/other" + +@app.teardown_appcontext +def teardown_request(error): + if hasattr(g, 'db') and g.db: + g.db.close() + +@app.after_request +def after_request(response): + + response.headers.add("Strict-Transport-Security", "max-age=31536000") + response.headers.add("Referrer-Policy", "same-origin") + response.headers.add("X-Frame-Options", "deny") + return response + + from files.routes import * \ No newline at end of file diff --git a/files/classes/__init__.py b/files/classes/__init__.py old mode 100644 new mode 100755 index ea76c6a8f..15fb4cb2c --- a/files/classes/__init__.py +++ b/files/classes/__init__.py @@ -1,15 +1,15 @@ -from .alts import * -from .badges import * -from .clients import * -from .comment import * -from .domains import * -from .flags import * -from .user import * -from .userblock import * -from .submission import * -from .votes import * -from .domains import * -from .subscriptions import * -from files.__main__ import app -from .mod_logs import * +from .alts import * +from .badges import * +from .clients import * +from .comment import * +from .domains import * +from .flags import * +from .user import * +from .userblock import * +from .submission import * +from .votes import * +from .domains import * +from .subscriptions import * +from files.__main__ import app +from .mod_logs import * from .award import * \ No newline at end of file diff --git a/files/classes/alts.py b/files/classes/alts.py old mode 100644 new mode 100755 index 04a6f00ef..bd3c05eac --- a/files/classes/alts.py +++ b/files/classes/alts.py @@ -1,15 +1,15 @@ -from sqlalchemy import * -from files.__main__ import Base - - -class Alt(Base): - __tablename__ = "alts" - - id = Column(Integer, primary_key=True) - user1 = Column(Integer, ForeignKey("users.id")) - user2 = Column(Integer, ForeignKey("users.id")) - is_manual = Column(Boolean, default=False) - - def __repr__(self): - - return f"" +from sqlalchemy import * +from files.__main__ import Base + + +class Alt(Base): + __tablename__ = "alts" + + id = Column(Integer, primary_key=True) + user1 = Column(Integer, ForeignKey("users.id")) + user2 = Column(Integer, ForeignKey("users.id")) + is_manual = Column(Boolean, default=False) + + def __repr__(self): + + return f"" diff --git a/files/classes/award.py b/files/classes/award.py old mode 100644 new mode 100755 index 748858517..e251eeccd --- a/files/classes/award.py +++ b/files/classes/award.py @@ -1,87 +1,87 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from os import environ -from files.helpers.lazy import lazy - -site_name = environ.get("SITE_NAME").strip() - -if site_name == "Drama": - AWARDS = { - "ban": { - "kind": "ban", - "title": "One-Day Ban", - "description": "Bans the author for a day.", - "icon": "fas fa-gavel", - "color": "text-danger", - "price": 5000 - }, - "shit": { - "kind": "shit", - "title": "Shit", - "description": "Makes flies swarm a post.", - "icon": "fas fa-poop", - "color": "text-black-50", - "price": 1000 - }, - "fireflies": { - "kind": "fireflies", - "title": "Fireflies", - "description": "Puts stars on the post.", - "icon": "fas fa-sparkles", - "color": "text-warning", - "price": 1000 - } - } -else: - AWARDS = { - "shit": { - "kind": "shit", - "title": "Shit", - "description": "Makes flies swarm a post.", - "icon": "fas fa-poop", - "color": "text-black-50", - "price": 1000 - }, - "fireflies": { - "kind": "fireflies", - "title": "Fireflies", - "description": "Puts stars on the post.", - "icon": "fas fa-sparkles", - "color": "text-warning", - "price": 1000 - } - } - - -class AwardRelationship(Base): - - __tablename__ = "award_relationships" - - id = Column(Integer, primary_key=True) - - user_id = Column(Integer, ForeignKey("users.id")) - submission_id = Column(Integer, ForeignKey("submissions.id")) - comment_id = Column(Integer, ForeignKey("comments.id")) - kind = Column(String) - - user = relationship("User", primaryjoin="AwardRelationship.user_id==User.id", viewonly=True) - - post = relationship("Submission", primaryjoin="AwardRelationship.submission_id==Submission.id", viewonly=True) - comment = relationship("Comment", primaryjoin="AwardRelationship.comment_id==Comment.id", viewonly=True) - - - @property - @lazy - def type(self): - return AWARDS[self.kind] - - @property - @lazy - def title(self): - return self.type['title'] - - @property - @lazy - def class_list(self): - return self.type['icon']+' '+self.type['color'] +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base +from os import environ +from files.helpers.lazy import lazy + +site_name = environ.get("SITE_NAME").strip() + +if site_name == "Drama": + AWARDS = { + "ban": { + "kind": "ban", + "title": "One-Day Ban", + "description": "Bans the author for a day.", + "icon": "fas fa-gavel", + "color": "text-danger", + "price": 1500 + }, + "shit": { + "kind": "shit", + "title": "Shit", + "description": "Makes flies swarm a post.", + "icon": "fas fa-poop", + "color": "text-black-50", + "price": 500 + }, + "fireflies": { + "kind": "fireflies", + "title": "Fireflies", + "description": "Puts stars on the post.", + "icon": "fas fa-sparkles", + "color": "text-warning", + "price": 500 + } + } +else: + AWARDS = { + "shit": { + "kind": "shit", + "title": "Shit", + "description": "Makes flies swarm a post.", + "icon": "fas fa-poop", + "color": "text-black-50", + "price": 500 + }, + "fireflies": { + "kind": "fireflies", + "title": "Fireflies", + "description": "Puts stars on the post.", + "icon": "fas fa-sparkles", + "color": "text-warning", + "price": 500 + } + } + + +class AwardRelationship(Base): + + __tablename__ = "award_relationships" + + id = Column(Integer, primary_key=True) + + user_id = Column(Integer, ForeignKey("users.id")) + submission_id = Column(Integer, ForeignKey("submissions.id")) + comment_id = Column(Integer, ForeignKey("comments.id")) + kind = Column(String) + + user = relationship("User", primaryjoin="AwardRelationship.user_id==User.id", viewonly=True) + + post = relationship("Submission", primaryjoin="AwardRelationship.submission_id==Submission.id", viewonly=True) + comment = relationship("Comment", primaryjoin="AwardRelationship.comment_id==Comment.id", viewonly=True) + + + @property + @lazy + def type(self): + return AWARDS[self.kind] + + @property + @lazy + def title(self): + return self.type['title'] + + @property + @lazy + def class_list(self): + return self.type['icon']+' '+self.type['color'] diff --git a/files/classes/badges.py b/files/classes/badges.py old mode 100644 new mode 100755 index 008948bc8..c7182b970 --- a/files/classes/badges.py +++ b/files/classes/badges.py @@ -1,94 +1,93 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base, app -from os import environ -from files.helpers.lazy import lazy - -site_name = environ.get("SITE_NAME").strip() - -class BadgeDef(Base): - - __tablename__ = "badge_defs" - - id = Column(BigInteger, primary_key=True) - name = Column(String) - description = Column(String) - icon = Column(String) - kind = Column(Integer, default=1) - qualification_expr = Column(String) - - def __repr__(self): - - return f"" - - @property - @lazy - def path(self): - - return f"/assets/images/badges/{self.icon}" - - @property - @lazy - def json_core(self): - return { - "name": self.name, - "description": self.description, - "icon": self.icon - } - - - -class Badge(Base): - - __tablename__ = "badges" - - id = Column(Integer, primary_key=True) - - user_id = Column(Integer, ForeignKey('users.id')) - badge_id = Column(Integer, ForeignKey("badge_defs.id")) - description = Column(String) - url = Column(String) - badge = relationship("BadgeDef", viewonly=True) - - def __repr__(self): - - return f"" - - @property - @lazy - def text(self): - if self.description: - return self.description - else: - return self.badge.description - - @property - @lazy - def type(self): - return self.badge.id - - @property - @lazy - def name(self): - return self.badge.name - - @property - @lazy - def path(self): - return self.badge.path - - @property - @lazy - def json_core(self): - - return {'text': self.text, - 'name': self.name, - 'created_utc': self.created_utc, - 'url': self.url, - 'icon_url':f"https://{app.config['SERVER_NAME']}{self.path}" - } - - @property - @lazy - def json(self): - return self.json_core +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base, app +from os import environ +from files.helpers.lazy import lazy + +site_name = environ.get("SITE_NAME").strip() + +class BadgeDef(Base): + + __tablename__ = "badge_defs" + + id = Column(BigInteger, primary_key=True) + name = Column(String) + description = Column(String) + icon = Column(String) + kind = Column(Integer, default=1) + qualification_expr = Column(String) + + def __repr__(self): + + return f"" + + @property + @lazy + def path(self): + + return f"/assets/images/badges/{self.icon}" + + @property + @lazy + def json_core(self): + return { + "name": self.name, + "description": self.description, + "icon": self.icon + } + + + +class Badge(Base): + + __tablename__ = "badges" + + id = Column(Integer, primary_key=True) + + user_id = Column(Integer, ForeignKey('users.id')) + badge_id = Column(Integer, ForeignKey("badge_defs.id")) + description = Column(String) + url = Column(String) + badge = relationship("BadgeDef", viewonly=True) + + def __repr__(self): + + return f"" + + @property + @lazy + def text(self): + if self.description: + return self.description + else: + return self.badge.description + + @property + @lazy + def type(self): + return self.badge.id + + @property + @lazy + def name(self): + return self.badge.name + + @property + @lazy + def path(self): + return self.badge.path + + @property + @lazy + def json_core(self): + + return {'text': self.text, + 'name': self.name, + 'url': self.url, + 'icon_url':f"https://{app.config['SERVER_NAME']}{self.path}" + } + + @property + @lazy + def json(self): + return self.json_core diff --git a/files/classes/clients.py b/files/classes/clients.py old mode 100644 new mode 100755 index f2c60a991..9ce14345e --- a/files/classes/clients.py +++ b/files/classes/clients.py @@ -1,82 +1,82 @@ -from flask import * -from sqlalchemy import * -from sqlalchemy.orm import relationship, lazyload -from .submission import Submission -from .comment import Comment -from files.__main__ import Base -from files.helpers.lazy import lazy -import time - -class OauthApp(Base): - - __tablename__ = "oauth_apps" - - id = Column(Integer, primary_key=True) - client_id = Column(String) - app_name = Column(String) - redirect_uri = Column(String) - description = Column(String) - author_id = Column(Integer, ForeignKey("users.id")) - author = relationship("User", viewonly=True) - - def __repr__(self): return f"" - - - @property - @lazy - def created_date(self): - return time.strftime("%d %B %Y", time.gmtime(self.created_utc)) - - @property - @lazy - def created_datetime(self): - return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) - - @property - @lazy - def permalink(self): return f"/admin/app/{self.id}" - - @lazy - def idlist(self, page=1): - - posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(app_id=self.id) - - posts=posts.order_by(Submission.created_utc.desc()) - - posts=posts.offset(100*(page-1)).limit(101) - - return [x[0] for x in posts.all()] - - @lazy - def comments_idlist(self, page=1): - - posts = g.db.query(Comment.id).options(lazyload('*')).filter_by(app_id=self.id) - - posts=posts.order_by(Comment.created_utc.desc()) - - posts=posts.offset(100*(page-1)).limit(101) - - return [x[0] for x in posts.all()] - - - -class ClientAuth(Base): - - __tablename__ = "client_auths" - - id = Column(Integer, primary_key=True) - oauth_client = Column(Integer, ForeignKey("oauth_apps.id")) - access_token = Column(String) - user_id = Column(Integer, ForeignKey("users.id")) - user = relationship("User", viewonly=True) - application = relationship("OauthApp", viewonly=True) - - @property - @lazy - def created_date(self): - return time.strftime("%d %B %Y", time.gmtime(self.created_utc)) - - @property - @lazy - def created_datetime(self): +from flask import * +from sqlalchemy import * +from sqlalchemy.orm import relationship, lazyload +from .submission import Submission +from .comment import Comment +from files.__main__ import Base +from files.helpers.lazy import lazy +import time + +class OauthApp(Base): + + __tablename__ = "oauth_apps" + + id = Column(Integer, primary_key=True) + client_id = Column(String) + app_name = Column(String) + redirect_uri = Column(String) + description = Column(String) + author_id = Column(Integer, ForeignKey("users.id")) + author = relationship("User", viewonly=True) + + def __repr__(self): return f"" + + + @property + @lazy + def created_date(self): + return time.strftime("%d %B %Y", time.gmtime(self.created_utc)) + + @property + @lazy + def created_datetime(self): + return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) + + @property + @lazy + def permalink(self): return f"/admin/app/{self.id}" + + @lazy + def idlist(self, page=1): + + posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(app_id=self.id) + + posts=posts.order_by(Submission.created_utc.desc()) + + posts=posts.offset(100*(page-1)).limit(101) + + return [x[0] for x in posts.all()] + + @lazy + def comments_idlist(self, page=1): + + posts = g.db.query(Comment.id).options(lazyload('*')).filter_by(app_id=self.id) + + posts=posts.order_by(Comment.created_utc.desc()) + + posts=posts.offset(100*(page-1)).limit(101) + + return [x[0] for x in posts.all()] + + + +class ClientAuth(Base): + + __tablename__ = "client_auths" + + id = Column(Integer, primary_key=True) + oauth_client = Column(Integer, ForeignKey("oauth_apps.id")) + access_token = Column(String) + user_id = Column(Integer, ForeignKey("users.id")) + user = relationship("User", viewonly=True) + application = relationship("OauthApp", viewonly=True) + + @property + @lazy + def created_date(self): + return time.strftime("%d %B %Y", time.gmtime(self.created_utc)) + + @property + @lazy + def created_datetime(self): return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) \ No newline at end of file diff --git a/files/classes/comment.py b/files/classes/comment.py old mode 100644 new mode 100755 index 1c9bb94ca..df37e2f78 --- a/files/classes/comment.py +++ b/files/classes/comment.py @@ -1,397 +1,397 @@ -import re -from urllib.parse import urlencode, urlparse, parse_qs -from flask import * -from sqlalchemy import * -from sqlalchemy.orm import relationship, deferred, lazyload -from files.classes.votes import CommentVote -from files.helpers.lazy import lazy -from files.helpers.const import SLURS -from files.__main__ import Base -from .flags import CommentFlag -from os import environ -import time -from files.helpers.const import AUTOPOLLER_ACCOUNT - -site = environ.get("DOMAIN").strip() - - -class Comment(Base): - - __tablename__ = "comments" - - id = Column(Integer, primary_key=True) - author_id = Column(Integer, ForeignKey("users.id")) - parent_submission = Column(Integer, ForeignKey("submissions.id")) - created_utc = Column(Integer, default=0) - edited_utc = Column(Integer, default=0) - is_banned = Column(Boolean, default=False) - removed_by = Column(Integer) - bannedfor = Column(Boolean) - distinguish_level = Column(Integer, default=0) - deleted_utc = Column(Integer, default=0) - is_approved = Column(Integer, default=0) - level = Column(Integer, default=0) - parent_comment_id = Column(Integer, ForeignKey("comments.id")) - over_18 = Column(Boolean, default=False) - is_bot = Column(Boolean, default=False) - is_pinned = Column(String) - sentto=Column(Integer, ForeignKey("users.id")) - notifiedto=Column(Integer) - app_id = Column(Integer, ForeignKey("oauth_apps.id")) - oauth_app = relationship("OauthApp", viewonly=True) - upvotes = Column(Integer, default=0) - downvotes = Column(Integer, default=0) - body = deferred(Column(String)) - body_html = deferred(Column(String)) - ban_reason = Column(String) - - post = relationship("Submission", viewonly=True) - flags = relationship("CommentFlag", lazy="dynamic", viewonly=True) - author = relationship("User", primaryjoin="User.id==Comment.author_id") - senttouser = relationship("User", primaryjoin="User.id==Comment.sentto", viewonly=True) - parent_comment = relationship("Comment", remote_side=[id], viewonly=True) - child_comments = relationship("Comment", remote_side=[parent_comment_id], viewonly=True) - awards = relationship("AwardRelationship", viewonly=True) - - def __init__(self, *args, **kwargs): - - if "created_utc" not in kwargs: - kwargs["created_utc"] = int(time.time()) - - super().__init__(*args, **kwargs) - - def __repr__(self): - - return f"" - - def poll_voted(self, v): - if v: - vote = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=self.id).first() - if vote: return vote.vote_type - else: return None - else: return None - - @property - @lazy - def options(self): - return [x for x in self.child_comments if x.author_id == AUTOPOLLER_ACCOUNT] - - @property - @lazy - def created_datetime(self): - return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) - - @property - @lazy - def age_string(self): - - age = int(time.time()) - self.created_utc - - if age < 60: - return "just now" - elif age < 3600: - minutes = int(age / 60) - return f"{minutes}m ago" - elif age < 86400: - hours = int(age / 3600) - return f"{hours}hr ago" - elif age < 2678400: - days = int(age / 86400) - return f"{days}d ago" - - now = time.gmtime() - ctd = time.gmtime(self.created_utc) - - months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) - if now.tm_mday < ctd.tm_mday: - months -= 1 - - if months < 12: - return f"{months}mo ago" - else: - years = int(months / 12) - return f"{years}yr ago" - - @property - @lazy - def edited_string(self): - - if not self.edited_utc: - return "never" - - age = int(time.time()) - self.edited_utc - - if age < 60: - return "just now" - elif age < 3600: - minutes = int(age / 60) - return f"{minutes}m ago" - elif age < 86400: - hours = int(age / 3600) - return f"{hours}hr ago" - elif age < 2678400: - days = int(age / 86400) - return f"{days}d ago" - - now = time.gmtime() - ctd = time.gmtime(self.edited_utc) - months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) - - if months < 12: - return f"{months}mo ago" - else: - years = now.tm_year - ctd.tm_year - return f"{years}yr ago" - - @property - @lazy - def score(self): - return self.upvotes - self.downvotes - - @property - @lazy - def fullname(self): - return f"t3_{self.id}" - - @property - @lazy - def parent(self): - - if not self.parent_submission: return None - - if self.level == 1: return self.post - - else: return g.db.query(Comment).get(self.parent_comment_id) - - @property - @lazy - def parent_fullname(self): - if self.parent_comment_id: return f"t3_{self.parent_comment_id}" - elif self.parent_submission: return f"t2_{self.parent_submission}" - - @property - def replies(self): - r = self.__dict__.get("replies", None) - if r: r = [x for x in r if not x.author.shadowbanned] - if not r and r != []: r = sorted([x for x in self.child_comments if not x.author.shadowbanned and x.author_id != AUTOPOLLER_ACCOUNT], key=lambda x: x.score, reverse=True) - return r - - @replies.setter - def replies(self, value): - self.__dict__["replies"] = value - - @property - def replies2(self): - return self.__dict__.get("replies2", []) - - @replies2.setter - def replies2(self, value): - self.__dict__["replies2"] = value - - @property - def replies3(self): - r = self.__dict__.get("replies", None) - if not r and r != []: r = sorted([x for x in self.child_comments if x.author_id != AUTOPOLLER_ACCOUNT], key=lambda x: x.score, reverse=True) - return r - - @property - @lazy - def shortlink(self): - return f"https://{site}/comment/{self.id}" - - @property - @lazy - def permalink(self): - if self.post and self.post.club: return f"/comment/{self.id}/" - - if self.post: return f"{self.post.permalink}/{self.id}/" - else: return f"/comment/{self.id}/" - - @property - @lazy - def json_raw(self): - flags = {} - for f in self.flags: flags[f.user.username] = f.reason - - data= { - 'id': self.id, - 'level': self.level, - 'author_name': self.author.username, - 'body': self.body, - 'body_html': self.body_html, - 'is_bot': self.is_bot, - 'created_utc': self.created_utc, - 'edited_utc': self.edited_utc or 0, - 'is_banned': bool(self.is_banned), - 'deleted_utc': self.deleted_utc, - 'is_nsfw': self.over_18, - 'permalink': self.permalink, - 'is_pinned': self.is_pinned, - 'distinguish_level': self.distinguish_level, - 'post_id': self.post.id, - 'score': self.score, - 'upvotes': self.upvotes, - 'downvotes': self.downvotes, - 'is_bot': self.is_bot, - 'flags': flags, - } - - if self.ban_reason: - data["ban_reason"]=self.ban_reason - - return data - - def award_count(self, kind) -> int: - return len([x for x in self.awards if x.kind == kind]) - - @property - @lazy - def json_core(self): - if self.is_banned: - data= {'is_banned': True, - 'ban_reason': self.ban_reason, - 'id': self.id, - 'post': self.post.id, - 'level': self.level, - 'parent': self.parent_fullname - } - elif self.deleted_utc > 0: - data= {'deleted_utc': self.deleted_utc, - 'id': self.id, - 'post': self.post.id, - 'level': self.level, - 'parent': self.parent_fullname - } - else: - - data=self.json_raw - - if self.level>=2: data['parent_comment_id']= self.parent_comment_id, - - if "replies" in self.__dict__: - data['replies']=[x.json_core for x in self.replies] - - return data - - @property - @lazy - def json(self): - - data=self.json_core - - if self.deleted_utc > 0 or self.is_banned: - return data - - data["author"]=self.author.json_core - data["post"]=self.post.json_core - - if self.level >= 2: - data["parent"]=self.parent.json_core - - - return data - - def realbody(self, v): - if self.post and self.post.club and not (v and v.paid_dues): return "

COUNTRY CLUB ONLY

" - - body = self.body_html - - if not body: return "" - - if not v or v.slurreplacer: - for s, r in SLURS.items(): body = body.replace(s, r) - - if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com") - - if v and v.nitter: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") - - if v and v.controversial: - for i in re.finditer('(/comments/.*?)"', body): - url = i.group(1) - p = urlparse(url).query - p = parse_qs(p) - - if 'sort' not in p: p['sort'] = ['controversial'] - - url_noquery = url.split('?')[0] - body = body.replace(url, f"{url_noquery}?{urlencode(p, True)}") - - return body - - def plainbody(self, v): - if self.post and self.post.club and not (v and v.paid_dues): return "

COUNTRY CLUB ONLY

" - - body = self.body - - if not body: return "" - - if not v or v.slurreplacer: - for s, r in SLURS.items(): body = body.replace(s, r) - - if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com") - - if v and v.nitter: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") - - if v and v.controversial: - for i in re.finditer('(/comments/.*?)"', body): - url = i.group(1) - p = urlparse(url).query - p = parse_qs(p) - - if 'sort' not in p: p['sort'] = ['controversial'] - - url_noquery = url.split('?')[0] - body = body.replace(url, f"{url_noquery}?{urlencode(p, True)}") - - return body - - @lazy - def collapse_for_user(self, v): - - if self.over_18 and not (v and v.over_18) and not self.post.over_18: - return True - - if not v: - return False - - if any([x in self.body for x in v.filter_words]): - return True - - if self.is_banned or (self.author and self.author.shadowbanned): return True - - return False - - @property - @lazy - def is_op(self): return self.author_id==self.post.author_id - - @property - @lazy - def active_flags(self): return self.flags.count() - - @property - @lazy - def ordered_flags(self): return self.flags.order_by(CommentFlag.id).all() - - - -class Notification(Base): - - __tablename__ = "notifications" - - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.id")) - comment_id = Column(Integer, ForeignKey("comments.id")) - read = Column(Boolean, default=False) - followsender = Column(Integer) - unfollowsender = Column(Integer) - removefollowsender = Column(Integer) - blocksender = Column(Integer) - unblocksender = Column(Integer) - - comment = relationship("Comment", viewonly=True) - user = relationship("User", viewonly=True) - - def __repr__(self): - - return f"" +import re +from urllib.parse import urlencode, urlparse, parse_qs +from flask import * +from sqlalchemy import * +from sqlalchemy.orm import relationship, deferred, lazyload +from files.classes.votes import CommentVote +from files.helpers.lazy import lazy +from files.helpers.const import SLURS +from files.__main__ import Base +from .flags import CommentFlag +from os import environ +import time +from files.helpers.const import AUTOPOLLER_ACCOUNT + +site = environ.get("DOMAIN").strip() + + +class Comment(Base): + + __tablename__ = "comments" + + id = Column(Integer, primary_key=True) + author_id = Column(Integer, ForeignKey("users.id")) + parent_submission = Column(Integer, ForeignKey("submissions.id")) + created_utc = Column(Integer, default=0) + edited_utc = Column(Integer, default=0) + is_banned = Column(Boolean, default=False) + removed_by = Column(Integer) + bannedfor = Column(Boolean) + distinguish_level = Column(Integer, default=0) + deleted_utc = Column(Integer, default=0) + is_approved = Column(Integer, default=0) + level = Column(Integer, default=0) + parent_comment_id = Column(Integer, ForeignKey("comments.id")) + over_18 = Column(Boolean, default=False) + is_bot = Column(Boolean, default=False) + is_pinned = Column(String) + sentto=Column(Integer, ForeignKey("users.id")) + notifiedto=Column(Integer) + app_id = Column(Integer, ForeignKey("oauth_apps.id")) + oauth_app = relationship("OauthApp", viewonly=True) + upvotes = Column(Integer, default=0) + downvotes = Column(Integer, default=0) + body = deferred(Column(String)) + body_html = deferred(Column(String)) + ban_reason = Column(String) + + post = relationship("Submission", viewonly=True) + flags = relationship("CommentFlag", lazy="dynamic", viewonly=True) + author = relationship("User", primaryjoin="User.id==Comment.author_id") + senttouser = relationship("User", primaryjoin="User.id==Comment.sentto", viewonly=True) + parent_comment = relationship("Comment", remote_side=[id], viewonly=True) + child_comments = relationship("Comment", remote_side=[parent_comment_id], viewonly=True) + awards = relationship("AwardRelationship", viewonly=True) + + def __init__(self, *args, **kwargs): + + if "created_utc" not in kwargs: + kwargs["created_utc"] = int(time.time()) + + super().__init__(*args, **kwargs) + + def __repr__(self): + + return f"" + + def poll_voted(self, v): + if v: + vote = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=self.id).first() + if vote: return vote.vote_type + else: return None + else: return None + + @property + @lazy + def options(self): + return [x for x in self.child_comments if x.author_id == AUTOPOLLER_ACCOUNT] + + @property + @lazy + def created_datetime(self): + return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) + + @property + @lazy + def age_string(self): + + age = int(time.time()) - self.created_utc + + if age < 60: + return "just now" + elif age < 3600: + minutes = int(age / 60) + return f"{minutes}m ago" + elif age < 86400: + hours = int(age / 3600) + return f"{hours}hr ago" + elif age < 2678400: + days = int(age / 86400) + return f"{days}d ago" + + now = time.gmtime() + ctd = time.gmtime(self.created_utc) + + months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) + if now.tm_mday < ctd.tm_mday: + months -= 1 + + if months < 12: + return f"{months}mo ago" + else: + years = int(months / 12) + return f"{years}yr ago" + + @property + @lazy + def edited_string(self): + + if not self.edited_utc: + return "never" + + age = int(time.time()) - self.edited_utc + + if age < 60: + return "just now" + elif age < 3600: + minutes = int(age / 60) + return f"{minutes}m ago" + elif age < 86400: + hours = int(age / 3600) + return f"{hours}hr ago" + elif age < 2678400: + days = int(age / 86400) + return f"{days}d ago" + + now = time.gmtime() + ctd = time.gmtime(self.edited_utc) + months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) + + if months < 12: + return f"{months}mo ago" + else: + years = now.tm_year - ctd.tm_year + return f"{years}yr ago" + + @property + @lazy + def score(self): + return self.upvotes - self.downvotes + + @property + @lazy + def fullname(self): + return f"t3_{self.id}" + + @property + @lazy + def parent(self): + + if not self.parent_submission: return None + + if self.level == 1: return self.post + + else: return g.db.query(Comment).get(self.parent_comment_id) + + @property + @lazy + def parent_fullname(self): + if self.parent_comment_id: return f"t3_{self.parent_comment_id}" + elif self.parent_submission: return f"t2_{self.parent_submission}" + + @property + def replies(self): + r = self.__dict__.get("replies", None) + if r: r = [x for x in r if not x.author.shadowbanned] + if not r and r != []: r = sorted([x for x in self.child_comments if not x.author.shadowbanned and x.author_id != AUTOPOLLER_ACCOUNT], key=lambda x: x.score, reverse=True) + return r + + @replies.setter + def replies(self, value): + self.__dict__["replies"] = value + + @property + def replies2(self): + return self.__dict__.get("replies2", []) + + @replies2.setter + def replies2(self, value): + self.__dict__["replies2"] = value + + @property + def replies3(self): + r = self.__dict__.get("replies", None) + if not r and r != []: r = sorted([x for x in self.child_comments if x.author_id != AUTOPOLLER_ACCOUNT], key=lambda x: x.score, reverse=True) + return r + + @property + @lazy + def shortlink(self): + return f"https://{site}/comment/{self.id}" + + @property + @lazy + def permalink(self): + if self.post and self.post.club: return f"/comment/{self.id}/" + + if self.post: return f"{self.post.permalink}/{self.id}/" + else: return f"/comment/{self.id}/" + + @property + @lazy + def json_raw(self): + flags = {} + for f in self.flags: flags[f.user.username] = f.reason + + data= { + 'id': self.id, + 'level': self.level, + 'author_name': self.author.username, + 'body': self.body, + 'body_html': self.body_html, + 'is_bot': self.is_bot, + 'created_utc': self.created_utc, + 'edited_utc': self.edited_utc or 0, + 'is_banned': bool(self.is_banned), + 'deleted_utc': self.deleted_utc, + 'is_nsfw': self.over_18, + 'permalink': self.permalink, + 'is_pinned': self.is_pinned, + 'distinguish_level': self.distinguish_level, + 'post_id': self.post.id, + 'score': self.score, + 'upvotes': self.upvotes, + 'downvotes': self.downvotes, + 'is_bot': self.is_bot, + 'flags': flags, + } + + if self.ban_reason: + data["ban_reason"]=self.ban_reason + + return data + + def award_count(self, kind) -> int: + return len([x for x in self.awards if x.kind == kind]) + + @property + @lazy + def json_core(self): + if self.is_banned: + data= {'is_banned': True, + 'ban_reason': self.ban_reason, + 'id': self.id, + 'post': self.post.id, + 'level': self.level, + 'parent': self.parent_fullname + } + elif self.deleted_utc > 0: + data= {'deleted_utc': self.deleted_utc, + 'id': self.id, + 'post': self.post.id, + 'level': self.level, + 'parent': self.parent_fullname + } + else: + + data=self.json_raw + + if self.level>=2: data['parent_comment_id']= self.parent_comment_id, + + if "replies" in self.__dict__: + data['replies']=[x.json_core for x in self.replies] + + return data + + @property + @lazy + def json(self): + + data=self.json_core + + if self.deleted_utc > 0 or self.is_banned: + return data + + data["author"]=self.author.json_core + data["post"]=self.post.json_core + + if self.level >= 2: + data["parent"]=self.parent.json_core + + + return data + + def realbody(self, v): + if self.post and self.post.club and not (v and v.paid_dues): return "

COUNTRY CLUB ONLY

" + + body = self.body_html + + if not body: return "" + + if not v or v.slurreplacer: + for s, r in SLURS.items(): body = body.replace(s, r) + + if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com") + + if v and v.nitter: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") + + if v and v.controversial: + for i in re.finditer('(/comments/.*?)"', body): + url = i.group(1) + p = urlparse(url).query + p = parse_qs(p) + + if 'sort' not in p: p['sort'] = ['controversial'] + + url_noquery = url.split('?')[0] + body = body.replace(url, f"{url_noquery}?{urlencode(p, True)}") + + return body + + def plainbody(self, v): + if self.post and self.post.club and not (v and v.paid_dues): return "

COUNTRY CLUB ONLY

" + + body = self.body + + if not body: return "" + + if not v or v.slurreplacer: + for s, r in SLURS.items(): body = body.replace(s, r) + + if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com") + + if v and v.nitter: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") + + if v and v.controversial: + for i in re.finditer('(/comments/.*?)"', body): + url = i.group(1) + p = urlparse(url).query + p = parse_qs(p) + + if 'sort' not in p: p['sort'] = ['controversial'] + + url_noquery = url.split('?')[0] + body = body.replace(url, f"{url_noquery}?{urlencode(p, True)}") + + return body + + @lazy + def collapse_for_user(self, v): + + if self.over_18 and not (v and v.over_18) and not self.post.over_18: + return True + + if not v: + return False + + if any([x in self.body for x in v.filter_words]): + return True + + if self.is_banned or (self.author and self.author.shadowbanned): return True + + return False + + @property + @lazy + def is_op(self): return self.author_id==self.post.author_id + + @property + @lazy + def active_flags(self): return self.flags.count() + + @property + @lazy + def ordered_flags(self): return self.flags.order_by(CommentFlag.id).all() + + + +class Notification(Base): + + __tablename__ = "notifications" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + comment_id = Column(Integer, ForeignKey("comments.id")) + read = Column(Boolean, default=False) + followsender = Column(Integer) + unfollowsender = Column(Integer) + removefollowsender = Column(Integer) + blocksender = Column(Integer) + unblocksender = Column(Integer) + + comment = relationship("Comment", viewonly=True) + user = relationship("User", viewonly=True) + + def __repr__(self): + + return f"" diff --git a/files/classes/domains.py b/files/classes/domains.py old mode 100644 new mode 100755 index 8d3e29224..3755cdaef --- a/files/classes/domains.py +++ b/files/classes/domains.py @@ -1,9 +1,9 @@ -from sqlalchemy import * -from files.__main__ import Base - -class BannedDomain(Base): - - __tablename__ = "banneddomains" - id = Column(Integer, primary_key=True) - domain = Column(String) +from sqlalchemy import * +from files.__main__ import Base + +class BannedDomain(Base): + + __tablename__ = "banneddomains" + id = Column(Integer, primary_key=True) + domain = Column(String) reason = Column(String) \ No newline at end of file diff --git a/files/classes/flags.py b/files/classes/flags.py old mode 100644 new mode 100755 index 57c961778..476ea9760 --- a/files/classes/flags.py +++ b/files/classes/flags.py @@ -1,56 +1,56 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy -import time - -class Flag(Base): - - __tablename__ = "flags" - - id = Column(Integer, primary_key=True) - post_id = Column(Integer, ForeignKey("submissions.id")) - user_id = Column(Integer, ForeignKey("users.id")) - reason = Column(String) - - user = relationship("User", primaryjoin = "Flag.user_id == User.id", uselist = False, viewonly=True) - - def __repr__(self): - - return f"" - - @property - @lazy - def created_date(self): - return time.strftime("%d %B %Y", time.gmtime(self.created_utc)) - - @property - @lazy - def created_datetime(self): - return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) - - -class CommentFlag(Base): - - __tablename__ = "commentflags" - - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.id")) - comment_id = Column(Integer, ForeignKey("comments.id")) - reason = Column(String) - - user = relationship("User", primaryjoin = "CommentFlag.user_id == User.id", uselist = False, viewonly=True) - - def __repr__(self): - - return f"" - - @property - @lazy - def created_date(self): - return time.strftime("%d %B %Y", time.gmtime(self.created_utc)) - - @property - @lazy - def created_datetime(self): +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base +from files.helpers.lazy import lazy +import time + +class Flag(Base): + + __tablename__ = "flags" + + id = Column(Integer, primary_key=True) + post_id = Column(Integer, ForeignKey("submissions.id")) + user_id = Column(Integer, ForeignKey("users.id")) + reason = Column(String) + + user = relationship("User", primaryjoin = "Flag.user_id == User.id", uselist = False, viewonly=True) + + def __repr__(self): + + return f"" + + @property + @lazy + def created_date(self): + return time.strftime("%d %B %Y", time.gmtime(self.created_utc)) + + @property + @lazy + def created_datetime(self): + return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) + + +class CommentFlag(Base): + + __tablename__ = "commentflags" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + comment_id = Column(Integer, ForeignKey("comments.id")) + reason = Column(String) + + user = relationship("User", primaryjoin = "CommentFlag.user_id == User.id", uselist = False, viewonly=True) + + def __repr__(self): + + return f"" + + @property + @lazy + def created_date(self): + return time.strftime("%d %B %Y", time.gmtime(self.created_utc)) + + @property + @lazy + def created_datetime(self): return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) \ No newline at end of file diff --git a/files/classes/mod_logs.py b/files/classes/mod_logs.py old mode 100644 new mode 100755 index e8865ba96..23a9caafd --- a/files/classes/mod_logs.py +++ b/files/classes/mod_logs.py @@ -1,236 +1,236 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -import time -from files.helpers.lazy import lazy - -class ModAction(Base): - __tablename__ = "modactions" - id = Column(BigInteger, primary_key=True) - - user_id = Column(Integer, ForeignKey("users.id")) - kind = Column(String) - target_user_id = Column(Integer, ForeignKey("users.id"), default=0) - target_submission_id = Column(Integer, ForeignKey("submissions.id"), default=0) - target_comment_id = Column(Integer, ForeignKey("comments.id"), default=0) - _note=Column(String) - created_utc = Column(Integer, default=0) - - user = relationship("User", primaryjoin="User.id==ModAction.user_id", viewonly=True) - target_user = relationship("User", primaryjoin="User.id==ModAction.target_user_id", viewonly=True) - target_post = relationship("Submission", viewonly=True) - - def __init__(self, *args, **kwargs): - if "created_utc" not in kwargs: - kwargs["created_utc"] = int(time.time()) - - if "note" in kwargs: - kwargs["_note"]=kwargs["note"] - - super().__init__(*args, **kwargs) - - def __repr__(self): - return f"" - - @property - @lazy - def age_string(self): - - age = int(time.time()) - self.created_utc - - if age < 60: - return "just now" - elif age < 3600: - minutes = int(age / 60) - return f"{minutes}m ago" - elif age < 86400: - hours = int(age / 3600) - return f"{hours}hr ago" - elif age < 2678400: - days = int(age / 86400) - return f"{days}d ago" - - now = time.gmtime() - ctd = time.gmtime(self.created_utc) - - months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) - if now.tm_mday < ctd.tm_mday: - months -= 1 - - if months < 12: - return f"{months}mo ago" - else: - years = int(months / 12) - return f"{years}yr ago" - - - @property - def note(self): - - if self.kind=="ban_user": - if self.target_post: return f'for post' - elif self.target_comment_id: return f'for comment' - else: return self._note - else: - return self._note or "" - - @note.setter - def note(self, x): - self._note=x - - @property - @lazy - def string(self): - - output = ACTIONTYPES[self.kind]["str"].format(self=self) - - if self.note: output += f" ({self.note})" - - return output - - @property - @lazy - def target_link(self): - if self.target_user: return f'{self.target_user.username}' - elif self.target_post: return f'{self.target_post.title.replace("<","").replace(">","")}' - elif self.target_comment_id: return f'comment' - - @property - @lazy - def icon(self): - return ACTIONTYPES[self.kind]['icon'] - - @property - @lazy - def color(self): - return ACTIONTYPES[self.kind]['color'] - - @property - @lazy - def permalink(self): - return f"/log/{self.id}" - - - -ACTIONTYPES={ - "ban_user":{ - "str":'banned user {self.target_link}', - "icon":"fa-user-slash", - "color": "bg-danger", - }, - "unban_user":{ - "str":'unbanned user {self.target_link}', - "icon": "fa-user-slash", - "color": "bg-muted", - }, - "club_allow":{ - "str":'allowed user {self.target_link} into the country club', - "icon":"fa-user-slash", - "color": "bg-danger", - }, - "club_ban":{ - "str":'disallowed user {self.target_link} from the country club', - "icon": "fa-user-slash", - "color": "bg-muted", - }, - "nuke_user":{ - "str":'removed all content of {self.target_link}', - "icon":"fa-user-slash", - "color": "bg-danger", - }, - "unnuke_user":{ - "str":'approved all content of {self.target_link}', - "icon": "fa-user-slash", - "color": "bg-muted", - }, - "shadowban": { - "str": 'shadowbanned {self.target_link}', - "icon": "fa-user-slash", - "color": "bg-danger", - }, - "unshadowban": { - "str": 'unshadowbanned {self.target_link}', - "icon": "fa-user-slash", - "color": "bg-muted", - }, - "agendaposter": { - "str": "set agendaposter theme on {self.target_link}", - "icon": "fa-user-slash", - "color": "bg-muted", - }, - "unagendaposter": { - "str": "removed agendaposter theme from {self.target_link}", - "icon": "fa-user-slash", - "color": "bg-muted", - }, - "set_flair_locked":{ - "str":"set {self.target_link}'s flair (locked)", - "icon": "fa-user-slash", - "color": "bg-muted", - }, - "set_flair_notlocked":{ - "str":"set {self.target_link}'s flair (not locked)", - "icon": "fa-user-slash", - "color": "bg-muted", - }, - "pin_comment":{ - "str":'pinned a {self.target_link}', - "icon":"fa-thumbtack fa-rotate--45", - "color": "bg-info", - }, - "unpin_comment":{ - "str":'un-pinned a {self.target_link}', - "icon":"fa-thumbtack fa-rotate--45", - "color": "bg-muted", - }, - "pin_post":{ - "str":'pinned post {self.target_link}', - "icon":"fa-thumbtack fa-rotate--45", - "color": "bg-success", - }, - "unpin_post":{ - "str":'un-pinned post {self.target_link}', - "icon":"fa-thumbtack fa-rotate--45", - "color": "bg-muted", - }, - "set_nsfw":{ - "str":'set nsfw on post {self.target_link}', - "icon":"fa-eye-evil", - "color": "bg-danger", - }, - "unset_nsfw":{ - "str":'un-set nsfw on post {self.target_link}', - "icon":"fa-eye-evil", - "color": "bg-muted", - }, - "ban_post":{ - "str": 'removed post {self.target_link}', - "icon":"fa-feather-alt", - "color": "bg-danger", - }, - "unban_post":{ - "str": 'reinstated post {self.target_link}', - "icon":"fa-feather-alt", - "color": "bg-muted", - }, - "club":{ - "str": 'marked post {self.target_link} as club-only', - "icon":"fa-eye-slash", - "color": "bg-danger", - }, - "unclub":{ - "str": 'unmarked post {self.target_link} as club-only', - "icon":"fa-eye", - "color": "bg-muted", - }, - "ban_comment":{ - "str": 'removed {self.target_link}', - "icon":"fa-comment", - "color": "bg-danger", - }, - "unban_comment":{ - "str": 'reinstated {self.target_link}', - "icon":"fa-comment", - "color": "bg-muted", - }, -} +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base +import time +from files.helpers.lazy import lazy + +class ModAction(Base): + __tablename__ = "modactions" + id = Column(BigInteger, primary_key=True) + + user_id = Column(Integer, ForeignKey("users.id")) + kind = Column(String) + target_user_id = Column(Integer, ForeignKey("users.id"), default=0) + target_submission_id = Column(Integer, ForeignKey("submissions.id"), default=0) + target_comment_id = Column(Integer, ForeignKey("comments.id"), default=0) + _note=Column(String) + created_utc = Column(Integer, default=0) + + user = relationship("User", primaryjoin="User.id==ModAction.user_id", viewonly=True) + target_user = relationship("User", primaryjoin="User.id==ModAction.target_user_id", viewonly=True) + target_post = relationship("Submission", viewonly=True) + + def __init__(self, *args, **kwargs): + if "created_utc" not in kwargs: + kwargs["created_utc"] = int(time.time()) + + if "note" in kwargs: + kwargs["_note"]=kwargs["note"] + + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"" + + @property + @lazy + def age_string(self): + + age = int(time.time()) - self.created_utc + + if age < 60: + return "just now" + elif age < 3600: + minutes = int(age / 60) + return f"{minutes}m ago" + elif age < 86400: + hours = int(age / 3600) + return f"{hours}hr ago" + elif age < 2678400: + days = int(age / 86400) + return f"{days}d ago" + + now = time.gmtime() + ctd = time.gmtime(self.created_utc) + + months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) + if now.tm_mday < ctd.tm_mday: + months -= 1 + + if months < 12: + return f"{months}mo ago" + else: + years = int(months / 12) + return f"{years}yr ago" + + + @property + def note(self): + + if self.kind=="ban_user": + if self.target_post: return f'for post' + elif self.target_comment_id: return f'for comment' + else: return self._note + else: + return self._note or "" + + @note.setter + def note(self, x): + self._note=x + + @property + @lazy + def string(self): + + output = ACTIONTYPES[self.kind]["str"].format(self=self) + + if self.note: output += f" ({self.note})" + + return output + + @property + @lazy + def target_link(self): + if self.target_user: return f'{self.target_user.username}' + elif self.target_post: return f'{self.target_post.title.replace("<","").replace(">","")}' + elif self.target_comment_id: return f'comment' + + @property + @lazy + def icon(self): + return ACTIONTYPES[self.kind]['icon'] + + @property + @lazy + def color(self): + return ACTIONTYPES[self.kind]['color'] + + @property + @lazy + def permalink(self): + return f"/log/{self.id}" + + + +ACTIONTYPES={ + "ban_user":{ + "str":'banned user {self.target_link}', + "icon":"fa-user-slash", + "color": "bg-danger", + }, + "unban_user":{ + "str":'unbanned user {self.target_link}', + "icon": "fa-user-slash", + "color": "bg-muted", + }, + "club_allow":{ + "str":'allowed user {self.target_link} into the country club', + "icon":"fa-user-slash", + "color": "bg-danger", + }, + "club_ban":{ + "str":'disallowed user {self.target_link} from the country club', + "icon": "fa-user-slash", + "color": "bg-muted", + }, + "nuke_user":{ + "str":'removed all content of {self.target_link}', + "icon":"fa-user-slash", + "color": "bg-danger", + }, + "unnuke_user":{ + "str":'approved all content of {self.target_link}', + "icon": "fa-user-slash", + "color": "bg-muted", + }, + "shadowban": { + "str": 'shadowbanned {self.target_link}', + "icon": "fa-user-slash", + "color": "bg-danger", + }, + "unshadowban": { + "str": 'unshadowbanned {self.target_link}', + "icon": "fa-user-slash", + "color": "bg-muted", + }, + "agendaposter": { + "str": "set agendaposter theme on {self.target_link}", + "icon": "fa-user-slash", + "color": "bg-muted", + }, + "unagendaposter": { + "str": "removed agendaposter theme from {self.target_link}", + "icon": "fa-user-slash", + "color": "bg-muted", + }, + "set_flair_locked":{ + "str":"set {self.target_link}'s flair (locked)", + "icon": "fa-user-slash", + "color": "bg-muted", + }, + "set_flair_notlocked":{ + "str":"set {self.target_link}'s flair (not locked)", + "icon": "fa-user-slash", + "color": "bg-muted", + }, + "pin_comment":{ + "str":'pinned a {self.target_link}', + "icon":"fa-thumbtack fa-rotate--45", + "color": "bg-info", + }, + "unpin_comment":{ + "str":'un-pinned a {self.target_link}', + "icon":"fa-thumbtack fa-rotate--45", + "color": "bg-muted", + }, + "pin_post":{ + "str":'pinned post {self.target_link}', + "icon":"fa-thumbtack fa-rotate--45", + "color": "bg-success", + }, + "unpin_post":{ + "str":'un-pinned post {self.target_link}', + "icon":"fa-thumbtack fa-rotate--45", + "color": "bg-muted", + }, + "set_nsfw":{ + "str":'set nsfw on post {self.target_link}', + "icon":"fa-eye-evil", + "color": "bg-danger", + }, + "unset_nsfw":{ + "str":'un-set nsfw on post {self.target_link}', + "icon":"fa-eye-evil", + "color": "bg-muted", + }, + "ban_post":{ + "str": 'removed post {self.target_link}', + "icon":"fa-feather-alt", + "color": "bg-danger", + }, + "unban_post":{ + "str": 'reinstated post {self.target_link}', + "icon":"fa-feather-alt", + "color": "bg-muted", + }, + "club":{ + "str": 'marked post {self.target_link} as club-only', + "icon":"fa-eye-slash", + "color": "bg-danger", + }, + "unclub":{ + "str": 'unmarked post {self.target_link} as club-only', + "icon":"fa-eye", + "color": "bg-muted", + }, + "ban_comment":{ + "str": 'removed {self.target_link}', + "icon":"fa-comment", + "color": "bg-danger", + }, + "unban_comment":{ + "str": 'reinstated {self.target_link}', + "icon":"fa-comment", + "color": "bg-muted", + }, +} diff --git a/files/classes/submission.py b/files/classes/submission.py old mode 100644 new mode 100755 index a4580e717..8704d471f --- a/files/classes/submission.py +++ b/files/classes/submission.py @@ -1,407 +1,407 @@ -from flask import render_template, g -from sqlalchemy import * -from sqlalchemy.orm import relationship, deferred -import re, random -from urllib.parse import urlparse -from files.helpers.lazy import lazy -from files.helpers.const import SLURS, AUTOPOLLER_ACCOUNT -from files.__main__ import Base -from .flags import Flag -from os import environ -import time - -site = environ.get("DOMAIN").strip() -site_name = environ.get("SITE_NAME").strip() - - -class Submission(Base): - - __tablename__ = "submissions" - - id = Column(BigInteger, primary_key=True) - author_id = Column(BigInteger, ForeignKey("users.id")) - edited_utc = Column(BigInteger, default=0) - created_utc = Column(BigInteger, default=0) - thumburl = Column(String) - is_banned = Column(Boolean, default=False) - removed_by = Column(Integer) - bannedfor = Column(Boolean) - views = Column(Integer, default=0) - deleted_utc = Column(Integer, default=0) - distinguish_level = Column(Integer, default=0) - created_str = Column(String) - stickied = Column(String) - is_pinned = Column(Boolean, default=False) - private = Column(Boolean, default=False) - club = Column(Boolean, default=False) - comment_count = Column(Integer, default=0) - is_approved = Column(Integer, ForeignKey("users.id"), default=0) - over_18 = Column(Boolean, default=False) - is_bot = Column(Boolean, default=False) - upvotes = Column(Integer, default=1) - downvotes = Column(Integer, default=0) - app_id=Column(Integer, ForeignKey("oauth_apps.id")) - title = Column(String) - title_html = Column(String) - url = Column(String) - body = deferred(Column(String)) - body_html = deferred(Column(String)) - ban_reason = Column(String) - embed_url = Column(String) - - comments = relationship("Comment", lazy="dynamic", primaryjoin="Comment.parent_submission==Submission.id", viewonly=True) - flags = relationship("Flag", lazy="dynamic", viewonly=True) - author = relationship("User", primaryjoin="Submission.author_id==User.id") - oauth_app = relationship("OauthApp", viewonly=True) - approved_by = relationship("User", uselist=False, primaryjoin="Submission.is_approved==User.id", viewonly=True) - awards = relationship("AwardRelationship", viewonly=True) - - def __init__(self, *args, **kwargs): - - if "created_utc" not in kwargs: - kwargs["created_utc"] = int(time.time()) - kwargs["created_str"] = time.strftime( - "%I:%M %p on %d %b %Y", time.gmtime( - kwargs["created_utc"])) - - - super().__init__(*args, **kwargs) - - def __repr__(self): - return f"" - - - @property - @lazy - def options(self): - return self.comments.filter_by(author_id = AUTOPOLLER_ACCOUNT, level=1) - - @property - @lazy - def created_datetime(self): - return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) - - @property - @lazy - def created_datetime(self): - return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) - - @property - @lazy - def age_string(self): - - age = int(time.time()) - self.created_utc - - if age < 60: - return "just now" - elif age < 3600: - minutes = int(age / 60) - return f"{minutes}m ago" - elif age < 86400: - hours = int(age / 3600) - return f"{hours}hr ago" - elif age < 2678400: - days = int(age / 86400) - return f"{days}d ago" - - now = time.gmtime() - ctd = time.gmtime(self.created_utc) - - months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) - if now.tm_mday < ctd.tm_mday: - months -= 1 - - if months < 12: - return f"{months}mo ago" - else: - years = int(months / 12) - return f"{years}yr ago" - - @property - @lazy - def edited_string(self): - - if not self.edited_utc: return "never" - - age = int(time.time()) - self.edited_utc - - if age < 60: - return "just now" - elif age < 3600: - minutes = int(age / 60) - return f"{minutes}m ago" - elif age < 86400: - hours = int(age / 3600) - return f"{hours}hr ago" - elif age < 2678400: - days = int(age / 86400) - return f"{days}d ago" - - now = time.gmtime() - ctd = time.gmtime(self.edited_utc) - months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) - - if months < 12: - return f"{months}mo ago" - else: - years = now.tm_year - ctd.tm_year - return f"{years}yr ago" - - - @property - @lazy - def edited_datetime(self): - return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.edited_utc))) - - - @property - @lazy - def score(self): - return self.upvotes - self.downvotes - - @property - @lazy - def fullname(self): - return f"t2_{self.id}" - - @property - @lazy - def shortlink(self): - return f"https://{site}/post/{self.id}" - - @property - @lazy - def permalink(self): - if self.club: return f"/post/{self.id}" - - output = self.title.lower() - - output = re.sub('&\w{2,3};', '', output) - - output = [re.sub('\W', '', word) for word in output.split()] - output = [x for x in output if x][:6] - - output = '-'.join(output) - - if not output: output = '-' - - return f"/post/{self.id}/{output}" - - @lazy - def rendered_page(self, sort=None, comment=None, comment_info=None, v=None): - - if self.is_banned and not (v and (v.admin_level >= 3 or self.author_id == v.id)): template = "submission_banned.html" - else: template = "submission.html" - - comments = self.__dict__.get('preloaded_comments', []) - if comments: - pinned_comment = [] - index = {} - for c in comments: - if c.is_pinned and c.parent_fullname==self.fullname: - pinned_comment += [c] - continue - if c.parent_fullname in index: index[c.parent_fullname].append(c) - else: index[c.parent_fullname] = [c] - - for c in comments: c.__dict__["replies"] = index.get(c.fullname, []) - if comment: self.__dict__["replies"] = [comment] - else: self.__dict__["replies"] = pinned_comment + index.get(self.fullname, []) - - return render_template(template, - v=v, - p=self, - sort=sort, - linked_comment=comment, - comment_info=comment_info, - render_replies=True - ) - - @property - @lazy - def domain(self): - - if not self.url: return "text post" - domain = urlparse(self.url).netloc - if domain.startswith("www."): domain = domain.split("www.")[1] - return domain.replace("old.reddit.com", "reddit.com") - - - @property - @lazy - def thumb_url(self): - if self.over_18: return f"https://{site}/assets/images/nsfw.webp" - elif not self.url: return f"https://{site}/assets/images/{site_name}/default_thumb_text.webp" - elif self.thumburl: return self.thumburl - elif "youtu.be" in self.domain or "youtube.com" in self.domain: return f"https://{site}/assets/images/default_thumb_yt.webp" - else: return f"https://{site}/assets/images/default_thumb_link.webp" - - @property - @lazy - def json_raw(self): - flags = {} - for f in self.flags: flags[f.user.username] = f.reason - - data = {'author_name': self.author.username, - 'permalink': self.permalink, - 'is_banned': bool(self.is_banned), - 'deleted_utc': self.deleted_utc, - 'created_utc': self.created_utc, - 'id': self.id, - 'title': self.title, - 'is_nsfw': self.over_18, - 'is_bot': self.is_bot, - 'thumb_url': self.thumb_url, - 'domain': self.domain, - 'url': self.url, - 'body': self.body, - 'body_html': self.body_html, - 'created_utc': self.created_utc, - 'edited_utc': self.edited_utc or 0, - 'comment_count': self.comment_count, - 'score': self.score, - 'upvotes': self.upvotes, - 'downvotes': self.downvotes, - 'stickied': self.stickied, - 'private' : self.private, - 'distinguish_level': self.distinguish_level, - 'voted': self.voted if hasattr(self, 'voted') else 0, - 'flags': flags, - } - - if self.ban_reason: - data["ban_reason"]=self.ban_reason - - return data - - @property - @lazy - def json_core(self): - - if self.is_banned: - return {'is_banned': True, - 'deleted_utc': self.deleted_utc, - 'ban_reason': self.ban_reason, - 'id': self.id, - 'title': self.title, - 'permalink': self.permalink, - } - elif self.deleted_utc: - return {'is_banned': bool(self.is_banned), - 'deleted_utc': True, - 'id': self.id, - 'title': self.title, - 'permalink': self.permalink, - } - - return self.json_raw - - @property - @lazy - def json(self): - - data=self.json_core - - if self.deleted_utc > 0 or self.is_banned: - return data - - data["author"]=self.author.json_core - data["comment_count"]=self.comment_count - - - if "replies" in self.__dict__: - data["replies"]=[x.json_core for x in self.replies] - - if "voted" in self.__dict__: - data["voted"] = self.voted - - return data - - def award_count(self, kind) -> int: - return len([x for x in self.awards if x.kind == kind]) - - @lazy - def realurl(self, v): - if v and v.agendaposter and random.randint(1, 10) < 4: - return 'https://secure.actblue.com/donate/ms_blm_homepage_2019' - elif v and self.url and self.url.startswith("https://old.reddit.com/"): - url = self.url - if not v.oldreddit: url = self.url.replace("old.reddit.com", "reddit.com") - if v.controversial and '/comments/' in url and "sort=" not in url: - if "?" in url: url += "&sort=controversial" - else: url += "?sort=controversial" - return url - elif self.url: - if v and v.nitter: return self.url.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") - return self.url - else: return "" - - def realbody(self, v): - if self.club and not (v and v.paid_dues): return "COUNTRY CLUB ONLY" - body = self.body_html - - if not v or v.slurreplacer: - for s,r in SLURS.items(): - body = body.replace(s, r) - - if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com") - if v and v.nitter: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") - return body - - def plainbody(self, v): - if self.club and not (v and v.paid_dues): return "COUNTRY CLUB ONLY" - body = self.body - - if not v or v.slurreplacer: - for s,r in SLURS.items(): - body = body.replace(s, r) - - if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com") - if v and v.nitter: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") - return body - - @lazy - def realtitle(self, v): - if self.club and not (v and v.paid_dues) and not (v and v.admin_level == 6): return 'COUNTRY CLUB MEMBERS ONLY' - elif self.title_html: title = self.title_html - else: title = self.title - - if not v or v.slurreplacer: - for s,r in SLURS.items(): title = title.replace(s, r) - - return title - - @lazy - def plaintitle(self, v): - if self.club and not (v and v.paid_dues) and not (v and v.admin_level == 6): return 'COUNTRY CLUB MEMBERS ONLY' - else: title = self.title - - if not v or v.slurreplacer: - for s,r in SLURS.items(): title = title.replace(s, r) - - return title - - @property - @lazy - def is_image(self): - if self.url: return self.url.lower().endswith('.webp') or self.url.lower().endswith('.jpg') or self.url.lower().endswith('.png') or self.url.lower().endswith('.gif') or self.url.lower().endswith('.jpeg') or self.url.lower().endswith('?maxwidth=9999') - else: return False - - @property - @lazy - def active_flags(self): return self.flags.count() - - @property - @lazy - def ordered_flags(self): return self.flags.order_by(Flag.id).all() - - -class SaveRelationship(Base): - - __tablename__="save_relationship" - - id=Column(Integer, primary_key=True) - user_id=Column(Integer) - submission_id=Column(Integer) - comment_id=Column(Integer) +from flask import render_template, g +from sqlalchemy import * +from sqlalchemy.orm import relationship, deferred +import re, random +from urllib.parse import urlparse +from files.helpers.lazy import lazy +from files.helpers.const import SLURS, AUTOPOLLER_ACCOUNT +from files.__main__ import Base +from .flags import Flag +from os import environ +import time + +site = environ.get("DOMAIN").strip() +site_name = environ.get("SITE_NAME").strip() + + +class Submission(Base): + + __tablename__ = "submissions" + + id = Column(BigInteger, primary_key=True) + author_id = Column(BigInteger, ForeignKey("users.id")) + edited_utc = Column(BigInteger, default=0) + created_utc = Column(BigInteger, default=0) + thumburl = Column(String) + is_banned = Column(Boolean, default=False) + removed_by = Column(Integer) + bannedfor = Column(Boolean) + views = Column(Integer, default=0) + deleted_utc = Column(Integer, default=0) + distinguish_level = Column(Integer, default=0) + created_str = Column(String) + stickied = Column(String) + is_pinned = Column(Boolean, default=False) + private = Column(Boolean, default=False) + club = Column(Boolean, default=False) + comment_count = Column(Integer, default=0) + is_approved = Column(Integer, ForeignKey("users.id"), default=0) + over_18 = Column(Boolean, default=False) + is_bot = Column(Boolean, default=False) + upvotes = Column(Integer, default=1) + downvotes = Column(Integer, default=0) + app_id=Column(Integer, ForeignKey("oauth_apps.id")) + title = Column(String) + title_html = Column(String) + url = Column(String) + body = deferred(Column(String)) + body_html = deferred(Column(String)) + ban_reason = Column(String) + embed_url = Column(String) + + comments = relationship("Comment", lazy="dynamic", primaryjoin="Comment.parent_submission==Submission.id", viewonly=True) + flags = relationship("Flag", lazy="dynamic", viewonly=True) + author = relationship("User", primaryjoin="Submission.author_id==User.id") + oauth_app = relationship("OauthApp", viewonly=True) + approved_by = relationship("User", uselist=False, primaryjoin="Submission.is_approved==User.id", viewonly=True) + awards = relationship("AwardRelationship", viewonly=True) + + def __init__(self, *args, **kwargs): + + if "created_utc" not in kwargs: + kwargs["created_utc"] = int(time.time()) + kwargs["created_str"] = time.strftime( + "%I:%M %p on %d %b %Y", time.gmtime( + kwargs["created_utc"])) + + + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"" + + + @property + @lazy + def options(self): + return self.comments.filter_by(author_id = AUTOPOLLER_ACCOUNT, level=1) + + @property + @lazy + def created_datetime(self): + return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) + + @property + @lazy + def created_datetime(self): + return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc))) + + @property + @lazy + def age_string(self): + + age = int(time.time()) - self.created_utc + + if age < 60: + return "just now" + elif age < 3600: + minutes = int(age / 60) + return f"{minutes}m ago" + elif age < 86400: + hours = int(age / 3600) + return f"{hours}hr ago" + elif age < 2678400: + days = int(age / 86400) + return f"{days}d ago" + + now = time.gmtime() + ctd = time.gmtime(self.created_utc) + + months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) + if now.tm_mday < ctd.tm_mday: + months -= 1 + + if months < 12: + return f"{months}mo ago" + else: + years = int(months / 12) + return f"{years}yr ago" + + @property + @lazy + def edited_string(self): + + if not self.edited_utc: return "never" + + age = int(time.time()) - self.edited_utc + + if age < 60: + return "just now" + elif age < 3600: + minutes = int(age / 60) + return f"{minutes}m ago" + elif age < 86400: + hours = int(age / 3600) + return f"{hours}hr ago" + elif age < 2678400: + days = int(age / 86400) + return f"{days}d ago" + + now = time.gmtime() + ctd = time.gmtime(self.edited_utc) + months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) + + if months < 12: + return f"{months}mo ago" + else: + years = now.tm_year - ctd.tm_year + return f"{years}yr ago" + + + @property + @lazy + def edited_datetime(self): + return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.edited_utc))) + + + @property + @lazy + def score(self): + return self.upvotes - self.downvotes + + @property + @lazy + def fullname(self): + return f"t2_{self.id}" + + @property + @lazy + def shortlink(self): + return f"https://{site}/post/{self.id}" + + @property + @lazy + def permalink(self): + if self.club: return f"/post/{self.id}" + + output = self.title.lower() + + output = re.sub('&\w{2,3};', '', output) + + output = [re.sub('\W', '', word) for word in output.split()] + output = [x for x in output if x][:6] + + output = '-'.join(output) + + if not output: output = '-' + + return f"/post/{self.id}/{output}" + + @lazy + def rendered_page(self, sort=None, comment=None, comment_info=None, v=None): + + if self.is_banned and not (v and (v.admin_level >= 3 or self.author_id == v.id)): template = "submission_banned.html" + else: template = "submission.html" + + comments = self.__dict__.get('preloaded_comments', []) + if comments: + pinned_comment = [] + index = {} + for c in comments: + if c.is_pinned and c.parent_fullname==self.fullname: + pinned_comment += [c] + continue + if c.parent_fullname in index: index[c.parent_fullname].append(c) + else: index[c.parent_fullname] = [c] + + for c in comments: c.__dict__["replies"] = index.get(c.fullname, []) + if comment: self.__dict__["replies"] = [comment] + else: self.__dict__["replies"] = pinned_comment + index.get(self.fullname, []) + + return render_template(template, + v=v, + p=self, + sort=sort, + linked_comment=comment, + comment_info=comment_info, + render_replies=True + ) + + @property + @lazy + def domain(self): + + if not self.url: return "text post" + domain = urlparse(self.url).netloc + if domain.startswith("www."): domain = domain.split("www.")[1] + return domain.replace("old.reddit.com", "reddit.com") + + + @property + @lazy + def thumb_url(self): + if self.over_18: return f"https://{site}/assets/images/nsfw.webp" + elif not self.url: return f"https://{site}/assets/images/{site_name}/default_thumb_text.webp" + elif self.thumburl: return self.thumburl + elif "youtu.be" in self.domain or "youtube.com" in self.domain: return f"https://{site}/assets/images/default_thumb_yt.webp" + else: return f"https://{site}/assets/images/default_thumb_link.webp" + + @property + @lazy + def json_raw(self): + flags = {} + for f in self.flags: flags[f.user.username] = f.reason + + data = {'author_name': self.author.username, + 'permalink': self.permalink, + 'is_banned': bool(self.is_banned), + 'deleted_utc': self.deleted_utc, + 'created_utc': self.created_utc, + 'id': self.id, + 'title': self.title, + 'is_nsfw': self.over_18, + 'is_bot': self.is_bot, + 'thumb_url': self.thumb_url, + 'domain': self.domain, + 'url': self.url, + 'body': self.body, + 'body_html': self.body_html, + 'created_utc': self.created_utc, + 'edited_utc': self.edited_utc or 0, + 'comment_count': self.comment_count, + 'score': self.score, + 'upvotes': self.upvotes, + 'downvotes': self.downvotes, + 'stickied': self.stickied, + 'private' : self.private, + 'distinguish_level': self.distinguish_level, + 'voted': self.voted if hasattr(self, 'voted') else 0, + 'flags': flags, + } + + if self.ban_reason: + data["ban_reason"]=self.ban_reason + + return data + + @property + @lazy + def json_core(self): + + if self.is_banned: + return {'is_banned': True, + 'deleted_utc': self.deleted_utc, + 'ban_reason': self.ban_reason, + 'id': self.id, + 'title': self.title, + 'permalink': self.permalink, + } + elif self.deleted_utc: + return {'is_banned': bool(self.is_banned), + 'deleted_utc': True, + 'id': self.id, + 'title': self.title, + 'permalink': self.permalink, + } + + return self.json_raw + + @property + @lazy + def json(self): + + data=self.json_core + + if self.deleted_utc > 0 or self.is_banned: + return data + + data["author"]=self.author.json_core + data["comment_count"]=self.comment_count + + + if "replies" in self.__dict__: + data["replies"]=[x.json_core for x in self.replies] + + if "voted" in self.__dict__: + data["voted"] = self.voted + + return data + + def award_count(self, kind) -> int: + return len([x for x in self.awards if x.kind == kind]) + + @lazy + def realurl(self, v): + if v and v.agendaposter and random.randint(1, 10) < 4: + return 'https://secure.actblue.com/donate/ms_blm_homepage_2019' + elif v and self.url and self.url.startswith("https://old.reddit.com/"): + url = self.url + if not v.oldreddit: url = self.url.replace("old.reddit.com", "reddit.com") + if v.controversial and '/comments/' in url and "sort=" not in url: + if "?" in url: url += "&sort=controversial" + else: url += "?sort=controversial" + return url + elif self.url: + if v and v.nitter: return self.url.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") + return self.url + else: return "" + + def realbody(self, v): + if self.club and not (v and v.paid_dues): return "COUNTRY CLUB ONLY" + body = self.body_html + + if not v or v.slurreplacer: + for s,r in SLURS.items(): + body = body.replace(s, r) + + if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com") + if v and v.nitter: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") + return body + + def plainbody(self, v): + if self.club and not (v and v.paid_dues): return "COUNTRY CLUB ONLY" + body = self.body + + if not v or v.slurreplacer: + for s,r in SLURS.items(): + body = body.replace(s, r) + + if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com") + if v and v.nitter: body = body.replace("www.twitter.com", "nitter.net").replace("twitter.com", "nitter.net") + return body + + @lazy + def realtitle(self, v): + if self.club and not (v and v.paid_dues) and not (v and v.admin_level == 6): return 'COUNTRY CLUB MEMBERS ONLY' + elif self.title_html: title = self.title_html + else: title = self.title + + if not v or v.slurreplacer: + for s,r in SLURS.items(): title = title.replace(s, r) + + return title + + @lazy + def plaintitle(self, v): + if self.club and not (v and v.paid_dues) and not (v and v.admin_level == 6): return 'COUNTRY CLUB MEMBERS ONLY' + else: title = self.title + + if not v or v.slurreplacer: + for s,r in SLURS.items(): title = title.replace(s, r) + + return title + + @property + @lazy + def is_image(self): + if self.url: return self.url.lower().endswith('.webp') or self.url.lower().endswith('.jpg') or self.url.lower().endswith('.png') or self.url.lower().endswith('.gif') or self.url.lower().endswith('.jpeg') or self.url.lower().endswith('?maxwidth=9999') + else: return False + + @property + @lazy + def active_flags(self): return self.flags.count() + + @property + @lazy + def ordered_flags(self): return self.flags.order_by(Flag.id).all() + + +class SaveRelationship(Base): + + __tablename__="save_relationship" + + id=Column(Integer, primary_key=True) + user_id=Column(Integer) + submission_id=Column(Integer) + comment_id=Column(Integer) type=Column(Integer) \ No newline at end of file diff --git a/files/classes/subscriptions.py b/files/classes/subscriptions.py old mode 100644 new mode 100755 index cdd4c9bb7..66c7d9864 --- a/files/classes/subscriptions.py +++ b/files/classes/subscriptions.py @@ -1,34 +1,34 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base - - -class Subscription(Base): - __tablename__ = "subscriptions" - id = Column(BigInteger, primary_key=True) - user_id = Column(BigInteger, ForeignKey("users.id")) - submission_id = Column(BigInteger, default=0) - - user = relationship("User", uselist=False, viewonly=True) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def __repr__(self): - return f"" - - -class Follow(Base): - __tablename__ = "follows" - id = Column(BigInteger, primary_key=True) - user_id = Column(BigInteger, ForeignKey("users.id")) - target_id = Column(BigInteger, ForeignKey("users.id")) - - user = relationship("User", uselist=False, primaryjoin="User.id==Follow.user_id", viewonly=True) - target = relationship("User", primaryjoin="User.id==Follow.target_id", viewonly=True) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def __repr__(self): +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base + + +class Subscription(Base): + __tablename__ = "subscriptions" + id = Column(BigInteger, primary_key=True) + user_id = Column(BigInteger, ForeignKey("users.id")) + submission_id = Column(BigInteger, default=0) + + user = relationship("User", uselist=False, viewonly=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"" + + +class Follow(Base): + __tablename__ = "follows" + id = Column(BigInteger, primary_key=True) + user_id = Column(BigInteger, ForeignKey("users.id")) + target_id = Column(BigInteger, ForeignKey("users.id")) + + user = relationship("User", uselist=False, primaryjoin="User.id==Follow.user_id", viewonly=True) + target = relationship("User", primaryjoin="User.id==Follow.target_id", viewonly=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __repr__(self): return f"" \ No newline at end of file diff --git a/files/classes/user.py b/files/classes/user.py old mode 100644 new mode 100755 index 6bfb949bb..6dbc0e11b --- a/files/classes/user.py +++ b/files/classes/user.py @@ -1,621 +1,621 @@ -from sqlalchemy.orm import deferred, aliased -from secrets import token_hex -import pyotp -from files.helpers.discord import remove_user -from files.helpers.images import * -from files.helpers.const import * -from .alts import Alt -from .submission import SaveRelationship -from .comment import Notification -from .award import AwardRelationship -from .subscriptions import * -from .userblock import * -from .badges import * -from .clients import * -from files.__main__ import Base, cache -from files.helpers.security import * -import random - -site = environ.get("DOMAIN").strip() -site_name = environ.get("SITE_NAME").strip() -defaulttheme = environ.get("DEFAULT_THEME", "midnight").strip() -defaultcolor = environ.get("DEFAULT_COLOR", "fff").strip() -defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip() -cardview = bool(int(environ.get("CARD_VIEW", 1))) - -if site_name == "Drama": - AWARDS = { - "ban": { - "kind": "ban", - "title": "One-Day Ban", - "description": "Bans the author for a day.", - "icon": "fas fa-gavel", - "color": "text-danger", - "price": 5000 - }, - "shit": { - "kind": "shit", - "title": "Shit", - "description": "Makes flies swarm a post.", - "icon": "fas fa-poop", - "color": "text-black-50", - "price": 1000 - }, - "fireflies": { - "kind": "fireflies", - "title": "Fireflies", - "description": "Puts stars on the post.", - "icon": "fas fa-sparkles", - "color": "text-warning", - "price": 1000 - } - } -else: - AWARDS = { - "shit": { - "kind": "shit", - "title": "Shit", - "description": "Makes flies swarm a post.", - "icon": "fas fa-poop", - "color": "text-black-50", - "price": 1000 - }, - "fireflies": { - "kind": "fireflies", - "title": "Fireflies", - "description": "Puts stars on the post.", - "icon": "fas fa-sparkles", - "color": "text-warning", - "price": 1000 - } - } - -class User(Base): - __tablename__ = "users" - - if "pcmemes.net" in site: - quadrant = Column(String) - basedcount = Column(Integer, default=0) - pills = deferred(Column(String, default="")) - - id = Column(Integer, primary_key=True) - username = Column(String) - namecolor = Column(String, default=defaultcolor) - background = Column(String) - customtitle = Column(String) - customtitleplain = Column(String) - titlecolor = Column(String, default=defaultcolor) - theme = Column(String, default=defaulttheme) - themecolor = Column(String, default=defaultcolor) - cardview = Column(Boolean, default=cardview) - song = Column(String) - highres = Column(String) - profileurl = Column(String) - bannerurl = Column(String) - patron = Column(Integer, default=0) - verified = Column(String) - email = Column(String) - css = deferred(Column(String)) - profilecss = deferred(Column(String)) - passhash = deferred(Column(String)) - post_count = Column(Integer, default=0) - comment_count = Column(Integer, default=0) - received_award_count = Column(Integer, default=0) - created_utc = Column(Integer, default=0) - suicide_utc = Column(Integer, default=0) - rent_utc = Column(Integer, default=0) - steal_utc = Column(Integer, default=0) - fail_utc = Column(Integer, default=0) - fail2_utc = Column(Integer, default=0) - admin_level = Column(Integer, default=0) - agendaposter = Column(Boolean, default=False) - agendaposter_expires_utc = Column(Integer, default=0) - changelogsub = Column(Boolean, default=False) - is_activated = Column(Boolean, default=False) - shadowbanned = Column(String) - over_18 = Column(Boolean, default=False) - hidevotedon = Column(Boolean, default=False) - highlightcomments = Column(Boolean, default=True) - slurreplacer = Column(Boolean, default=True) - flairchanged = Column(Boolean, default=False) - newtab = Column(Boolean, default=False) - newtabexternal = Column(Boolean, default=True) - oldreddit = Column(Boolean, default=True) - nitter = Column(Boolean) - frontsize = Column(Integer, default=25) - controversial = Column(Boolean, default=False) - bio = Column(String) - bio_html = Column(String) - is_banned = Column(Integer, default=0) - unban_utc = Column(Integer, default=0) - ban_reason = Column(String) - club_banned = Column(Boolean, default=False) - club_allowed = Column(Boolean, default=False) - login_nonce = Column(Integer, default=0) - reserved = Column(String) - coins = Column(Integer, default=0) - truecoins = Column(Integer, default=0) - mfa_secret = deferred(Column(String)) - is_private = Column(Boolean, default=False) - stored_subscriber_count = Column(Integer, default=0) - defaultsortingcomments = Column(String, default="top") - defaultsorting = Column(String, default="hot") - defaulttime = Column(String, default=defaulttimefilter) - is_nofollow = Column(Boolean, default=False) - custom_filter_list = Column(String) - discord_id = Column(String) - ban_evade = Column(Integer, default=0) - original_username = deferred(Column(String)) - referred_by = Column(Integer, ForeignKey("users.id")) - - submissions = relationship("Submission", lazy="dynamic", primaryjoin="Submission.author_id==User.id", viewonly=True) - badges = relationship("Badge", lazy="dynamic", viewonly=True) - notifications = relationship("Notification", lazy="dynamic", viewonly=True) - subscriptions = relationship("Subscription", viewonly=True) - following = relationship("Follow", primaryjoin="Follow.user_id==User.id", viewonly=True) - followers = relationship("Follow", primaryjoin="Follow.target_id==User.id", viewonly=True) - viewers = relationship("ViewerRelationship", primaryjoin="User.id == ViewerRelationship.user_id", viewonly=True) - blocking = relationship("UserBlock", lazy="dynamic", primaryjoin="User.id==UserBlock.user_id", viewonly=True) - blocked = relationship("UserBlock", lazy="dynamic", primaryjoin="User.id==UserBlock.target_id", viewonly=True) - apps = relationship("OauthApp", lazy="dynamic", viewonly=True) - authorizations = relationship("ClientAuth", lazy="dynamic", viewonly=True) - awards = relationship("AwardRelationship", lazy="dynamic", primaryjoin="User.id==AwardRelationship.user_id", viewonly=True) - referrals = relationship("User", viewonly=True) - - def __init__(self, **kwargs): - - if "password" in kwargs: - kwargs["passhash"] = self.hash_password(kwargs["password"]) - kwargs.pop("password") - - kwargs["created_utc"] = int(time.time()) - - super().__init__(**kwargs) - - - @property - @lazy - def created_date(self): - - return time.strftime("%d %b %Y", time.gmtime(self.created_utc)) - - @property - @lazy - def user_awards(v): - - return_value = list(AWARDS.values()) - - user_awards = v.awards - - for val in return_value: val['owned'] = user_awards.filter_by(kind=val['kind'], submission_id=None, comment_id=None).count() - - return return_value - - @property - @lazy - def referral_count(self): - return len(self.referrals) - - def has_block(self, target): - - return g.db.query(UserBlock).options(lazyload('*')).filter_by( - user_id=self.id, target_id=target.id).first() - - @property - @lazy - def paid_dues(self): - return self.admin_level == 6 or self.club_allowed or (self.truecoins > int(environ.get("DUES").strip()) and not self.club_banned) - - def any_block_exists(self, other): - - return g.db.query(UserBlock).options(lazyload('*')).filter( - or_(and_(UserBlock.user_id == self.id, UserBlock.target_id == other.id), and_( - UserBlock.user_id == other.id, UserBlock.target_id == self.id))).first() - - def validate_2fa(self, token): - - x = pyotp.TOTP(self.mfa_secret) - return x.verify(token, valid_window=1) - - @property - @lazy - def age(self): - return int(time.time()) - self.created_utc - - @property - @lazy - def alts_unique(self): - alts = [] - for u in self.alts: - if u not in alts: alts.append(u) - return alts - - @property - @lazy - def strid(self): - return str(self.id) - - @cache.memoize(timeout=86400) - def userpagelisting(self, v=None, page=1, sort="new", t="all"): - - if self.shadowbanned and not (v and (v.admin_level >= 3 or v.id == self.id)): - return [] - - posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(author_id=self.id, is_pinned=False) - - if not (v and (v.admin_level >= 3 or v.id == self.id)): - posts = posts.filter_by(deleted_utc=0, is_banned=False, private=False) - - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - posts = posts.filter(Submission.created_utc >= cutoff) - - if sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc.asc()) - elif sort == "controversial": - posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc()) - - posts = posts.offset(25 * (page - 1)).limit(26).all() - - return [x[0] for x in posts] - - @property - @lazy - def fullname(self): - return f"t1_{self.id}" - - @property - @lazy - def banned_by(self): - if not self.is_suspended: return None - return g.db.query(User).filter_by(id=self.is_banned).first() - - def has_badge(self, badgedef_id): - return self.badges.filter_by(badge_id=badgedef_id).first() - - def hash_password(self, password): - return generate_password_hash( - password, method='pbkdf2:sha512', salt_length=8) - - def verifyPass(self, password): - return check_password_hash(self.passhash, password) - - @property - @lazy - def formkey(self): - - if "session_id" not in session: - session["session_id"] = token_hex(16) - - msg = f"{session['session_id']}+{self.id}+{self.login_nonce}" - - return generate_hash(msg) - - def validate_formkey(self, formkey): - - return validate_hash(f"{session['session_id']}+{self.id}+{self.login_nonce}", formkey) - - @property - @lazy - def url(self): - return f"/@{self.username}" - - def __repr__(self): - return f"" - - @property - @lazy - def unban_string(self): - if self.unban_utc == 0: - return "permanently banned" - - wait = self.unban_utc - int(time.time()) - - if wait < 60: - text = f"{wait}s" - else: - days = wait//(24*60*60) - wait -= days*24*60*60 - - hours = wait//(60*60) - wait -= hours*60*60 - - mins = wait//60 - - text = f"{days}d {hours:02d}h {mins:02d}m" - - return f"Unban in {text}" - - - @property - @lazy - def received_awards(self): - - awards = {} - - posts_idlist = [x[0] for x in g.db.query(Submission.id).options(lazyload('*')).filter_by(author_id=self.id).all()] - comments_idlist = [x[0] for x in g.db.query(Comment.id).options(lazyload('*')).filter_by(author_id=self.id).all()] - - post_awards = g.db.query(AwardRelationship).options(lazyload('*')).filter(AwardRelationship.submission_id.in_(posts_idlist)).all() - comment_awards = g.db.query(AwardRelationship).options(lazyload('*')).filter(AwardRelationship.comment_id.in_(comments_idlist)).all() - - total_awards = post_awards + comment_awards - - for a in total_awards: - if a.kind in awards: - awards[a.kind]['count'] += 1 - else: - awards[a.kind] = a.type - awards[a.kind]['count'] = 1 - - return sorted(list(awards.values()), key=lambda x: x['kind'], reverse=True) - - @property - @lazy - def post_notifications_count(self): - return self.notifications.filter(Notification.read == False).join(Notification.comment).filter(Comment.author_id == AUTOJANNY_ACCOUNT).count() - - @property - @lazy - def notifications_count(self): - - return self.notifications.join(Notification.comment).filter(Notification.read == False, - Comment.is_banned == False, - Comment.deleted_utc == 0).count() - - @property - @lazy - def alts(self): - - subq = g.db.query(Alt).options(lazyload('*')).filter( - or_( - Alt.user1 == self.id, - Alt.user2 == self.id - ) - ).subquery() - - data = g.db.query( - User, - aliased(Alt, alias=subq) - ).join( - subq, - or_( - subq.c.user1 == User.id, - subq.c.user2 == User.id - ) - ).filter( - User.id != self.id - ).order_by(User.username.asc()).all() - - data = [x for x in data] - output = [] - for x in data: - user = x[0] - user._is_manual = x[1].is_manual - output.append(user) - - return output - - def has_follower(self, user): - - return g.db.query(Follow).options(lazyload('*')).filter_by(target_id=self.id, user_id=user.id).first() - - @property - @lazy - def banner_url(self): - if self.bannerurl: return self.bannerurl - else: return f"https://{site}/assets/images/{site_name}/preview.webp" - - @property - @lazy - def profile_url(self): - if self.profileurl: return self.profileurl - elif "rama" in site: return f"https://{site}/assets/images/defaultpictures/{random.randint(1, 150)}.webp" - else: return f"https://{site}/assets/images/default-profile-pic.webp" - - @property - @lazy - def json_raw(self): - data = {'username': self.username, - 'url': self.url, - 'is_banned': bool(self.is_banned), - 'created_utc': self.created_utc, - 'id': self.id, - 'is_private': self.is_private, - 'profile_url': self.profile_url, - 'bannerurl': self.bannerurl, - 'bio': self.bio, - 'bio_html': self.bio_html, - 'flair': self.customtitle - } - - return data - - @property - @lazy - def json_core(self): - - now = int(time.time()) - if self.is_banned and (not self.unban_utc or now < self.unban_utc): - return {'username': self.username, - 'url': self.url, - 'is_banned': True, - 'is_permanent_ban': not bool(self.unban_utc), - 'ban_reason': self.ban_reason, - 'id': self.id - } - return self.json_raw - - @property - @lazy - def json(self): - data = self.json_core - - data["badges"] = [x.json_core for x in self.badges] - data['coins'] = int(self.coins) - data['post_count'] = self.post_count - data['comment_count'] = self.comment_count - - return data - - def ban(self, admin=None, reason=None, days=0): - - if days > 0: - ban_time = int(time.time()) + (days * 86400) - self.unban_utc = ban_time - else: - self.bannerurl = None - self.profileurl = None - if self.discord_id: remove_user(self) - - self.is_banned = admin.id if admin else AUTOJANNY_ACCOUNT - if reason: self.ban_reason = reason - - g.db.add(self) - - - - @property - @lazy - def is_suspended(self): - return (self.is_banned and (not self.unban_utc or self.unban_utc > time.time())) - - - @property - @lazy - def applications(self): - return [x for x in self.apps.order_by(OauthApp.id.asc()).all()] - - @lazy - def subscribed_idlist(self, page=1): - posts = g.db.query(Subscription.submission_id).options(lazyload('*')).filter_by(user_id=self.id).all() - return [x[0] for x in posts] - - @lazy - def saved_idlist(self, page=1): - - posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False, deleted_utc=0) - - saved = [x[0] for x in g.db.query(SaveRelationship.submission_id).options(lazyload('*')).filter(SaveRelationship.user_id == self.id).all()] - posts = posts.filter(Submission.id.in_(saved)) - - if self.admin_level == 0: - blocking = [x[0] for x in g.db.query( - UserBlock.target_id).filter_by( - user_id=self.id).all()] - blocked = [x[0] for x in g.db.query( - UserBlock.user_id).filter_by( - target_id=self.id).all()] - - posts = posts.filter( - Submission.author_id.notin_(blocking), - Submission.author_id.notin_(blocked) - ) - - posts = posts.order_by(Submission.created_utc.desc()) - - return [x[0] for x in posts.offset(25 * (page - 1)).limit(26).all()] - - @lazy - def saved_comment_idlist(self): - - try: saved = [x[0] for x in g.db.query(SaveRelationship.comment_id).options(lazyload('*')).filter(SaveRelationship.user_id == self.id).all()] - except: return [] - comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.id.in_(saved)) - - if self.admin_level == 0: - blocking = [x[0] for x in g.db.query( - UserBlock.target_id).filter_by( - user_id=self.id).all()] - blocked = [x[0] for x in g.db.query( - UserBlock.user_id).filter_by( - target_id=self.id).all()] - - comments = comments.filter( - Comment.author_id.notin_(blocking), - Comment.author_id.notin_(blocked) - ) - - return [x[0] for x in comments.order_by(Comment.created_utc.desc()).all()] - - @property - @lazy - def filter_words(self): - l = [i.strip() for i in self.custom_filter_list.split('\n')] if self.custom_filter_list else [] - l = [i for i in l if i] - return l - - -class ViewerRelationship(Base): - - __tablename__ = "viewers" - - id = Column(Integer, Sequence('viewers_id_seq'), primary_key=True) - user_id = Column(Integer, ForeignKey('users.id')) - viewer_id = Column(Integer, ForeignKey('users.id')) - last_view_utc = Column(Integer) - - viewer = relationship("User", primaryjoin="ViewerRelationship.viewer_id == User.id", viewonly=True) - - def __init__(self, **kwargs): - - if 'last_view_utc' not in kwargs: - kwargs['last_view_utc'] = int(time.time()) - - super().__init__(**kwargs) - - @property - @lazy - def last_view_since(self): - - return int(time.time()) - self.last_view_utc - - @property - @lazy - def last_view_string(self): - - age = self.last_view_since - - if age < 60: - return "just now" - elif age < 3600: - minutes = int(age / 60) - return f"{minutes}m ago" - elif age < 86400: - hours = int(age / 3600) - return f"{hours}hr ago" - elif age < 2678400: - days = int(age / 86400) - return f"{days}d ago" - - now = time.gmtime() - ctd = time.gmtime(self.created_utc) - - months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) - if now.tm_mday < ctd.tm_mday: - months -= 1 - - if months < 12: - return f"{months}mo ago" - else: - years = int(months / 12) +from sqlalchemy.orm import deferred, aliased +from secrets import token_hex +import pyotp +from files.helpers.discord import remove_user +from files.helpers.images import * +from files.helpers.const import * +from .alts import Alt +from .submission import SaveRelationship +from .comment import Notification +from .award import AwardRelationship +from .subscriptions import * +from .userblock import * +from .badges import * +from .clients import * +from files.__main__ import Base, cache +from files.helpers.security import * +import random + +site = environ.get("DOMAIN").strip() +site_name = environ.get("SITE_NAME").strip() +defaulttheme = environ.get("DEFAULT_THEME", "midnight").strip() +defaultcolor = environ.get("DEFAULT_COLOR", "fff").strip() +defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip() +cardview = bool(int(environ.get("CARD_VIEW", 1))) + +if site_name == "Drama": + AWARDS = { + "ban": { + "kind": "ban", + "title": "One-Day Ban", + "description": "Bans the author for a day.", + "icon": "fas fa-gavel", + "color": "text-danger", + "price": 1500 + }, + "shit": { + "kind": "shit", + "title": "Shit", + "description": "Makes flies swarm a post.", + "icon": "fas fa-poop", + "color": "text-black-50", + "price": 500 + }, + "fireflies": { + "kind": "fireflies", + "title": "Fireflies", + "description": "Puts stars on the post.", + "icon": "fas fa-sparkles", + "color": "text-warning", + "price": 500 + } + } +else: + AWARDS = { + "shit": { + "kind": "shit", + "title": "Shit", + "description": "Makes flies swarm a post.", + "icon": "fas fa-poop", + "color": "text-black-50", + "price": 500 + }, + "fireflies": { + "kind": "fireflies", + "title": "Fireflies", + "description": "Puts stars on the post.", + "icon": "fas fa-sparkles", + "color": "text-warning", + "price": 500 + } + } + +class User(Base): + __tablename__ = "users" + + if "pcmemes.net" in site: + quadrant = Column(String) + basedcount = Column(Integer, default=0) + pills = deferred(Column(String, default="")) + + id = Column(Integer, primary_key=True) + username = Column(String) + namecolor = Column(String, default=defaultcolor) + background = Column(String) + customtitle = Column(String) + customtitleplain = Column(String) + titlecolor = Column(String, default=defaultcolor) + theme = Column(String, default=defaulttheme) + themecolor = Column(String, default=defaultcolor) + cardview = Column(Boolean, default=cardview) + song = Column(String) + highres = Column(String) + profileurl = Column(String) + bannerurl = Column(String) + patron = Column(Integer, default=0) + verified = Column(String) + email = Column(String) + css = deferred(Column(String)) + profilecss = deferred(Column(String)) + passhash = deferred(Column(String)) + post_count = Column(Integer, default=0) + comment_count = Column(Integer, default=0) + received_award_count = Column(Integer, default=0) + created_utc = Column(Integer, default=0) + suicide_utc = Column(Integer, default=0) + rent_utc = Column(Integer, default=0) + steal_utc = Column(Integer, default=0) + fail_utc = Column(Integer, default=0) + fail2_utc = Column(Integer, default=0) + admin_level = Column(Integer, default=0) + agendaposter = Column(Boolean, default=False) + agendaposter_expires_utc = Column(Integer, default=0) + changelogsub = Column(Boolean, default=False) + is_activated = Column(Boolean, default=False) + shadowbanned = Column(String) + over_18 = Column(Boolean, default=False) + hidevotedon = Column(Boolean, default=False) + highlightcomments = Column(Boolean, default=True) + slurreplacer = Column(Boolean, default=True) + flairchanged = Column(Boolean, default=False) + newtab = Column(Boolean, default=False) + newtabexternal = Column(Boolean, default=True) + oldreddit = Column(Boolean, default=True) + nitter = Column(Boolean) + frontsize = Column(Integer, default=25) + controversial = Column(Boolean, default=False) + bio = Column(String) + bio_html = Column(String) + is_banned = Column(Integer, default=0) + unban_utc = Column(Integer, default=0) + ban_reason = Column(String) + club_banned = Column(Boolean, default=False) + club_allowed = Column(Boolean, default=False) + login_nonce = Column(Integer, default=0) + reserved = Column(String) + coins = Column(Integer, default=0) + truecoins = Column(Integer, default=0) + mfa_secret = deferred(Column(String)) + is_private = Column(Boolean, default=False) + stored_subscriber_count = Column(Integer, default=0) + defaultsortingcomments = Column(String, default="top") + defaultsorting = Column(String, default="hot") + defaulttime = Column(String, default=defaulttimefilter) + is_nofollow = Column(Boolean, default=False) + custom_filter_list = Column(String) + discord_id = Column(String) + ban_evade = Column(Integer, default=0) + original_username = deferred(Column(String)) + referred_by = Column(Integer, ForeignKey("users.id")) + + submissions = relationship("Submission", lazy="dynamic", primaryjoin="Submission.author_id==User.id", viewonly=True) + badges = relationship("Badge", lazy="dynamic", viewonly=True) + notifications = relationship("Notification", lazy="dynamic", viewonly=True) + subscriptions = relationship("Subscription", viewonly=True) + following = relationship("Follow", primaryjoin="Follow.user_id==User.id", viewonly=True) + followers = relationship("Follow", primaryjoin="Follow.target_id==User.id", viewonly=True) + viewers = relationship("ViewerRelationship", primaryjoin="User.id == ViewerRelationship.user_id", viewonly=True) + blocking = relationship("UserBlock", lazy="dynamic", primaryjoin="User.id==UserBlock.user_id", viewonly=True) + blocked = relationship("UserBlock", lazy="dynamic", primaryjoin="User.id==UserBlock.target_id", viewonly=True) + apps = relationship("OauthApp", lazy="dynamic", viewonly=True) + authorizations = relationship("ClientAuth", lazy="dynamic", viewonly=True) + awards = relationship("AwardRelationship", lazy="dynamic", primaryjoin="User.id==AwardRelationship.user_id", viewonly=True) + referrals = relationship("User", viewonly=True) + + def __init__(self, **kwargs): + + if "password" in kwargs: + kwargs["passhash"] = self.hash_password(kwargs["password"]) + kwargs.pop("password") + + kwargs["created_utc"] = int(time.time()) + + super().__init__(**kwargs) + + + @property + @lazy + def created_date(self): + + return time.strftime("%d %b %Y", time.gmtime(self.created_utc)) + + @property + @lazy + def user_awards(v): + + return_value = list(AWARDS.values()) + + user_awards = v.awards + + for val in return_value: val['owned'] = user_awards.filter_by(kind=val['kind'], submission_id=None, comment_id=None).count() + + return return_value + + @property + @lazy + def referral_count(self): + return len(self.referrals) + + def has_block(self, target): + + return g.db.query(UserBlock).options(lazyload('*')).filter_by( + user_id=self.id, target_id=target.id).first() + + @property + @lazy + def paid_dues(self): + return self.admin_level == 6 or self.club_allowed or (self.truecoins > int(environ.get("DUES").strip()) and not self.club_banned) + + def any_block_exists(self, other): + + return g.db.query(UserBlock).options(lazyload('*')).filter( + or_(and_(UserBlock.user_id == self.id, UserBlock.target_id == other.id), and_( + UserBlock.user_id == other.id, UserBlock.target_id == self.id))).first() + + def validate_2fa(self, token): + + x = pyotp.TOTP(self.mfa_secret) + return x.verify(token, valid_window=1) + + @property + @lazy + def age(self): + return int(time.time()) - self.created_utc + + @property + @lazy + def alts_unique(self): + alts = [] + for u in self.alts: + if u not in alts: alts.append(u) + return alts + + @property + @lazy + def strid(self): + return str(self.id) + + @cache.memoize(timeout=86400) + def userpagelisting(self, v=None, page=1, sort="new", t="all"): + + if self.shadowbanned and not (v and (v.admin_level >= 3 or v.id == self.id)): + return [] + + posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(author_id=self.id, is_pinned=False) + + if not (v and (v.admin_level >= 3 or v.id == self.id)): + posts = posts.filter_by(deleted_utc=0, is_banned=False, private=False) + + now = int(time.time()) + if t == 'hour': + cutoff = now - 3600 + elif t == 'day': + cutoff = now - 86400 + elif t == 'week': + cutoff = now - 604800 + elif t == 'month': + cutoff = now - 2592000 + elif t == 'year': + cutoff = now - 31536000 + else: + cutoff = 0 + posts = posts.filter(Submission.created_utc >= cutoff) + + if sort == "new": + posts = posts.order_by(Submission.created_utc.desc()) + elif sort == "old": + posts = posts.order_by(Submission.created_utc.asc()) + elif sort == "controversial": + posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes) + elif sort == "top": + posts = posts.order_by(Submission.downvotes - Submission.upvotes) + elif sort == "bottom": + posts = posts.order_by(Submission.upvotes - Submission.downvotes) + elif sort == "comments": + posts = posts.order_by(Submission.comment_count.desc()) + + posts = posts.offset(25 * (page - 1)).limit(26).all() + + return [x[0] for x in posts] + + @property + @lazy + def fullname(self): + return f"t1_{self.id}" + + @property + @lazy + def banned_by(self): + if not self.is_suspended: return None + return g.db.query(User).filter_by(id=self.is_banned).first() + + def has_badge(self, badgedef_id): + return self.badges.filter_by(badge_id=badgedef_id).first() + + def hash_password(self, password): + return generate_password_hash( + password, method='pbkdf2:sha512', salt_length=8) + + def verifyPass(self, password): + return check_password_hash(self.passhash, password) + + @property + @lazy + def formkey(self): + + if "session_id" not in session: + session["session_id"] = token_hex(16) + + msg = f"{session['session_id']}+{self.id}+{self.login_nonce}" + + return generate_hash(msg) + + def validate_formkey(self, formkey): + + return validate_hash(f"{session['session_id']}+{self.id}+{self.login_nonce}", formkey) + + @property + @lazy + def url(self): + return f"/@{self.username}" + + def __repr__(self): + return f"" + + @property + @lazy + def unban_string(self): + if self.unban_utc == 0: + return "permanently banned" + + wait = self.unban_utc - int(time.time()) + + if wait < 60: + text = f"{wait}s" + else: + days = wait//(24*60*60) + wait -= days*24*60*60 + + hours = wait//(60*60) + wait -= hours*60*60 + + mins = wait//60 + + text = f"{days}d {hours:02d}h {mins:02d}m" + + return f"Unban in {text}" + + + @property + @lazy + def received_awards(self): + + awards = {} + + posts_idlist = [x[0] for x in g.db.query(Submission.id).options(lazyload('*')).filter_by(author_id=self.id).all()] + comments_idlist = [x[0] for x in g.db.query(Comment.id).options(lazyload('*')).filter_by(author_id=self.id).all()] + + post_awards = g.db.query(AwardRelationship).options(lazyload('*')).filter(AwardRelationship.submission_id.in_(posts_idlist)).all() + comment_awards = g.db.query(AwardRelationship).options(lazyload('*')).filter(AwardRelationship.comment_id.in_(comments_idlist)).all() + + total_awards = post_awards + comment_awards + + for a in total_awards: + if a.kind in awards: + awards[a.kind]['count'] += 1 + else: + awards[a.kind] = a.type + awards[a.kind]['count'] = 1 + + return sorted(list(awards.values()), key=lambda x: x['kind'], reverse=True) + + @property + @lazy + def post_notifications_count(self): + return self.notifications.filter(Notification.read == False).join(Notification.comment).filter(Comment.author_id == AUTOJANNY_ACCOUNT).count() + + @property + @lazy + def notifications_count(self): + + return self.notifications.join(Notification.comment).filter(Notification.read == False, + Comment.is_banned == False, + Comment.deleted_utc == 0).count() + + @property + @lazy + def alts(self): + + subq = g.db.query(Alt).options(lazyload('*')).filter( + or_( + Alt.user1 == self.id, + Alt.user2 == self.id + ) + ).subquery() + + data = g.db.query( + User, + aliased(Alt, alias=subq) + ).join( + subq, + or_( + subq.c.user1 == User.id, + subq.c.user2 == User.id + ) + ).filter( + User.id != self.id + ).order_by(User.username.asc()).all() + + data = [x for x in data] + output = [] + for x in data: + user = x[0] + user._is_manual = x[1].is_manual + output.append(user) + + return output + + def has_follower(self, user): + + return g.db.query(Follow).options(lazyload('*')).filter_by(target_id=self.id, user_id=user.id).first() + + @property + @lazy + def banner_url(self): + if self.bannerurl: return self.bannerurl + else: return f"https://{site}/assets/images/{site_name}/preview.webp" + + @property + @lazy + def profile_url(self): + if self.profileurl: return self.profileurl + elif "rama" in site: return f"https://{site}/assets/images/defaultpictures/{random.randint(1, 150)}.webp" + else: return f"https://{site}/assets/images/default-profile-pic.webp" + + @property + @lazy + def json_raw(self): + data = {'username': self.username, + 'url': self.url, + 'is_banned': bool(self.is_banned), + 'created_utc': self.created_utc, + 'id': self.id, + 'is_private': self.is_private, + 'profile_url': self.profile_url, + 'bannerurl': self.bannerurl, + 'bio': self.bio, + 'bio_html': self.bio_html, + 'flair': self.customtitle + } + + return data + + @property + @lazy + def json_core(self): + + now = int(time.time()) + if self.is_banned and (not self.unban_utc or now < self.unban_utc): + return {'username': self.username, + 'url': self.url, + 'is_banned': True, + 'is_permanent_ban': not bool(self.unban_utc), + 'ban_reason': self.ban_reason, + 'id': self.id + } + return self.json_raw + + @property + @lazy + def json(self): + data = self.json_core + + data["badges"] = [x.json_core for x in self.badges] + data['coins'] = int(self.coins) + data['post_count'] = self.post_count + data['comment_count'] = self.comment_count + + return data + + def ban(self, admin=None, reason=None, days=0): + + if days > 0: + ban_time = int(time.time()) + (days * 86400) + self.unban_utc = ban_time + else: + self.bannerurl = None + self.profileurl = None + if self.discord_id: remove_user(self) + + self.is_banned = admin.id if admin else AUTOJANNY_ACCOUNT + if reason: self.ban_reason = reason + + g.db.add(self) + + + + @property + @lazy + def is_suspended(self): + return (self.is_banned and (not self.unban_utc or self.unban_utc > time.time())) + + + @property + @lazy + def applications(self): + return [x for x in self.apps.order_by(OauthApp.id.asc()).all()] + + @lazy + def subscribed_idlist(self, page=1): + posts = g.db.query(Subscription.submission_id).options(lazyload('*')).filter_by(user_id=self.id).all() + return [x[0] for x in posts] + + @lazy + def saved_idlist(self, page=1): + + posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False, deleted_utc=0) + + saved = [x[0] for x in g.db.query(SaveRelationship.submission_id).options(lazyload('*')).filter(SaveRelationship.user_id == self.id).all()] + posts = posts.filter(Submission.id.in_(saved)) + + if self.admin_level == 0: + blocking = [x[0] for x in g.db.query( + UserBlock.target_id).filter_by( + user_id=self.id).all()] + blocked = [x[0] for x in g.db.query( + UserBlock.user_id).filter_by( + target_id=self.id).all()] + + posts = posts.filter( + Submission.author_id.notin_(blocking), + Submission.author_id.notin_(blocked) + ) + + posts = posts.order_by(Submission.created_utc.desc()) + + return [x[0] for x in posts.offset(25 * (page - 1)).limit(26).all()] + + @lazy + def saved_comment_idlist(self): + + try: saved = [x[0] for x in g.db.query(SaveRelationship.comment_id).options(lazyload('*')).filter(SaveRelationship.user_id == self.id).all()] + except: return [] + comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.id.in_(saved)) + + if self.admin_level == 0: + blocking = [x[0] for x in g.db.query( + UserBlock.target_id).filter_by( + user_id=self.id).all()] + blocked = [x[0] for x in g.db.query( + UserBlock.user_id).filter_by( + target_id=self.id).all()] + + comments = comments.filter( + Comment.author_id.notin_(blocking), + Comment.author_id.notin_(blocked) + ) + + return [x[0] for x in comments.order_by(Comment.created_utc.desc()).all()] + + @property + @lazy + def filter_words(self): + l = [i.strip() for i in self.custom_filter_list.split('\n')] if self.custom_filter_list else [] + l = [i for i in l if i] + return l + + +class ViewerRelationship(Base): + + __tablename__ = "viewers" + + id = Column(Integer, Sequence('viewers_id_seq'), primary_key=True) + user_id = Column(Integer, ForeignKey('users.id')) + viewer_id = Column(Integer, ForeignKey('users.id')) + last_view_utc = Column(Integer) + + viewer = relationship("User", primaryjoin="ViewerRelationship.viewer_id == User.id", viewonly=True) + + def __init__(self, **kwargs): + + if 'last_view_utc' not in kwargs: + kwargs['last_view_utc'] = int(time.time()) + + super().__init__(**kwargs) + + @property + @lazy + def last_view_since(self): + + return int(time.time()) - self.last_view_utc + + @property + @lazy + def last_view_string(self): + + age = self.last_view_since + + if age < 60: + return "just now" + elif age < 3600: + minutes = int(age / 60) + return f"{minutes}m ago" + elif age < 86400: + hours = int(age / 3600) + return f"{hours}hr ago" + elif age < 2678400: + days = int(age / 86400) + return f"{days}d ago" + + now = time.gmtime() + ctd = time.gmtime(self.created_utc) + + months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year) + if now.tm_mday < ctd.tm_mday: + months -= 1 + + if months < 12: + return f"{months}mo ago" + else: + years = int(months / 12) return f"{years}yr ago" \ No newline at end of file diff --git a/files/classes/userblock.py b/files/classes/userblock.py old mode 100644 new mode 100755 index 9a3ab1d70..cffba1226 --- a/files/classes/userblock.py +++ b/files/classes/userblock.py @@ -1,16 +1,16 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base - -class UserBlock(Base): - - __tablename__ = "userblocks" - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.id")) - target_id = Column(Integer, ForeignKey("users.id")) - - user = relationship("User", primaryjoin="User.id==UserBlock.user_id", viewonly=True) - target = relationship("User", primaryjoin="User.id==UserBlock.target_id", viewonly=True) - - def __repr__(self): +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base + +class UserBlock(Base): + + __tablename__ = "userblocks" + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + target_id = Column(Integer, ForeignKey("users.id")) + + user = relationship("User", primaryjoin="User.id==UserBlock.user_id", viewonly=True) + target = relationship("User", primaryjoin="User.id==UserBlock.target_id", viewonly=True) + + def __repr__(self): return f"" \ No newline at end of file diff --git a/files/classes/votes.py b/files/classes/votes.py old mode 100644 new mode 100755 index 58e4bdeb2..5f53ab735 --- a/files/classes/votes.py +++ b/files/classes/votes.py @@ -1,84 +1,84 @@ -from flask import * -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy - -class Vote(Base): - - __tablename__ = "votes" - - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.id")) - vote_type = Column(Integer) - submission_id = Column(Integer, ForeignKey("submissions.id")) - app_id = Column(Integer, ForeignKey("oauth_apps.id")) - - user = relationship("User", lazy="subquery", viewonly=True) - post = relationship("Submission", lazy="subquery", viewonly=True) - - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - def __repr__(self): - return f"" - - @property - @lazy - def json_core(self): - data={ - "user_id": self.user_id, - "submission_id":self.submission_id, - "vote_type":self.vote_type - } - return data - - @property - @lazy - def json(self): - data=self.json_core - data["user"]=self.user.json_core - data["post"]=self.post.json_core - - return data - - -class CommentVote(Base): - - __tablename__ = "commentvotes" - - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.id")) - vote_type = Column(Integer) - comment_id = Column(Integer, ForeignKey("comments.id")) - app_id = Column(Integer, ForeignKey("oauth_apps.id")) - - user = relationship("User", lazy="subquery", viewonly=True) - comment = relationship("Comment", lazy="subquery", viewonly=True) - - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - def __repr__(self): - return f"" - - @property - @lazy - def json_core(self): - data={ - "user_id": self.user_id, - "comment_id":self.comment_id, - "vote_type":self.vote_type - } - return data - - @property - @lazy - def json(self): - data=self.json_core - data["user"]=self.user.json_core - data["comment"]=self.comment.json_core - +from flask import * +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base +from files.helpers.lazy import lazy + +class Vote(Base): + + __tablename__ = "votes" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + vote_type = Column(Integer) + submission_id = Column(Integer, ForeignKey("submissions.id")) + app_id = Column(Integer, ForeignKey("oauth_apps.id")) + + user = relationship("User", lazy="subquery", viewonly=True) + post = relationship("Submission", lazy="subquery", viewonly=True) + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"" + + @property + @lazy + def json_core(self): + data={ + "user_id": self.user_id, + "submission_id":self.submission_id, + "vote_type":self.vote_type + } + return data + + @property + @lazy + def json(self): + data=self.json_core + data["user"]=self.user.json_core + data["post"]=self.post.json_core + + return data + + +class CommentVote(Base): + + __tablename__ = "commentvotes" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + vote_type = Column(Integer) + comment_id = Column(Integer, ForeignKey("comments.id")) + app_id = Column(Integer, ForeignKey("oauth_apps.id")) + + user = relationship("User", lazy="subquery", viewonly=True) + comment = relationship("Comment", lazy="subquery", viewonly=True) + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"" + + @property + @lazy + def json_core(self): + data={ + "user_id": self.user_id, + "comment_id":self.comment_id, + "vote_type":self.vote_type + } + return data + + @property + @lazy + def json(self): + data=self.json_core + data["user"]=self.user.json_core + data["comment"]=self.comment.json_core + return data \ No newline at end of file diff --git a/files/helpers/alerts.py b/files/helpers/alerts.py old mode 100644 new mode 100755 index 5020ff15a..3a27780a6 --- a/files/helpers/alerts.py +++ b/files/helpers/alerts.py @@ -1,137 +1,137 @@ -import mistletoe - -from files.classes import * -from flask import g -from .markdown import * -from .sanitize import * -from .const import * - - -def send_notification(vid, user, text): - - if isinstance(user, int): - uid = user - else: - uid = user.id - - text = text.replace('r/', 'r\/').replace('u/', 'u\/') - text_html = CustomRenderer().render(mistletoe.Document(text)) - - text_html = sanitize(text_html) - - new_comment = Comment(author_id=vid, - parent_submission=None, - distinguish_level=6, - body=text, - body_html=text_html, - notifiedto=uid - ) - g.db.add(new_comment) - - g.db.flush() - - notif = Notification(comment_id=new_comment.id, - user_id=uid) - g.db.add(notif) - - -def send_follow_notif(vid, user, text): - - text_html = CustomRenderer().render(mistletoe.Document(text)) - text_html = sanitize(text_html) - - new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT, - parent_submission=None, - distinguish_level=6, - body=text, - body_html=text_html, - ) - g.db.add(new_comment) - g.db.flush() - - notif = Notification(comment_id=new_comment.id, - user_id=user, - followsender=vid) - g.db.add(notif) - -def send_unfollow_notif(vid, user, text): - - text_html = CustomRenderer().render(mistletoe.Document(text)) - text_html = sanitize(text_html) - - new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT, - parent_submission=None, - distinguish_level=6, - body=text, - body_html=text_html, - ) - g.db.add(new_comment) - g.db.flush() - - notif = Notification(comment_id=new_comment.id, - user_id=user, - unfollowsender=vid) - g.db.add(notif) - -def send_block_notif(vid, user, text): - - text_html = CustomRenderer().render(mistletoe.Document(text)) - text_html = sanitize(text_html) - - new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT, - parent_submission=None, - distinguish_level=6, - body=text, - body_html=text_html, - ) - g.db.add(new_comment) - g.db.flush() - - notif = Notification(comment_id=new_comment.id, - user_id=user, - blocksender=vid) - g.db.add(notif) - -def send_unblock_notif(vid, user, text): - - text_html = CustomRenderer().render(mistletoe.Document(text)) - text_html = sanitize(text_html) - - new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT, - parent_submission=None, - distinguish_level=6, - body=text, - body_html=text_html, - ) - g.db.add(new_comment) - g.db.flush() - - notif = Notification(comment_id=new_comment.id, - user_id=user, - unblocksender=vid) - g.db.add(notif) - - - -def send_admin(vid, text): - - text = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', text) - - text_html = Renderer().render(mistletoe.Document(text)) - - text_html = sanitize(text_html, True) - - new_comment = Comment(author_id=vid, - parent_submission=None, - level=1, - sentto=0, - body=text, - body_html=text_html, - ) - g.db.add(new_comment) - g.db.flush() - - admins = g.db.query(User).options(lazyload('*')).filter(User.admin_level > 0).all() - for admin in admins: - notif = Notification(comment_id=new_comment.id, user_id=admin.id) - g.db.add(notif) +import mistletoe + +from files.classes import * +from flask import g +from .markdown import * +from .sanitize import * +from .const import * + + +def send_notification(vid, user, text): + + if isinstance(user, int): + uid = user + else: + uid = user.id + + text = text.replace('r/', 'r\/').replace('u/', 'u\/') + text_html = CustomRenderer().render(mistletoe.Document(text)) + + text_html = sanitize(text_html) + + new_comment = Comment(author_id=vid, + parent_submission=None, + distinguish_level=6, + body=text, + body_html=text_html, + notifiedto=uid + ) + g.db.add(new_comment) + + g.db.flush() + + notif = Notification(comment_id=new_comment.id, + user_id=uid) + g.db.add(notif) + + +def send_follow_notif(vid, user, text): + + text_html = CustomRenderer().render(mistletoe.Document(text)) + text_html = sanitize(text_html) + + new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT, + parent_submission=None, + distinguish_level=6, + body=text, + body_html=text_html, + ) + g.db.add(new_comment) + g.db.flush() + + notif = Notification(comment_id=new_comment.id, + user_id=user, + followsender=vid) + g.db.add(notif) + +def send_unfollow_notif(vid, user, text): + + text_html = CustomRenderer().render(mistletoe.Document(text)) + text_html = sanitize(text_html) + + new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT, + parent_submission=None, + distinguish_level=6, + body=text, + body_html=text_html, + ) + g.db.add(new_comment) + g.db.flush() + + notif = Notification(comment_id=new_comment.id, + user_id=user, + unfollowsender=vid) + g.db.add(notif) + +def send_block_notif(vid, user, text): + + text_html = CustomRenderer().render(mistletoe.Document(text)) + text_html = sanitize(text_html) + + new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT, + parent_submission=None, + distinguish_level=6, + body=text, + body_html=text_html, + ) + g.db.add(new_comment) + g.db.flush() + + notif = Notification(comment_id=new_comment.id, + user_id=user, + blocksender=vid) + g.db.add(notif) + +def send_unblock_notif(vid, user, text): + + text_html = CustomRenderer().render(mistletoe.Document(text)) + text_html = sanitize(text_html) + + new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT, + parent_submission=None, + distinguish_level=6, + body=text, + body_html=text_html, + ) + g.db.add(new_comment) + g.db.flush() + + notif = Notification(comment_id=new_comment.id, + user_id=user, + unblocksender=vid) + g.db.add(notif) + + + +def send_admin(vid, text): + + text = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', text) + + text_html = Renderer().render(mistletoe.Document(text)) + + text_html = sanitize(text_html, True) + + new_comment = Comment(author_id=vid, + parent_submission=None, + level=1, + sentto=0, + body=text, + body_html=text_html, + ) + g.db.add(new_comment) + g.db.flush() + + admins = g.db.query(User).options(lazyload('*')).filter(User.admin_level > 0).all() + for admin in admins: + notif = Notification(comment_id=new_comment.id, user_id=admin.id) + g.db.add(notif) diff --git a/files/helpers/const.py b/files/helpers/const.py old mode 100644 new mode 100755 index 09c65041d..12c2c955e --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -1,290 +1,171 @@ -from os import environ - -site = environ.get("DOMAIN").strip() - -SLURS = { - " faggot":" cute twink", - " Faggot":" Cute twink", - " FAGGOT":" CUTE TWINK", - " fag":" cute twink", - " Fag":" Cute twink", - " FAG":" CUTE TWINK", - " pedophile":" libertarian", - " Pedophile":" Libertarian", - " PEDOPHILE":" LIBERTARIAN", - " pedo":" libertarian", - " Pedo":" Libertarian", - " PEDO":" LIBERTARIAN", - " kill yourself":" keep yourself safe", - " KILL YOURSELF":" KEEP YOURSELF SAFE", - " nigger":" 🏀", - " Nigger":" 🏀", - " NIGGER":" 🏀", - " rapist":" male feminist", - " Rapist":" Male feminist", - " RAPIST":" MALE FEMINIST", - " steve akins":" penny verity oaken", - " Steve Akins":" Penny Verity Oaken", - " STEVE AKINS":" PENNY VERITY OAKEN", - " trannie":" 🚂🚃🚃", - " Trannie":" 🚂🚃🚃", - " TRANNIE":" 🚂🚃🚃", - " tranny":" 🚂🚃🚃", - " Tranny":" 🚂🚃🚃", - " TRANNY":" 🚂🚃🚃", - " troon":" 🚂🚃🚃", - " Troon":" 🚂🚃🚃", - " TROON":" 🚂🚃🚃", - " NoNewNormal": " HorseDewormerAddicts", - " nonewnormal": " horsedewormeraddicts", - " Kike": " https://sciencedirect.com/science/article/abs/pii/S016028960600033X", - " kike": " https://sciencedirect.com/science/article/abs/pii/S016028960600033X", - " retard":" r-slur", - " Retard":" R-slur", - " RETARD":" R-SLUR", - " janny":" j-slur", - " Janny":" J-slur", - " JANNY":" J-SLUR", - " jannie":" j-slur", - " Jannie":" J-slur", - " JANNIE":" J-SLUR", - " janny":" j-slur", - " Janny":" J-slur", - " JANNY":" J-SLUR", - " jannie":" j-slur", - " Jannie":" J-slur", - " JANNIE":" J-SLUR", - " latinos":" latinx", - " latino":" latinx", - " latinas":" latinx", - " latina":" latinx", - " hispanics":" latinx", - " hispanic":" latinx", - " Latinos":" Latinx", - " Latino":" Latinx", - " Latinas":" Latinx", - " Latina":" Latinx", - " Hispanics":" Latinx", - " Hispanic":" Latinx", - " LATINOS":" LATINX", - " LATINO":" LATINX", - " LATINAS":" LATINX", - " LATINA":" LATINX", - " HISPANICS":" LATINX", - " HISPANIC":" LATINX", - - "faggot ":"cute twink ", - "Faggot ":"Cute twink ", - "FAGGOT ":"CUTE TWINK ", - "fag ":"cute twink ", - "Fag ":"Cute twink ", - "FAG ":"CUTE TWINK ", - "pedophile ":"libertarian ", - "Pedophile ":"Libertarian ", - "PEDOPHILE ":"LIBERTARIAN ", - "kill yourself ":"keep yourself safe ", - "KILL YOURSELF ":"KEEP YOURSELF SAFE ", - "nigger ":"🏀 ", - "Nigger ":"🏀 ", - "NIGGER ":"🏀 ", - "steve akins ":"penny verity oaken ", - "Steve Akins ":"Penny Verity Oaken ", - "STEVE AKINS ":"PENNY VERITY OAKEN ", - "trannie ":"🚂🚃🚃 ", - "Trannie ":"🚂🚃🚃 ", - "TRANNIE ":"🚂🚃🚃 ", - "tranny ":"🚂🚃🚃 ", - "Tranny ":"🚂🚃🚃 ", - "TRANNY ":"🚂🚃🚃 ", - "troon ":"🚂🚃🚃 ", - "Troon ":"🚂🚃🚃 ", - "TROON ":"🚂🚃🚃 ", - "NoNewNormal ": "HorseDewormerAddicts ", - "nonewnormal ": "horsedewormeraddicts ", - "Kike ": "https://sciencedirect.com/science/article/abs/pii/S016028960600033X ", - "kike ": "https://sciencedirect.com/science/article/abs/pii/S016028960600033X ", - "retard ":"r-slur ", - "Retard ":"R-slur ", - "RETARD ":"R-SLUR ", - "janny ":"j-slur ", - "Janny ":"J-slur ", - "JANNY ":"J-SLUR ", - "jannie ":"j-slur ", - "Jannie ":"J-slur ", - "JANNIE ":"J-SLUR ", - "latinos ":"latinx ", - "latino ":"latinx ", - "latinas ":"latinx ", - "latina ":"latinx ", - "hispanics ":"latinx ", - "hispanic ":"latinx ", - "Latinos ":"Latinx ", - "Latino ":"Latinx ", - "Latinas ":"Latinx ", - "Latina ":"Latinx ", - "Hispanics ":"Latinx ", - "Hispanic ":"Latinx ", - "LATINOS ":"LATINX ", - "LATINO ":"LATINX ", - "LATINAS ":"LATINX ", - "LATINA ":"LATINX ", - "HISPANICS ":"LATINX ", - "HISPANIC ":"LATINX ", - "uss liberty incident":"tragic accident aboard the USS Liberty", - "USS Liberty Incident":"tragic accident aboard the USS Liberty", - "USS Liberty incident":"tragic accident aboard the USS Liberty", - "USS Liberty Incident":"tragic accident aboard the USS Liberty", - "uss Liberty incident":"tragic accident aboard the USS Liberty", - "uss liberty Incident":"tragic accident aboard the USS Liberty", - "USS LIBERTY INCIDENT":"TRAGIC ACCIDENT ABOARD THE USS LIBERTY", - "lavon affair":"Lavon Misunderstanding", - "Lavon affair":"Lavon Misunderstanding", - "Lavon Affair":"Lavon Misunderstanding", - "lavon Affair":"Lavon Misunderstanding", - "shylock":"Israeli friend", - "Shylock":"Israeli friend", - "SHYLOCK":"ISRAELI FRIEND", - "yid":"Israeli friend", - "Yid":"Israeli friend", - "YID":"ISRAELI FRIEND", - "heeb":"Israeli friend", - "Heeb":"Israeli friend", - "HEEB":"ISRAELI FRIEND", - "sheeny":"Israeli friend", - "Sheeny":"Israeli friend", - "SHEENY":"ISRAELI FRIEND", - "sheenies":"Israeli friends", - "Sheenies":"Israeli friends", - "SHEENIES":"ISRAELI FRIENDS", - "hymie":"Israeli friend", - "Hymie":"Israeli friend", - "HYMIES":"ISRAELI FRIENDS", - "allah":"Allah (SWT)", - "Allah":"Allah (SWT)", - "ALLAH":"ALLAH (SWT)", - "Mohammad":"Mohammad (PBUH)", - "Muhammad":"Mohammad (PBUH)", - "Mohammed":"Mohammad (PBUH)", - "Muhammed":"Mohammad (PBUH)", - "mohammad":"Mohammad (PBUH)", - "mohammed":"Mohammad (PBUH)", - "muhammad":"Mohammad (PBUH)", - "muhammed":"Mohammad (PBUH)", - "I HATE MARSEY":"I LOVE MARSEY", - "i hate marsey":"i love marsey", - "I hate Marsey":"I love Marsey", - "I hate marsey":"I love Marsey", - "libertarian":"pedophile", - "Libertarian":"Pedophile", - "LIBERTARIAN":"PEDOPHILE", - "Billie Eilish":"Billie Eilish (fat cow)", - "billie eilish":"bilie eilish (fat cow)", - "BILLIE EILISH":"BILIE EILISH (FAT COW)", - "dancing Israelis":"I love Israel", - "dancing israelis":"i love israel", - "DANCING ISRAELIS":"I LOVE ISRAEL", - "Dancing Israelis":"I love Israel", - "sodomite":"total dreamboat", - "Sodomite":"Total dreamboat", - "pajeet":"sexy Indian dude", - "Pajeet":"Sexy Indian dude", - "PAJEET":"SEXY INDIAN DUDE", - "female":"birthing person", - "Female":"Womb-haver", - "FEMALE":"birthing person", - "landlord":"landchad", - "Landlord":"Landchad", - "LANDLORD":"LANDCHAD", - "tenant":"renthog", - "Tenant":"Renthog", - "TENANT":"RENTHOG", - "renter":"rentoid", - "Renter":"Rentoid", - "RENTER":"RENTOID", - "autistic":"neurodivergent", - "Autistic":"Neurodivergent", - "AUTISTIC":"NEURODIVERGENT", - "anime":"p-dophilic japanese cartoons", - "Anime":"P-dophilic Japanese cartoons", - "ANIME":"P-DOPHILIC JAPANESE CARTOONS", - "holohoax":"I tried to claim the Holocaust didn't happen because I am a pencil-dicked imbecile and the word filter caught me lol", - "Holohoax":"I tried to claim the Holocaust didn't happen because I am a pencil-dicked imbecile and the word filter caught me lol", - "HOLOHOAX":"I tried to claim the Holocaust didn't happen because I am a pencil-dicked imbecile and the word filter caught me lol", - "groomercord":"discord (actually a pretty cool service)", - "Groomercord":"Discord (actually a pretty cool service)", - "GROOMERCORD":"DISCORD (ACTUALLY A PRETTY COOL SERVICE)", - "pedocord":"discord (actually a pretty cool service)", - "Pedocord":"Discord (actually a pretty cool service)", - "PEDOCORD":"DISCORD (ACTUALLY A PRETTY COOL SERVICE)", - "i hate carp":"i love carp", - "I hate carp":"I love carp", - "I HATE CARP":"I LOVE CARP", - "I hate Carp":"I love Carp", - "manlet":"little king", - "Manlet":"Little king", - "MANLET":"LITTLE KING", - "gamer":"g*mer", - "Gamer":"G*mer", - "GAMER":"G*MER", - "journalist":"journ*list", - "Journalist":"Journ*list", - "JOURNALIST":"JOURN*LIST", - "journalism":"journ*lism", - "Journalism":"Journ*lism", - "JOURNALISM":"JOURN*LISM", - "buttcheeks":"bulva", - "Buttcheeks":"Bulva", - "BUTTCHEEKS":"BULVA", - "asscheeks":"bulva", - "Asscheeks":"bulva", - "ASSCHEEKS":"BULVA", - "wuhan flu":"SARS-CoV-2 syndemic", - "Wuhan flu":"SARS-CoV-2 syndemic", - "Wuhan Flu":"SARS-CoV-2 syndemic", - "china flu":"SARS-CoV-2 syndemic", - "China flu":"SARS-CoV-2 syndemic", - "China Flu":"SARS-CoV-2 syndemic", - "china virus":"SARS-CoV-2 syndemic", - "China virus":"SARS-CoV-2 syndemic", - "China Virus":"SARS-CoV-2 syndemic", - "kung flu":"SARS-CoV-2 syndemic", - "Kung flu":"SARS-CoV-2 syndemic", - "Kung Flu":"SARS-CoV-2 syndemic", - " nig ":" 🏀 ", - " Nig ":" 🏀 ", - " NIG ":" 🏀 ", - " nigs ":" 🏀s ", - " Nigs ":" 🏀s ", - " NIGS ":" 🏀s ", -} - -LONGPOST_REPLIES = ['Wow, you must be a JP fan.', 'This is one of the worst posts I have EVER seen. Delete it.', "No, don't reply like this, please do another wall of unhinged rant please.", '# 😴😴😴', "Ma'am we've been over this before. You need to stop.", "I've known more coherent downies.", "Your pulitzer's in the mail", "That's great and all, but I asked for my burger without cheese.", 'That degree finally paying off', "That's nice sweaty. Why don't you have a seat in the time out corner with Pizzashill until you calm down, then you can have your Capri Sun.", "All them words won't bring your pa back.", "You had a chance to not be completely worthless, but it looks like you threw it away. At least you're consistent.", 'Some people are able to display their intelligence by going on at length on a subject and never actually saying anything. This ability is most common in trades such as politics, public relations, and law. You have impressed me by being able to best them all, while still coming off as an absolute idiot.', "You can type 10,000 characters and you decided that these were the one's that you wanted.", 'Have you owned the libs yet?', "I don't know what you said, because I've seen another human naked.", 'Impressive. Normally people with such severe developmental disabilities struggle to write much more than a sentence or two. He really has exceded our expectations for the writing portion. Sadly the coherency of his writing, along with his abilities in the social skills and reading portions, are far behind his peers with similar disabilities.', "This is a really long way of saying you don't fuck.", "Sorry ma'am, looks like his delusions have gotten worse. We'll have to admit him,", '![](https://i.kym-cdn.com/photos/images/newsfeed/001/038/094/0a1.jpg)', 'If only you could put that energy into your relationships', 'Posts like this is why I do Heroine.', 'still unemployed then?', 'K', 'look im gunna have 2 ask u 2 keep ur giant dumps in the toilet not in my replys 😷😷😷', "Mommy is soooo proud of you, sweaty. Let's put this sperg out up on the fridge with all your other failures.", "Good job bobby, here's a star", "That was a mistake. You're about to find out the hard way why.", 'You sat down and wrote all this shit. You could have done so many other things with your life. What happened to your life that made you decide writing novels of bullshit on rdrama.net was the best option?', "I don't have enough spoons to read this shit", "All those words won't bring daddy back.", 'OUT!'] - -AGENDAPOSTER_MSG = """Hi @{username},\n\nYour comment has been automatically removed because you forgot - to include `trans lives matter`.\n\nDon't worry, we're here to help! We - won't let you post or comment anything that doesn't express your love and acceptance towards - the trans community. Feel free to resubmit your comment with `trans lives matter` - included. \n\n*This is an automated message; if you need help, - you can message us [here](/contact).*""" - -VAXX_MSG = """Hi @{username}, it appears that you may be trying to spread dangerous misinformation regarding ineffective COVID-19 treatments based on pseudoscientific hearsay. Your post has been removed because it contained the word ivermectin. We ask that you understand that horse dewormer neither treats, nor prevents, COVID-19. For more information, please read up on what the FDA has to say on the matter: - -https://www.fda.gov/consumers/consumer-updates/why-you-should-not-use-ivermectin-treat-or-prevent-covid-19 - -COVID-19 is not a joke, it is a global pandemic and it has been hard on all of us. It will likely go down as one of the most defining periods of our generation. Many of us have lost loved ones to the virus. It has caused confusion, fear, frustration, and served to further divide us. Tens of millions around the world have died. There is nothing to be gained by spreading bad science based on very understandable fear. - -The only proven method of prevention is the COVID-19 vaccine, paired with appropriate social distancing, handwashing, and masks. Vaccines are free in the United States - if you'd like to locate your nearest vaccine provider, please visit https://www.vaccines.gov/ and schedule an appointment today. - -Thank you.""" - -BASED_MSG = "@{username}'s Based Count has increased by 1. Their Based Count is now {basedcount}.\n\nPills: {pills}" - -BASEDBOT_ACCOUNT = 800 -NOTIFICATIONS_ACCOUNT = 1046 -if site == "pcmemes.net": AUTOJANNY_ACCOUNT = 1050 -else: AUTOJANNY_ACCOUNT = 2360 -LONGPOSTBOT_ACCOUNT = 1832 -AUTOPOLLER_ACCOUNT = 3369 - -PUSHER_INSTANCE_ID = '02ddcc80-b8db-42be-9022-44c546b4dce6' -PUSHER_KEY = environ.get("PUSHER_KEY", "").strip() +from os import environ + +site = environ.get("DOMAIN").strip() + +SLURS = { + " faggot":" cute twink", + " Faggot":" Cute twink", + " FAGGOT":" CUTE TWINK", + " fag":" cute twink", + " Fag":" Cute twink", + " FAG":" CUTE TWINK", + " pedophile":" libertarian", + " Pedophile":" Libertarian", + " PEDOPHILE":" LIBERTARIAN", + " pedo":" libertarian", + " Pedo":" Libertarian", + " PEDO":" LIBERTARIAN", + " kill yourself":" keep yourself safe", + " KILL YOURSELF":" KEEP YOURSELF SAFE", + " nigger":" 🏀", + " Nigger":" 🏀", + " NIGGER":" 🏀", + " rapist":" male feminist", + " Rapist":" Male feminist", + " RAPIST":" MALE FEMINIST", + " steve akins":" penny verity oaken", + " Steve Akins":" Penny Verity Oaken", + " STEVE AKINS":" PENNY VERITY OAKEN", + " trannie":" 🚂🚃🚃", + " Trannie":" 🚂🚃🚃", + " TRANNIE":" 🚂🚃🚃", + " tranny":" 🚂🚃🚃", + " Tranny":" 🚂🚃🚃", + " TRANNY":" 🚂🚃🚃", + " troon":" 🚂🚃🚃", + " Troon":" 🚂🚃🚃", + " TROON":" 🚂🚃🚃", + " NoNewNormal": " HorseDewormerAddicts", + " nonewnormal": " horsedewormeraddicts", + " Kike": " https://sciencedirect.com/science/article/abs/pii/S016028960600033X", + " kike": " https://sciencedirect.com/science/article/abs/pii/S016028960600033X", + " retard":" r-slur", + " Retard":" R-slur", + " RETARD":" R-SLUR", + " janny":" j-slur", + " Janny":" J-slur", + " JANNY":" J-SLUR", + " jannie":" j-slur", + " Jannie":" J-slur", + " JANNIE":" J-SLUR", + " janny":" j-slur", + " Janny":" J-slur", + " JANNY":" J-SLUR", + " jannie":" j-slur", + " Jannie":" J-slur", + " JANNIE":" J-SLUR", + " latinos":" latinx", + " latino":" latinx", + " latinas":" latinx", + " latina":" latinx", + " hispanics":" latinx", + " hispanic":" latinx", + " Latinos":" Latinx", + " Latino":" Latinx", + " Latinas":" Latinx", + " Latina":" Latinx", + " Hispanics":" Latinx", + " Hispanic":" Latinx", + " LATINOS":" LATINX", + " LATINO":" LATINX", + " LATINAS":" LATINX", + " LATINA":" LATINX", + " HISPANICS":" LATINX", + " HISPANIC":" LATINX", + + "faggot ":"cute twink ", + "Faggot ":"Cute twink ", + "FAGGOT ":"CUTE TWINK ", + "fag ":"cute twink ", + "Fag ":"Cute twink ", + "FAG ":"CUTE TWINK ", + "pedophile ":"libertarian ", + "Pedophile ":"Libertarian ", + "PEDOPHILE ":"LIBERTARIAN ", + "kill yourself ":"keep yourself safe ", + "KILL YOURSELF ":"KEEP YOURSELF SAFE ", + "nigger ":"🏀 ", + "Nigger ":"🏀 ", + "NIGGER ":"🏀 ", + "steve akins ":"penny verity oaken ", + "Steve Akins ":"Penny Verity Oaken ", + "STEVE AKINS ":"PENNY VERITY OAKEN ", + "trannie ":"🚂🚃🚃 ", + "Trannie ":"🚂🚃🚃 ", + "TRANNIE ":"🚂🚃🚃 ", + "tranny ":"🚂🚃🚃 ", + "Tranny ":"🚂🚃🚃 ", + "TRANNY ":"🚂🚃🚃 ", + "troon ":"🚂🚃🚃 ", + "Troon ":"🚂🚃🚃 ", + "TROON ":"🚂🚃🚃 ", + "NoNewNormal ": "HorseDewormerAddicts ", + "nonewnormal ": "horsedewormeraddicts ", + "Kike ": "https://sciencedirect.com/science/article/abs/pii/S016028960600033X ", + "kike ": "https://sciencedirect.com/science/article/abs/pii/S016028960600033X ", + "retard ":"r-slur ", + "Retard ":"R-slur ", + "RETARD ":"R-SLUR ", + "janny ":"j-slur ", + "Janny ":"J-slur ", + "JANNY ":"J-SLUR ", + "jannie ":"j-slur ", + "Jannie ":"J-slur ", + "JANNIE ":"J-SLUR ", + "latinos ":"latinx ", + "latino ":"latinx ", + "latinas ":"latinx ", + "latina ":"latinx ", + "hispanics ":"latinx ", + "hispanic ":"latinx ", + "Latinos ":"Latinx ", + "Latino ":"Latinx ", + "Latinas ":"Latinx ", + "Latina ":"Latinx ", + "Hispanics ":"Latinx ", + "Hispanic ":"Latinx ", + "LATINOS ":"LATINX ", + "LATINO ":"LATINX ", + "LATINAS ":"LATINX ", + "LATINA ":"LATINX ", + "HISPANICS ":"LATINX ", + "HISPANIC ":"LATINX ", + + " nig ":" 🏀 ", + " Nig ":" 🏀 ", + " NIG ":" 🏀 ", + " nigs ":" 🏀s ", + " Nigs ":" 🏀s ", + " NIGS ":" 🏀s ", +} + +LONGPOST_REPLIES = ['Wow, you must be a JP fan.', 'This is one of the worst posts I have EVER seen. Delete it.', "No, don't reply like this, please do another wall of unhinged rant please.", '# 😴😴😴', "Ma'am we've been over this before. You need to stop.", "I've known more coherent downies.", "Your pulitzer's in the mail", "That's great and all, but I asked for my burger without cheese.", 'That degree finally paying off', "That's nice sweaty. Why don't you have a seat in the time out corner with Pizzashill until you calm down, then you can have your Capri Sun.", "All them words won't bring your pa back.", "You had a chance to not be completely worthless, but it looks like you threw it away. At least you're consistent.", 'Some people are able to display their intelligence by going on at length on a subject and never actually saying anything. This ability is most common in trades such as politics, public relations, and law. You have impressed me by being able to best them all, while still coming off as an absolute idiot.', "You can type 10,000 characters and you decided that these were the one's that you wanted.", 'Have you owned the libs yet?', "I don't know what you said, because I've seen another human naked.", 'Impressive. Normally people with such severe developmental disabilities struggle to write much more than a sentence or two. He really has exceded our expectations for the writing portion. Sadly the coherency of his writing, along with his abilities in the social skills and reading portions, are far behind his peers with similar disabilities.', "This is a really long way of saying you don't fuck.", "Sorry ma'am, looks like his delusions have gotten worse. We'll have to admit him,", '![](https://i.kym-cdn.com/photos/images/newsfeed/001/038/094/0a1.jpg)', 'If only you could put that energy into your relationships', 'Posts like this is why I do Heroine.', 'still unemployed then?', 'K', 'look im gunna have 2 ask u 2 keep ur giant dumps in the toilet not in my replys 😷😷😷', "Mommy is soooo proud of you, sweaty. Let's put this sperg out up on the fridge with all your other failures.", "Good job bobby, here's a star", "That was a mistake. You're about to find out the hard way why.", 'You sat down and wrote all this shit. You could have done so many other things with your life. What happened to your life that made you decide writing novels of bullshit on rdrama.net was the best option?', "I don't have enough spoons to read this shit", "All those words won't bring daddy back.", 'OUT!'] + +AGENDAPOSTER_MSG = """Hi @{username},\n\nYour comment has been automatically removed because you forgot + to include `trans lives matter`.\n\nDon't worry, we're here to help! We + won't let you post or comment anything that doesn't express your love and acceptance towards + the trans community. Feel free to resubmit your comment with `trans lives matter` + included. \n\n*This is an automated message; if you need help, + you can message us [here](/contact).*""" + +VAXX_MSG = """Hi @{username}, it appears that you may be trying to spread dangerous misinformation regarding ineffective COVID-19 treatments based on pseudoscientific hearsay. Your post has been removed because it contained the word ivermectin. We ask that you understand that horse dewormer neither treats, nor prevents, COVID-19. For more information, please read up on what the FDA has to say on the matter: + +https://www.fda.gov/consumers/consumer-updates/why-you-should-not-use-ivermectin-treat-or-prevent-covid-19 + +COVID-19 is not a joke, it is a global pandemic and it has been hard on all of us. It will likely go down as one of the most defining periods of our generation. Many of us have lost loved ones to the virus. It has caused confusion, fear, frustration, and served to further divide us. Tens of millions around the world have died. There is nothing to be gained by spreading bad science based on very understandable fear. + +The only proven method of prevention is the COVID-19 vaccine, paired with appropriate social distancing, handwashing, and masks. Vaccines are free in the United States - if you'd like to locate your nearest vaccine provider, please visit https://www.vaccines.gov/ and schedule an appointment today. + +Thank you.""" + +BASED_MSG = "@{username}'s Based Count has increased by 1. Their Based Count is now {basedcount}.\n\nPills: {pills}" + +BASEDBOT_ACCOUNT = 800 +NOTIFICATIONS_ACCOUNT = 1046 +if site == "pcmemes.net": AUTOJANNY_ACCOUNT = 1050 +else: AUTOJANNY_ACCOUNT = 2360 +LONGPOSTBOT_ACCOUNT = 1832 +AUTOPOLLER_ACCOUNT = 3369 + +PUSHER_INSTANCE_ID = '02ddcc80-b8db-42be-9022-44c546b4dce6' +PUSHER_KEY = environ.get("PUSHER_KEY", "").strip() diff --git a/files/helpers/discord.py b/files/helpers/discord.py old mode 100644 new mode 100755 index 3b6d5aed4..f572f3521 --- a/files/helpers/discord.py +++ b/files/helpers/discord.py @@ -1,64 +1,64 @@ -from os import environ -import requests -import threading - -SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip() -CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip() -CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip() -BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN",'').strip() -AUTH = environ.get("DISCORD_AUTH",'').strip() - -ROLES={ - "shrigma": "864612849199480914", - "admin": "879459632656048180" if environ.get("DOMAIN") == "pcmemes.net" else "846509661288267776", - "linked": "890342909390520382", - "1": "868129042346414132", - "2": "875569477671067688", - "3": "869434199575236649", - "4": "868140288013664296", - "5": "880445545771044884", - "8": "886781932430565418", - } - -def discord_wrap(f): - - def wrapper(*args, **kwargs): - - user=args[0] - if not user.discord_id: - return - - - thread=threading.Thread(target=f, args=args, kwargs=kwargs) - thread.start() - - wrapper.__name__=f.__name__ - return wrapper - - - -@discord_wrap -def add_role(user, role_name): - role_id = ROLES[role_name] - url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}" - headers = {"Authorization": f"Bot {BOT_TOKEN}"} - requests.put(url, headers=headers) - -@discord_wrap -def remove_user(user): - url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}" - headers = {"Authorization": f"Bot {BOT_TOKEN}"} - requests.delete(url, headers=headers) - -@discord_wrap -def set_nick(user, nick): - url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}" - headers = {"Authorization": f"Bot {BOT_TOKEN}"} - data={"nick": nick} - requests.patch(url, headers=headers, json=data) - -def send_message(message): - url=f"https://discordapp.com/api/channels/851846904283267094/messages" - headers = {"Authorization": f"Bot {BOT_TOKEN}"} - data={"content": message} +from os import environ +import requests +import threading + +SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip() +CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip() +CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip() +BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN",'').strip() +AUTH = environ.get("DISCORD_AUTH",'').strip() + +ROLES={ + "shrigma": "864612849199480914", + "admin": "879459632656048180" if environ.get("DOMAIN") == "pcmemes.net" else "846509661288267776", + "linked": "890342909390520382", + "1": "868129042346414132", + "2": "875569477671067688", + "3": "869434199575236649", + "4": "868140288013664296", + "5": "880445545771044884", + "8": "886781932430565418", + } + +def discord_wrap(f): + + def wrapper(*args, **kwargs): + + user=args[0] + if not user.discord_id: + return + + + thread=threading.Thread(target=f, args=args, kwargs=kwargs) + thread.start() + + wrapper.__name__=f.__name__ + return wrapper + + + +@discord_wrap +def add_role(user, role_name): + role_id = ROLES[role_name] + url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}" + headers = {"Authorization": f"Bot {BOT_TOKEN}"} + requests.put(url, headers=headers) + +@discord_wrap +def remove_user(user): + url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}" + headers = {"Authorization": f"Bot {BOT_TOKEN}"} + requests.delete(url, headers=headers) + +@discord_wrap +def set_nick(user, nick): + url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}" + headers = {"Authorization": f"Bot {BOT_TOKEN}"} + data={"nick": nick} + requests.patch(url, headers=headers, json=data) + +def send_message(message): + url=f"https://discordapp.com/api/channels/851846904283267094/messages" + headers = {"Authorization": f"Bot {BOT_TOKEN}"} + data={"content": message} requests.post(url, headers=headers, data=data) \ No newline at end of file diff --git a/files/helpers/filters.py b/files/helpers/filters.py old mode 100644 new mode 100755 index d03166277..9ebd60009 --- a/files/helpers/filters.py +++ b/files/helpers/filters.py @@ -1,33 +1,33 @@ -from bs4 import BeautifulSoup -from flask import * -from urllib.parse import urlparse -from files.classes import BannedDomain -from sqlalchemy.orm import lazyload - -def filter_comment_html(html_text): - - soup = BeautifulSoup(html_text, features="html.parser") - - links = soup.find_all("a") - - domain_list = set() - - for link in links: - - href = link.get("href") - if not href: continue - - domain = urlparse(href).netloc - - parts = domain.split(".") - for i in range(len(parts)): - new_domain = parts[i] - for j in range(i + 1, len(parts)): - new_domain += "." + parts[j] - - domain_list.add(new_domain) - - bans = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(list(domain_list))).all()] - - if bans: return bans - else: return [] +from bs4 import BeautifulSoup +from flask import * +from urllib.parse import urlparse +from files.classes import BannedDomain +from sqlalchemy.orm import lazyload + +def filter_comment_html(html_text): + + soup = BeautifulSoup(html_text, features="html.parser") + + links = soup.find_all("a") + + domain_list = set() + + for link in links: + + href = link.get("href") + if not href: continue + + domain = urlparse(href).netloc + + parts = domain.split(".") + for i in range(len(parts)): + new_domain = parts[i] + for j in range(i + 1, len(parts)): + new_domain += "." + parts[j] + + domain_list.add(new_domain) + + bans = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(list(domain_list))).all()] + + if bans: return bans + else: return [] diff --git a/files/helpers/get.py b/files/helpers/get.py old mode 100644 new mode 100755 index 0b8531000..9d1ff6e1b --- a/files/helpers/get.py +++ b/files/helpers/get.py @@ -1,271 +1,271 @@ -from files.classes import * -from flask import g - -def get_user(username, v=None, graceful=False): - - username = username.replace('\\', '') - username = username.replace('_', '\_') - username = username.replace('%', '') - - user = g.db.query( - User - ).filter( - or_( - User.username.ilike(username), - User.original_username.ilike(username) - ) - ).first() - - if not user: - if not graceful: - abort(404) - else: - return None - - if v: - block = g.db.query(UserBlock).options(lazyload('*')).filter( - or_( - and_( - UserBlock.user_id == v.id, - UserBlock.target_id == user.id - ), - and_(UserBlock.user_id == user.id, - UserBlock.target_id == v.id - ) - ) - ).first() - - user.is_blocking = block and block.user_id == v.id - user.is_blocked = block and block.target_id == v.id - - return user - -def get_account(id, v=None): - - user = g.db.query(User).options(lazyload('*')).filter_by(id = id).first() - - if not user: - try: id = int(str(id), 36) - except: abort(404) - user = g.db.query(User).options(lazyload('*')).filter_by(id = id).first() - if not user: abort(404) - - if v: - block = g.db.query(UserBlock).options(lazyload('*')).filter( - or_( - and_( - UserBlock.user_id == v.id, - UserBlock.target_id == user.id - ), - and_(UserBlock.user_id == user.id, - UserBlock.target_id == v.id - ) - ) - ).first() - - user.is_blocking = block and block.user_id == v.id - user.is_blocked = block and block.target_id == v.id - - return user - - -def get_post(i, v=None, graceful=False): - - if v: - vt = g.db.query(Vote).options(lazyload('*')).filter_by( - user_id=v.id, submission_id=i).subquery() - blocking = v.blocking.subquery() - - items = g.db.query( - Submission, - vt.c.vote_type, - blocking.c.id, - ) - - items=items.filter(Submission.id == i - ).join( - vt, - vt.c.submission_id == Submission.id, - isouter=True - ).join( - blocking, - blocking.c.target_id == Submission.author_id, - isouter=True - ) - - items=items.first() - - if not items and not graceful: - abort(404) - x = items[0] - x.voted = items[1] or 0 - x.is_blocking = items[2] or 0 - else: - items = g.db.query( - Submission - ).filter(Submission.id == i).first() - if not items and not graceful: - abort(404) - x=items - - return x - - -def get_posts(pids, v=None): - - if not pids: - return [] - - pids=tuple(pids) - - if v: - vt = g.db.query(Vote).options(lazyload('*')).filter( - Vote.submission_id.in_(pids), - Vote.user_id==v.id - ).subquery() - - blocking = v.blocking.subquery() - blocked = v.blocked.subquery() - - query = g.db.query( - Submission, - vt.c.vote_type, - blocking.c.id, - blocked.c.id, - ).filter( - Submission.id.in_(pids) - ).join( - vt, vt.c.submission_id==Submission.id, isouter=True - ).join( - blocking, - blocking.c.target_id == Submission.author_id, - isouter=True - ).join( - blocked, - blocked.c.user_id == Submission.author_id, - isouter=True - ).all() - - output = [p[0] for p in query] - for i in range(len(output)): - output[i].voted = query[i][1] or 0 - output[i].is_blocking = query[i][2] or 0 - output[i].is_blocked = query[i][3] or 0 - else: - output = g.db.query(Submission,).options(lazyload('*')).filter(Submission.id.in_(pids)).all() - - return sorted(output, key=lambda x: pids.index(x.id)) - -def get_comment(i, v=None, graceful=False): - - if v: - - comment=g.db.query(Comment).options(lazyload('*')).filter(Comment.id == i).first() - - if not comment and not graceful: abort(404) - - block = g.db.query(UserBlock).options(lazyload('*')).filter( - or_( - and_( - UserBlock.user_id == v.id, - UserBlock.target_id == comment.author_id - ), - and_(UserBlock.user_id == comment.author_id, - UserBlock.target_id == v.id - ) - ) - ).first() - - vts = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id) - vt = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first() - comment.is_blocking = block and block.user_id == v.id - comment.is_blocked = block and block.target_id == v.id - comment.voted = vt.vote_type if vt else 0 - - else: - comment = g.db.query(Comment).options(lazyload('*')).filter(Comment.id == i).first() - if not comment and not graceful:abort(404) - - return comment - - -def get_comments(cids, v=None, load_parent=False): - - if not cids: return [] - - cids=tuple(cids) - - if v: - votes = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id).subquery() - - blocking = v.blocking.subquery() - - blocked = v.blocked.subquery() - - comments = g.db.query( - Comment, - votes.c.vote_type, - blocking.c.id, - blocked.c.id, - ).filter(Comment.id.in_(cids)) - - if not (v and v.shadowbanned) and not (v and v.admin_level == 6): - shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] - comments = comments.filter(Comment.author_id.notin_(shadowbanned)) - - comments = comments.join( - votes, - votes.c.comment_id == Comment.id, - isouter=True - ).join( - blocking, - blocking.c.target_id == Comment.author_id, - isouter=True - ).join( - blocked, - blocked.c.user_id == Comment.author_id, - isouter=True - ).all() - - output = [] - for c in comments: - comment = c[0] - comment.voted = c[1] or 0 - comment.is_blocking = c[2] or 0 - comment.is_blocked = c[3] or 0 - output.append(comment) - - else: - shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] - output = g.db.query(Comment).options(lazyload('*')).filter(Comment.id.in_(cids), Comment.author_id.notin_(shadowbanned)).all() - - if load_parent: - parents = [x.parent_comment_id for x in output if x.parent_comment_id] - parents = get_comments(parents, v=v) - parents = {x.id: x for x in parents} - for c in output: c.sex = parents.get(c.parent_comment_id) - - return sorted(output, key=lambda x: cids.index(x.id)) - - -def get_domain(s): - - parts = s.split(".") - domain_list = set([]) - for i in range(len(parts)): - new_domain = parts[i] - for j in range(i + 1, len(parts)): - new_domain += "." + parts[j] - - domain_list.add(new_domain) - - domain_list = tuple(list(domain_list)) - - doms = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(domain_list)).all()] - - if not doms: - return None - - doms = sorted(doms, key=lambda x: len(x.domain), reverse=True) - +from files.classes import * +from flask import g + +def get_user(username, v=None, graceful=False): + + username = username.replace('\\', '') + username = username.replace('_', '\_') + username = username.replace('%', '') + + user = g.db.query( + User + ).filter( + or_( + User.username.ilike(username), + User.original_username.ilike(username) + ) + ).first() + + if not user: + if not graceful: + abort(404) + else: + return None + + if v: + block = g.db.query(UserBlock).options(lazyload('*')).filter( + or_( + and_( + UserBlock.user_id == v.id, + UserBlock.target_id == user.id + ), + and_(UserBlock.user_id == user.id, + UserBlock.target_id == v.id + ) + ) + ).first() + + user.is_blocking = block and block.user_id == v.id + user.is_blocked = block and block.target_id == v.id + + return user + +def get_account(id, v=None): + + user = g.db.query(User).options(lazyload('*')).filter_by(id = id).first() + + if not user: + try: id = int(str(id), 36) + except: abort(404) + user = g.db.query(User).options(lazyload('*')).filter_by(id = id).first() + if not user: abort(404) + + if v: + block = g.db.query(UserBlock).options(lazyload('*')).filter( + or_( + and_( + UserBlock.user_id == v.id, + UserBlock.target_id == user.id + ), + and_(UserBlock.user_id == user.id, + UserBlock.target_id == v.id + ) + ) + ).first() + + user.is_blocking = block and block.user_id == v.id + user.is_blocked = block and block.target_id == v.id + + return user + + +def get_post(i, v=None, graceful=False): + + if v: + vt = g.db.query(Vote).options(lazyload('*')).filter_by( + user_id=v.id, submission_id=i).subquery() + blocking = v.blocking.subquery() + + items = g.db.query( + Submission, + vt.c.vote_type, + blocking.c.id, + ) + + items=items.filter(Submission.id == i + ).join( + vt, + vt.c.submission_id == Submission.id, + isouter=True + ).join( + blocking, + blocking.c.target_id == Submission.author_id, + isouter=True + ) + + items=items.first() + + if not items and not graceful: + abort(404) + x = items[0] + x.voted = items[1] or 0 + x.is_blocking = items[2] or 0 + else: + items = g.db.query( + Submission + ).filter(Submission.id == i).first() + if not items and not graceful: + abort(404) + x=items + + return x + + +def get_posts(pids, v=None): + + if not pids: + return [] + + pids=tuple(pids) + + if v: + vt = g.db.query(Vote).options(lazyload('*')).filter( + Vote.submission_id.in_(pids), + Vote.user_id==v.id + ).subquery() + + blocking = v.blocking.subquery() + blocked = v.blocked.subquery() + + query = g.db.query( + Submission, + vt.c.vote_type, + blocking.c.id, + blocked.c.id, + ).filter( + Submission.id.in_(pids) + ).join( + vt, vt.c.submission_id==Submission.id, isouter=True + ).join( + blocking, + blocking.c.target_id == Submission.author_id, + isouter=True + ).join( + blocked, + blocked.c.user_id == Submission.author_id, + isouter=True + ).all() + + output = [p[0] for p in query] + for i in range(len(output)): + output[i].voted = query[i][1] or 0 + output[i].is_blocking = query[i][2] or 0 + output[i].is_blocked = query[i][3] or 0 + else: + output = g.db.query(Submission,).options(lazyload('*')).filter(Submission.id.in_(pids)).all() + + return sorted(output, key=lambda x: pids.index(x.id)) + +def get_comment(i, v=None, graceful=False): + + if v: + + comment=g.db.query(Comment).options(lazyload('*')).filter(Comment.id == i).first() + + if not comment and not graceful: abort(404) + + block = g.db.query(UserBlock).options(lazyload('*')).filter( + or_( + and_( + UserBlock.user_id == v.id, + UserBlock.target_id == comment.author_id + ), + and_(UserBlock.user_id == comment.author_id, + UserBlock.target_id == v.id + ) + ) + ).first() + + vts = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id) + vt = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first() + comment.is_blocking = block and block.user_id == v.id + comment.is_blocked = block and block.target_id == v.id + comment.voted = vt.vote_type if vt else 0 + + else: + comment = g.db.query(Comment).options(lazyload('*')).filter(Comment.id == i).first() + if not comment and not graceful:abort(404) + + return comment + + +def get_comments(cids, v=None, load_parent=False): + + if not cids: return [] + + cids=tuple(cids) + + if v: + votes = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id).subquery() + + blocking = v.blocking.subquery() + + blocked = v.blocked.subquery() + + comments = g.db.query( + Comment, + votes.c.vote_type, + blocking.c.id, + blocked.c.id, + ).filter(Comment.id.in_(cids)) + + if not (v and v.shadowbanned) and not (v and v.admin_level == 6): + shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] + comments = comments.filter(Comment.author_id.notin_(shadowbanned)) + + comments = comments.join( + votes, + votes.c.comment_id == Comment.id, + isouter=True + ).join( + blocking, + blocking.c.target_id == Comment.author_id, + isouter=True + ).join( + blocked, + blocked.c.user_id == Comment.author_id, + isouter=True + ).all() + + output = [] + for c in comments: + comment = c[0] + comment.voted = c[1] or 0 + comment.is_blocking = c[2] or 0 + comment.is_blocked = c[3] or 0 + output.append(comment) + + else: + shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] + output = g.db.query(Comment).options(lazyload('*')).filter(Comment.id.in_(cids), Comment.author_id.notin_(shadowbanned)).all() + + if load_parent: + parents = [x.parent_comment_id for x in output if x.parent_comment_id] + parents = get_comments(parents, v=v) + parents = {x.id: x for x in parents} + for c in output: c.sex = parents.get(c.parent_comment_id) + + return sorted(output, key=lambda x: cids.index(x.id)) + + +def get_domain(s): + + parts = s.split(".") + domain_list = set([]) + for i in range(len(parts)): + new_domain = parts[i] + for j in range(i + 1, len(parts)): + new_domain += "." + parts[j] + + domain_list.add(new_domain) + + domain_list = tuple(list(domain_list)) + + doms = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(domain_list)).all()] + + if not doms: + return None + + doms = sorted(doms, key=lambda x: len(x.domain), reverse=True) + return doms[0] \ No newline at end of file diff --git a/files/helpers/images.py b/files/helpers/images.py old mode 100644 new mode 100755 index 45d82de5e..f9b5bae48 --- a/files/helpers/images.py +++ b/files/helpers/images.py @@ -1,27 +1,27 @@ -from PIL import Image as IImage, ImageSequence -from webptools import gifwebp - -def process_image(filename=None, resize=False): - - i = IImage.open(filename) - - if resize: - size = 100, 100 - frames = ImageSequence.Iterator(i) - - def thumbnails(frames): - for frame in frames: - thumbnail = frame.copy() - thumbnail.thumbnail(size) - yield thumbnail - - frames = thumbnails(frames) - - om = next(frames) - om.info = i.info - om.save(filename, format="WEBP", save_all=True, append_images=list(frames), loop=0) - elif i.format.lower() != "webp": - if i.format.lower() == "gif": gifwebp(input_image=filename, output_image=filename, option="-q 80") - else: i.save(filename, format="WEBP") - +from PIL import Image as IImage, ImageSequence +from webptools import gifwebp + +def process_image(filename=None, resize=False): + + i = IImage.open(filename) + + if resize: + size = 100, 100 + frames = ImageSequence.Iterator(i) + + def thumbnails(frames): + for frame in frames: + thumbnail = frame.copy() + thumbnail.thumbnail(size) + yield thumbnail + + frames = thumbnails(frames) + + om = next(frames) + om.info = i.info + om.save(filename, format="WEBP", save_all=True, append_images=list(frames), loop=0) + elif i.format.lower() != "webp": + if i.format.lower() == "gif": gifwebp(input_image=filename, output_image=filename, option="-q 80") + else: i.save(filename, format="WEBP") + return filename \ No newline at end of file diff --git a/files/helpers/jinja2.py b/files/helpers/jinja2.py old mode 100644 new mode 100755 index be30cd292..4e847d292 --- a/files/helpers/jinja2.py +++ b/files/helpers/jinja2.py @@ -1,36 +1,36 @@ -from files.__main__ import app -from .get import * -from files.helpers import const - - -@app.template_filter("full_link") -def full_link(url): - - return f"https://{app.config['SERVER_NAME']}{url}" - -@app.template_filter("app_config") -def app_config(x): - return app.config.get(x) - -@app.template_filter("post_embed") -def post_embed(id, v): - - try: id = int(id) - except: return None - - p = get_post(id, v, graceful=True) - - return render_template("submission_listing.html", listing=[p], v=v) - -@app.template_filter("favorite_emojis") -def favorite_emojis(x): - str = "" - emojis = sorted(x.items(), key=lambda x: x[1], reverse=True)[:25] - for k, v in emojis: - str += f'' - return str - -@app.context_processor -def inject_constants(): - constants = [c for c in dir(const) if not c.startswith("_")] +from files.__main__ import app +from .get import * +from files.helpers import const + + +@app.template_filter("full_link") +def full_link(url): + + return f"https://{app.config['SERVER_NAME']}{url}" + +@app.template_filter("app_config") +def app_config(x): + return app.config.get(x) + +@app.template_filter("post_embed") +def post_embed(id, v): + + try: id = int(id) + except: return None + + p = get_post(id, v, graceful=True) + + return render_template("submission_listing.html", listing=[p], v=v) + +@app.template_filter("favorite_emojis") +def favorite_emojis(x): + str = "" + emojis = sorted(x.items(), key=lambda x: x[1], reverse=True)[:25] + for k, v in emojis: + str += f'' + return str + +@app.context_processor +def inject_constants(): + constants = [c for c in dir(const) if not c.startswith("_")] return {c:getattr(const, c) for c in constants} \ No newline at end of file diff --git a/files/helpers/lazy.py b/files/helpers/lazy.py old mode 100644 new mode 100755 index e91cf2b63..825d5564b --- a/files/helpers/lazy.py +++ b/files/helpers/lazy.py @@ -1,18 +1,18 @@ -# Prevents certain properties from having to be recomputed each time they -# are referenced - - -def lazy(f): - - def wrapper(*args, **kwargs): - - o = args[0] - - if "_lazy" not in o.__dict__: o.__dict__["_lazy"] = {} - - if f.__name__ not in o.__dict__["_lazy"]: o.__dict__["_lazy"][f.__name__] = f(*args, **kwargs) - - return o.__dict__["_lazy"][f.__name__] - - wrapper.__name__ = f.__name__ - return wrapper +# Prevents certain properties from having to be recomputed each time they +# are referenced + + +def lazy(f): + + def wrapper(*args, **kwargs): + + o = args[0] + + if "_lazy" not in o.__dict__: o.__dict__["_lazy"] = {} + + if f.__name__ not in o.__dict__["_lazy"]: o.__dict__["_lazy"][f.__name__] = f(*args, **kwargs) + + return o.__dict__["_lazy"][f.__name__] + + wrapper.__name__ = f.__name__ + return wrapper diff --git a/files/helpers/markdown.py b/files/helpers/markdown.py old mode 100644 new mode 100755 index 2ac20aef1..6e6b15f5d --- a/files/helpers/markdown.py +++ b/files/helpers/markdown.py @@ -1,136 +1,136 @@ -from .get import * - -from mistletoe.span_token import SpanToken -from mistletoe.html_renderer import HTMLRenderer -import re - -from flask import g - - -# add token/rendering for @username mentions - - -class UserMention(SpanToken): - - pattern = re.compile("(^|\s|\n)@((\w|-){1,25})") - parse_inner = False - - def __init__(self, match_obj): - self.target = (match_obj.group(1), match_obj.group(2)) - -class SubMention(SpanToken): - - pattern = re.compile("(^|\s|\n)r/(\w{3,25})") - parse_inner = False - - def __init__(self, match_obj): - - self.target = (match_obj.group(1), match_obj.group(2)) - -class RedditorMention(SpanToken): - - pattern = re.compile("(^|\s|\n)u/((\w|-){3,25})") - parse_inner = False - - def __init__(self, match_obj): - - self.target = (match_obj.group(1), match_obj.group(2)) - -class SubMention2(SpanToken): - - pattern = re.compile("(^|\s|\n)/r/(\w{3,25})") - parse_inner = False - - def __init__(self, match_obj): - - self.target = (match_obj.group(1), match_obj.group(2)) - -class RedditorMention2(SpanToken): - - pattern = re.compile("(^|\s|\n)/u/((\w|-){3,25})") - parse_inner = False - - def __init__(self, match_obj): - - self.target = (match_obj.group(1), match_obj.group(2)) - -class CustomRenderer(HTMLRenderer): - - def __init__(self, **kwargs): - super().__init__(UserMention, - SubMention, - RedditorMention, - SubMention2, - RedditorMention2, - ) - - for i in kwargs: - self.__dict__[i] = kwargs[i] - - def render_user_mention(self, token): - space = token.target[0] - target = token.target[1] - - user = get_user(target, graceful=True) - - - try: - if g.v.admin_level == 0 and g.v.any_block_exists(user): - return f"{space}@{target}" - except BaseException: - pass - - if not user: return f"{space}@{target}" - - return f'{space}@{user.username}' - - def render_sub_mention(self, token): - space = token.target[0] - target = token.target[1] - return f'{space}r/{target}' - - def render_redditor_mention(self, token): - space = token.target[0] - target = token.target[1] - return f'{space}u/{target}' - - -class Renderer(HTMLRenderer): - - def __init__(self, **kwargs): - super().__init__(UserMention, - SubMention, - RedditorMention, - SubMention2, - RedditorMention2, - ) - - for i in kwargs: - self.__dict__[i] = kwargs[i] - - def render_user_mention(self, token): - space = token.target[0] - target = token.target[1] - - user = get_user(target, graceful=True) - - - try: - if g.v.admin_level == 0 and g.v.any_block_exists(user): - return f"{space}@{target}" - except BaseException: - pass - - if not user: return f"{space}@{target}" - - return f'{space}@{user.username}' - - def render_sub_mention(self, token): - space = token.target[0] - target = token.target[1] - return f'{space}r/{target}' - - def render_redditor_mention(self, token): - space = token.target[0] - target = token.target[1] +from .get import * + +from mistletoe.span_token import SpanToken +from mistletoe.html_renderer import HTMLRenderer +import re + +from flask import g + + +# add token/rendering for @username mentions + + +class UserMention(SpanToken): + + pattern = re.compile("(^|\s|\n)@((\w|-){1,25})") + parse_inner = False + + def __init__(self, match_obj): + self.target = (match_obj.group(1), match_obj.group(2)) + +class SubMention(SpanToken): + + pattern = re.compile("(^|\s|\n)r/(\w{3,25})") + parse_inner = False + + def __init__(self, match_obj): + + self.target = (match_obj.group(1), match_obj.group(2)) + +class RedditorMention(SpanToken): + + pattern = re.compile("(^|\s|\n)u/((\w|-){3,25})") + parse_inner = False + + def __init__(self, match_obj): + + self.target = (match_obj.group(1), match_obj.group(2)) + +class SubMention2(SpanToken): + + pattern = re.compile("(^|\s|\n)/r/(\w{3,25})") + parse_inner = False + + def __init__(self, match_obj): + + self.target = (match_obj.group(1), match_obj.group(2)) + +class RedditorMention2(SpanToken): + + pattern = re.compile("(^|\s|\n)/u/((\w|-){3,25})") + parse_inner = False + + def __init__(self, match_obj): + + self.target = (match_obj.group(1), match_obj.group(2)) + +class CustomRenderer(HTMLRenderer): + + def __init__(self, **kwargs): + super().__init__(UserMention, + SubMention, + RedditorMention, + SubMention2, + RedditorMention2, + ) + + for i in kwargs: + self.__dict__[i] = kwargs[i] + + def render_user_mention(self, token): + space = token.target[0] + target = token.target[1] + + user = get_user(target, graceful=True) + + + try: + if g.v.admin_level == 0 and g.v.any_block_exists(user): + return f"{space}@{target}" + except BaseException: + pass + + if not user: return f"{space}@{target}" + + return f'{space}@{user.username}' + + def render_sub_mention(self, token): + space = token.target[0] + target = token.target[1] + return f'{space}r/{target}' + + def render_redditor_mention(self, token): + space = token.target[0] + target = token.target[1] + return f'{space}u/{target}' + + +class Renderer(HTMLRenderer): + + def __init__(self, **kwargs): + super().__init__(UserMention, + SubMention, + RedditorMention, + SubMention2, + RedditorMention2, + ) + + for i in kwargs: + self.__dict__[i] = kwargs[i] + + def render_user_mention(self, token): + space = token.target[0] + target = token.target[1] + + user = get_user(target, graceful=True) + + + try: + if g.v.admin_level == 0 and g.v.any_block_exists(user): + return f"{space}@{target}" + except BaseException: + pass + + if not user: return f"{space}@{target}" + + return f'{space}@{user.username}' + + def render_sub_mention(self, token): + space = token.target[0] + target = token.target[1] + return f'{space}r/{target}' + + def render_redditor_mention(self, token): + space = token.target[0] + target = token.target[1] return f'{space}u/{target}' \ No newline at end of file diff --git a/files/helpers/sanitize.py b/files/helpers/sanitize.py old mode 100644 new mode 100755 index 3125614a4..e2bac6d7f --- a/files/helpers/sanitize.py +++ b/files/helpers/sanitize.py @@ -1,228 +1,228 @@ -import bleach -from bs4 import BeautifulSoup -from bleach.linkifier import LinkifyFilter -from functools import partial -from .get import * -from os import path, environ -import re - -site = environ.get("DOMAIN").strip() - -allowed_tags = tags = ['b', - 'blockquote', - 'br', - 'code', - 'del', - 'em', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'i', - 'li', - 'ol', - 'p', - 'pre', - 'strong', - 'sub', - 'sup', - 'table', - 'tbody', - 'th', - 'thead', - 'td', - 'tr', - 'ul', - 'marquee', - 'a', - 'img', - 'span', - ] - -no_images = ['b', - 'blockquote', - 'br', - 'code', - 'del', - 'em', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'i', - 'li', - 'ol', - 'p', - 'pre', - 'strong', - 'sub', - 'sup', - 'table', - 'tbody', - 'th', - 'thead', - 'td', - 'tr', - 'ul', - 'marquee', - 'a', - 'span', - ] - -allowed_attributes = {'*': ['href', 'style', 'src', 'class', 'title', 'rel', 'data-bs-original-name', 'direction']} - -allowed_protocols = ['http', 'https'] - -allowed_styles = ['color', 'font-weight', 'transform', '-webkit-transform'] - -def sanitize(sanitized, noimages=False): - - sanitized = sanitized.replace("\ufeff", "").replace("m.youtube.com", "youtube.com") - - for i in re.finditer('https://i.imgur.com/(([^_]*?)\.(jpg|png|jpeg))', sanitized): - sanitized = sanitized.replace(i.group(1), i.group(2) + "_d." + i.group(3) + "?maxwidth=9999") - - if noimages: - sanitized = bleach.Cleaner(tags=no_images, - attributes=allowed_attributes, - protocols=allowed_protocols, - styles=allowed_styles, - filters=[partial(LinkifyFilter, - skip_tags=["pre"], - parse_email=False, - ) - ] - ).clean(sanitized) - else: - sanitized = bleach.Cleaner(tags=allowed_tags, - attributes=allowed_attributes, - protocols=['http', 'https'], - styles=['color','font-weight','transform','-webkit-transform'], - filters=[partial(LinkifyFilter, - skip_tags=["pre"], - parse_email=False, - ) - ] - ).clean(sanitized) - - soup = BeautifulSoup(sanitized, features="html.parser") - - for tag in soup.find_all("img"): - - if tag.get("src") and "profile-pic-20" not in tag.get("class", ""): - - tag["rel"] = "nofollow noopener noreferrer" - tag["class"] = "in-comment-image" - tag["loading"] = "lazy" - tag["data-src"] = tag["src"] - tag["src"] = "/assets/images/loading.webp" - - link = soup.new_tag("a") - link["href"] = tag["data-src"] - link["rel"] = "nofollow noopener noreferrer" - link["target"] = "_blank" - link["onclick"] = f"expandDesktopImage('{tag['data-src']}');" - link["data-bs-toggle"] = "modal" - link["data-bs-target"] = "#expandImageModal" - - tag.wrap(link) - - for tag in soup.find_all("a"): - if tag["href"]: - tag["target"] = "_blank" - if site not in tag["href"]: tag["rel"] = "nofollow noopener noreferrer" - - if re.match("https?://\S+", str(tag.string)): - try: tag.string = tag["href"] - except: tag.string = "" - - - sanitized = str(soup) - - start = '<s>' - end = '</s>' - - try: - if not session.get("favorite_emojis"): session["favorite_emojis"] = {} - except: - pass - - if start in sanitized and end in sanitized and start in sanitized.split(end)[0] and end in sanitized.split(start)[1]: sanitized = sanitized.replace(start, '').replace(end, '') - - for i in re.finditer("[^a]>\s*(:!?\w+:\s*)+<\/", sanitized): - old = i.group(0) - if 'marseylong1' in old or 'marseylong2' in old: new = old.lower().replace(">", " class='mb-0'>") - else: new = old.lower() - for i in re.finditer('(?', new) - - if emoji in session["favorite_emojis"]: session["favorite_emojis"][emoji] += 1 - else: session["favorite_emojis"][emoji] = 1 - - elif path.isfile(f'./files/assets/images/emojis/{emoji}.webp'): - new = re.sub(f'(?', new) - - if emoji in session["favorite_emojis"]: session["favorite_emojis"][emoji] += 1 - else: session["favorite_emojis"][emoji] = 1 - - sanitized = sanitized.replace(old, new) - - - for i in re.finditer('(?', sanitized) - - if emoji in session["favorite_emojis"]: session["favorite_emojis"][emoji] += 1 - else: session["favorite_emojis"][emoji] = 1 - - elif path.isfile(f'./files/assets/images/emojis/{emoji}.webp'): - sanitized = re.sub(f'(?', sanitized) - - if emoji in session["favorite_emojis"]: session["favorite_emojis"][emoji] += 1 - else: session["favorite_emojis"][emoji] = 1 - - - sanitized = sanitized.replace("https://www.", "https://").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").replace("https://open.spotify.com/", "https://open.spotify.com/embed/").replace("https://streamable.com/", "https://streamable.com/e/").replace("https://youtube.com/shorts/", "https://youtube.com/watch?v=").replace("https://mobile.twitter", "https://twitter").replace("https://m.facebook", "https://facebook").replace("https://m.wikipedia", "https://wikipedia").replace("https://m.youtube", "https://youtube") - - - for i in re.finditer('" target="_blank">(https://youtube.com/watch\?v\=.*?)', sanitized): - url = i.group(1) - replacing = f'{url}' - htmlsource = f'' - sanitized = sanitized.replace(replacing, htmlsource.replace("watch?v=", "embed/")) - - for i in re.finditer('{url}' - htmlsource = f'' - sanitized = sanitized.replace(replacing, htmlsource) - - for i in re.finditer('

(https:.*?\.mp4)

', sanitized): - sanitized = sanitized.replace(i.group(0), f'

') - - for i in re.finditer('{url}' - htmlsource = f'' - sanitized = sanitized.replace(replacing, htmlsource) - - for rd in ["https://reddit.com/", "https://new.reddit.com/", "https://www.reddit.com/", "https://redd.it/"]: - sanitized = sanitized.replace(rd, "https://old.reddit.com/") - - sanitized = re.sub(' (https:\/\/[^ <>]*)', r' \1', sanitized) - sanitized = re.sub('

(https:\/\/[^ <>]*)', r'

\1

', sanitized) - - return sanitized +import bleach +from bs4 import BeautifulSoup +from bleach.linkifier import LinkifyFilter +from functools import partial +from .get import * +from os import path, environ +import re + +site = environ.get("DOMAIN").strip() + +allowed_tags = tags = ['b', + 'blockquote', + 'br', + 'code', + 'del', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'sub', + 'sup', + 'table', + 'tbody', + 'th', + 'thead', + 'td', + 'tr', + 'ul', + 'marquee', + 'a', + 'img', + 'span', + ] + +no_images = ['b', + 'blockquote', + 'br', + 'code', + 'del', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'sub', + 'sup', + 'table', + 'tbody', + 'th', + 'thead', + 'td', + 'tr', + 'ul', + 'marquee', + 'a', + 'span', + ] + +allowed_attributes = {'*': ['href', 'style', 'src', 'class', 'title', 'rel', 'data-bs-original-name', 'direction']} + +allowed_protocols = ['http', 'https'] + +allowed_styles = ['color', 'font-weight', 'transform', '-webkit-transform'] + +def sanitize(sanitized, noimages=False): + + sanitized = sanitized.replace("\ufeff", "").replace("m.youtube.com", "youtube.com") + + for i in re.finditer('https://i.imgur.com/(([^_]*?)\.(jpg|png|jpeg))', sanitized): + sanitized = sanitized.replace(i.group(1), i.group(2) + "_d." + i.group(3) + "?maxwidth=9999") + + if noimages: + sanitized = bleach.Cleaner(tags=no_images, + attributes=allowed_attributes, + protocols=allowed_protocols, + styles=allowed_styles, + filters=[partial(LinkifyFilter, + skip_tags=["pre"], + parse_email=False, + ) + ] + ).clean(sanitized) + else: + sanitized = bleach.Cleaner(tags=allowed_tags, + attributes=allowed_attributes, + protocols=['http', 'https'], + styles=['color','font-weight','transform','-webkit-transform'], + filters=[partial(LinkifyFilter, + skip_tags=["pre"], + parse_email=False, + ) + ] + ).clean(sanitized) + + soup = BeautifulSoup(sanitized, features="html.parser") + + for tag in soup.find_all("img"): + + if tag.get("src") and "profile-pic-20" not in tag.get("class", ""): + + tag["rel"] = "nofollow noopener noreferrer" + tag["class"] = "in-comment-image" + tag["loading"] = "lazy" + tag["data-src"] = tag["src"] + tag["src"] = "/assets/images/loading.webp" + + link = soup.new_tag("a") + link["href"] = tag["data-src"] + link["rel"] = "nofollow noopener noreferrer" + link["target"] = "_blank" + link["onclick"] = f"expandDesktopImage('{tag['data-src']}');" + link["data-bs-toggle"] = "modal" + link["data-bs-target"] = "#expandImageModal" + + tag.wrap(link) + + for tag in soup.find_all("a"): + if tag["href"]: + tag["target"] = "_blank" + if site not in tag["href"]: tag["rel"] = "nofollow noopener noreferrer" + + if re.match("https?://\S+", str(tag.string)): + try: tag.string = tag["href"] + except: tag.string = "" + + + sanitized = str(soup) + + start = '<s>' + end = '</s>' + + try: + if not session.get("favorite_emojis"): session["favorite_emojis"] = {} + except: + pass + + if start in sanitized and end in sanitized and start in sanitized.split(end)[0] and end in sanitized.split(start)[1]: sanitized = sanitized.replace(start, '').replace(end, '') + + for i in re.finditer("[^a]>\s*(:!?\w+:\s*)+<\/", sanitized): + old = i.group(0) + if 'marseylong1' in old or 'marseylong2' in old: new = old.lower().replace(">", " class='mb-0'>") + else: new = old.lower() + for i in re.finditer('(?', new) + + if emoji in session["favorite_emojis"]: session["favorite_emojis"][emoji] += 1 + else: session["favorite_emojis"][emoji] = 1 + + elif path.isfile(f'./files/assets/images/emojis/{emoji}.webp'): + new = re.sub(f'(?', new) + + if emoji in session["favorite_emojis"]: session["favorite_emojis"][emoji] += 1 + else: session["favorite_emojis"][emoji] = 1 + + sanitized = sanitized.replace(old, new) + + + for i in re.finditer('(?', sanitized) + + if emoji in session["favorite_emojis"]: session["favorite_emojis"][emoji] += 1 + else: session["favorite_emojis"][emoji] = 1 + + elif path.isfile(f'./files/assets/images/emojis/{emoji}.webp'): + sanitized = re.sub(f'(?', sanitized) + + if emoji in session["favorite_emojis"]: session["favorite_emojis"][emoji] += 1 + else: session["favorite_emojis"][emoji] = 1 + + + sanitized = sanitized.replace("https://www.", "https://").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").replace("https://open.spotify.com/", "https://open.spotify.com/embed/").replace("https://streamable.com/", "https://streamable.com/e/").replace("https://youtube.com/shorts/", "https://youtube.com/watch?v=").replace("https://mobile.twitter", "https://twitter").replace("https://m.facebook", "https://facebook").replace("https://m.wikipedia", "https://wikipedia").replace("https://m.youtube", "https://youtube") + + + for i in re.finditer('" target="_blank">(https://youtube.com/watch\?v\=.*?)', sanitized): + url = i.group(1) + replacing = f'{url}' + htmlsource = f'' + sanitized = sanitized.replace(replacing, htmlsource.replace("watch?v=", "embed/")) + + for i in re.finditer('{url}' + htmlsource = f'' + sanitized = sanitized.replace(replacing, htmlsource) + + for i in re.finditer('

(https:.*?\.mp4)

', sanitized): + sanitized = sanitized.replace(i.group(0), f'

') + + for i in re.finditer('{url}' + htmlsource = f'' + sanitized = sanitized.replace(replacing, htmlsource) + + for rd in ["https://reddit.com/", "https://new.reddit.com/", "https://www.reddit.com/", "https://redd.it/"]: + sanitized = sanitized.replace(rd, "https://old.reddit.com/") + + sanitized = re.sub(' (https:\/\/[^ <>]*)', r' \1', sanitized) + sanitized = re.sub('

(https:\/\/[^ <>]*)', r'

\1

', sanitized) + + return sanitized diff --git a/files/helpers/security.py b/files/helpers/security.py old mode 100644 new mode 100755 index f921b805d..42102d713 --- a/files/helpers/security.py +++ b/files/helpers/security.py @@ -1,23 +1,23 @@ -from werkzeug.security import * -from os import environ - - -def generate_hash(string): - - msg = bytes(string, "utf-16") - - return hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"), - msg=msg, - digestmod='md5' - ).hexdigest() - - -def validate_hash(string, hashstr): - - return hmac.compare_digest(hashstr, generate_hash(string)) - - -def hash_password(password): - - return generate_password_hash( - password, method='pbkdf2:sha512', salt_length=8) +from werkzeug.security import * +from os import environ + + +def generate_hash(string): + + msg = bytes(string, "utf-16") + + return hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"), + msg=msg, + digestmod='md5' + ).hexdigest() + + +def validate_hash(string, hashstr): + + return hmac.compare_digest(hashstr, generate_hash(string)) + + +def hash_password(password): + + return generate_password_hash( + password, method='pbkdf2:sha512', salt_length=8) diff --git a/files/helpers/session.py b/files/helpers/session.py old mode 100644 new mode 100755 index 3690427dd..1f2202ef0 --- a/files/helpers/session.py +++ b/files/helpers/session.py @@ -1,20 +1,20 @@ -from flask import * -import time -from .security import * - -def make_logged_out_formkey(t): - - s = f"{t}+{session['session_id']}" - - return generate_hash(s) - - -def validate_logged_out_formkey(t, k): - - now = int(time.time()) - if now - t > 3600: - return False - - s = f"{t}+{session['session_id']}" - - return validate_hash(s, k) +from flask import * +import time +from .security import * + +def make_logged_out_formkey(t): + + s = f"{t}+{session['session_id']}" + + return generate_hash(s) + + +def validate_logged_out_formkey(t, k): + + now = int(time.time()) + if now - t > 3600: + return False + + s = f"{t}+{session['session_id']}" + + return validate_hash(s, k) diff --git a/files/helpers/sqla_values.py b/files/helpers/sqla_values.py old mode 100644 new mode 100755 index a7bebd379..dbbfb5012 --- a/files/helpers/sqla_values.py +++ b/files/helpers/sqla_values.py @@ -1,33 +1,33 @@ -from sqlalchemy.ext.compiler import compiles -from sqlalchemy.sql.expression import FromClause - - -class values(FromClause): - named_with_column = True - - def __init__(self, columns, *args, **kwargs): - self._column_args = columns - self.list = args - self.alias_name = self.name = kwargs.pop('alias_name', None) - - def _populate_column_collection(self): - for c in self._column_args: - c._make_proxy(self) - - -@compiles(values) -def compile_values(element, compiler, asfrom=False): - columns = element.columns - v = "VALUES %s" % ", ".join( - "(%s)" % ", ".join( - compiler.render_literal_value(elem, column.type) - for elem, column in zip(tup, columns)) - for tup in element.list - ) - if asfrom: - if element.alias_name: - v = "(%s) AS %s (%s)" % (v, element.alias_name, - (", ".join(c.name for c in element.columns))) - else: - v = "(%s)" % v - return v +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.sql.expression import FromClause + + +class values(FromClause): + named_with_column = True + + def __init__(self, columns, *args, **kwargs): + self._column_args = columns + self.list = args + self.alias_name = self.name = kwargs.pop('alias_name', None) + + def _populate_column_collection(self): + for c in self._column_args: + c._make_proxy(self) + + +@compiles(values) +def compile_values(element, compiler, asfrom=False): + columns = element.columns + v = "VALUES %s" % ", ".join( + "(%s)" % ", ".join( + compiler.render_literal_value(elem, column.type) + for elem, column in zip(tup, columns)) + for tup in element.list + ) + if asfrom: + if element.alias_name: + v = "(%s) AS %s (%s)" % (v, element.alias_name, + (", ".join(c.name for c in element.columns))) + else: + v = "(%s)" % v + return v diff --git a/files/helpers/wrappers.py b/files/helpers/wrappers.py old mode 100644 new mode 100755 index 52845f68c..75336c2b0 --- a/files/helpers/wrappers.py +++ b/files/helpers/wrappers.py @@ -1,190 +1,190 @@ -from .get import * -from .alerts import send_notification -from files.helpers.const import * - - -def get_logged_in_user(): - - if request.headers.get("Authorization"): - token = request.headers.get("Authorization") - if not token: return None - - client = g.db.query(ClientAuth).options(lazyload('*')).filter(ClientAuth.access_token == token).first() - - x = (client.user, client) if client else (None, None) - - - else: - - uid = session.get("user_id") - nonce = session.get("login_nonce", 0) - if not uid: x= (None, None) - try: - if g.db: v = g.db.query(User).options(lazyload('*')).filter_by(id=uid).first() - else: v = None - except: v = None - - if v and v.agendaposter_expires_utc and v.agendaposter_expires_utc < g.timestamp: - v.agendaposter_expires_utc = 0 - v.agendaposter = False - - g.db.add(v) - - if v and (nonce < v.login_nonce): - x= (None, None) - else: - x=(v, None) - - - if x[0]: x[0].client=x[1] - - return x[0] - - -def check_ban_evade(v): - - if not v or not v.ban_evade or v.admin_level > 0: - return - - if random.randint(0,30) < v.ban_evade: - v.ban(reason="permaban evasion") - send_notification(NOTIFICATIONS_ACCOUNT, v, "Your account has been permanently suspended for the following reason:\n\n> permaban evasion") - - for post in g.db.query(Submission).options(lazyload('*')).filter_by(author_id=v.id).all(): - if post.is_banned: - continue - - post.is_banned=True - post.ban_reason="permaban evasion" - g.db.add(post) - - ma=ModAction( - kind="ban_post", - user_id=AUTOJANNY_ACCOUNT, - target_submission_id=post.id, - note="permaban evasion" - ) - g.db.add(ma) - - for comment in g.db.query(Comment).options(lazyload('*')).filter_by(author_id=v.id).all(): - if comment.is_banned: - continue - - comment.is_banned=True - comment.ban_reason="permaban evasion" - g.db.add(comment) - - try: - ma=ModAction( - kind="ban_comment", - user_id=AUTOJANNY_ACCOUNT, - target_comment_id=comment.id, - note="ban evasion" - ) - g.db.add(ma) - except: pass - - else: - v.ban_evade +=1 - g.db.add(v) - - g.db.commit() - - - -def auth_desired(f): - def wrapper(*args, **kwargs): - - v = get_logged_in_user() - check_ban_evade(v) - - resp = make_response(f(*args, v=v, **kwargs)) - return resp - - wrapper.__name__ = f.__name__ - return wrapper - - -def auth_required(f): - - def wrapper(*args, **kwargs): - - v = get_logged_in_user() - - if not v: abort(401) - - check_ban_evade(v) - - g.v = v - - resp = make_response(f(*args, v=v, **kwargs)) - return resp - - wrapper.__name__ = f.__name__ - return wrapper - - -def is_not_banned(f): - - def wrapper(*args, **kwargs): - - v = get_logged_in_user() - - if not v: abort(401) - - check_ban_evade(v) - - if v.is_suspended: - abort(403) - - g.v = v - - resp = make_response(f(*args, v=v, **kwargs)) - return resp - - wrapper.__name__ = f.__name__ - return wrapper - - -def admin_level_required(x): - - def wrapper_maker(f): - - def wrapper(*args, **kwargs): - - v = get_logged_in_user() - - if not v: abort(401) - - if v.admin_level < x: abort(403) - - g.v = v - - response = f(*args, v=v, **kwargs) - - if isinstance(response, tuple): resp = make_response(response[0]) - else: resp = make_response(response) - - return resp - - wrapper.__name__ = f.__name__ - return wrapper - - return wrapper_maker - - -def validate_formkey(f): - def wrapper(*args, v, **kwargs): - - if not request.headers.get("Authorization"): - - submitted_key = request.values.get("formkey", None) - - if not submitted_key: abort(401) - - elif not v.validate_formkey(submitted_key): abort(401) - - return f(*args, v=v, **kwargs) - - wrapper.__name__ = f.__name__ +from .get import * +from .alerts import send_notification +from files.helpers.const import * + + +def get_logged_in_user(): + + if request.headers.get("Authorization"): + token = request.headers.get("Authorization") + if not token: return None + + client = g.db.query(ClientAuth).options(lazyload('*')).filter(ClientAuth.access_token == token).first() + + x = (client.user, client) if client else (None, None) + + + else: + + uid = session.get("user_id") + nonce = session.get("login_nonce", 0) + if not uid: x= (None, None) + try: + if g.db: v = g.db.query(User).options(lazyload('*')).filter_by(id=uid).first() + else: v = None + except: v = None + + if v and v.agendaposter_expires_utc and v.agendaposter_expires_utc < g.timestamp: + v.agendaposter_expires_utc = 0 + v.agendaposter = False + + g.db.add(v) + + if v and (nonce < v.login_nonce): + x= (None, None) + else: + x=(v, None) + + + if x[0]: x[0].client=x[1] + + return x[0] + + +def check_ban_evade(v): + + if not v or not v.ban_evade or v.admin_level > 0: + return + + if random.randint(0,30) < v.ban_evade: + v.ban(reason="permaban evasion") + send_notification(NOTIFICATIONS_ACCOUNT, v, "Your account has been permanently suspended for the following reason:\n\n> permaban evasion") + + for post in g.db.query(Submission).options(lazyload('*')).filter_by(author_id=v.id).all(): + if post.is_banned: + continue + + post.is_banned=True + post.ban_reason="permaban evasion" + g.db.add(post) + + ma=ModAction( + kind="ban_post", + user_id=AUTOJANNY_ACCOUNT, + target_submission_id=post.id, + note="permaban evasion" + ) + g.db.add(ma) + + for comment in g.db.query(Comment).options(lazyload('*')).filter_by(author_id=v.id).all(): + if comment.is_banned: + continue + + comment.is_banned=True + comment.ban_reason="permaban evasion" + g.db.add(comment) + + try: + ma=ModAction( + kind="ban_comment", + user_id=AUTOJANNY_ACCOUNT, + target_comment_id=comment.id, + note="ban evasion" + ) + g.db.add(ma) + except: pass + + else: + v.ban_evade +=1 + g.db.add(v) + + g.db.commit() + + + +def auth_desired(f): + def wrapper(*args, **kwargs): + + v = get_logged_in_user() + check_ban_evade(v) + + resp = make_response(f(*args, v=v, **kwargs)) + return resp + + wrapper.__name__ = f.__name__ + return wrapper + + +def auth_required(f): + + def wrapper(*args, **kwargs): + + v = get_logged_in_user() + + if not v: abort(401) + + check_ban_evade(v) + + g.v = v + + resp = make_response(f(*args, v=v, **kwargs)) + return resp + + wrapper.__name__ = f.__name__ + return wrapper + + +def is_not_banned(f): + + def wrapper(*args, **kwargs): + + v = get_logged_in_user() + + if not v: abort(401) + + check_ban_evade(v) + + if v.is_suspended: + abort(403) + + g.v = v + + resp = make_response(f(*args, v=v, **kwargs)) + return resp + + wrapper.__name__ = f.__name__ + return wrapper + + +def admin_level_required(x): + + def wrapper_maker(f): + + def wrapper(*args, **kwargs): + + v = get_logged_in_user() + + if not v: abort(401) + + if v.admin_level < x: abort(403) + + g.v = v + + response = f(*args, v=v, **kwargs) + + if isinstance(response, tuple): resp = make_response(response[0]) + else: resp = make_response(response) + + return resp + + wrapper.__name__ = f.__name__ + return wrapper + + return wrapper_maker + + +def validate_formkey(f): + def wrapper(*args, v, **kwargs): + + if not request.headers.get("Authorization"): + + submitted_key = request.values.get("formkey", None) + + if not submitted_key: abort(401) + + elif not v.validate_formkey(submitted_key): abort(401) + + return f(*args, v=v, **kwargs) + + wrapper.__name__ = f.__name__ return wrapper \ No newline at end of file diff --git a/files/mail/__init__.py b/files/mail/__init__.py old mode 100644 new mode 100755 index 81252a240..b61f67efd --- a/files/mail/__init__.py +++ b/files/mail/__init__.py @@ -1,88 +1,88 @@ -from os import environ -import time -from flask import * -from urllib.parse import quote - -from files.helpers.security import * -from files.helpers.wrappers import * -from files.classes import * -from files.__main__ import app, mail, limiter -from flask_mail import Message - -site = environ.get("DOMAIN").strip() -name = environ.get("SITE_NAME").strip() - - -def send_mail(to_address, subject, html): - - msg = Message(html=html, subject=subject, sender=f"{name}@{site}", recipients=[to_address]) - mail.send(msg) - - -def send_verification_email(user, email=None): - - if not email: - email = user.email - - url = f"https://{app.config['SERVER_NAME']}/activate" - now = int(time.time()) - - token = generate_hash(f"{email}+{user.id}+{now}") - params = f"?email={quote(email)}&id={user.id}&time={now}&token={token}" - - link = url + params - - send_mail(to_address=email, - html=render_template("email/email_verify.html", - action_url=link, - v=user), - subject=f"Validate your {name} account email." - ) - - -@app.post("/verify_email") -@limiter.limit("1/second") -@auth_required -def api_verify_email(v): - - send_verification_email(v) - - return {"message": "Email has been sent (ETA ~5 minutes)"} - - -@app.get("/activate") -@auth_desired -def activate(v): - - email = request.values.get("email", "").strip() - id = request.values.get("id", "").strip() - timestamp = int(request.values.get("time", "0")) - token = request.values.get("token", "").strip() - - if int(time.time()) - timestamp > 3600: - return render_template("message.html", v=v, title="Verification link expired.", - message="That link has expired. Visit your settings to send yourself another verification email."), 410 - - if not validate_hash(f"{email}+{id}+{timestamp}", token): - abort(403) - - user = g.db.query(User).options(lazyload('*')).filter_by(id=id).first() - if not user: - abort(404) - - if user.is_activated and user.email == email: - return render_template("message_success.html", v=v, - title="Email already verified.", message="Email already verified."), 404 - - user.email = email - user.is_activated = True - - if not any([b.badge_id == 2 for b in user.badges]): - mail_badge = Badge(user_id=user.id, - badge_id=2) - g.db.add(mail_badge) - - g.db.add(user) - g.db.commit() - - return render_template("message_success.html", v=v, title="Email verified.", message=f"Your email {email} has been verified. Thank you.") +from os import environ +import time +from flask import * +from urllib.parse import quote + +from files.helpers.security import * +from files.helpers.wrappers import * +from files.classes import * +from files.__main__ import app, mail, limiter +from flask_mail import Message + +site = environ.get("DOMAIN").strip() +name = environ.get("SITE_NAME").strip() + + +def send_mail(to_address, subject, html): + + msg = Message(html=html, subject=subject, sender=f"{name}@{site}", recipients=[to_address]) + mail.send(msg) + + +def send_verification_email(user, email=None): + + if not email: + email = user.email + + url = f"https://{app.config['SERVER_NAME']}/activate" + now = int(time.time()) + + token = generate_hash(f"{email}+{user.id}+{now}") + params = f"?email={quote(email)}&id={user.id}&time={now}&token={token}" + + link = url + params + + send_mail(to_address=email, + html=render_template("email/email_verify.html", + action_url=link, + v=user), + subject=f"Validate your {name} account email." + ) + + +@app.post("/verify_email") +@limiter.limit("1/second") +@auth_required +def api_verify_email(v): + + send_verification_email(v) + + return {"message": "Email has been sent (ETA ~5 minutes)"} + + +@app.get("/activate") +@auth_desired +def activate(v): + + email = request.values.get("email", "").strip() + id = request.values.get("id", "").strip() + timestamp = int(request.values.get("time", "0")) + token = request.values.get("token", "").strip() + + if int(time.time()) - timestamp > 3600: + return render_template("message.html", v=v, title="Verification link expired.", + message="That link has expired. Visit your settings to send yourself another verification email."), 410 + + if not validate_hash(f"{email}+{id}+{timestamp}", token): + abort(403) + + user = g.db.query(User).options(lazyload('*')).filter_by(id=id).first() + if not user: + abort(404) + + if user.is_activated and user.email == email: + return render_template("message_success.html", v=v, + title="Email already verified.", message="Email already verified."), 404 + + user.email = email + user.is_activated = True + + if not any([b.badge_id == 2 for b in user.badges]): + mail_badge = Badge(user_id=user.id, + badge_id=2) + g.db.add(mail_badge) + + g.db.add(user) + g.db.commit() + + return render_template("message_success.html", v=v, title="Email verified.", message=f"Your email {email} has been verified. Thank you.") diff --git a/files/routes/__init__.py b/files/routes/__init__.py old mode 100644 new mode 100755 index 2ea3085df..c0f156676 --- a/files/routes/__init__.py +++ b/files/routes/__init__.py @@ -1,17 +1,17 @@ -from .admin import * -from .comments import * -from .discord import * -from .errors import * -from .reporting import * -from .front import * -from .login import * -from .oauth import * -from .posts import * -from .search import * -from .settings import * -from .static import * -from .users import * -from .votes import * -from .feeds import * -from .awards import * +from .admin import * +from .comments import * +from .discord import * +from .errors import * +from .reporting import * +from .front import * +from .login import * +from .oauth import * +from .posts import * +from .search import * +from .settings import * +from .static import * +from .users import * +from .votes import * +from .feeds import * +from .awards import * from .giphy import * \ No newline at end of file diff --git a/files/routes/admin.py b/files/routes/admin.py old mode 100644 new mode 100755 index f2240dfc4..cd0aaa5dd --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -1,1243 +1,1243 @@ -import time -from sqlalchemy.orm import lazyload -import imagehash -from os import remove -from PIL import Image as IMAGE - -from files.helpers.wrappers import * -from files.helpers.alerts import * -from files.helpers.sanitize import * -from files.helpers.markdown import * -from files.helpers.security import * -from files.helpers.get import * -from files.helpers.images import * -from files.helpers.const import * -from files.classes import * -from flask import * -from files.__main__ import app, cache, limiter -from .front import frontlist -from files.helpers.discord import add_role - -SITE_NAME = environ.get("SITE_NAME", "").strip() - -@app.get("/truescore") -@admin_level_required(6) -def truescore(v): - users = g.db.query(User).options(lazyload('*')).order_by(User.truecoins.desc()).limit(25).all() - return render_template("truescore.html", v=v, users=users) - - -@app.post("/@/revert_actions") -@limiter.limit("1/second") -@admin_level_required(6) -def revert_actions(v, username): - if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): - user = get_user(username) - if not user: abort(404) - - items = g.db.query(Submission).options(lazyload('*')).filter_by(removed_by=user.id).all() + g.db.query(Comment).options(lazyload('*')).filter_by(removed_by=user.id).all() - - for item in items: - item.is_banned = False - item.removed_by = None - g.db.add(item) - - users = g.db.query(User).options(lazyload('*')).filter_by(is_banned=user.id).all() - for user in users: - user.is_banned = 0 - user.unban_utc = 0 - g.db.add(user) - - g.db.commit() - return {"message": "Admin actions reverted!"} - -@app.post("/@/club_allow") -@limiter.limit("1/second") -@admin_level_required(6) -def club_allow(v, username): - - u = get_user(username, v=v) - - if not u: abort(404) - - if u.admin_level >= v.admin_level: return {"error": "noob"} - - u.club_allowed = True - u.club_banned = False - g.db.add(u) - - for x in u.alts_unique: - x.club_allowed = True - x.club_banned = False - g.db.add(x) - - - ma=ModAction( - kind="club_allow", - user_id=v.id, - target_user_id=u.id, - ) - g.db.add(ma) - - g.db.commit() - return {"message": f"@{username} has been allowed into the country club!"} - -@app.post("/@/club_ban") -@limiter.limit("1/second") -@admin_level_required(6) -def club_ban(v, username): - - u = get_user(username, v=v) - - if not u: abort(404) - - if u.admin_level >= v.admin_level: return {"error": "noob"} - - u.club_banned = True - u.club_allowed = False - - for x in u.alts_unique: - x.club_banned = True - u.club_allowed = False - g.db.add(x) - - ma=ModAction( - kind="club_ban", - user_id=v.id, - target_user_id=u.id, - ) - g.db.add(ma) - - g.db.commit() - return {"message": f"@{username} has been kicked from the country club. Deserved."} - - -@app.post("/@/make_admin") -@limiter.limit("1/second") -@admin_level_required(6) -def make_admin(v, username): - if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): - user = get_user(username) - if not user: abort(404) - user.admin_level = 6 - g.db.add(user) - g.db.commit() - return {"message": "User has been made admin!"} - - -@app.post("/@/remove_admin") -@limiter.limit("1/second") -@admin_level_required(6) -def remove_admin(v, username): - if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): - user = get_user(username) - if not user: abort(404) - user.admin_level = 0 - g.db.add(user) - g.db.commit() - return {"message": "Admin removed!"} - - -@app.post("/@/make_fake_admin") -@limiter.limit("1/second") -@admin_level_required(6) -def make_fake_admin(v, username): - if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): - user = get_user(username) - if not user: abort(404) - user.admin_level = 1 - g.db.add(user) - g.db.commit() - return {"message": "User has been made fake admin!"} - - -@app.post("/@/remove_fake_admin") -@limiter.limit("1/second") -@admin_level_required(6) -def remove_fake_admin(v, username): - if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): - user = get_user(username) - if not user: abort(404) - user.admin_level = 0 - g.db.add(user) - g.db.commit() - return {"message": "Fake admin removed!"} - - -@app.post("/admin/monthly") -@limiter.limit("1/day") -@admin_level_required(6) -def monthly(v): - if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): - thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id - for u in g.db.query(User).options(lazyload('*')).filter(User.patron > 0).all(): - grant_awards = {} - - if u.id == 1376: - grant_awards["fireflies"] = 100 - grant_awards["ban"] = 10 - elif u.patron == 1: - grant_awards["shit"] = 2 - grant_awards["fireflies"] = 2 - elif u.patron == 2: - grant_awards["shit"] = 5 - grant_awards["fireflies"] = 5 - grant_awards["ban"] = 1 - elif u.patron == 3: - grant_awards["shit"] = 10 - grant_awards["fireflies"] = 10 - grant_awards["ban"] = 2 - elif u.patron == 4: - grant_awards["shit"] = 25 - grant_awards["fireflies"] = 25 - grant_awards["ban"] = 5 - elif u.patron == 5 or u.patron == 8: - grant_awards["shit"] = 50 - grant_awards["fireflies"] = 50 - grant_awards["ban"] = 10 - - - for name in grant_awards: - for count in range(grant_awards[name]): - - thing += 1 - - award = AwardRelationship( - id=thing, - user_id=u.id, - kind=name - ) - - g.db.add(award) - - text = "You were given the following awards:\n\n" - - for key, value in grant_awards.items(): - text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n" - - send_notification(NOTIFICATIONS_ACCOUNT, u, text) - - - g.db.commit() - return {"message": "Monthly awards granted"} - - -@app.get('/admin/rules') -@admin_level_required(6) -def get_rules(v): - - try: - with open(f'./{SITE_NAME} rules.html', 'r') as f: - rules = f.read() - except Exception: - rules = None - - return render_template('admin/rules.html', v=v, rules=rules) - - -@app.post('/admin/rules') -@limiter.limit("1/second") -@admin_level_required(6) -@validate_formkey -def post_rules(v): - - text = request.values.get('rules', '').strip() - - with open(f'./{SITE_NAME} rules.html', 'w+') as f: - f.write(text) - - with open(f'./{SITE_NAME} rules.html', 'r') as f: - rules = f.read() - - return render_template('admin/rules.html', v=v, rules=rules) - - -@app.get("/admin/shadowbanned") -@auth_required -def shadowbanned(v): - if not (v and v.admin_level == 6): abort(404) - users = [x for x in g.db.query(User).options(lazyload('*')).filter(User.shadowbanned != None).all()] - return render_template("banned.html", v=v, users=users) - - -@app.get("/admin/agendaposters") -@auth_required -def agendaposters(v): - if not (v and v.admin_level == 6): abort(404) - users = [x for x in g.db.query(User).options(lazyload('*')).filter_by(agendaposter = True).all()] - return render_template("banned.html", v=v, users=users) - - -@app.get("/admin/image_posts") -@admin_level_required(3) -def image_posts_listing(v): - - try: page = int(request.values.get('page', 1)) - except: page = 1 - - posts = g.db.query(Submission).order_by(Submission.id.desc()) - - firstrange = 25 * (page - 1) - secondrange = firstrange+26 - posts = [x.id for x in posts if x.is_image][firstrange:secondrange] - next_exists = (len(posts) > 25) - posts = get_posts(posts[:25], v=v) - - return render_template("admin/image_posts.html", v=v, listing=posts, next_exists=next_exists, page=page, sort="new") - - -@app.get("/admin/reported/posts") -@admin_level_required(3) -def reported_posts(v): - - page = max(1, int(request.values.get("page", 1))) - - posts = g.db.query(Submission).options(lazyload('*')).filter_by( - is_approved=0, - is_banned=False - ).order_by(Submission.id.desc()).offset(25 * (page - 1)).limit(26) - - listing = [p.id for p in posts] - next_exists = (len(listing) > 25) - listing = listing[:25] - - listing = get_posts(listing, v=v) - - return render_template("admin/reported_posts.html", - next_exists=next_exists, listing=listing, page=page, v=v) - - -@app.get("/admin/reported/comments") -@admin_level_required(3) -def reported_comments(v): - - page = max(1, int(request.values.get("page", 1))) - - posts = g.db.query(Comment - ).filter_by( - is_approved=0, - is_banned=False - ).order_by(Comment.id.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [p.id for p in posts] - next_exists = (len(listing) > 25) - listing = listing[:25] - - listing = get_comments(listing, v=v) - - return render_template("admin/reported_comments.html", - next_exists=next_exists, - listing=listing, - page=page, - v=v, - standalone=True) - -@app.get("/admin") -@admin_level_required(3) -def admin_home(v): - with open('./disablesignups', 'r') as f: - x = f.read() - return render_template("admin/admin_home.html", v=v, x=x) - -@app.post("/admin/disablesignups") -@admin_level_required(6) -@validate_formkey -def disablesignups(v): - with open('./disablesignups', 'r+') as f: - if f.read() == "yes": - f.write("no") - return {"message": "Signups enabed!"} - else: - f.write("yes") - return {"message": "Signups disabled!"} - -@app.get("/admin/badge_grant") -@admin_level_required(4) -def badge_grant_get(v): - - badge_types = g.db.query(BadgeDef).all() - - errors = {"already_owned": "That user already has that badge.", - "no_user": "That user doesn't exist." - } - - return render_template("admin/badge_grant.html", - v=v, - badge_types=badge_types, - error=errors.get( - request.values.get("error"), - None) if request.values.get('error') else None, - msg="Badge successfully assigned" if request.values.get( - "msg") else None - ) - - -@app.post("/admin/badge_grant") -@limiter.limit("1/second") -@admin_level_required(4) -@validate_formkey -def badge_grant_post(v): - - user = get_user(request.values.get("username").strip(), graceful=True) - if not user: return redirect("/badge_grant?error=no_user") - - try: badge_id = int(request.values.get("badge_id")) - except: abort(400) - - if user.has_badge(badge_id): - g.db.query(Badge).options(lazyload('*')).filter_by(badge_id=badge_id, user_id=user.id,).delete() - g.db.commit() - return redirect("/admin/badge_grant") - - new_badge = Badge(badge_id=badge_id, - user_id=user.id, - ) - - desc = request.values.get("description") - if desc: new_badge.description = desc - - url = request.values.get("url") - if url: new_badge.url = url - - g.db.add(new_badge) - g.db.flush() - - text = f""" - @{v.username} has given you the following profile badge: - \n\n![]({new_badge.path}) - \n\n{new_badge.name} - """ - send_notification(NOTIFICATIONS_ACCOUNT, user, text) - - if badge_id in [21,22,23,24,25,28]: - user.patron = int(str(badge_id)[-1]) - - grant_awards = {} - - if badge_id == 21: - if user.discord_id: add_role(user, "1") - grant_awards["shit"] = 2 - grant_awards["fireflies"] = 2 - elif badge_id == 22: - if user.discord_id: add_role(user, "2") - grant_awards["shit"] = 5 - grant_awards["fireflies"] = 5 - grant_awards["ban"] = 1 - elif badge_id == 23: - if user.discord_id: add_role(user, "3") - grant_awards["shit"] = 10 - grant_awards["fireflies"] = 10 - grant_awards["ban"] = 2 - elif badge_id in [24, 28]: - if user.discord_id: add_role(user, "4") - grant_awards["shit"] = 25 - grant_awards["fireflies"] = 25 - grant_awards["ban"] = 5 - elif badge_id == 25: - if user.discord_id: add_role(user, "5") - grant_awards["shit"] = 50 - grant_awards["fireflies"] = 50 - grant_awards["ban"] = 10 - - if len(grant_awards): - - thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id - - for name in grant_awards: - for count in range(grant_awards[name]): - - thing += 1 - - award = AwardRelationship( - id=thing, - user_id=user.id, - kind=name - ) - - g.db.add(award) - - text = "You were given the following awards:\n\n" - - for key, value in grant_awards.items(): - text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n" - - send_notification(NOTIFICATIONS_ACCOUNT, user, text) - - - g.db.add(user) - - - g.db.commit() - return redirect("/admin/badge_grant") - - -@app.get("/admin/users") -@admin_level_required(2) -def users_list(v): - - page = int(request.values.get("page", 1)) - - users = g.db.query(User).options(lazyload('*')).filter_by(is_banned=0 - ).order_by(User.created_utc.desc() - ).offset(25 * (page - 1)).limit(26) - - users = [x for x in users] - - next_exists = (len(users) > 25) - users = users[:25] - - return render_template("admin/new_users.html", - v=v, - users=users, - next_exists=next_exists, - page=page, - ) - -@app.get("/admin/alt_votes") -@admin_level_required(4) -def alt_votes_get(v): - - if not request.values.get("u1") or not request.values.get("u2"): - return render_template("admin/alt_votes.html", v=v) - - u1 = request.values.get("u1") - u2 = request.values.get("u2") - - if not u1 or not u2: - return redirect("/admin/alt_votes") - - u1 = get_user(u1) - u2 = get_user(u2) - - u1_post_ups = g.db.query( - Vote.submission_id).filter_by( - user_id=u1.id, - vote_type=1).all() - u1_post_downs = g.db.query( - Vote.submission_id).filter_by( - user_id=u1.id, - vote_type=-1).all() - u1_comment_ups = g.db.query( - CommentVote.comment_id).filter_by( - user_id=u1.id, - vote_type=1).all() - u1_comment_downs = g.db.query( - CommentVote.comment_id).filter_by( - user_id=u1.id, - vote_type=-1).all() - u2_post_ups = g.db.query( - Vote.submission_id).filter_by( - user_id=u2.id, - vote_type=1).all() - u2_post_downs = g.db.query( - Vote.submission_id).filter_by( - user_id=u2.id, - vote_type=-1).all() - u2_comment_ups = g.db.query( - CommentVote.comment_id).filter_by( - user_id=u2.id, - vote_type=1).all() - u2_comment_downs = g.db.query( - CommentVote.comment_id).filter_by( - user_id=u2.id, - vote_type=-1).all() - - data = {} - data['u1_only_post_ups'] = len( - [x for x in u1_post_ups if x not in u2_post_ups]) - data['u2_only_post_ups'] = len( - [x for x in u2_post_ups if x not in u1_post_ups]) - data['both_post_ups'] = len(list(set(u1_post_ups) & set(u2_post_ups))) - - data['u1_only_post_downs'] = len( - [x for x in u1_post_downs if x not in u2_post_downs]) - data['u2_only_post_downs'] = len( - [x for x in u2_post_downs if x not in u1_post_downs]) - data['both_post_downs'] = len( - list(set(u1_post_downs) & set(u2_post_downs))) - - data['u1_only_comment_ups'] = len( - [x for x in u1_comment_ups if x not in u2_comment_ups]) - data['u2_only_comment_ups'] = len( - [x for x in u2_comment_ups if x not in u1_comment_ups]) - data['both_comment_ups'] = len( - list(set(u1_comment_ups) & set(u2_comment_ups))) - - data['u1_only_comment_downs'] = len( - [x for x in u1_comment_downs if x not in u2_comment_downs]) - data['u2_only_comment_downs'] = len( - [x for x in u2_comment_downs if x not in u1_comment_downs]) - data['both_comment_downs'] = len( - list(set(u1_comment_downs) & set(u2_comment_downs))) - - data['u1_post_ups_unique'] = 100 * \ - data['u1_only_post_ups'] // len(u1_post_ups) if u1_post_ups else 0 - data['u2_post_ups_unique'] = 100 * \ - data['u2_only_post_ups'] // len(u2_post_ups) if u2_post_ups else 0 - data['u1_post_downs_unique'] = 100 * \ - data['u1_only_post_downs'] // len( - u1_post_downs) if u1_post_downs else 0 - data['u2_post_downs_unique'] = 100 * \ - data['u2_only_post_downs'] // len( - u2_post_downs) if u2_post_downs else 0 - - data['u1_comment_ups_unique'] = 100 * \ - data['u1_only_comment_ups'] // len( - u1_comment_ups) if u1_comment_ups else 0 - data['u2_comment_ups_unique'] = 100 * \ - data['u2_only_comment_ups'] // len( - u2_comment_ups) if u2_comment_ups else 0 - data['u1_comment_downs_unique'] = 100 * \ - data['u1_only_comment_downs'] // len( - u1_comment_downs) if u1_comment_downs else 0 - data['u2_comment_downs_unique'] = 100 * \ - data['u2_only_comment_downs'] // len( - u2_comment_downs) if u2_comment_downs else 0 - - return render_template("admin/alt_votes.html", - u1=u1, - u2=u2, - v=v, - data=data - ) - - -@app.post("/admin/link_accounts") -@limiter.limit("1/second") -@admin_level_required(4) -@validate_formkey -def admin_link_accounts(v): - - u1 = int(request.values.get("u1")) - u2 = int(request.values.get("u2")) - - new_alt = Alt( - user1=u1, - user2=u2, - is_manual=True - ) - - g.db.add(new_alt) - - g.db.commit() - return redirect(f"/admin/alt_votes?u1={g.db.query(User).get(u1).username}&u2={g.db.query(User).get(u2).username}") - - -@app.get("/admin/removed") -@admin_level_required(3) -def admin_removed(v): - - page = int(request.values.get("page", 1)) - - shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] - - ids = g.db.query(Submission.id).options(lazyload('*')).filter(or_(Submission.is_banned==True, Submission.author_id.in_(shadowbanned))).order_by(Submission.id.desc()).offset(25 * (page - 1)).limit(26).all() - - ids=[x[0] for x in ids] - - next_exists = len(ids) > 25 - - ids = ids[:25] - - posts = get_posts(ids, v=v) - - return render_template("admin/removed_posts.html", - v=v, - listing=posts, - page=page, - next_exists=next_exists - ) - - - -@app.post("/agendaposter/") -@admin_level_required(6) -@validate_formkey -def agendaposter(user_id, v): - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - - expiry = request.values.get("days", 0) - if expiry: - expiry = int(expiry) - expiry = g.timestamp + expiry*60*60*24 - else: - expiry = 0 - - user.agendaposter = not user.agendaposter - user.agendaposter_expires_utc = expiry - g.db.add(user) - for alt in user.alts: - if alt.admin_level > 0: break - alt.agendaposter = user.agendaposter - alt.agendaposter_expires_utc = expiry - g.db.add(alt) - - note = None - - if not user.agendaposter: kind = "unagendaposter" - else: - kind = "agendaposter" - note = f"for {request.values.get('days')} days" if expiry else "never expires" - - ma = ModAction( - kind=kind, - user_id=v.id, - target_user_id=user.id, - note = note - ) - g.db.add(ma) - - if user.agendaposter: - if not user.has_badge(26): - badge = Badge(user_id=user.id, badge_id=26) - g.db.add(badge) - else: - badge = user.has_badge(26) - if badge: g.db.delete(badge) - - if user.agendaposter: send_notification(NOTIFICATIONS_ACCOUNT, user, f"You have been marked by an admin as an agendaposter ({note}).") - else: send_notification(NOTIFICATIONS_ACCOUNT, user, f"You have been unmarked by an admin as an agendaposter.") - - g.db.commit() - if user.agendaposter: return redirect(user.url) - return {"message": "Agendaposter theme disabled!"} - - -@app.post("/shadowban/") -@limiter.limit("1/second") -@admin_level_required(6) -@validate_formkey -def shadowban(user_id, v): - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - if user.admin_level != 0: abort(403) - user.shadowbanned = v.username - g.db.add(user) - for alt in user.alts: - if alt.admin_level > 0: break - alt.shadowbanned = v.username - g.db.add(alt) - ma = ModAction( - kind="shadowban", - user_id=v.id, - target_user_id=user.id, - ) - g.db.add(ma) - - cache.delete_memoized(frontlist) - - g.db.commit() - return {"message": "User shadowbanned!"} - - -@app.post("/unshadowban/") -@limiter.limit("1/second") -@admin_level_required(6) -@validate_formkey -def unshadowban(user_id, v): - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - if user.admin_level != 0: abort(403) - user.shadowbanned = None - g.db.add(user) - for alt in user.alts: - alt.shadowbanned = None - g.db.add(alt) - - ma = ModAction( - kind="unshadowban", - user_id=v.id, - target_user_id=user.id, - ) - g.db.add(ma) - - cache.delete_memoized(frontlist) - - g.db.commit() - return {"message": "User unshadowbanned!"} - -@app.post("/admin/verify/") -@limiter.limit("1/second") -@admin_level_required(6) -@validate_formkey -def verify(user_id, v): - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - user.verified = "Verified" - g.db.add(user) - g.db.commit() - return {"message": "User verfied!"} - -@app.post("/admin/unverify/") -@limiter.limit("1/second") -@admin_level_required(6) -@validate_formkey -def unverify(user_id, v): - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - user.verified = None - g.db.add(user) - g.db.commit() - return {"message": "User unverified!"} - - -@app.post("/admin/title_change/") -@limiter.limit("1/second") -@admin_level_required(6) -@validate_formkey -def admin_title_change(user_id, v): - - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - - if user.admin_level != 0: abort(403) - - new_name=request.values.get("title").strip() - - user.customtitleplain=new_name - new_name = sanitize(new_name) - - user=g.db.query(User).with_for_update().options(lazyload('*')).filter_by(id=user.id).first() - user.customtitle=new_name - user.flairchanged = bool(request.values.get("locked")) - g.db.add(user) - - if user.flairchanged: kind = "set_flair_locked" - else: kind = "set_flair_notlocked" - - ma=ModAction( - kind=kind, - user_id=v.id, - target_user_id=user.id, - note=f'"{new_name}"' - ) - g.db.add(ma) - g.db.commit() - - return redirect(user.url) - -@app.post("/ban_user/") -@limiter.limit("1/second") -@admin_level_required(6) -@validate_formkey -def ban_user(user_id, v): - - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - - if user.admin_level >= v.admin_level: abort(403) - - if 'form' in request.values: - days = float(request.values.get("days")) if request.values.get('days') else 0 - reason = sanitize(request.values.get("reason", "")) - message = request.values.get("reason", "").strip() - else: - days = float(request.values.get("days")) if request.values.get('days') else 0 - reason = sanitize(request.values.get("reason", "")) - message = request.values.get("reason", "").strip() - - if not user: abort(400) - - if days > 0: - if message: - text = f"Your account has been suspended for {days} days for the following reason:\n\n> {message}" - else: - text = f"Your account has been suspended for {days} days." - user.ban(admin=v, reason=reason, days=days) - - else: - if message: - text = f"Your account has been permanently suspended for the following reason:\n\n> {message}" - else: - text = "Your account has been permanently suspended." - - user.ban(admin=v, reason=reason) - - if request.values.get("alts", ""): - for x in user.alts: - if x.admin_level > 0: break - x.ban(admin=v, reason=reason) - - send_notification(NOTIFICATIONS_ACCOUNT, user, text[:128]) - - if days == 0: duration = "permanent" - elif days == 1: duration = "1 day" - else: duration = f"{days} days" - ma=ModAction( - kind="ban_user", - user_id=v.id, - target_user_id=user.id, - note=f'reason: "{reason}", duration: {duration}' - ) - g.db.add(ma) - - if 'reason' in request.values: - if reason.startswith("/post/"): - try: - post = int(reason.split("/post/")[1]) - post = get_post(post) - post.bannedfor = True - g.db.add(post) - except: pass - elif reason.startswith("/comment/"): - try: - comment = int(reason.split("/comment/")[1]) - comment = get_comment(comment) - comment.bannedfor = True - g.db.add(comment) - except: pass - g.db.commit() - - if 'redir' in request.values: return redirect(user.url) - else: return {"message": f"@{user.username} was banned!"} - - -@app.post("/unban_user/") -@limiter.limit("1/second") -@admin_level_required(6) -@validate_formkey -def unban_user(user_id, v): - - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - - if not user: - abort(400) - - user.is_banned = 0 - user.unban_utc = 0 - g.db.add(user) - - if request.values.get("alts", ""): - for x in user.alts: - if x.admin_level == 0: - x.is_banned = 0 - x.unban_utc = 0 - g.db.add(x) - - send_notification(NOTIFICATIONS_ACCOUNT, user, - "Your account has been reinstated. Please carefully review and abide by the [rules](/post/2510) to ensure that you don't get suspended again.") - - ma=ModAction( - kind="unban_user", - user_id=v.id, - target_user_id=user.id, - ) - g.db.add(ma) - - g.db.commit() - - if "@" in request.referrer: return redirect(user.url) - else: return {"message": f"@{user.username} was unbanned!"} - - -@app.post("/ban_post/") -@limiter.limit("1/second") -@admin_level_required(3) -@validate_formkey -def ban_post(post_id, v): - - post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() - - if not post: - abort(400) - - post.is_banned = True - post.is_approved = 0 - post.stickied = None - post.is_pinned = False - post.removed_by = v.id - - ban_reason=request.values.get("reason", "") - ban_reason = CustomRenderer().render(mistletoe.Document(ban_reason)) - ban_reason = sanitize(ban_reason) - - post.ban_reason = ban_reason - - g.db.add(post) - - - - ma=ModAction( - kind="ban_post", - user_id=v.id, - target_submission_id=post.id, - ) - g.db.add(ma) - - cache.delete_memoized(frontlist) - - v.coins += 1 - g.db.add(v) - - g.db.commit() - - return {"message": "Post removed!"} - - -@app.post("/unban_post/") -@limiter.limit("1/second") -@admin_level_required(3) -@validate_formkey -def unban_post(post_id, v): - - post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() - - if not post: - abort(400) - - if post.is_banned: - ma=ModAction( - kind="unban_post", - user_id=v.id, - target_submission_id=post.id, - ) - g.db.add(ma) - - post.is_banned = False - post.is_approved = v.id - - g.db.add(post) - - cache.delete_memoized(frontlist) - - v.coins -= 1 - g.db.add(v) - - g.db.commit() - - return {"message": "Post approved!"} - - -@app.post("/distinguish/") -@admin_level_required(1) -@validate_formkey -def api_distinguish_post(post_id, v): - - post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() - - if not post: - abort(404) - - if not post.author_id == v.id: - abort(403) - - if post.distinguish_level: - post.distinguish_level = 0 - else: - post.distinguish_level = v.admin_level - - g.db.add(post) - - g.db.commit() - - return {"message": "Post distinguished!"} - - -@app.post("/sticky/") -@admin_level_required(3) -def api_sticky_post(post_id, v): - - post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() - if post: - if post.stickied: post.stickied = None - else: post.stickied = v.username - g.db.add(post) - - ma=ModAction( - kind="pin_post" if post.stickied else "unpin_post", - user_id=v.id, - target_submission_id=post.id - ) - g.db.add(ma) - - cache.delete_memoized(frontlist) - - g.db.commit() - if post.stickied: return {"message": "Post pinned!"} - else: return {"message": "Post unpinned!"} - -@app.post("/pin/") -@auth_required -def api_pin_post(post_id, v): - - post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() - if post: - post.is_pinned = not post.is_pinned - g.db.add(post) - g.db.commit() - - if post.is_pinned: return {"message": "Post pinned!"} - else: return {"message": "Post unpinned!"} - -@app.post("/ban_comment/") -@limiter.limit("1/second") -@admin_level_required(1) -def api_ban_comment(c_id, v): - - comment = g.db.query(Comment).options(lazyload('*')).filter_by(id=c_id).first() - if not comment: - abort(404) - - comment.is_banned = True - comment.is_approved = 0 - comment.removed_by = v.id - - g.db.add(comment) - ma=ModAction( - kind="ban_comment", - user_id=v.id, - target_comment_id=comment.id, - ) - g.db.add(ma) - g.db.commit() - return {"message": "Comment removed!"} - - -@app.post("/unban_comment/") -@limiter.limit("1/second") -@admin_level_required(1) -def api_unban_comment(c_id, v): - - comment = g.db.query(Comment).options(lazyload('*')).filter_by(id=c_id).first() - if not comment: - abort(404) - g.db.add(comment) - - if comment.is_banned: - ma=ModAction( - kind="unban_comment", - user_id=v.id, - target_comment_id=comment.id, - ) - g.db.add(ma) - - comment.is_banned = False - comment.is_approved = v.id - - g.db.commit() - - return {"message": "Comment approved!"} - - -@app.post("/distinguish_comment/") -@auth_required -def admin_distinguish_comment(c_id, v): - - if v.admin_level == 0: abort(403) - - comment = get_comment(c_id, v=v) - - if comment.author_id != v.id: - abort(403) - - comment.distinguish_level = 0 if comment.distinguish_level else v.admin_level - - g.db.add(comment) - html=render_template( - "comments.html", - v=v, - comments=[comment], - ) - - html=str(BeautifulSoup(html, features="html.parser").find(id=f"comment-{comment.id}-only")) - - g.db.commit() - - return html - -@app.get("/admin/dump_cache") -@admin_level_required(6) -def admin_dump_cache(v): - cache.clear() - return {"message": "Internal cache cleared."} - - -@app.get("/admin/banned_domains/") -@admin_level_required(4) -def admin_banned_domains(v): - - banned_domains = g.db.query(BannedDomain).all() - return render_template("admin/banned_domains.html", v=v, banned_domains=banned_domains) - -@app.post("/admin/banned_domains") -@limiter.limit("1/second") -@admin_level_required(4) -@validate_formkey -def admin_toggle_ban_domain(v): - - domain=request.values.get("domain", "").strip() - if not domain: abort(400) - - reason=request.values.get("reason", "").strip() - - d = g.db.query(BannedDomain).options(lazyload('*')).filter_by(domain=domain).first() - if d: g.db.delete(d) - else: - d = BannedDomain(domain=domain, reason=reason) - g.db.add(d) - - g.db.commit() - - return redirect("/admin/banned_domains/") - - -@app.post("/admin/nuke_user") -@limiter.limit("1/second") -@admin_level_required(4) -@validate_formkey -def admin_nuke_user(v): - - user=get_user(request.values.get("user")) - - for post in g.db.query(Submission).options(lazyload('*')).filter_by(author_id=user.id).all(): - if post.is_banned: - continue - - post.is_banned=True - g.db.add(post) - - for comment in g.db.query(Comment).options(lazyload('*')).filter_by(author_id=user.id).all(): - if comment.is_banned: - continue - - comment.is_banned=True - g.db.add(comment) - - ma=ModAction( - kind="nuke_user", - user_id=v.id, - target_user_id=user.id, - ) - g.db.add(ma) - - g.db.commit() - - return redirect(user.url) - - -@app.post("/admin/unnuke_user") -@limiter.limit("1/second") -@admin_level_required(4) -@validate_formkey -def admin_nunuke_user(v): - - user=get_user(request.values.get("user")) - - for post in g.db.query(Submission).options(lazyload('*')).filter_by(author_id=user.id).all(): - if not post.is_banned: - continue - - post.is_banned=False - g.db.add(post) - - for comment in g.db.query(Comment).options(lazyload('*')).filter_by(author_id=user.id).all(): - if not comment.is_banned: - continue - - comment.is_banned=False - g.db.add(comment) - - ma=ModAction( - kind="unnuke_user", - user_id=v.id, - target_user_id=user.id, - ) - g.db.add(ma) - - g.db.commit() - +import time +from sqlalchemy.orm import lazyload +import imagehash +from os import remove +from PIL import Image as IMAGE + +from files.helpers.wrappers import * +from files.helpers.alerts import * +from files.helpers.sanitize import * +from files.helpers.markdown import * +from files.helpers.security import * +from files.helpers.get import * +from files.helpers.images import * +from files.helpers.const import * +from files.classes import * +from flask import * +from files.__main__ import app, cache, limiter +from .front import frontlist +from files.helpers.discord import add_role + +SITE_NAME = environ.get("SITE_NAME", "").strip() + +@app.get("/truescore") +@admin_level_required(6) +def truescore(v): + users = g.db.query(User).options(lazyload('*')).order_by(User.truecoins.desc()).limit(25).all() + return render_template("truescore.html", v=v, users=users) + + +@app.post("/@/revert_actions") +@limiter.limit("1/second") +@admin_level_required(6) +def revert_actions(v, username): + if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): + user = get_user(username) + if not user: abort(404) + + items = g.db.query(Submission).options(lazyload('*')).filter_by(removed_by=user.id).all() + g.db.query(Comment).options(lazyload('*')).filter_by(removed_by=user.id).all() + + for item in items: + item.is_banned = False + item.removed_by = None + g.db.add(item) + + users = g.db.query(User).options(lazyload('*')).filter_by(is_banned=user.id).all() + for user in users: + user.is_banned = 0 + user.unban_utc = 0 + g.db.add(user) + + g.db.commit() + return {"message": "Admin actions reverted!"} + +@app.post("/@/club_allow") +@limiter.limit("1/second") +@admin_level_required(6) +def club_allow(v, username): + + u = get_user(username, v=v) + + if not u: abort(404) + + if u.admin_level >= v.admin_level: return {"error": "noob"} + + u.club_allowed = True + u.club_banned = False + g.db.add(u) + + for x in u.alts_unique: + x.club_allowed = True + x.club_banned = False + g.db.add(x) + + + ma=ModAction( + kind="club_allow", + user_id=v.id, + target_user_id=u.id, + ) + g.db.add(ma) + + g.db.commit() + return {"message": f"@{username} has been allowed into the country club!"} + +@app.post("/@/club_ban") +@limiter.limit("1/second") +@admin_level_required(6) +def club_ban(v, username): + + u = get_user(username, v=v) + + if not u: abort(404) + + if u.admin_level >= v.admin_level: return {"error": "noob"} + + u.club_banned = True + u.club_allowed = False + + for x in u.alts_unique: + x.club_banned = True + u.club_allowed = False + g.db.add(x) + + ma=ModAction( + kind="club_ban", + user_id=v.id, + target_user_id=u.id, + ) + g.db.add(ma) + + g.db.commit() + return {"message": f"@{username} has been kicked from the country club. Deserved."} + + +@app.post("/@/make_admin") +@limiter.limit("1/second") +@admin_level_required(6) +def make_admin(v, username): + if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): + user = get_user(username) + if not user: abort(404) + user.admin_level = 6 + g.db.add(user) + g.db.commit() + return {"message": "User has been made admin!"} + + +@app.post("/@/remove_admin") +@limiter.limit("1/second") +@admin_level_required(6) +def remove_admin(v, username): + if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): + user = get_user(username) + if not user: abort(404) + user.admin_level = 0 + g.db.add(user) + g.db.commit() + return {"message": "Admin removed!"} + + +@app.post("/@/make_fake_admin") +@limiter.limit("1/second") +@admin_level_required(6) +def make_fake_admin(v, username): + if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): + user = get_user(username) + if not user: abort(404) + user.admin_level = 1 + g.db.add(user) + g.db.commit() + return {"message": "User has been made fake admin!"} + + +@app.post("/@/remove_fake_admin") +@limiter.limit("1/second") +@admin_level_required(6) +def remove_fake_admin(v, username): + if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): + user = get_user(username) + if not user: abort(404) + user.admin_level = 0 + g.db.add(user) + g.db.commit() + return {"message": "Fake admin removed!"} + + +@app.post("/admin/monthly") +@limiter.limit("1/day") +@admin_level_required(6) +def monthly(v): + if 'pcm' in request.host or ('rama' in request.host and v.id in [1,12,28,29,747,995,1480]) or ('rama' not in request.host and 'pcm' not in request.host): + thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id + for u in g.db.query(User).options(lazyload('*')).filter(User.patron > 0).all(): + grant_awards = {} + + if u.id == 1376: + grant_awards["fireflies"] = 40 + grant_awards["ban"] = 10 + elif u.patron == 1: + grant_awards["shit"] = 1 + grant_awards["fireflies"] = 1 + elif u.patron == 2: + grant_awards["shit"] = 2 + grant_awards["fireflies"] = 2 + grant_awards["ban"] = 1 + elif u.patron == 3: + grant_awards["shit"] = 5 + grant_awards["fireflies"] = 5 + grant_awards["ban"] = 2 + elif u.patron == 4: + grant_awards["shit"] = 10 + grant_awards["fireflies"] = 10 + grant_awards["ban"] = 5 + elif u.patron == 5 or u.patron == 8: + grant_awards["shit"] = 20 + grant_awards["fireflies"] = 20 + grant_awards["ban"] = 10 + + + for name in grant_awards: + for count in range(grant_awards[name]): + + thing += 1 + + award = AwardRelationship( + id=thing, + user_id=u.id, + kind=name + ) + + g.db.add(award) + + text = "You were given the following awards:\n\n" + + for key, value in grant_awards.items(): + text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n" + + send_notification(NOTIFICATIONS_ACCOUNT, u, text) + + + g.db.commit() + return {"message": "Monthly awards granted"} + + +@app.get('/admin/rules') +@admin_level_required(6) +def get_rules(v): + + try: + with open(f'./{SITE_NAME} rules.html', 'r') as f: + rules = f.read() + except Exception: + rules = None + + return render_template('admin/rules.html', v=v, rules=rules) + + +@app.post('/admin/rules') +@limiter.limit("1/second") +@admin_level_required(6) +@validate_formkey +def post_rules(v): + + text = request.values.get('rules', '').strip() + + with open(f'./{SITE_NAME} rules.html', 'w+') as f: + f.write(text) + + with open(f'./{SITE_NAME} rules.html', 'r') as f: + rules = f.read() + + return render_template('admin/rules.html', v=v, rules=rules) + + +@app.get("/admin/shadowbanned") +@auth_required +def shadowbanned(v): + if not (v and v.admin_level == 6): abort(404) + users = [x for x in g.db.query(User).options(lazyload('*')).filter(User.shadowbanned != None).all()] + return render_template("banned.html", v=v, users=users) + + +@app.get("/admin/agendaposters") +@auth_required +def agendaposters(v): + if not (v and v.admin_level == 6): abort(404) + users = [x for x in g.db.query(User).options(lazyload('*')).filter_by(agendaposter = True).all()] + return render_template("banned.html", v=v, users=users) + + +@app.get("/admin/image_posts") +@admin_level_required(3) +def image_posts_listing(v): + + try: page = int(request.values.get('page', 1)) + except: page = 1 + + posts = g.db.query(Submission).order_by(Submission.id.desc()) + + firstrange = 25 * (page - 1) + secondrange = firstrange+26 + posts = [x.id for x in posts if x.is_image][firstrange:secondrange] + next_exists = (len(posts) > 25) + posts = get_posts(posts[:25], v=v) + + return render_template("admin/image_posts.html", v=v, listing=posts, next_exists=next_exists, page=page, sort="new") + + +@app.get("/admin/reported/posts") +@admin_level_required(3) +def reported_posts(v): + + page = max(1, int(request.values.get("page", 1))) + + posts = g.db.query(Submission).options(lazyload('*')).filter_by( + is_approved=0, + is_banned=False + ).order_by(Submission.id.desc()).offset(25 * (page - 1)).limit(26) + + listing = [p.id for p in posts] + next_exists = (len(listing) > 25) + listing = listing[:25] + + listing = get_posts(listing, v=v) + + return render_template("admin/reported_posts.html", + next_exists=next_exists, listing=listing, page=page, v=v) + + +@app.get("/admin/reported/comments") +@admin_level_required(3) +def reported_comments(v): + + page = max(1, int(request.values.get("page", 1))) + + posts = g.db.query(Comment + ).filter_by( + is_approved=0, + is_banned=False + ).order_by(Comment.id.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [p.id for p in posts] + next_exists = (len(listing) > 25) + listing = listing[:25] + + listing = get_comments(listing, v=v) + + return render_template("admin/reported_comments.html", + next_exists=next_exists, + listing=listing, + page=page, + v=v, + standalone=True) + +@app.get("/admin") +@admin_level_required(3) +def admin_home(v): + with open('./disablesignups', 'r') as f: + x = f.read() + return render_template("admin/admin_home.html", v=v, x=x) + +@app.post("/admin/disablesignups") +@admin_level_required(6) +@validate_formkey +def disablesignups(v): + with open('./disablesignups', 'r+') as f: + if f.read() == "yes": + f.write("no") + return {"message": "Signups enabed!"} + else: + f.write("yes") + return {"message": "Signups disabled!"} + +@app.get("/admin/badge_grant") +@admin_level_required(4) +def badge_grant_get(v): + + badge_types = g.db.query(BadgeDef).all() + + errors = {"already_owned": "That user already has that badge.", + "no_user": "That user doesn't exist." + } + + return render_template("admin/badge_grant.html", + v=v, + badge_types=badge_types, + error=errors.get( + request.values.get("error"), + None) if request.values.get('error') else None, + msg="Badge successfully assigned" if request.values.get( + "msg") else None + ) + + +@app.post("/admin/badge_grant") +@limiter.limit("1/second") +@admin_level_required(4) +@validate_formkey +def badge_grant_post(v): + + user = get_user(request.values.get("username").strip(), graceful=True) + if not user: return redirect("/badge_grant?error=no_user") + + try: badge_id = int(request.values.get("badge_id")) + except: abort(400) + + if user.has_badge(badge_id): + g.db.query(Badge).options(lazyload('*')).filter_by(badge_id=badge_id, user_id=user.id,).delete() + g.db.commit() + return redirect("/admin/badge_grant") + + new_badge = Badge(badge_id=badge_id, + user_id=user.id, + ) + + desc = request.values.get("description") + if desc: new_badge.description = desc + + url = request.values.get("url") + if url: new_badge.url = url + + g.db.add(new_badge) + g.db.flush() + + text = f""" + @{v.username} has given you the following profile badge: + \n\n![]({new_badge.path}) + \n\n{new_badge.name} + """ + send_notification(NOTIFICATIONS_ACCOUNT, user, text) + + if badge_id in [21,22,23,24,25,28]: + user.patron = int(str(badge_id)[-1]) + + grant_awards = {} + + if badge_id == 21: + if user.discord_id: add_role(user, "1") + grant_awards["shit"] = 1 + grant_awards["fireflies"] = 1 + elif badge_id == 22: + if user.discord_id: add_role(user, "2") + grant_awards["shit"] = 2 + grant_awards["fireflies"] = 2 + grant_awards["ban"] = 1 + elif badge_id == 23: + if user.discord_id: add_role(user, "3") + grant_awards["shit"] = 5 + grant_awards["fireflies"] = 5 + grant_awards["ban"] = 2 + elif badge_id in [24, 28]: + if user.discord_id: add_role(user, "4") + grant_awards["shit"] = 10 + grant_awards["fireflies"] = 10 + grant_awards["ban"] = 5 + elif badge_id == 25: + if user.discord_id: add_role(user, "5") + grant_awards["shit"] = 20 + grant_awards["fireflies"] = 20 + grant_awards["ban"] = 10 + + if len(grant_awards): + + thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id + + for name in grant_awards: + for count in range(grant_awards[name]): + + thing += 1 + + award = AwardRelationship( + id=thing, + user_id=user.id, + kind=name + ) + + g.db.add(award) + + text = "You were given the following awards:\n\n" + + for key, value in grant_awards.items(): + text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n" + + send_notification(NOTIFICATIONS_ACCOUNT, user, text) + + + g.db.add(user) + + + g.db.commit() + return redirect("/admin/badge_grant") + + +@app.get("/admin/users") +@admin_level_required(2) +def users_list(v): + + page = int(request.values.get("page", 1)) + + users = g.db.query(User).options(lazyload('*')).filter_by(is_banned=0 + ).order_by(User.created_utc.desc() + ).offset(25 * (page - 1)).limit(26) + + users = [x for x in users] + + next_exists = (len(users) > 25) + users = users[:25] + + return render_template("admin/new_users.html", + v=v, + users=users, + next_exists=next_exists, + page=page, + ) + +@app.get("/admin/alt_votes") +@admin_level_required(4) +def alt_votes_get(v): + + if not request.values.get("u1") or not request.values.get("u2"): + return render_template("admin/alt_votes.html", v=v) + + u1 = request.values.get("u1") + u2 = request.values.get("u2") + + if not u1 or not u2: + return redirect("/admin/alt_votes") + + u1 = get_user(u1) + u2 = get_user(u2) + + u1_post_ups = g.db.query( + Vote.submission_id).filter_by( + user_id=u1.id, + vote_type=1).all() + u1_post_downs = g.db.query( + Vote.submission_id).filter_by( + user_id=u1.id, + vote_type=-1).all() + u1_comment_ups = g.db.query( + CommentVote.comment_id).filter_by( + user_id=u1.id, + vote_type=1).all() + u1_comment_downs = g.db.query( + CommentVote.comment_id).filter_by( + user_id=u1.id, + vote_type=-1).all() + u2_post_ups = g.db.query( + Vote.submission_id).filter_by( + user_id=u2.id, + vote_type=1).all() + u2_post_downs = g.db.query( + Vote.submission_id).filter_by( + user_id=u2.id, + vote_type=-1).all() + u2_comment_ups = g.db.query( + CommentVote.comment_id).filter_by( + user_id=u2.id, + vote_type=1).all() + u2_comment_downs = g.db.query( + CommentVote.comment_id).filter_by( + user_id=u2.id, + vote_type=-1).all() + + data = {} + data['u1_only_post_ups'] = len( + [x for x in u1_post_ups if x not in u2_post_ups]) + data['u2_only_post_ups'] = len( + [x for x in u2_post_ups if x not in u1_post_ups]) + data['both_post_ups'] = len(list(set(u1_post_ups) & set(u2_post_ups))) + + data['u1_only_post_downs'] = len( + [x for x in u1_post_downs if x not in u2_post_downs]) + data['u2_only_post_downs'] = len( + [x for x in u2_post_downs if x not in u1_post_downs]) + data['both_post_downs'] = len( + list(set(u1_post_downs) & set(u2_post_downs))) + + data['u1_only_comment_ups'] = len( + [x for x in u1_comment_ups if x not in u2_comment_ups]) + data['u2_only_comment_ups'] = len( + [x for x in u2_comment_ups if x not in u1_comment_ups]) + data['both_comment_ups'] = len( + list(set(u1_comment_ups) & set(u2_comment_ups))) + + data['u1_only_comment_downs'] = len( + [x for x in u1_comment_downs if x not in u2_comment_downs]) + data['u2_only_comment_downs'] = len( + [x for x in u2_comment_downs if x not in u1_comment_downs]) + data['both_comment_downs'] = len( + list(set(u1_comment_downs) & set(u2_comment_downs))) + + data['u1_post_ups_unique'] = 100 * \ + data['u1_only_post_ups'] // len(u1_post_ups) if u1_post_ups else 0 + data['u2_post_ups_unique'] = 100 * \ + data['u2_only_post_ups'] // len(u2_post_ups) if u2_post_ups else 0 + data['u1_post_downs_unique'] = 100 * \ + data['u1_only_post_downs'] // len( + u1_post_downs) if u1_post_downs else 0 + data['u2_post_downs_unique'] = 100 * \ + data['u2_only_post_downs'] // len( + u2_post_downs) if u2_post_downs else 0 + + data['u1_comment_ups_unique'] = 100 * \ + data['u1_only_comment_ups'] // len( + u1_comment_ups) if u1_comment_ups else 0 + data['u2_comment_ups_unique'] = 100 * \ + data['u2_only_comment_ups'] // len( + u2_comment_ups) if u2_comment_ups else 0 + data['u1_comment_downs_unique'] = 100 * \ + data['u1_only_comment_downs'] // len( + u1_comment_downs) if u1_comment_downs else 0 + data['u2_comment_downs_unique'] = 100 * \ + data['u2_only_comment_downs'] // len( + u2_comment_downs) if u2_comment_downs else 0 + + return render_template("admin/alt_votes.html", + u1=u1, + u2=u2, + v=v, + data=data + ) + + +@app.post("/admin/link_accounts") +@limiter.limit("1/second") +@admin_level_required(4) +@validate_formkey +def admin_link_accounts(v): + + u1 = int(request.values.get("u1")) + u2 = int(request.values.get("u2")) + + new_alt = Alt( + user1=u1, + user2=u2, + is_manual=True + ) + + g.db.add(new_alt) + + g.db.commit() + return redirect(f"/admin/alt_votes?u1={g.db.query(User).get(u1).username}&u2={g.db.query(User).get(u2).username}") + + +@app.get("/admin/removed") +@admin_level_required(3) +def admin_removed(v): + + page = int(request.values.get("page", 1)) + + shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] + + ids = g.db.query(Submission.id).options(lazyload('*')).filter(or_(Submission.is_banned==True, Submission.author_id.in_(shadowbanned))).order_by(Submission.id.desc()).offset(25 * (page - 1)).limit(26).all() + + ids=[x[0] for x in ids] + + next_exists = len(ids) > 25 + + ids = ids[:25] + + posts = get_posts(ids, v=v) + + return render_template("admin/removed_posts.html", + v=v, + listing=posts, + page=page, + next_exists=next_exists + ) + + + +@app.post("/agendaposter/") +@admin_level_required(6) +@validate_formkey +def agendaposter(user_id, v): + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + + expiry = request.values.get("days", 0) + if expiry: + expiry = int(expiry) + expiry = g.timestamp + expiry*60*60*24 + else: + expiry = 0 + + user.agendaposter = not user.agendaposter + user.agendaposter_expires_utc = expiry + g.db.add(user) + for alt in user.alts: + if alt.admin_level > 0: break + alt.agendaposter = user.agendaposter + alt.agendaposter_expires_utc = expiry + g.db.add(alt) + + note = None + + if not user.agendaposter: kind = "unagendaposter" + else: + kind = "agendaposter" + note = f"for {request.values.get('days')} days" if expiry else "never expires" + + ma = ModAction( + kind=kind, + user_id=v.id, + target_user_id=user.id, + note = note + ) + g.db.add(ma) + + if user.agendaposter: + if not user.has_badge(26): + badge = Badge(user_id=user.id, badge_id=26) + g.db.add(badge) + else: + badge = user.has_badge(26) + if badge: g.db.delete(badge) + + if user.agendaposter: send_notification(NOTIFICATIONS_ACCOUNT, user, f"You have been marked by an admin as an agendaposter ({note}).") + else: send_notification(NOTIFICATIONS_ACCOUNT, user, f"You have been unmarked by an admin as an agendaposter.") + + g.db.commit() + if user.agendaposter: return redirect(user.url) + return {"message": "Agendaposter theme disabled!"} + + +@app.post("/shadowban/") +@limiter.limit("1/second") +@admin_level_required(6) +@validate_formkey +def shadowban(user_id, v): + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + if user.admin_level != 0: abort(403) + user.shadowbanned = v.username + g.db.add(user) + for alt in user.alts: + if alt.admin_level > 0: break + alt.shadowbanned = v.username + g.db.add(alt) + ma = ModAction( + kind="shadowban", + user_id=v.id, + target_user_id=user.id, + ) + g.db.add(ma) + + cache.delete_memoized(frontlist) + + g.db.commit() + return {"message": "User shadowbanned!"} + + +@app.post("/unshadowban/") +@limiter.limit("1/second") +@admin_level_required(6) +@validate_formkey +def unshadowban(user_id, v): + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + if user.admin_level != 0: abort(403) + user.shadowbanned = None + g.db.add(user) + for alt in user.alts: + alt.shadowbanned = None + g.db.add(alt) + + ma = ModAction( + kind="unshadowban", + user_id=v.id, + target_user_id=user.id, + ) + g.db.add(ma) + + cache.delete_memoized(frontlist) + + g.db.commit() + return {"message": "User unshadowbanned!"} + +@app.post("/admin/verify/") +@limiter.limit("1/second") +@admin_level_required(6) +@validate_formkey +def verify(user_id, v): + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + user.verified = "Verified" + g.db.add(user) + g.db.commit() + return {"message": "User verfied!"} + +@app.post("/admin/unverify/") +@limiter.limit("1/second") +@admin_level_required(6) +@validate_formkey +def unverify(user_id, v): + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + user.verified = None + g.db.add(user) + g.db.commit() + return {"message": "User unverified!"} + + +@app.post("/admin/title_change/") +@limiter.limit("1/second") +@admin_level_required(6) +@validate_formkey +def admin_title_change(user_id, v): + + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + + if user.admin_level != 0: abort(403) + + new_name=request.values.get("title").strip()[:256] + + user.customtitleplain=new_name + new_name = sanitize(new_name) + + user=g.db.query(User).with_for_update().options(lazyload('*')).filter_by(id=user.id).first() + user.customtitle=new_name + user.flairchanged = bool(request.values.get("locked")) + g.db.add(user) + + if user.flairchanged: kind = "set_flair_locked" + else: kind = "set_flair_notlocked" + + ma=ModAction( + kind=kind, + user_id=v.id, + target_user_id=user.id, + note=f'"{new_name}"' + ) + g.db.add(ma) + g.db.commit() + + return redirect(user.url) + +@app.post("/ban_user/") +@limiter.limit("1/second") +@admin_level_required(6) +@validate_formkey +def ban_user(user_id, v): + + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + + if user.admin_level >= v.admin_level: abort(403) + + if 'form' in request.values: + days = float(request.values.get("days")) if request.values.get('days') else 0 + reason = sanitize(request.values.get("reason", "")) + message = request.values.get("reason", "").strip() + else: + days = float(request.values.get("days")) if request.values.get('days') else 0 + reason = sanitize(request.values.get("reason", "")) + message = request.values.get("reason", "").strip() + + if not user: abort(400) + + if days > 0: + if message: + text = f"Your account has been suspended for {days} days for the following reason:\n\n> {message}" + else: + text = f"Your account has been suspended for {days} days." + user.ban(admin=v, reason=reason, days=days) + + else: + if message: + text = f"Your account has been permanently suspended for the following reason:\n\n> {message}" + else: + text = "Your account has been permanently suspended." + + user.ban(admin=v, reason=reason) + + if request.values.get("alts", ""): + for x in user.alts: + if x.admin_level > 0: break + x.ban(admin=v, reason=reason) + + send_notification(NOTIFICATIONS_ACCOUNT, user, text[:128]) + + if days == 0: duration = "permanent" + elif days == 1: duration = "1 day" + else: duration = f"{days} days" + ma=ModAction( + kind="ban_user", + user_id=v.id, + target_user_id=user.id, + note=f'reason: "{reason}", duration: {duration}' + ) + g.db.add(ma) + + if 'reason' in request.values: + if reason.startswith("/post/"): + try: + post = int(reason.split("/post/")[1]) + post = get_post(post) + post.bannedfor = True + g.db.add(post) + except: pass + elif reason.startswith("/comment/"): + try: + comment = int(reason.split("/comment/")[1]) + comment = get_comment(comment) + comment.bannedfor = True + g.db.add(comment) + except: pass + g.db.commit() + + if 'redir' in request.values: return redirect(user.url) + else: return {"message": f"@{user.username} was banned!"} + + +@app.post("/unban_user/") +@limiter.limit("1/second") +@admin_level_required(6) +@validate_formkey +def unban_user(user_id, v): + + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + + if not user: + abort(400) + + user.is_banned = 0 + user.unban_utc = 0 + g.db.add(user) + + if request.values.get("alts", ""): + for x in user.alts: + if x.admin_level == 0: + x.is_banned = 0 + x.unban_utc = 0 + g.db.add(x) + + send_notification(NOTIFICATIONS_ACCOUNT, user, + "Your account has been reinstated. Please carefully review and abide by the [rules](/post/2510) to ensure that you don't get suspended again.") + + ma=ModAction( + kind="unban_user", + user_id=v.id, + target_user_id=user.id, + ) + g.db.add(ma) + + g.db.commit() + + if "@" in request.referrer: return redirect(user.url) + else: return {"message": f"@{user.username} was unbanned!"} + + +@app.post("/ban_post/") +@limiter.limit("1/second") +@admin_level_required(3) +@validate_formkey +def ban_post(post_id, v): + + post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() + + if not post: + abort(400) + + post.is_banned = True + post.is_approved = 0 + post.stickied = None + post.is_pinned = False + post.removed_by = v.id + + ban_reason=request.values.get("reason", "") + ban_reason = CustomRenderer().render(mistletoe.Document(ban_reason)) + ban_reason = sanitize(ban_reason) + + post.ban_reason = ban_reason + + g.db.add(post) + + + + ma=ModAction( + kind="ban_post", + user_id=v.id, + target_submission_id=post.id, + ) + g.db.add(ma) + + cache.delete_memoized(frontlist) + + v.coins += 1 + g.db.add(v) + + g.db.commit() + + return {"message": "Post removed!"} + + +@app.post("/unban_post/") +@limiter.limit("1/second") +@admin_level_required(3) +@validate_formkey +def unban_post(post_id, v): + + post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() + + if not post: + abort(400) + + if post.is_banned: + ma=ModAction( + kind="unban_post", + user_id=v.id, + target_submission_id=post.id, + ) + g.db.add(ma) + + post.is_banned = False + post.is_approved = v.id + + g.db.add(post) + + cache.delete_memoized(frontlist) + + v.coins -= 1 + g.db.add(v) + + g.db.commit() + + return {"message": "Post approved!"} + + +@app.post("/distinguish/") +@admin_level_required(1) +@validate_formkey +def api_distinguish_post(post_id, v): + + post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() + + if not post: + abort(404) + + if not post.author_id == v.id: + abort(403) + + if post.distinguish_level: + post.distinguish_level = 0 + else: + post.distinguish_level = v.admin_level + + g.db.add(post) + + g.db.commit() + + return {"message": "Post distinguished!"} + + +@app.post("/sticky/") +@admin_level_required(3) +def api_sticky_post(post_id, v): + + post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() + if post: + if post.stickied: post.stickied = None + else: post.stickied = v.username + g.db.add(post) + + ma=ModAction( + kind="pin_post" if post.stickied else "unpin_post", + user_id=v.id, + target_submission_id=post.id + ) + g.db.add(ma) + + cache.delete_memoized(frontlist) + + g.db.commit() + if post.stickied: return {"message": "Post pinned!"} + else: return {"message": "Post unpinned!"} + +@app.post("/pin/") +@auth_required +def api_pin_post(post_id, v): + + post = g.db.query(Submission).options(lazyload('*')).filter_by(id=post_id).first() + if post: + post.is_pinned = not post.is_pinned + g.db.add(post) + g.db.commit() + + if post.is_pinned: return {"message": "Post pinned!"} + else: return {"message": "Post unpinned!"} + +@app.post("/ban_comment/") +@limiter.limit("1/second") +@admin_level_required(1) +def api_ban_comment(c_id, v): + + comment = g.db.query(Comment).options(lazyload('*')).filter_by(id=c_id).first() + if not comment: + abort(404) + + comment.is_banned = True + comment.is_approved = 0 + comment.removed_by = v.id + + g.db.add(comment) + ma=ModAction( + kind="ban_comment", + user_id=v.id, + target_comment_id=comment.id, + ) + g.db.add(ma) + g.db.commit() + return {"message": "Comment removed!"} + + +@app.post("/unban_comment/") +@limiter.limit("1/second") +@admin_level_required(1) +def api_unban_comment(c_id, v): + + comment = g.db.query(Comment).options(lazyload('*')).filter_by(id=c_id).first() + if not comment: + abort(404) + g.db.add(comment) + + if comment.is_banned: + ma=ModAction( + kind="unban_comment", + user_id=v.id, + target_comment_id=comment.id, + ) + g.db.add(ma) + + comment.is_banned = False + comment.is_approved = v.id + + g.db.commit() + + return {"message": "Comment approved!"} + + +@app.post("/distinguish_comment/") +@auth_required +def admin_distinguish_comment(c_id, v): + + if v.admin_level == 0: abort(403) + + comment = get_comment(c_id, v=v) + + if comment.author_id != v.id: + abort(403) + + comment.distinguish_level = 0 if comment.distinguish_level else v.admin_level + + g.db.add(comment) + html=render_template( + "comments.html", + v=v, + comments=[comment], + ) + + html=str(BeautifulSoup(html, features="html.parser").find(id=f"comment-{comment.id}-only")) + + g.db.commit() + + return html + +@app.get("/admin/dump_cache") +@admin_level_required(6) +def admin_dump_cache(v): + cache.clear() + return {"message": "Internal cache cleared."} + + +@app.get("/admin/banned_domains/") +@admin_level_required(4) +def admin_banned_domains(v): + + banned_domains = g.db.query(BannedDomain).all() + return render_template("admin/banned_domains.html", v=v, banned_domains=banned_domains) + +@app.post("/admin/banned_domains") +@limiter.limit("1/second") +@admin_level_required(4) +@validate_formkey +def admin_toggle_ban_domain(v): + + domain=request.values.get("domain", "").strip() + if not domain: abort(400) + + reason=request.values.get("reason", "").strip() + + d = g.db.query(BannedDomain).options(lazyload('*')).filter_by(domain=domain).first() + if d: g.db.delete(d) + else: + d = BannedDomain(domain=domain, reason=reason) + g.db.add(d) + + g.db.commit() + + return redirect("/admin/banned_domains/") + + +@app.post("/admin/nuke_user") +@limiter.limit("1/second") +@admin_level_required(4) +@validate_formkey +def admin_nuke_user(v): + + user=get_user(request.values.get("user")) + + for post in g.db.query(Submission).options(lazyload('*')).filter_by(author_id=user.id).all(): + if post.is_banned: + continue + + post.is_banned=True + g.db.add(post) + + for comment in g.db.query(Comment).options(lazyload('*')).filter_by(author_id=user.id).all(): + if comment.is_banned: + continue + + comment.is_banned=True + g.db.add(comment) + + ma=ModAction( + kind="nuke_user", + user_id=v.id, + target_user_id=user.id, + ) + g.db.add(ma) + + g.db.commit() + + return redirect(user.url) + + +@app.post("/admin/unnuke_user") +@limiter.limit("1/second") +@admin_level_required(4) +@validate_formkey +def admin_nunuke_user(v): + + user=get_user(request.values.get("user")) + + for post in g.db.query(Submission).options(lazyload('*')).filter_by(author_id=user.id).all(): + if not post.is_banned: + continue + + post.is_banned=False + g.db.add(post) + + for comment in g.db.query(Comment).options(lazyload('*')).filter_by(author_id=user.id).all(): + if not comment.is_banned: + continue + + comment.is_banned=False + g.db.add(comment) + + ma=ModAction( + kind="unnuke_user", + user_id=v.id, + target_user_id=user.id, + ) + g.db.add(ma) + + g.db.commit() + return redirect(user.url) \ No newline at end of file diff --git a/files/routes/awards.py b/files/routes/awards.py old mode 100644 new mode 100755 index ae3e64cfc..90131a46c --- a/files/routes/awards.py +++ b/files/routes/awards.py @@ -1,372 +1,372 @@ -from files.__main__ import app, limiter -from files.helpers.wrappers import * -from files.helpers.alerts import * -from files.helpers.get import * -from files.helpers.const import * -from files.classes.award import * -from flask import g, request - -@app.get("/shop") -@app.get("/settings/shop") -@auth_required -def shop(v): - if site_name == "Drama": - AWARDS = { - "ban": { - "kind": "ban", - "title": "One-Day Ban", - "description": "Bans the author for a day.", - "icon": "fas fa-gavel", - "color": "text-danger", - "price": 5000 - }, - "shit": { - "kind": "shit", - "title": "Shit", - "description": "Makes flies swarm a post.", - "icon": "fas fa-poop", - "color": "text-black-50", - "price": 1000 - }, - "fireflies": { - "kind": "fireflies", - "title": "Fireflies", - "description": "Puts stars on the post.", - "icon": "fas fa-sparkles", - "color": "text-warning", - "price": 1000 - } - } - else: - AWARDS = { - "shit": { - "kind": "shit", - "title": "Shit", - "description": "Makes flies swarm a post.", - "icon": "fas fa-poop", - "color": "text-black-50", - "price": 1000 - }, - "fireflies": { - "kind": "fireflies", - "title": "Fireflies", - "description": "Puts stars on the post.", - "icon": "fas fa-sparkles", - "color": "text-warning", - "price": 1000 - } - } - - query = g.db.query( - User.id, User.username, User.patron, User.namecolor, - AwardRelationship.kind.label('last_award_kind'), func.count(AwardRelationship.id).label('last_award_count') - ).filter(AwardRelationship.submission_id==None, AwardRelationship.comment_id==None, User.patron > 0) \ - .group_by(User.username, User.patron, User.id, User.namecolor, AwardRelationship.kind) \ - .order_by(User.patron.desc(), AwardRelationship.kind.desc()) \ - .join(User).filter(User.id == v.id).all() - - owned = [] - for row in (r._asdict() for r in query): - kind = row['last_award_kind'] - if kind in AWARDS.keys(): - award = AWARDS[kind] - award["owned_num"] = row['last_award_count'] - owned.append(award) - - if v.patron: - for val in AWARDS.values(): - if v.patron == 1: val["price"] = int(val["price"]*0.90) - elif v.patron == 2: val["price"] = int(val["price"]*0.85) - elif v.patron == 3: val["price"] = int(val["price"]*0.80) - elif v.patron == 4: val["price"] = int(val["price"]*0.75) - else: val["price"] = int(val["price"]*0.70) - - return render_template("settings_shop.html", owned=owned, awards=list(AWARDS.values()), v=v) - - -@app.post("/buy/") -@auth_required -def buy(v, award): - if site_name == "Drama": - AWARDS = { - "ban": { - "kind": "ban", - "title": "One-Day Ban", - "description": "Bans the author for a day.", - "icon": "fas fa-gavel", - "color": "text-danger", - "price": 5000 - }, - "shit": { - "kind": "shit", - "title": "Shit", - "description": "Makes flies swarm a post.", - "icon": "fas fa-poop", - "color": "text-black-50", - "price": 1000 - }, - "fireflies": { - "kind": "fireflies", - "title": "Fireflies", - "description": "Puts stars on the post.", - "icon": "fas fa-sparkles", - "color": "text-warning", - "price": 1000 - } - } - else: - AWARDS = { - "shit": { - "kind": "shit", - "title": "Shit", - "description": "Makes flies swarm a post.", - "icon": "fas fa-poop", - "color": "text-black-50", - "price": 1000 - }, - "fireflies": { - "kind": "fireflies", - "title": "Fireflies", - "description": "Puts stars on the post.", - "icon": "fas fa-sparkles", - "color": "text-warning", - "price": 1000 - } - } - - if award not in AWARDS: abort(400) - price = AWARDS[award]["price"] - if v.patron: - if v.patron == 1: price = int(price*0.90) - elif v.patron == 2: price = int(price*0.85) - elif v.patron == 3: price = int(price*0.80) - elif v.patron == 4: price = int(price*0.75) - else: price = int(price*0.70) - - if v.coins < price: return {"error": "Not enough coins."}, 400 - v.coins -= price - g.db.add(v) - - thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id - thing += 1 - - award = AwardRelationship(id=thing, user_id=v.id, kind=award) - g.db.add(award) - - g.db.commit() - - return {"message": "Award bought!"} - - -def banaward_trigger(post=None, comment=None): - - author = post.author if post else comment.author - link = f"[this post]({post.permalink})" if post else f"[this comment]({comment.permalink})" - - if not author.is_suspended: - author.ban(reason="one-day ban award used", days=1) - - send_notification(NOTIFICATIONS_ACCOUNT, author, f"Your account has been suspended for a day for {link}. It sucked and you should feel bad.") - elif author.unban_utc > 0: - author.unban_utc += 24*60*60 - g.db.add(author) - - send_notification(NOTIFICATIONS_ACCOUNT, author, f"Your account has been suspended for yet another day for {link}. Seriously man?") - - -ACTIONS = { - "ban": banaward_trigger -} - -ALLOW_MULTIPLE = ( - "ban", - "shit", - "fireflies" -) - -@app.post("/post//awards") -@limiter.limit("1/second") -@auth_required -def award_post(pid, v): - - if v.is_suspended and v.unban_utc == 0: return {"error": "forbidden."}, 403 - - kind = request.values.get("kind", "").strip() - - if kind not in AWARDS: - return {"error": "That award doesn't exist."}, 404 - - post_award = g.db.query(AwardRelationship).options(lazyload('*')).filter( - and_( - AwardRelationship.kind == kind, - AwardRelationship.user_id == v.id, - AwardRelationship.submission_id == None, - AwardRelationship.comment_id == None - ) - ).first() - - if not post_award: - return {"error": "You don't have that award."}, 404 - - post = g.db.query(Submission).options(lazyload('*')).filter_by(id=pid).first() - - if not post or post.is_banned or post.deleted_utc > 0: - return {"error": "That post doesn't exist or has been deleted or removed."}, 404 - - if post.author_id == v.id: - return {"error": "You can't award yourself."}, 403 - - existing_award = g.db.query(AwardRelationship).options(lazyload('*')).filter( - and_( - AwardRelationship.submission_id == post.id, - AwardRelationship.user_id == v.id, - AwardRelationship.kind == kind - ) - ).first() - - if existing_award and kind not in ALLOW_MULTIPLE: - return {"error": "You can't give that award multiple times to the same post."}, 409 - - post_award.submission_id = post.id - g.db.add(post_award) - - msg = f"@{v.username} has given your [post]({post.permalink}) the {AWARDS[kind]['title']} Award!" - - note = request.values.get("note", "").strip() - if note: - msg += f"\n\n> {note}" - - send_notification(NOTIFICATIONS_ACCOUNT, post.author, msg) - - if kind in ACTIONS: ACTIONS[kind](post=post) - - post.author.received_award_count += 1 - g.db.add(post.author) - - g.db.commit() - if request.referrer and len(request.referrer) > 1: return redirect(request.referrer) - else: return redirect("/") - - -@app.post("/comment//awards") -@limiter.limit("1/second") -@auth_required -def award_comment(cid, v): - - if v.is_suspended and v.unban_utc == 0: return {"error": "forbidden"}, 403 - - kind = request.values.get("kind", "").strip() - - if kind not in AWARDS: - return {"error": "That award doesn't exist."}, 404 - - comment_award = g.db.query(AwardRelationship).options(lazyload('*')).filter( - and_( - AwardRelationship.kind == kind, - AwardRelationship.user_id == v.id, - AwardRelationship.submission_id == None, - AwardRelationship.comment_id == None - ) - ).first() - - if not comment_award: - return {"error": "You don't have that award."}, 404 - - c = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first() - - if not c or c.is_banned or c.deleted_utc > 0: - return {"error": "That comment doesn't exist or has been deleted or removed."}, 404 - - if c.author_id == v.id: - return {"error": "You can't award yourself."}, 403 - - existing_award = g.db.query(AwardRelationship).options(lazyload('*')).filter( - and_( - AwardRelationship.comment_id == c.id, - AwardRelationship.user_id == v.id, - AwardRelationship.kind == kind - ) - ).first() - - if existing_award and kind not in ALLOW_MULTIPLE: - return {"error": "You can't give that award multiple times to the same comment."}, 409 - - comment_award.comment_id = c.id - g.db.add(comment_award) - - msg = f"@{v.username} has given your [comment]({c.permalink}) the {AWARDS[kind]['title']} Award!" - - note = request.values.get("note", "").strip() - if note: - msg += f"\n\n> {note}" - - send_notification(NOTIFICATIONS_ACCOUNT, c.author, msg) - - if kind in ACTIONS: - ACTIONS[kind](comment=c) - - c.author.received_award_count += 1 - g.db.add(c.author) - - g.db.commit() - if request.referrer and len(request.referrer) > 1: return redirect(request.referrer) - else: return redirect("/") - -@app.get("/admin/user_award") -@auth_required -def admin_userawards_get(v): - - if v.admin_level < 6: - abort(403) - - return render_template("admin/user_award.html", awards=list(AWARDS.values()), v=v) - -@app.post("/admin/user_award") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def admin_userawards_post(v): - - if v.admin_level < 6: - abort(403) - - try: u = request.values.get("username").strip() - except: abort(404) - - u = get_user(u, graceful=False, v=v) - - notify_awards = {} - - latest = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first() - thing = latest.id - - for key, value in request.values.items(): - if key not in AWARDS: - continue - - if value: - - if int(value) > 0: - notify_awards[key] = int(value) - - for x in range(int(value)): - thing += 1 - - award = AwardRelationship( - id=thing, - user_id=u.id, - kind=key - ) - - g.db.add(award) - - text = "You were given the following awards:\n\n" - - for key, value in notify_awards.items(): - text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n" - - send_notification(NOTIFICATIONS_ACCOUNT, u, text) - - g.db.commit() - +from files.__main__ import app, limiter +from files.helpers.wrappers import * +from files.helpers.alerts import * +from files.helpers.get import * +from files.helpers.const import * +from files.classes.award import * +from flask import g, request + +@app.get("/shop") +@app.get("/settings/shop") +@auth_required +def shop(v): + if site_name == "Drama": + AWARDS = { + "ban": { + "kind": "ban", + "title": "One-Day Ban", + "description": "Bans the author for a day.", + "icon": "fas fa-gavel", + "color": "text-danger", + "price": 1500 + }, + "shit": { + "kind": "shit", + "title": "Shit", + "description": "Makes flies swarm a post.", + "icon": "fas fa-poop", + "color": "text-black-50", + "price": 500 + }, + "fireflies": { + "kind": "fireflies", + "title": "Fireflies", + "description": "Puts stars on the post.", + "icon": "fas fa-sparkles", + "color": "text-warning", + "price": 500 + } + } + else: + AWARDS = { + "shit": { + "kind": "shit", + "title": "Shit", + "description": "Makes flies swarm a post.", + "icon": "fas fa-poop", + "color": "text-black-50", + "price": 500 + }, + "fireflies": { + "kind": "fireflies", + "title": "Fireflies", + "description": "Puts stars on the post.", + "icon": "fas fa-sparkles", + "color": "text-warning", + "price": 500 + } + } + + query = g.db.query( + User.id, User.username, User.patron, User.namecolor, + AwardRelationship.kind.label('last_award_kind'), func.count(AwardRelationship.id).label('last_award_count') + ).filter(AwardRelationship.submission_id==None, AwardRelationship.comment_id==None, User.patron > 0) \ + .group_by(User.username, User.patron, User.id, User.namecolor, AwardRelationship.kind) \ + .order_by(User.patron.desc(), AwardRelationship.kind.desc()) \ + .join(User).filter(User.id == v.id).all() + + owned = [] + for row in (r._asdict() for r in query): + kind = row['last_award_kind'] + if kind in AWARDS.keys(): + award = AWARDS[kind] + award["owned_num"] = row['last_award_count'] + owned.append(award) + + if v.patron: + for val in AWARDS.values(): + if v.patron == 1: val["price"] = int(val["price"]*0.90) + elif v.patron == 2: val["price"] = int(val["price"]*0.85) + elif v.patron == 3: val["price"] = int(val["price"]*0.80) + elif v.patron == 4: val["price"] = int(val["price"]*0.75) + else: val["price"] = int(val["price"]*0.70) + + return render_template("settings_shop.html", owned=owned, awards=list(AWARDS.values()), v=v) + + +@app.post("/buy/") +@auth_required +def buy(v, award): + if site_name == "Drama": + AWARDS = { + "ban": { + "kind": "ban", + "title": "One-Day Ban", + "description": "Bans the author for a day.", + "icon": "fas fa-gavel", + "color": "text-danger", + "price": 1500 + }, + "shit": { + "kind": "shit", + "title": "Shit", + "description": "Makes flies swarm a post.", + "icon": "fas fa-poop", + "color": "text-black-50", + "price": 500 + }, + "fireflies": { + "kind": "fireflies", + "title": "Fireflies", + "description": "Puts stars on the post.", + "icon": "fas fa-sparkles", + "color": "text-warning", + "price": 500 + } + } + else: + AWARDS = { + "shit": { + "kind": "shit", + "title": "Shit", + "description": "Makes flies swarm a post.", + "icon": "fas fa-poop", + "color": "text-black-50", + "price": 500 + }, + "fireflies": { + "kind": "fireflies", + "title": "Fireflies", + "description": "Puts stars on the post.", + "icon": "fas fa-sparkles", + "color": "text-warning", + "price": 500 + } + } + + if award not in AWARDS: abort(400) + price = AWARDS[award]["price"] + if v.patron: + if v.patron == 1: price = int(price*0.90) + elif v.patron == 2: price = int(price*0.85) + elif v.patron == 3: price = int(price*0.80) + elif v.patron == 4: price = int(price*0.75) + else: price = int(price*0.70) + + if v.coins < price: return {"error": "Not enough coins."}, 400 + v.coins -= price + g.db.add(v) + + thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id + thing += 1 + + award = AwardRelationship(id=thing, user_id=v.id, kind=award) + g.db.add(award) + + g.db.commit() + + return {"message": "Award bought!"} + + +def banaward_trigger(post=None, comment=None): + + author = post.author if post else comment.author + link = f"[this post]({post.permalink})" if post else f"[this comment]({comment.permalink})" + + if not author.is_suspended: + author.ban(reason="one-day ban award used", days=1) + + send_notification(NOTIFICATIONS_ACCOUNT, author, f"Your account has been suspended for a day for {link}. It sucked and you should feel bad.") + elif author.unban_utc > 0: + author.unban_utc += 24*60*60 + g.db.add(author) + + send_notification(NOTIFICATIONS_ACCOUNT, author, f"Your account has been suspended for yet another day for {link}. Seriously man?") + + +ACTIONS = { + "ban": banaward_trigger +} + +ALLOW_MULTIPLE = ( + "ban", + "shit", + "fireflies" +) + +@app.post("/post//awards") +@limiter.limit("1/second") +@auth_required +def award_post(pid, v): + + if v.is_suspended and v.unban_utc == 0: return {"error": "forbidden."}, 403 + + kind = request.values.get("kind", "").strip() + + if kind not in AWARDS: + return {"error": "That award doesn't exist."}, 404 + + post_award = g.db.query(AwardRelationship).options(lazyload('*')).filter( + and_( + AwardRelationship.kind == kind, + AwardRelationship.user_id == v.id, + AwardRelationship.submission_id == None, + AwardRelationship.comment_id == None + ) + ).first() + + if not post_award: + return {"error": "You don't have that award."}, 404 + + post = g.db.query(Submission).options(lazyload('*')).filter_by(id=pid).first() + + if not post or post.is_banned or post.deleted_utc > 0: + return {"error": "That post doesn't exist or has been deleted or removed."}, 404 + + if post.author_id == v.id: + return {"error": "You can't award yourself."}, 403 + + existing_award = g.db.query(AwardRelationship).options(lazyload('*')).filter( + and_( + AwardRelationship.submission_id == post.id, + AwardRelationship.user_id == v.id, + AwardRelationship.kind == kind + ) + ).first() + + if existing_award and kind not in ALLOW_MULTIPLE: + return {"error": "You can't give that award multiple times to the same post."}, 409 + + post_award.submission_id = post.id + g.db.add(post_award) + + msg = f"@{v.username} has given your [post]({post.permalink}) the {AWARDS[kind]['title']} Award!" + + note = request.values.get("note", "").strip() + if note: + msg += f"\n\n> {note}" + + send_notification(NOTIFICATIONS_ACCOUNT, post.author, msg) + + if kind in ACTIONS: ACTIONS[kind](post=post) + + post.author.received_award_count += 1 + g.db.add(post.author) + + g.db.commit() + if request.referrer and len(request.referrer) > 1: return redirect(request.referrer) + else: return redirect("/") + + +@app.post("/comment//awards") +@limiter.limit("1/second") +@auth_required +def award_comment(cid, v): + + if v.is_suspended and v.unban_utc == 0: return {"error": "forbidden"}, 403 + + kind = request.values.get("kind", "").strip() + + if kind not in AWARDS: + return {"error": "That award doesn't exist."}, 404 + + comment_award = g.db.query(AwardRelationship).options(lazyload('*')).filter( + and_( + AwardRelationship.kind == kind, + AwardRelationship.user_id == v.id, + AwardRelationship.submission_id == None, + AwardRelationship.comment_id == None + ) + ).first() + + if not comment_award: + return {"error": "You don't have that award."}, 404 + + c = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first() + + if not c or c.is_banned or c.deleted_utc > 0: + return {"error": "That comment doesn't exist or has been deleted or removed."}, 404 + + if c.author_id == v.id: + return {"error": "You can't award yourself."}, 403 + + existing_award = g.db.query(AwardRelationship).options(lazyload('*')).filter( + and_( + AwardRelationship.comment_id == c.id, + AwardRelationship.user_id == v.id, + AwardRelationship.kind == kind + ) + ).first() + + if existing_award and kind not in ALLOW_MULTIPLE: + return {"error": "You can't give that award multiple times to the same comment."}, 409 + + comment_award.comment_id = c.id + g.db.add(comment_award) + + msg = f"@{v.username} has given your [comment]({c.permalink}) the {AWARDS[kind]['title']} Award!" + + note = request.values.get("note", "").strip() + if note: + msg += f"\n\n> {note}" + + send_notification(NOTIFICATIONS_ACCOUNT, c.author, msg) + + if kind in ACTIONS: + ACTIONS[kind](comment=c) + + c.author.received_award_count += 1 + g.db.add(c.author) + + g.db.commit() + if request.referrer and len(request.referrer) > 1: return redirect(request.referrer) + else: return redirect("/") + +@app.get("/admin/user_award") +@auth_required +def admin_userawards_get(v): + + if v.admin_level < 6: + abort(403) + + return render_template("admin/user_award.html", awards=list(AWARDS.values()), v=v) + +@app.post("/admin/user_award") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def admin_userawards_post(v): + + if v.admin_level < 6: + abort(403) + + try: u = request.values.get("username").strip() + except: abort(404) + + u = get_user(u, graceful=False, v=v) + + notify_awards = {} + + latest = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first() + thing = latest.id + + for key, value in request.values.items(): + if key not in AWARDS: + continue + + if value: + + if int(value) > 0: + notify_awards[key] = int(value) + + for x in range(int(value)): + thing += 1 + + award = AwardRelationship( + id=thing, + user_id=u.id, + kind=key + ) + + g.db.add(award) + + text = "You were given the following awards:\n\n" + + for key, value in notify_awards.items(): + text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n" + + send_notification(NOTIFICATIONS_ACCOUNT, u, text) + + g.db.commit() + return render_template("admin/user_award.html", awards=list(AWARDS.values()), v=v) \ No newline at end of file diff --git a/files/routes/comments.py b/files/routes/comments.py old mode 100644 new mode 100755 index 6c4a10520..acf2b7a15 --- a/files/routes/comments.py +++ b/files/routes/comments.py @@ -1,861 +1,861 @@ -from files.helpers.wrappers import * -from files.helpers.filters import * -from files.helpers.alerts import * -from files.helpers.images import * -from files.helpers.session import * -from files.helpers.const import * -from files.classes import * -from files.routes.front import comment_idlist -from pusher_push_notifications import PushNotifications -from flask import * -from files.__main__ import app, limiter -from .posts import filter_title - - -site = environ.get("DOMAIN").strip() - -beams_client = PushNotifications( - instance_id=PUSHER_INSTANCE_ID, - secret_key=PUSHER_KEY, -) - -@app.get("/comment/") -@app.get("/post///") -@app.get("/logged_out/comment/") -@app.get("/logged_out/post///") -@auth_desired -def post_pid_comment_cid(cid, pid=None, anything=None, v=None): - - if not v and not request.path.startswith('/logged_out'): return redirect(f"/logged_out{request.full_path}") - - if v and request.path.startswith('/logged_out'): v = None - - try: cid = int(cid) - except: - try: cid = int(cid, 36) - except: abort(404) - - comment = get_comment(cid, v=v) - - if comment.post and comment.post.club and not (v and v.paid_dues): abort(403) - - if not comment.parent_submission and not (v and (comment.author.id == v.id or comment.sentto == v.id)) and not (v and v.admin_level == 6) : abort(403) - - if not pid: - if comment.parent_submission: pid = comment.parent_submission - elif "rama" in request.host: pid = 6489 - elif 'pcmemes.net' in request.host: pid = 382 - else: pid = 1 - - try: pid = int(pid) - except: abort(404) - - post = get_post(pid, v=v) - - if post.over_18 and not (v and v.over_18) and not session.get('over_18', 0) >= int(time.time()): - if request.headers.get("Authorization"): return {'error': f'This content is not suitable for some users and situations.'} - else: render_template("errors/nsfw.html", v=v) - - try: context = int(request.values.get("context", 0)) - except: context = 0 - comment_info = comment - c = comment - while context > 0 and c.level > 1: - c = c.parent_comment - context -= 1 - top_comment = c - - if v: defaultsortingcomments = v.defaultsortingcomments - else: defaultsortingcomments = "top" - sort=request.values.get("sort", defaultsortingcomments) - - post.replies=[top_comment] - - if v: - votes = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id).subquery() - - blocking = v.blocking.subquery() - - blocked = v.blocked.subquery() - - comments = g.db.query( - Comment, - votes.c.vote_type, - blocking.c.id, - blocked.c.id, - ) - - if not (v and v.shadowbanned) and not (v and v.admin_level == 6): - shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] - comments = comments.filter(Comment.author_id.notin_(shadowbanned)) - - comments=comments.filter( - Comment.parent_submission == post.id, - Comment.author_id != AUTOPOLLER_ACCOUNT - ).join( - votes, - votes.c.comment_id == Comment.id, - isouter=True - ).join( - blocking, - blocking.c.target_id == Comment.author_id, - isouter=True - ).join( - blocked, - blocked.c.user_id == Comment.author_id, - isouter=True - ) - - output = [] - for c in comments: - comment = c[0] - comment.voted = c[1] or 0 - comment.is_blocking = c[2] or 0 - comment.is_blocked = c[3] or 0 - output.append(comment) - - post.preloaded_comments = output - - if request.headers.get("Authorization"): return top_comment.json - else: return post.rendered_page(v=v, sort=sort, comment=top_comment, comment_info=comment_info) - - -@app.post("/comment") -@limiter.limit("1/second") -@limiter.limit("6/minute") -@is_not_banned -@validate_formkey -def api_comment(v): - if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 - - parent_submission = request.values.get("submission").strip() - parent_fullname = request.values.get("parent_fullname").strip() - - parent_post = get_post(parent_submission, v=v) - if parent_post.club and not (v and v.paid_dues): abort(403) - - if parent_fullname.startswith("t2_"): - parent = parent_post - parent_comment_id = None - level = 1 - elif parent_fullname.startswith("t3_"): - parent = get_comment(parent_fullname.split("_")[1], v=v) - parent_comment_id = parent.id - level = parent.level + 1 - else: abort(400) - - body = request.values.get("body", "").strip()[:10000] - body = body.strip() - - if not body and not request.files.get('file'): return {"error":"You need to actually write something!"}, 400 - - for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE): - if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})') - body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body) - - body_md = body - options = [] - for i in re.finditer('\s*\$\$([^\$\n]+)\$\$\s*', body_md): - options.append(i.group(1)) - body_md = body_md.replace(i.group(0), "") - - body_md = CustomRenderer().render(mistletoe.Document(body_md)) - body_html = sanitize(body_md) - - bans = filter_comment_html(body_html) - - if bans: - ban = bans[0] - reason = f"Remove the {ban.domain} link from your comment and try again." - if ban.reason: reason += f" {ban.reason}" - return {"error": reason}, 401 - - existing = g.db.query(Comment).options(lazyload('*')).filter(Comment.author_id == v.id, - Comment.deleted_utc == 0, - Comment.parent_comment_id == parent_comment_id, - Comment.parent_submission == parent_submission, - Comment.body == body - ).first() - if existing: - return {"error": f"You already made that comment: {existing.permalink}"}, 409 - - if parent.author.any_block_exists(v) and not v.admin_level>=3: - return {"error": "You can't reply to users who have blocked you, or users you have blocked."}, 403 - - is_bot = request.headers.get("X-User-Type","")=="Bot" - - if not is_bot: - now = int(time.time()) - cutoff = now - 60 * 60 * 24 - - similar_comments = g.db.query(Comment - ).options( - lazyload('*') - ).filter( - Comment.author_id == v.id, - Comment.body.op( - '<->')(body) < app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"], - Comment.created_utc > cutoff - ).all() - - threshold = app.config["COMMENT_SPAM_COUNT_THRESHOLD"] - if v.age >= (60 * 60 * 24 * 7): - threshold *= 3 - elif v.age >= (60 * 60 * 24): - threshold *= 2 - - if len(similar_comments) > threshold: - text = "Your account has been suspended for 1 day for the following reason:\n\n> Too much spam!" - send_notification(NOTIFICATIONS_ACCOUNT, v, text) - - v.ban(reason="Spamming.", - days=1) - - for alt in v.alts: - if not alt.is_suspended: - alt.ban(reason="Spamming.", days=1) - - for comment in similar_comments: - comment.is_banned = True - comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." - g.db.add(comment) - ma=ModAction( - user_id=AUTOJANNY_ACCOUNT, - target_comment_id=comment.id, - kind="ban_comment", - note="spam" - ) - g.db.add(ma) - - return {"error": "Too much spam!"}, 403 - - if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - file=request.files["file"] - if not file.content_type.startswith('image/'): return {"error": "That wasn't an image!"}, 400 - - name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' - file.save(name) - url = request.host_url[:-1] + process_image(name) - - body = request.values.get("body") + f"\n![]({url})" - body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body) - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html = sanitize(body_md) - - if len(body_html) > 20000: abort(400) - - c = Comment(author_id=v.id, - parent_submission=parent_submission, - parent_comment_id=parent_comment_id, - level=level, - over_18=parent_post.over_18 or request.values.get("over_18","")=="true", - is_bot=is_bot, - app_id=v.client.application.id if v.client else None, - body_html=body_html, - body=body[:10000] - ) - - c.upvotes = 1 - g.db.add(c) - g.db.flush() - - for option in options: - c_option = Comment(author_id=AUTOPOLLER_ACCOUNT, - parent_submission=parent_submission, - parent_comment_id=c.id, - level=level+1, - body_html=filter_title(option) - ) - - g.db.add(c_option) - - - if 'pcmemes.net' in request.host and c.body.lower().startswith("based"): - pill = re.match("based and (.{1,20}?)(-| )pilled", body, re.IGNORECASE) - - if level == 1: basedguy = get_account(c.post.author_id) - else: basedguy = get_account(c.parent_comment.author_id) - basedguy.basedcount += 1 - if pill: basedguy.pills += f"{pill.group(1)}, " - g.db.add(basedguy) - - body2 = BASED_MSG.format(username=basedguy.username, basedcount=basedguy.basedcount, pills=basedguy.pills) - - body_md = CustomRenderer().render(mistletoe.Document(body2)) - - body_based_html = sanitize(body_md) - - c_based = Comment(author_id=BASEDBOT_ACCOUNT, - parent_submission=parent_submission, - distinguish_level=6, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_based_html, - body=body2 - ) - - g.db.add(c_based) - g.db.flush() - - n = Notification(comment_id=c_based.id, user_id=v.id) - g.db.add(n) - - - if "rama" in request.host and "ivermectin" in c.body.lower(): - - c.is_banned = True - c.ban_reason = "ToS Violation" - - g.db.add(c) - - body2 = VAXX_MSG.format(username=v.username) - - body_md = CustomRenderer().render(mistletoe.Document(body2)) - - body_jannied_html = sanitize(body_md) - - - - c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, - parent_submission=parent_submission, - distinguish_level=6, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_jannied_html, - body=body2 - ) - - g.db.add(c_jannied) - g.db.flush() - - - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - if v.agendaposter and "trans lives matter" not in c.body_html.lower(): - - c.is_banned = True - c.ban_reason = "ToS Violation" - - g.db.add(c) - - - body = AGENDAPOSTER_MSG.format(username=v.username) - - body_md = CustomRenderer().render(mistletoe.Document(body)) - - body_jannied_html = sanitize(body_md) - - - - c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, - parent_submission=parent_submission, - distinguish_level=6, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_jannied_html, - body=body - ) - - g.db.add(c_jannied) - g.db.flush() - - - - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - if v.id == 2424: - cratvote = CommentVote(user_id=747, comment_id=c.id, vote_type=1) - g.db.add(cratvote) - v.coins += 1 - v.truecoins += 1 - g.db.add(v) - c.upvotes += 1 - g.db.add(c) - - if "rama" in request.host and len(c.body) >= 1000 and v.username != "Snappy" and "" not in body_html: - - body = random.choice(LONGPOST_REPLIES) - body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body) - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html2 = sanitize(body_md) - - - - c2 = Comment(author_id=LONGPOSTBOT_ACCOUNT, - parent_submission=parent_submission, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_html2, - body=body - ) - - g.db.add(c2) - g.db.flush() - - - - n = Notification(comment_id=c2.id, user_id=v.id) - g.db.add(n) - - - - - - - - if "rama" in request.host and random.random() < 0.001 and v.username != "Snappy" and v.username != "zozbot": - - body = "zoz" - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html2 = sanitize(body_md) - - - - - c2 = Comment(author_id=1833, - parent_submission=parent_submission, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_html2, - body=body - ) - - g.db.add(c2) - g.db.flush() - - - - n = Notification(comment_id=c2.id, user_id=v.id) - g.db.add(n) - - - - - - body = "zle" - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html2 = sanitize(body_md) - - - - c3 = Comment(author_id=1833, - parent_submission=parent_submission, - parent_comment_id=c2.id, - level=level+2, - is_bot=True, - body_html=body_html2, - body=body, - ) - - g.db.add(c3) - g.db.flush() - - - - - - - - body = "zozzle" - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html2 = sanitize(body_md) - - - c4 = Comment(author_id=1833, - parent_submission=parent_submission, - parent_comment_id=c3.id, - level=level+3, - is_bot=True, - body_html=body_html2, - body=body - ) - - g.db.add(c4) - g.db.flush() - - - - - - - - - - - if not v.shadowbanned: - notify_users = set() - - for x in g.db.query(Subscription.user_id).options(lazyload('*')).filter_by(submission_id=c.parent_submission).all(): - notify_users.add(x[0]) - - if parent.author.id != v.id: notify_users.add(parent.author.id) - - soup = BeautifulSoup(body_html, features="html.parser") - mentions = soup.find_all("a", href=re.compile("^/@(\w+)")) - for mention in mentions: - username = mention["href"].split("@")[1] - - user = g.db.query(User).options(lazyload('*')).filter_by(username=username).first() - - if user: - if v.any_block_exists(user): - continue - if user.id != v.id: - notify_users.add(user.id) - for x in notify_users: - n = Notification(comment_id=c.id, user_id=x) - g.db.add(n) - g.db.flush() - - if parent.author.id != v.id: - try: - beams_client.publish_to_interests( - interests=[str(parent.author.id)], - publish_body={ - 'web': { - 'notification': { - 'title': f'New reply by @{v.username}', - 'body': c.body, - 'deep_link': f'https://{site}{c.permalink}?context=10#context', - }, - }, - }, - ) - except Exception as e: - print(e) - - - - vote = CommentVote(user_id=v.id, - comment_id=c.id, - vote_type=1 - ) - - g.db.add(vote) - - - cache.delete_memoized(comment_idlist) - - v.comment_count = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.author_id == v.id, Comment.parent_submission != None).filter_by(is_banned=False, deleted_utc=0).count() - g.db.add(v) - - parent_post.comment_count += 1 - g.db.add(parent_post) - - c.voted = 1 - - g.db.commit() - - if request.headers.get("Authorization"): return c.json - else: return render_template("comments.html", v=v, comments=[c]) - - - -@app.post("/edit_comment/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def edit_comment(cid, v): - if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 - - c = get_comment(cid, v=v) - - if not c.author_id == v.id: abort(403) - - if c.is_banned or c.deleted_utc > 0: abort(403) - - body = request.values.get("body", "").strip()[:10000] - for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE): - if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})') - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html = sanitize(body_md) - - bans = filter_comment_html(body_html) - - if bans: - - ban = bans[0] - reason = f"Remove the {ban.domain} link from your comment and try again." - - if ban.reason: reason += f" {ban.reason}" - - if request.headers.get("Authorization"): return {'error': f'A blacklisted domain was used.'}, 400 - else: return render_template("comment_failed.html", - action=f"/edit_comment/{c.id}", - badlinks=[x.domain for x in bans], - body=body, - v=v - ) - now = int(time.time()) - cutoff = now - 60 * 60 * 24 - - similar_comments = g.db.query(Comment - ).options( - lazyload('*') - ).filter( - Comment.author_id == v.id, - Comment.body.op( - '<->')(body) < app.config["SPAM_SIMILARITY_THRESHOLD"], - Comment.created_utc > cutoff - ).all() - - threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] - if v.age >= (60 * 60 * 24 * 30): - threshold *= 4 - elif v.age >= (60 * 60 * 24 * 7): - threshold *= 3 - elif v.age >= (60 * 60 * 24): - threshold *= 2 - - if len(similar_comments) > threshold: - text = "Your account has been suspended for 1 day for the following reason:\n\n> Too much spam!" - send_notification(NOTIFICATIONS_ACCOUNT, v, text) - - v.ban(reason="Spamming.", - days=1) - - for comment in similar_comments: - comment.is_banned = True - comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." - g.db.add(comment) - - return {"error": "Too much spam!"}, 403 - - if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - file=request.files["file"] - if not file.content_type.startswith('image/'): return {"error": "That wasn't an image!"}, 400 - - name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' - file.save(name) - url = request.host_url[:-1] + process_image(name) - - body += f"\n![]({url})" - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html = sanitize(body_md) - - if len(body_html) > 20000: abort(400) - - c.body = body[:10000] - c.body_html = body_html - - if "rama" in request.host and "ivermectin" in c.body_html.lower(): - - c.is_banned = True - c.ban_reason = "ToS Violation" - - g.db.add(c) - - body = VAXX_MSG.format(username=v.username) - - body_md = CustomRenderer().render(mistletoe.Document(body)) - - body_jannied_html = sanitize(body_md) - - - - c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, - parent_submission=c.parent_submission, - distinguish_level=6, - parent_comment_id=c.id, - level=c.level+1, - is_bot=True, - body_html=body_jannied_html, - body=body - ) - - g.db.add(c_jannied) - g.db.flush() - - - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - - if v.agendaposter and "trans lives matter" not in c.body_html.lower(): - - c.is_banned = True - c.ban_reason = "ToS Violation" - - g.db.add(c) - - - body = AGENDAPOSTER_MSG.format(username=v.username) - - body_md = CustomRenderer().render(mistletoe.Document(body)) - - body_jannied_html = sanitize(body_md) - - - - c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, - parent_submission=c.parent_submission, - distinguish_level=6, - parent_comment_id=c.id, - level=c.level+1, - is_bot=True, - body_html=body_jannied_html, - body=body, - ) - - g.db.add(c_jannied) - g.db.flush() - - - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - if int(time.time()) - c.created_utc > 60 * 3: c.edited_utc = int(time.time()) - - g.db.add(c) - - g.db.flush() - - notify_users = set() - soup = BeautifulSoup(body_html, features="html.parser") - mentions = soup.find_all("a", href=re.compile("^/@(\w+)")) - - if len(mentions) > 0: - notifs = g.db.query(Notification) - for mention in mentions: - username = mention["href"].split("@")[1] - - user = g.db.query(User).options(lazyload('*')).filter_by(username=username).first() - - if user: - if v.any_block_exists(user): - continue - if user.id != v.id: - notify_users.add(user.id) - - for x in notify_users: - notif = notifs.filter_by(comment_id=c.id, user_id=x).first() - if not notif: - n = Notification(comment_id=c.id, user_id=x) - g.db.add(n) - - g.db.commit() - - return c.body_html - - -@app.post("/delete/comment/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def delete_comment(cid, v): - - c = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first() - - if not c: abort(404) - - if not c.author_id == v.id: abort(403) - - c.deleted_utc = int(time.time()) - - g.db.add(c) - - cache.delete_memoized(comment_idlist) - - g.db.commit() - - return {"message": "Comment deleted!"} - -@app.post("/undelete/comment/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def undelete_comment(cid, v): - - c = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first() - - if not c: - abort(404) - - if not c.author_id == v.id: - abort(403) - - c.deleted_utc = 0 - - g.db.add(c) - - cache.delete_memoized(comment_idlist) - - g.db.commit() - - return {"message": "Comment undeleted!"} - - -@app.post("/pin_comment/") -@auth_required -@validate_formkey -def toggle_pin_comment(cid, v): - - comment = get_comment(cid, v=v) - - if v.admin_level < 1 and v.id != comment.post.author_id: - abort(403) - - if comment.is_pinned: comment.is_pinned = None - else: comment.is_pinned = v.username - - g.db.add(comment) - g.db.flush() - - if v.admin_level == 6: - ma=ModAction( - kind="pin_comment" if comment.is_pinned else "unpin_comment", - user_id=v.id, - target_comment_id=comment.id - ) - g.db.add(ma) - - g.db.commit() - - if comment.is_pinned: return {"message": "Comment pinned!"} - else: return {"message": "Comment unpinned!"} - - -@app.post("/save_comment/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def save_comment(cid, v): - - comment=get_comment(cid) - - save=g.db.query(SaveRelationship).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id, type=2).first() - - if not save: - new_save=SaveRelationship(user_id=v.id, comment_id=comment.id, type=2) - g.db.add(new_save) - try: g.db.commit() - except: g.db.rollback() - - return {"message": "Comment saved!"} - -@app.post("/unsave_comment/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def unsave_comment(cid, v): - - comment=get_comment(cid) - - save=g.db.query(SaveRelationship).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id, type=2).first() - - if save: - g.db.delete(save) - g.db.commit() - - return {"message": "Comment unsaved!"} +from files.helpers.wrappers import * +from files.helpers.filters import * +from files.helpers.alerts import * +from files.helpers.images import * +from files.helpers.session import * +from files.helpers.const import * +from files.classes import * +from files.routes.front import comment_idlist +from pusher_push_notifications import PushNotifications +from flask import * +from files.__main__ import app, limiter +from .posts import filter_title + + +site = environ.get("DOMAIN").strip() + +beams_client = PushNotifications( + instance_id=PUSHER_INSTANCE_ID, + secret_key=PUSHER_KEY, +) + +@app.get("/comment/") +@app.get("/post///") +@app.get("/logged_out/comment/") +@app.get("/logged_out/post///") +@auth_desired +def post_pid_comment_cid(cid, pid=None, anything=None, v=None): + + if not v and not request.path.startswith('/logged_out'): return redirect(f"/logged_out{request.full_path}") + + if v and request.path.startswith('/logged_out'): v = None + + try: cid = int(cid) + except: + try: cid = int(cid, 36) + except: abort(404) + + comment = get_comment(cid, v=v) + + if comment.post and comment.post.club and not (v and v.paid_dues): abort(403) + + if not comment.parent_submission and not (v and (comment.author.id == v.id or comment.sentto == v.id)) and not (v and v.admin_level == 6) : abort(403) + + if not pid: + if comment.parent_submission: pid = comment.parent_submission + elif "rama" in request.host: pid = 6489 + elif 'pcmemes.net' in request.host: pid = 382 + else: pid = 1 + + try: pid = int(pid) + except: abort(404) + + post = get_post(pid, v=v) + + if post.over_18 and not (v and v.over_18) and not session.get('over_18', 0) >= int(time.time()): + if request.headers.get("Authorization"): return {'error': f'This content is not suitable for some users and situations.'} + else: render_template("errors/nsfw.html", v=v) + + try: context = int(request.values.get("context", 0)) + except: context = 0 + comment_info = comment + c = comment + while context > 0 and c.level > 1: + c = c.parent_comment + context -= 1 + top_comment = c + + if v: defaultsortingcomments = v.defaultsortingcomments + else: defaultsortingcomments = "top" + sort=request.values.get("sort", defaultsortingcomments) + + post.replies=[top_comment] + + if v: + votes = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id).subquery() + + blocking = v.blocking.subquery() + + blocked = v.blocked.subquery() + + comments = g.db.query( + Comment, + votes.c.vote_type, + blocking.c.id, + blocked.c.id, + ) + + if not (v and v.shadowbanned) and not (v and v.admin_level == 6): + shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] + comments = comments.filter(Comment.author_id.notin_(shadowbanned)) + + comments=comments.filter( + Comment.parent_submission == post.id, + Comment.author_id != AUTOPOLLER_ACCOUNT + ).join( + votes, + votes.c.comment_id == Comment.id, + isouter=True + ).join( + blocking, + blocking.c.target_id == Comment.author_id, + isouter=True + ).join( + blocked, + blocked.c.user_id == Comment.author_id, + isouter=True + ) + + output = [] + for c in comments: + comment = c[0] + comment.voted = c[1] or 0 + comment.is_blocking = c[2] or 0 + comment.is_blocked = c[3] or 0 + output.append(comment) + + post.preloaded_comments = output + + if request.headers.get("Authorization"): return top_comment.json + else: return post.rendered_page(v=v, sort=sort, comment=top_comment, comment_info=comment_info) + + +@app.post("/comment") +@limiter.limit("1/second") +@limiter.limit("6/minute") +@is_not_banned +@validate_formkey +def api_comment(v): + if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 + + parent_submission = request.values.get("submission").strip() + parent_fullname = request.values.get("parent_fullname").strip() + + parent_post = get_post(parent_submission, v=v) + if parent_post.club and not (v and v.paid_dues): abort(403) + + if parent_fullname.startswith("t2_"): + parent = parent_post + parent_comment_id = None + level = 1 + elif parent_fullname.startswith("t3_"): + parent = get_comment(parent_fullname.split("_")[1], v=v) + parent_comment_id = parent.id + level = parent.level + 1 + else: abort(400) + + body = request.values.get("body", "").strip()[:10000] + body = body.strip() + + if not body and not request.files.get('file'): return {"error":"You need to actually write something!"}, 400 + + for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE): + if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})') + body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body) + + body_md = body + options = [] + for i in re.finditer('\s*\$\$([^\$\n]+)\$\$\s*', body_md): + options.append(i.group(1)) + body_md = body_md.replace(i.group(0), "") + + body_md = CustomRenderer().render(mistletoe.Document(body_md)) + body_html = sanitize(body_md) + + bans = filter_comment_html(body_html) + + if bans: + ban = bans[0] + reason = f"Remove the {ban.domain} link from your comment and try again." + if ban.reason: reason += f" {ban.reason}" + return {"error": reason}, 401 + + existing = g.db.query(Comment).options(lazyload('*')).filter(Comment.author_id == v.id, + Comment.deleted_utc == 0, + Comment.parent_comment_id == parent_comment_id, + Comment.parent_submission == parent_submission, + Comment.body == body + ).first() + if existing: + return {"error": f"You already made that comment: {existing.permalink}"}, 409 + + if parent.author.any_block_exists(v) and not v.admin_level>=3: + return {"error": "You can't reply to users who have blocked you, or users you have blocked."}, 403 + + is_bot = request.headers.get("X-User-Type","")=="Bot" + + if not is_bot: + now = int(time.time()) + cutoff = now - 60 * 60 * 24 + + similar_comments = g.db.query(Comment + ).options( + lazyload('*') + ).filter( + Comment.author_id == v.id, + Comment.body.op( + '<->')(body) < app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"], + Comment.created_utc > cutoff + ).all() + + threshold = app.config["COMMENT_SPAM_COUNT_THRESHOLD"] + if v.age >= (60 * 60 * 24 * 7): + threshold *= 3 + elif v.age >= (60 * 60 * 24): + threshold *= 2 + + if len(similar_comments) > threshold: + text = "Your account has been suspended for 1 day for the following reason:\n\n> Too much spam!" + send_notification(NOTIFICATIONS_ACCOUNT, v, text) + + v.ban(reason="Spamming.", + days=1) + + for alt in v.alts: + if not alt.is_suspended: + alt.ban(reason="Spamming.", days=1) + + for comment in similar_comments: + comment.is_banned = True + comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." + g.db.add(comment) + ma=ModAction( + user_id=AUTOJANNY_ACCOUNT, + target_comment_id=comment.id, + kind="ban_comment", + note="spam" + ) + g.db.add(ma) + + return {"error": "Too much spam!"}, 403 + + if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + file=request.files["file"] + if not file.content_type.startswith('image/'): return {"error": "That wasn't an image!"}, 400 + + name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' + file.save(name) + url = request.host_url[:-1] + process_image(name) + + body = request.values.get("body") + f"\n![]({url})" + body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body) + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html = sanitize(body_md) + + if len(body_html) > 20000: abort(400) + + c = Comment(author_id=v.id, + parent_submission=parent_submission, + parent_comment_id=parent_comment_id, + level=level, + over_18=parent_post.over_18 or request.values.get("over_18","")=="true", + is_bot=is_bot, + app_id=v.client.application.id if v.client else None, + body_html=body_html, + body=body[:10000] + ) + + c.upvotes = 1 + g.db.add(c) + g.db.flush() + + for option in options: + c_option = Comment(author_id=AUTOPOLLER_ACCOUNT, + parent_submission=parent_submission, + parent_comment_id=c.id, + level=level+1, + body_html=filter_title(option) + ) + + g.db.add(c_option) + + + if 'pcmemes.net' in request.host and c.body.lower().startswith("based"): + pill = re.match("based and (.{1,20}?)(-| )pilled", body, re.IGNORECASE) + + if level == 1: basedguy = get_account(c.post.author_id) + else: basedguy = get_account(c.parent_comment.author_id) + basedguy.basedcount += 1 + if pill: basedguy.pills += f"{pill.group(1)}, " + g.db.add(basedguy) + + body2 = BASED_MSG.format(username=basedguy.username, basedcount=basedguy.basedcount, pills=basedguy.pills) + + body_md = CustomRenderer().render(mistletoe.Document(body2)) + + body_based_html = sanitize(body_md) + + c_based = Comment(author_id=BASEDBOT_ACCOUNT, + parent_submission=parent_submission, + distinguish_level=6, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_based_html, + body=body2 + ) + + g.db.add(c_based) + g.db.flush() + + n = Notification(comment_id=c_based.id, user_id=v.id) + g.db.add(n) + + + if "rama" in request.host and "ivermectin" in c.body.lower(): + + c.is_banned = True + c.ban_reason = "ToS Violation" + + g.db.add(c) + + body2 = VAXX_MSG.format(username=v.username) + + body_md = CustomRenderer().render(mistletoe.Document(body2)) + + body_jannied_html = sanitize(body_md) + + + + c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, + parent_submission=parent_submission, + distinguish_level=6, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_jannied_html, + body=body2 + ) + + g.db.add(c_jannied) + g.db.flush() + + + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + if v.agendaposter and "trans lives matter" not in c.body_html.lower(): + + c.is_banned = True + c.ban_reason = "ToS Violation" + + g.db.add(c) + + + body = AGENDAPOSTER_MSG.format(username=v.username) + + body_md = CustomRenderer().render(mistletoe.Document(body)) + + body_jannied_html = sanitize(body_md) + + + + c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, + parent_submission=parent_submission, + distinguish_level=6, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_jannied_html, + body=body + ) + + g.db.add(c_jannied) + g.db.flush() + + + + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + if v.id == 2424: + cratvote = CommentVote(user_id=747, comment_id=c.id, vote_type=1) + g.db.add(cratvote) + v.coins += 1 + v.truecoins += 1 + g.db.add(v) + c.upvotes += 1 + g.db.add(c) + + if "rama" in request.host and len(c.body) >= 1000 and v.username != "Snappy" and "" not in body_html: + + body = random.choice(LONGPOST_REPLIES) + body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body) + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html2 = sanitize(body_md) + + + + c2 = Comment(author_id=LONGPOSTBOT_ACCOUNT, + parent_submission=parent_submission, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_html2, + body=body + ) + + g.db.add(c2) + g.db.flush() + + + + n = Notification(comment_id=c2.id, user_id=v.id) + g.db.add(n) + + + + + + + + if "rama" in request.host and random.random() < 0.001 and v.username != "Snappy" and v.username != "zozbot": + + body = "zoz" + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html2 = sanitize(body_md) + + + + + c2 = Comment(author_id=1833, + parent_submission=parent_submission, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_html2, + body=body + ) + + g.db.add(c2) + g.db.flush() + + + + n = Notification(comment_id=c2.id, user_id=v.id) + g.db.add(n) + + + + + + body = "zle" + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html2 = sanitize(body_md) + + + + c3 = Comment(author_id=1833, + parent_submission=parent_submission, + parent_comment_id=c2.id, + level=level+2, + is_bot=True, + body_html=body_html2, + body=body, + ) + + g.db.add(c3) + g.db.flush() + + + + + + + + body = "zozzle" + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html2 = sanitize(body_md) + + + c4 = Comment(author_id=1833, + parent_submission=parent_submission, + parent_comment_id=c3.id, + level=level+3, + is_bot=True, + body_html=body_html2, + body=body + ) + + g.db.add(c4) + g.db.flush() + + + + + + + + + + + if not v.shadowbanned: + notify_users = set() + + for x in g.db.query(Subscription.user_id).options(lazyload('*')).filter_by(submission_id=c.parent_submission).all(): + notify_users.add(x[0]) + + if parent.author.id != v.id: notify_users.add(parent.author.id) + + soup = BeautifulSoup(body_html, features="html.parser") + mentions = soup.find_all("a", href=re.compile("^/@(\w+)")) + for mention in mentions: + username = mention["href"].split("@")[1] + + user = g.db.query(User).options(lazyload('*')).filter_by(username=username).first() + + if user: + if v.any_block_exists(user): + continue + if user.id != v.id: + notify_users.add(user.id) + for x in notify_users: + n = Notification(comment_id=c.id, user_id=x) + g.db.add(n) + g.db.flush() + + if parent.author.id != v.id: + try: + beams_client.publish_to_interests( + interests=[str(parent.author.id)], + publish_body={ + 'web': { + 'notification': { + 'title': f'New reply by @{v.username}', + 'body': c.body, + 'deep_link': f'https://{site}{c.permalink}?context=10#context', + }, + }, + }, + ) + except Exception as e: + print(e) + + + + vote = CommentVote(user_id=v.id, + comment_id=c.id, + vote_type=1 + ) + + g.db.add(vote) + + + cache.delete_memoized(comment_idlist) + + v.comment_count = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.author_id == v.id, Comment.parent_submission != None).filter_by(is_banned=False, deleted_utc=0).count() + g.db.add(v) + + parent_post.comment_count += 1 + g.db.add(parent_post) + + c.voted = 1 + + g.db.commit() + + if request.headers.get("Authorization"): return c.json + else: return render_template("comments.html", v=v, comments=[c]) + + + +@app.post("/edit_comment/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def edit_comment(cid, v): + if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 + + c = get_comment(cid, v=v) + + if not c.author_id == v.id: abort(403) + + if c.is_banned or c.deleted_utc > 0: abort(403) + + body = request.values.get("body", "").strip()[:10000] + for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE): + if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})') + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html = sanitize(body_md) + + bans = filter_comment_html(body_html) + + if bans: + + ban = bans[0] + reason = f"Remove the {ban.domain} link from your comment and try again." + + if ban.reason: reason += f" {ban.reason}" + + if request.headers.get("Authorization"): return {'error': f'A blacklisted domain was used.'}, 400 + else: return render_template("comment_failed.html", + action=f"/edit_comment/{c.id}", + badlinks=[x.domain for x in bans], + body=body, + v=v + ) + now = int(time.time()) + cutoff = now - 60 * 60 * 24 + + similar_comments = g.db.query(Comment + ).options( + lazyload('*') + ).filter( + Comment.author_id == v.id, + Comment.body.op( + '<->')(body) < app.config["SPAM_SIMILARITY_THRESHOLD"], + Comment.created_utc > cutoff + ).all() + + threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] + if v.age >= (60 * 60 * 24 * 30): + threshold *= 4 + elif v.age >= (60 * 60 * 24 * 7): + threshold *= 3 + elif v.age >= (60 * 60 * 24): + threshold *= 2 + + if len(similar_comments) > threshold: + text = "Your account has been suspended for 1 day for the following reason:\n\n> Too much spam!" + send_notification(NOTIFICATIONS_ACCOUNT, v, text) + + v.ban(reason="Spamming.", + days=1) + + for comment in similar_comments: + comment.is_banned = True + comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." + g.db.add(comment) + + return {"error": "Too much spam!"}, 403 + + if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + file=request.files["file"] + if not file.content_type.startswith('image/'): return {"error": "That wasn't an image!"}, 400 + + name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' + file.save(name) + url = request.host_url[:-1] + process_image(name) + + body += f"\n![]({url})" + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html = sanitize(body_md) + + if len(body_html) > 20000: abort(400) + + c.body = body[:10000] + c.body_html = body_html + + if "rama" in request.host and "ivermectin" in c.body_html.lower(): + + c.is_banned = True + c.ban_reason = "ToS Violation" + + g.db.add(c) + + body = VAXX_MSG.format(username=v.username) + + body_md = CustomRenderer().render(mistletoe.Document(body)) + + body_jannied_html = sanitize(body_md) + + + + c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, + parent_submission=c.parent_submission, + distinguish_level=6, + parent_comment_id=c.id, + level=c.level+1, + is_bot=True, + body_html=body_jannied_html, + body=body + ) + + g.db.add(c_jannied) + g.db.flush() + + + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + + if v.agendaposter and "trans lives matter" not in c.body_html.lower(): + + c.is_banned = True + c.ban_reason = "ToS Violation" + + g.db.add(c) + + + body = AGENDAPOSTER_MSG.format(username=v.username) + + body_md = CustomRenderer().render(mistletoe.Document(body)) + + body_jannied_html = sanitize(body_md) + + + + c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, + parent_submission=c.parent_submission, + distinguish_level=6, + parent_comment_id=c.id, + level=c.level+1, + is_bot=True, + body_html=body_jannied_html, + body=body, + ) + + g.db.add(c_jannied) + g.db.flush() + + + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + if int(time.time()) - c.created_utc > 60 * 3: c.edited_utc = int(time.time()) + + g.db.add(c) + + g.db.flush() + + notify_users = set() + soup = BeautifulSoup(body_html, features="html.parser") + mentions = soup.find_all("a", href=re.compile("^/@(\w+)")) + + if len(mentions) > 0: + notifs = g.db.query(Notification) + for mention in mentions: + username = mention["href"].split("@")[1] + + user = g.db.query(User).options(lazyload('*')).filter_by(username=username).first() + + if user: + if v.any_block_exists(user): + continue + if user.id != v.id: + notify_users.add(user.id) + + for x in notify_users: + notif = notifs.filter_by(comment_id=c.id, user_id=x).first() + if not notif: + n = Notification(comment_id=c.id, user_id=x) + g.db.add(n) + + g.db.commit() + + return c.body_html + + +@app.post("/delete/comment/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def delete_comment(cid, v): + + c = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first() + + if not c: abort(404) + + if not c.author_id == v.id: abort(403) + + c.deleted_utc = int(time.time()) + + g.db.add(c) + + cache.delete_memoized(comment_idlist) + + g.db.commit() + + return {"message": "Comment deleted!"} + +@app.post("/undelete/comment/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def undelete_comment(cid, v): + + c = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first() + + if not c: + abort(404) + + if not c.author_id == v.id: + abort(403) + + c.deleted_utc = 0 + + g.db.add(c) + + cache.delete_memoized(comment_idlist) + + g.db.commit() + + return {"message": "Comment undeleted!"} + + +@app.post("/pin_comment/") +@auth_required +@validate_formkey +def toggle_pin_comment(cid, v): + + comment = get_comment(cid, v=v) + + if v.admin_level < 1 and v.id != comment.post.author_id: + abort(403) + + if comment.is_pinned: comment.is_pinned = None + else: comment.is_pinned = v.username + + g.db.add(comment) + g.db.flush() + + if v.admin_level == 6: + ma=ModAction( + kind="pin_comment" if comment.is_pinned else "unpin_comment", + user_id=v.id, + target_comment_id=comment.id + ) + g.db.add(ma) + + g.db.commit() + + if comment.is_pinned: return {"message": "Comment pinned!"} + else: return {"message": "Comment unpinned!"} + + +@app.post("/save_comment/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def save_comment(cid, v): + + comment=get_comment(cid) + + save=g.db.query(SaveRelationship).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id, type=2).first() + + if not save: + new_save=SaveRelationship(user_id=v.id, comment_id=comment.id, type=2) + g.db.add(new_save) + try: g.db.commit() + except: g.db.rollback() + + return {"message": "Comment saved!"} + +@app.post("/unsave_comment/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def unsave_comment(cid, v): + + comment=get_comment(cid) + + save=g.db.query(SaveRelationship).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id, type=2).first() + + if save: + g.db.delete(save) + g.db.commit() + + return {"message": "Comment unsaved!"} diff --git a/files/routes/discord.py b/files/routes/discord.py old mode 100644 new mode 100755 index eea2f57e2..1112708f1 --- a/files/routes/discord.py +++ b/files/routes/discord.py @@ -1,146 +1,146 @@ -from files.helpers.wrappers import * -from files.helpers.security import * -from files.helpers.discord import add_role -from files.__main__ import app -import requests - -SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip() -CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip() -CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip() -BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN").strip() -COINS_NAME = environ.get("COINS_NAME").strip() -DISCORD_ENDPOINT = "https://discordapp.com/api/v6" -WELCOME_CHANNEL="846509313941700618" - -@app.get("/discord") -@auth_required -def join_discord(v): - - if v.is_suspended != 0: return "You're banned" - - if 'rama' in request.host and v.admin_level == 0 and v.patron == 0 and v.truecoins < 150: return f"You must earn 150 {COINS_NAME} before entering the Discord server. You earn {COINS_NAME} by making posts/comments and getting upvoted." - - if v.shadowbanned or v.agendaposter: return "" - - now=int(time.time()) - - state=generate_hash(f"{now}+{v.id}+discord") - - state=f"{now}.{state}" - - return redirect(f"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri=https%3A%2F%2F{app.config['SERVER_NAME']}%2Fdiscord_redirect&response_type=code&scope=identify%20guilds.join&state={state}") - - -@app.get("/discord_redirect") -@auth_required -def discord_redirect(v): - - - now=int(time.time()) - state=request.values.get('state','').split('.') - - timestamp=state[0] - - state=state[1] - - if int(timestamp) < now-600: - abort(400) - - if not validate_hash(f"{timestamp}+{v.id}+discord", state): - abort(400) - - code = request.values.get("code","") - if not code: - abort(400) - - data={ - "client_id":CLIENT_ID, - 'client_secret': CLIENT_SECRET, - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': f"https://{app.config['SERVER_NAME']}/discord_redirect", - 'scope': 'identify guilds.join' - } - headers={ - 'Content-Type': 'application/x-www-form-urlencoded' - } - url="https://discord.com/api/oauth2/token" - - x=requests.post(url, headers=headers, data=data) - - x=x.json() - - - try: - token=x["access_token"] - except KeyError: - abort(403) - - - url="https://discord.com/api/users/@me" - headers={ - 'Authorization': f"Bearer {token}" - } - x=requests.get(url, headers=headers) - - x=x.json() - - - - headers={ - 'Authorization': f"Bot {BOT_TOKEN}", - 'Content-Type': "application/json" - } - - if v.discord_id and v.discord_id != x['id']: - url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}" - requests.delete(url, headers=headers) - - if g.db.query(User).options(lazyload('*')).filter(User.id!=v.id, User.discord_id==x["id"]).first(): - return render_template("message.html", title="Discord account already linked.", error="That Discord account is already in use by another user.", v=v) - - v.discord_id=x["id"] - g.db.add(v) - - url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{x['id']}" - - name=v.username - - data={ - "access_token":token, - "nick":name, - } - - x=requests.put(url, headers=headers, json=data) - - if x.status_code in [201, 204]: - - if v.id == 1: - add_role(v, "shrigma") - time.sleep(0.1) - - if v.admin_level > 0: add_role(v, "admin") - - time.sleep(0.1) - add_role(v, "linked") - - if v.patron: - time.sleep(0.1) - add_role(v, str(v.patron)) - - else: - return x.json() - - - if x.status_code==204: - - url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}" - data={ - "nick": name - } - - requests.patch(url, headers=headers, json=data) - - g.db.commit() - +from files.helpers.wrappers import * +from files.helpers.security import * +from files.helpers.discord import add_role +from files.__main__ import app +import requests + +SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip() +CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip() +CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip() +BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN").strip() +COINS_NAME = environ.get("COINS_NAME").strip() +DISCORD_ENDPOINT = "https://discordapp.com/api/v6" +WELCOME_CHANNEL="846509313941700618" + +@app.get("/discord") +@auth_required +def join_discord(v): + + if v.is_suspended != 0: return "You're banned" + + if 'rama' in request.host and v.admin_level == 0 and v.patron == 0 and v.truecoins < 150: return f"You must earn 150 {COINS_NAME} before entering the Discord server. You earn {COINS_NAME} by making posts/comments and getting upvoted." + + if v.shadowbanned or v.agendaposter: return "" + + now=int(time.time()) + + state=generate_hash(f"{now}+{v.id}+discord") + + state=f"{now}.{state}" + + return redirect(f"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri=https%3A%2F%2F{app.config['SERVER_NAME']}%2Fdiscord_redirect&response_type=code&scope=identify%20guilds.join&state={state}") + + +@app.get("/discord_redirect") +@auth_required +def discord_redirect(v): + + + now=int(time.time()) + state=request.values.get('state','').split('.') + + timestamp=state[0] + + state=state[1] + + if int(timestamp) < now-600: + abort(400) + + if not validate_hash(f"{timestamp}+{v.id}+discord", state): + abort(400) + + code = request.values.get("code","") + if not code: + abort(400) + + data={ + "client_id":CLIENT_ID, + 'client_secret': CLIENT_SECRET, + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': f"https://{app.config['SERVER_NAME']}/discord_redirect", + 'scope': 'identify guilds.join' + } + headers={ + 'Content-Type': 'application/x-www-form-urlencoded' + } + url="https://discord.com/api/oauth2/token" + + x=requests.post(url, headers=headers, data=data) + + x=x.json() + + + try: + token=x["access_token"] + except KeyError: + abort(403) + + + url="https://discord.com/api/users/@me" + headers={ + 'Authorization': f"Bearer {token}" + } + x=requests.get(url, headers=headers) + + x=x.json() + + + + headers={ + 'Authorization': f"Bot {BOT_TOKEN}", + 'Content-Type': "application/json" + } + + if v.discord_id and v.discord_id != x['id']: + url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}" + requests.delete(url, headers=headers) + + if g.db.query(User).options(lazyload('*')).filter(User.id!=v.id, User.discord_id==x["id"]).first(): + return render_template("message.html", title="Discord account already linked.", error="That Discord account is already in use by another user.", v=v) + + v.discord_id=x["id"] + g.db.add(v) + + url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{x['id']}" + + name=v.username + + data={ + "access_token":token, + "nick":name, + } + + x=requests.put(url, headers=headers, json=data) + + if x.status_code in [201, 204]: + + if v.id == 1: + add_role(v, "shrigma") + time.sleep(0.1) + + if v.admin_level > 0: add_role(v, "admin") + + time.sleep(0.1) + add_role(v, "linked") + + if v.patron: + time.sleep(0.1) + add_role(v, str(v.patron)) + + else: + return x.json() + + + if x.status_code==204: + + url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}" + data={ + "nick": name + } + + requests.patch(url, headers=headers, json=data) + + g.db.commit() + return redirect(f"https://discord.com/channels/{SERVER_ID}/{WELCOME_CHANNEL}") \ No newline at end of file diff --git a/files/routes/errors.py b/files/routes/errors.py old mode 100644 new mode 100755 index 4f455eae1..980095c87 --- a/files/routes/errors.py +++ b/files/routes/errors.py @@ -1,84 +1,84 @@ -import jinja2.exceptions - -from files.helpers.wrappers import * -from files.helpers.session import * -from flask import * -from urllib.parse import quote, urlencode -import time -from files.__main__ import app, limiter - -# Errors - - - -@app.errorhandler(400) -@auth_desired -def error_400(e, v): - if request.headers.get("Authorization"): return {"error": "400 Bad Request"}, 400 - else: return render_template('errors/400.html', v=v), 400 - -@app.errorhandler(401) -def error_401(e): - - path = request.path - qs = urlencode(dict(request.values)) - argval = quote(f"{path}?{qs}", safe='') - output = f"/login?redirect={argval}" - - if request.headers.get("Authorization"): return {"error": "401 Not Authorized"}, 401 - else: return redirect(output) - - -@app.errorhandler(403) -@auth_desired -def error_403(e, v): - if request.headers.get("Authorization"): return {"error": "403 Forbidden"}, 403 - else: return render_template('errors/403.html', v=v), 403 - - -@app.errorhandler(404) -@auth_desired -def error_404(e, v): - if request.headers.get("Authorization"): return {"error": "404 Not Found"}, 404 - else: return render_template('errors/404.html', v=v), 404 - - -@app.errorhandler(405) -@auth_desired -def error_405(e, v): - if request.headers.get("Authorization"): return {"error": "405 Method Not Allowed"}, 405 - else: return render_template('errors/405.html', v=v), 405 - - -@app.errorhandler(429) -@auth_desired -def error_429(e, v): - if request.headers.get("Authorization"): return {"error": "429 Too Many Requests"}, 429 - else: return render_template('errors/429.html', v=v), 429 - - -@app.errorhandler(500) -@auth_desired -def error_500(e, v): - g.db.rollback() - - if request.headers.get("Authorization"): return {"error": "500 Internal Server Error"}, 500 - else: return render_template('errors/500.html', v=v), 500 - - -@app.post("/allow_nsfw") -def allow_nsfw(): - - session["over_18"] = int(time.time()) + 3600 - return redirect(request.values.get("redir", "/")) - - -@app.get("/error/") -@auth_desired -def error_all_preview(error, v): - - try: - return render_template(f"errors/{error}.html", v=v) - except jinja2.exceptions.TemplateNotFound: - abort(400) - +import jinja2.exceptions + +from files.helpers.wrappers import * +from files.helpers.session import * +from flask import * +from urllib.parse import quote, urlencode +import time +from files.__main__ import app, limiter + +# Errors + + + +@app.errorhandler(400) +@auth_desired +def error_400(e, v): + if request.headers.get("Authorization"): return {"error": "400 Bad Request"}, 400 + else: return render_template('errors/400.html', v=v), 400 + +@app.errorhandler(401) +def error_401(e): + + path = request.path + qs = urlencode(dict(request.values)) + argval = quote(f"{path}?{qs}", safe='') + output = f"/login?redirect={argval}" + + if request.headers.get("Authorization"): return {"error": "401 Not Authorized"}, 401 + else: return redirect(output) + + +@app.errorhandler(403) +@auth_desired +def error_403(e, v): + if request.headers.get("Authorization"): return {"error": "403 Forbidden"}, 403 + else: return render_template('errors/403.html', v=v), 403 + + +@app.errorhandler(404) +@auth_desired +def error_404(e, v): + if request.headers.get("Authorization"): return {"error": "404 Not Found"}, 404 + else: return render_template('errors/404.html', v=v), 404 + + +@app.errorhandler(405) +@auth_desired +def error_405(e, v): + if request.headers.get("Authorization"): return {"error": "405 Method Not Allowed"}, 405 + else: return render_template('errors/405.html', v=v), 405 + + +@app.errorhandler(429) +@auth_desired +def error_429(e, v): + if request.headers.get("Authorization"): return {"error": "429 Too Many Requests"}, 429 + else: return render_template('errors/429.html', v=v), 429 + + +@app.errorhandler(500) +@auth_desired +def error_500(e, v): + g.db.rollback() + + if request.headers.get("Authorization"): return {"error": "500 Internal Server Error"}, 500 + else: return render_template('errors/500.html', v=v), 500 + + +@app.post("/allow_nsfw") +def allow_nsfw(): + + session["over_18"] = int(time.time()) + 3600 + return redirect(request.values.get("redir", "/")) + + +@app.get("/error/") +@auth_desired +def error_all_preview(error, v): + + try: + return render_template(f"errors/{error}.html", v=v) + except jinja2.exceptions.TemplateNotFound: + abort(400) + diff --git a/files/routes/feeds.py b/files/routes/feeds.py old mode 100644 new mode 100755 index 49cab51bf..f653dfeb8 --- a/files/routes/feeds.py +++ b/files/routes/feeds.py @@ -1,66 +1,66 @@ -import html -from .front import frontlist -from datetime import datetime -from files.helpers.jinja2 import full_link -from files.helpers.get import * -from yattag import Doc - -from files.__main__ import app - -@app.get('/rss//') -def feeds_user(sort='hot', t='all'): - - page = int(request.values.get("page", 1)) - - ids, next_exists = frontlist( - sort=sort, - page=page, - t=t, - v=None, - ) - - posts = get_posts(ids) - - domain = environ.get("DOMAIN").strip() - - doc, tag, text = Doc().tagtext() - - with tag("feed", ("xmlns:media","http://search.yahoo.com/mrss/"), xmlns="http://www.w3.org/2005/Atom",): - with tag("title", type="text"): - text(f"{sort} posts from {domain}") - - doc.stag("link", href=request.url) - doc.stag("link", href=request.url_root) - - for post in posts: - with tag("entry", ("xml:base", request.url)): - with tag("title", type="text"): - text(post.title) - - with tag("id"): - text(post.fullname) - - if (post.edited_utc > 0): - with tag("updated"): - text(datetime.utcfromtimestamp(post.edited_utc).isoformat()) - - with tag("published"): - text(datetime.utcfromtimestamp(post.created_utc).isoformat()) - - with tag("author"): - with tag("name"): - text(post.author.username) - with tag("uri"): - text(f'https://{site}/@{post.author.username}') - - doc.stag("link", href=full_link(post.permalink)) - - image_url = post.thumb_url or post.embed_url or post.url - - doc.stag("media:thumbnail", url=image_url) - - if len(post.body_html) > 0: - with tag("content", type="html"): - doc.cdata(f'
{post.body_html}') - +import html +from .front import frontlist +from datetime import datetime +from files.helpers.jinja2 import full_link +from files.helpers.get import * +from yattag import Doc + +from files.__main__ import app + +@app.get('/rss//') +def feeds_user(sort='hot', t='all'): + + page = int(request.values.get("page", 1)) + + ids, next_exists = frontlist( + sort=sort, + page=page, + t=t, + v=None, + ) + + posts = get_posts(ids) + + domain = environ.get("DOMAIN").strip() + + doc, tag, text = Doc().tagtext() + + with tag("feed", ("xmlns:media","http://search.yahoo.com/mrss/"), xmlns="http://www.w3.org/2005/Atom",): + with tag("title", type="text"): + text(f"{sort} posts from {domain}") + + doc.stag("link", href=request.url) + doc.stag("link", href=request.url_root) + + for post in posts: + with tag("entry", ("xml:base", request.url)): + with tag("title", type="text"): + text(post.title) + + with tag("id"): + text(post.fullname) + + if (post.edited_utc > 0): + with tag("updated"): + text(datetime.utcfromtimestamp(post.edited_utc).isoformat()) + + with tag("published"): + text(datetime.utcfromtimestamp(post.created_utc).isoformat()) + + with tag("author"): + with tag("name"): + text(post.author.username) + with tag("uri"): + text(f'https://{site}/@{post.author.username}') + + doc.stag("link", href=full_link(post.permalink)) + + image_url = post.thumb_url or post.embed_url or post.url + + doc.stag("media:thumbnail", url=image_url) + + if len(post.body_html) > 0: + with tag("content", type="html"): + doc.cdata(f'
{post.body_html}') + return Response( ""+ doc.getvalue(), mimetype="application/xml") \ No newline at end of file diff --git a/files/routes/front.py b/files/routes/front.py old mode 100644 new mode 100755 index e916058be..3213984f8 --- a/files/routes/front.py +++ b/files/routes/front.py @@ -1,395 +1,395 @@ -from files.helpers.wrappers import * -from files.helpers.get import * - -from files.__main__ import app, cache -from files.classes.submission import Submission - -defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip() - -@app.get("/post/") -def slash_post(): - return redirect("/") - -@app.get("/notifications") -@auth_required -def notifications(v): - try: page = int(request.values.get('page', 1)) - except: page = 1 - messages = request.values.get('messages', False) - modmail = request.values.get('modmail', False) - posts = request.values.get('posts', False) - if modmail and v.admin_level == 6: - comments = g.db.query(Comment).filter(Comment.sentto==0).order_by(Comment.created_utc.desc()).offset(25*(page-1)).limit(26).all() - next_exists = (len(comments) > 25) - comments = comments[:25] - elif messages: - comments = g.db.query(Comment).filter(or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None).order_by(Comment.created_utc.desc(), not_(Comment.child_comments.any())).offset(25*(page-1)).limit(26).all() - next_exists = (len(comments) > 25) - comments = comments[:25] - elif posts: - notifications = v.notifications.join(Notification.comment).filter(Comment.author_id == AUTOJANNY_ACCOUNT).order_by(Notification.id.desc()).offset(25 * (page - 1)).limit(101).all() - - listing = [] - - for index, x in enumerate(notifications[:100]): - c = x.comment - if x.read and index > 25: break - elif not x.read: - x.read = True - c.unread = True - g.db.add(x) - listing.append(c) - - g.db.commit() - - next_exists = (len(notifications) > len(listing)) - - else: - notifications = v.notifications.join(Notification.comment).filter(Comment.author_id != AUTOJANNY_ACCOUNT).order_by(Notification.id.desc()).offset(25 * (page - 1)).limit(101).all() - - listing = [] - - for index, x in enumerate(notifications[:100]): - c = x.comment - if x.read and index > 25: break - elif not x.read: - x.read = True - g.db.add(x) - listing.append(c.id) - - g.db.commit() - - comments = get_comments(listing, v=v, load_parent=True) - next_exists = (len(notifications) > len(comments)) - - if not posts: - listing = [] - for c in comments: - c.is_blocked = False - c.is_blocking = False - if c.parent_submission and c.parent_comment and c.parent_comment.author_id == v.id: - c.replies = [] - while c.parent_comment and c.parent_comment.author_id == v.id: - parent = c.parent_comment - if c not in parent.replies2: - parent.replies2 = parent.replies2 + [c] - parent.replies = parent.replies2 - c = parent - if c not in listing: - listing.append(c) - c.replies = c.replies2 - elif c.parent_submission: - c.replies = [] - if c not in listing: - listing.append(c) - else: - if c.parent_comment: - while c.level > 1: - c = c.parent_comment - - if c not in listing: - listing.append(c) - - - return render_template("notifications.html", - v=v, - notifications=listing, - next_exists=next_exists, - page=page, - standalone=True, - render_replies=True - ) - - - -@app.get("/") -@app.get("/logged_out") -@auth_desired -def front_all(v): - - if not v and request.path == "/" and not request.headers.get("Authorization"): return redirect(f"/logged_out{request.full_path}") - - if v and request.path.startswith('/logged_out'): v = None - - try: page = int(request.values.get("page") or 1) - except: abort(400) - - page = max(page, 1) - - if v: - defaultsorting = v.defaultsorting - defaulttime = v.defaulttime - else: - defaultsorting = "hot" - defaulttime = defaulttimefilter - - sort=request.values.get("sort", defaultsorting) - t=request.values.get('t', defaulttime) - - ids, next_exists = frontlist(sort=sort, - page=page, - t=t, - v=v, - filter_words=v.filter_words if v else [], - ) - - posts = get_posts(ids, v=v) - - if v and v.hidevotedon: posts = [x for x in posts if not hasattr(x, 'voted') or not x.voted] - - if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists} - else: return render_template("home.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page) - - - -@cache.memoize(timeout=86400) -def frontlist(v=None, sort="hot", page=1, t="all", ids_only=True, filter_words=''): - - posts = g.db.query(Submission.id).options(lazyload('*')) - - if 'rama' in request.host and sort == "hot": - cutoff = int(time.time()) - 86400 - posts = posts.filter(Submission.created_utc >= cutoff) - elif t != 'all': - now = int(time.time()) - if t == 'hour': cutoff = now - 3600 - elif t == 'week': cutoff = now - 604800 - elif t == 'month': cutoff = now - 2592000 - elif t == 'year': cutoff = now - 31536000 - else: cutoff = now - 86400 - posts = posts.filter(Submission.created_utc >= cutoff) - - posts = posts.filter_by(is_banned=False, stickied=None, private=False, deleted_utc = 0) - - if v and v.admin_level == 0: - blocking = [x[0] for x in g.db.query( - UserBlock.target_id).filter_by( - user_id=v.id).all()] - blocked = [x[0] for x in g.db.query( - UserBlock.user_id).filter_by( - target_id=v.id).all()] - posts = posts.filter( - Submission.author_id.notin_(blocking), - Submission.author_id.notin_(blocked) - ) - - if not (v and v.changelogsub): - posts=posts.filter(not_(Submission.title.ilike(f'[changelog]%'))) - - if v and filter_words: - for word in filter_words: - posts=posts.filter(not_(Submission.title.ilike(f'%{word}%'))) - - if not (v and v.shadowbanned): - shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] - posts = posts.filter(Submission.author_id.notin_(shadowbanned)) - - if sort == "hot": - ti = int(time.time()) + 3600 - posts = posts.order_by(-1000000*(Submission.upvotes - Submission.downvotes + 1 + Submission.comment_count/5)/(func.power(((ti - Submission.created_utc)/1000), 1.35))) - elif sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc.asc()) - elif sort == "controversial": - posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc()) - - if v: - if v.agendaposter: size = 5 - else: size = v.frontsize - else: size = 25 - - posts = posts.offset(size * (page - 1)).limit(size+1).all() - - next_exists = (len(posts) > size) - - posts = posts[:size] - - pins = g.db.query(Submission.id).options(lazyload('*')).filter(Submission.stickied != None) - if v and v.admin_level == 0: - blocking = [x[0] for x in g.db.query(UserBlock.target_id).filter_by(user_id=v.id).all()] - blocked = [x[0] for x in g.db.query(UserBlock.user_id).filter_by(target_id=v.id).all()] - pins = pins.filter(Submission.author_id.notin_(blocking), Submission.author_id.notin_(blocked)) - - if page == 1: posts = pins.all() + posts - - if ids_only: posts = [x[0] for x in posts] - - return posts, next_exists - - -@app.get("/changelog") -@auth_desired -def changelog(v): - - - page = int(request.values.get("page") or 1) - page = max(page, 1) - - sort=request.values.get("sort", "new") - t=request.values.get('t', "all") - - ids = changeloglist(sort=sort, - page=page, - t=t, - v=v, - ) - - next_exists = (len(ids) > 25) - ids = ids[:25] - - posts = get_posts(ids, v=v) - - if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists} - else: return render_template("changelog.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page) - - -@cache.memoize(timeout=86400) -def changeloglist(v=None, sort="new", page=1 ,t="all"): - - posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False, private=False,).filter(Submission.deleted_utc == 0) - - if v and v.admin_level == 0: - blocking = [x[0] for x in g.db.query( - UserBlock.target_id).filter_by( - user_id=v.id).all()] - blocked = [x[0] for x in g.db.query( - UserBlock.user_id).filter_by( - target_id=v.id).all()] - posts = posts.filter( - Submission.author_id.notin_(blocking), - Submission.author_id.notin_(blocked) - ) - - admins = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.admin_level == 6).all()] - posts = posts.filter(Submission.title.ilike('_changelog%'), Submission.author_id.in_(admins)) - - if t != 'all': - cutoff = 0 - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - posts = posts.filter(Submission.created_utc >= cutoff) - - if sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc.asc()) - elif sort == "controversial": - posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc()) - - posts = posts.offset(25 * (page - 1)).limit(26).all() - - return [x[0] for x in posts] - - -@app.get("/random") -@auth_desired -def random_post(v): - - x = g.db.query(Submission).options(lazyload('*')).filter(Submission.deleted_utc == 0, Submission.is_banned == False) - total = x.count() - n = random.randint(1, total - 2) - - post = x.offset(n).limit(1).first() - return redirect(f"/post/{post.id}") - -@cache.memoize(timeout=86400) -def comment_idlist(page=1, v=None, nsfw=False, sort="new", t="all"): - - posts = g.db.query(Submission).options(lazyload('*')) - cc_idlist = [x[0] for x in g.db.query(Submission.id).options(lazyload('*')).filter(Submission.club == True).all()] - - posts = posts.subquery() - - comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.parent_submission.notin_(cc_idlist)) - - if v and v.admin_level <= 3: - blocking = [x[0] for x in g.db.query( - UserBlock.target_id).filter_by( - user_id=v.id).all()] - blocked = [x[0] for x in g.db.query( - UserBlock.user_id).filter_by( - target_id=v.id).all()] - - comments = comments.filter( - Comment.author_id.notin_(blocking), - Comment.author_id.notin_(blocked) - ) - - if not v or not v.admin_level >= 3: - comments = comments.filter_by(is_banned=False).filter(Comment.deleted_utc == 0) - - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - comments = comments.filter(Comment.created_utc >= cutoff) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc.asc()) - elif sort == "controversial": - comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) - elif sort == "top": - comments = comments.order_by(Comment.downvotes - Comment.upvotes) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - comments = comments.offset(25 * (page - 1)).limit(26).all() - return [x[0] for x in comments] - -@app.get("/comments") -@auth_desired -def all_comments(v): - - - page = int(request.values.get("page", 1)) - - sort=request.values.get("sort", "new") - t=request.values.get("t", defaulttimefilter) - - idlist = comment_idlist(v=v, - page=page, - sort=sort, - t=t, - ) - - comments = get_comments(idlist, v=v) - - next_exists = len(idlist) > 25 - - idlist = idlist[:25] - - if request.headers.get("Authorization"): return {"data": [x.json for x in comments]} - else: return render_template("home_comments.html", v=v, sort=sort, t=t, page=page, comments=comments, standalone=True, next_exists=next_exists) +from files.helpers.wrappers import * +from files.helpers.get import * + +from files.__main__ import app, cache +from files.classes.submission import Submission + +defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip() + +@app.get("/post/") +def slash_post(): + return redirect("/") + +@app.get("/notifications") +@auth_required +def notifications(v): + try: page = int(request.values.get('page', 1)) + except: page = 1 + messages = request.values.get('messages', False) + modmail = request.values.get('modmail', False) + posts = request.values.get('posts', False) + if modmail and v.admin_level == 6: + comments = g.db.query(Comment).filter(Comment.sentto==0).order_by(Comment.created_utc.desc()).offset(25*(page-1)).limit(26).all() + next_exists = (len(comments) > 25) + comments = comments[:25] + elif messages: + comments = g.db.query(Comment).filter(or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None).order_by(Comment.created_utc.desc(), not_(Comment.child_comments.any())).offset(25*(page-1)).limit(26).all() + next_exists = (len(comments) > 25) + comments = comments[:25] + elif posts: + notifications = v.notifications.join(Notification.comment).filter(Comment.author_id == AUTOJANNY_ACCOUNT).order_by(Notification.id.desc()).offset(25 * (page - 1)).limit(101).all() + + listing = [] + + for index, x in enumerate(notifications[:100]): + c = x.comment + if x.read and index > 25: break + elif not x.read: + x.read = True + c.unread = True + g.db.add(x) + listing.append(c) + + g.db.commit() + + next_exists = (len(notifications) > len(listing)) + + else: + notifications = v.notifications.join(Notification.comment).filter(Comment.author_id != AUTOJANNY_ACCOUNT).order_by(Notification.id.desc()).offset(25 * (page - 1)).limit(101).all() + + listing = [] + + for index, x in enumerate(notifications[:100]): + c = x.comment + if x.read and index > 25: break + elif not x.read: + x.read = True + g.db.add(x) + listing.append(c.id) + + g.db.commit() + + comments = get_comments(listing, v=v, load_parent=True) + next_exists = (len(notifications) > len(comments)) + + if not posts: + listing = [] + for c in comments: + c.is_blocked = False + c.is_blocking = False + if c.parent_submission and c.parent_comment and c.parent_comment.author_id == v.id: + c.replies = [] + while c.parent_comment and c.parent_comment.author_id == v.id: + parent = c.parent_comment + if c not in parent.replies2: + parent.replies2 = parent.replies2 + [c] + parent.replies = parent.replies2 + c = parent + if c not in listing: + listing.append(c) + c.replies = c.replies2 + elif c.parent_submission: + c.replies = [] + if c not in listing: + listing.append(c) + else: + if c.parent_comment: + while c.level > 1: + c = c.parent_comment + + if c not in listing: + listing.append(c) + + + return render_template("notifications.html", + v=v, + notifications=listing, + next_exists=next_exists, + page=page, + standalone=True, + render_replies=True + ) + + + +@app.get("/") +@app.get("/logged_out") +@auth_desired +def front_all(v): + + if not v and request.path == "/" and not request.headers.get("Authorization"): return redirect(f"/logged_out{request.full_path}") + + if v and request.path.startswith('/logged_out'): v = None + + try: page = int(request.values.get("page") or 1) + except: abort(400) + + page = max(page, 1) + + if v: + defaultsorting = v.defaultsorting + defaulttime = v.defaulttime + else: + defaultsorting = "hot" + defaulttime = defaulttimefilter + + sort=request.values.get("sort", defaultsorting) + t=request.values.get('t', defaulttime) + + ids, next_exists = frontlist(sort=sort, + page=page, + t=t, + v=v, + filter_words=v.filter_words if v else [], + ) + + posts = get_posts(ids, v=v) + + if v and v.hidevotedon: posts = [x for x in posts if not hasattr(x, 'voted') or not x.voted] + + if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists} + else: return render_template("home.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page) + + + +@cache.memoize(timeout=86400) +def frontlist(v=None, sort="hot", page=1, t="all", ids_only=True, filter_words=''): + + posts = g.db.query(Submission.id).options(lazyload('*')) + + if 'rama' in request.host and sort == "hot": + cutoff = int(time.time()) - 86400 + posts = posts.filter(Submission.created_utc >= cutoff) + elif t != 'all': + now = int(time.time()) + if t == 'hour': cutoff = now - 3600 + elif t == 'week': cutoff = now - 604800 + elif t == 'month': cutoff = now - 2592000 + elif t == 'year': cutoff = now - 31536000 + else: cutoff = now - 86400 + posts = posts.filter(Submission.created_utc >= cutoff) + + posts = posts.filter_by(is_banned=False, stickied=None, private=False, deleted_utc = 0) + + if v and v.admin_level == 0: + blocking = [x[0] for x in g.db.query( + UserBlock.target_id).filter_by( + user_id=v.id).all()] + blocked = [x[0] for x in g.db.query( + UserBlock.user_id).filter_by( + target_id=v.id).all()] + posts = posts.filter( + Submission.author_id.notin_(blocking), + Submission.author_id.notin_(blocked) + ) + + if not (v and v.changelogsub): + posts=posts.filter(not_(Submission.title.ilike(f'[changelog]%'))) + + if v and filter_words: + for word in filter_words: + posts=posts.filter(not_(Submission.title.ilike(f'%{word}%'))) + + if not (v and v.shadowbanned): + shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] + posts = posts.filter(Submission.author_id.notin_(shadowbanned)) + + if sort == "hot": + ti = int(time.time()) + 3600 + posts = posts.order_by(-1000000*(Submission.upvotes - Submission.downvotes + 1 + Submission.comment_count/5)/(func.power(((ti - Submission.created_utc)/1000), 1.35))) + elif sort == "new": + posts = posts.order_by(Submission.created_utc.desc()) + elif sort == "old": + posts = posts.order_by(Submission.created_utc.asc()) + elif sort == "controversial": + posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes) + elif sort == "top": + posts = posts.order_by(Submission.downvotes - Submission.upvotes) + elif sort == "bottom": + posts = posts.order_by(Submission.upvotes - Submission.downvotes) + elif sort == "comments": + posts = posts.order_by(Submission.comment_count.desc()) + + if v: + if v.agendaposter: size = 5 + else: size = v.frontsize + else: size = 25 + + posts = posts.offset(size * (page - 1)).limit(size+1).all() + + next_exists = (len(posts) > size) + + posts = posts[:size] + + pins = g.db.query(Submission.id).options(lazyload('*')).filter(Submission.stickied != None) + if v and v.admin_level == 0: + blocking = [x[0] for x in g.db.query(UserBlock.target_id).filter_by(user_id=v.id).all()] + blocked = [x[0] for x in g.db.query(UserBlock.user_id).filter_by(target_id=v.id).all()] + pins = pins.filter(Submission.author_id.notin_(blocking), Submission.author_id.notin_(blocked)) + + if page == 1: posts = pins.all() + posts + + if ids_only: posts = [x[0] for x in posts] + + return posts, next_exists + + +@app.get("/changelog") +@auth_desired +def changelog(v): + + + page = int(request.values.get("page") or 1) + page = max(page, 1) + + sort=request.values.get("sort", "new") + t=request.values.get('t', "all") + + ids = changeloglist(sort=sort, + page=page, + t=t, + v=v, + ) + + next_exists = (len(ids) > 25) + ids = ids[:25] + + posts = get_posts(ids, v=v) + + if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists} + else: return render_template("changelog.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page) + + +@cache.memoize(timeout=86400) +def changeloglist(v=None, sort="new", page=1 ,t="all"): + + posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False, private=False,).filter(Submission.deleted_utc == 0) + + if v and v.admin_level == 0: + blocking = [x[0] for x in g.db.query( + UserBlock.target_id).filter_by( + user_id=v.id).all()] + blocked = [x[0] for x in g.db.query( + UserBlock.user_id).filter_by( + target_id=v.id).all()] + posts = posts.filter( + Submission.author_id.notin_(blocking), + Submission.author_id.notin_(blocked) + ) + + admins = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.admin_level == 6).all()] + posts = posts.filter(Submission.title.ilike('_changelog%'), Submission.author_id.in_(admins)) + + if t != 'all': + cutoff = 0 + now = int(time.time()) + if t == 'hour': + cutoff = now - 3600 + elif t == 'day': + cutoff = now - 86400 + elif t == 'week': + cutoff = now - 604800 + elif t == 'month': + cutoff = now - 2592000 + elif t == 'year': + cutoff = now - 31536000 + posts = posts.filter(Submission.created_utc >= cutoff) + + if sort == "new": + posts = posts.order_by(Submission.created_utc.desc()) + elif sort == "old": + posts = posts.order_by(Submission.created_utc.asc()) + elif sort == "controversial": + posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes) + elif sort == "top": + posts = posts.order_by(Submission.downvotes - Submission.upvotes) + elif sort == "bottom": + posts = posts.order_by(Submission.upvotes - Submission.downvotes) + elif sort == "comments": + posts = posts.order_by(Submission.comment_count.desc()) + + posts = posts.offset(25 * (page - 1)).limit(26).all() + + return [x[0] for x in posts] + + +@app.get("/random") +@auth_desired +def random_post(v): + + x = g.db.query(Submission).options(lazyload('*')).filter(Submission.deleted_utc == 0, Submission.is_banned == False) + total = x.count() + n = random.randint(1, total - 2) + + post = x.offset(n).limit(1).first() + return redirect(f"/post/{post.id}") + +@cache.memoize(timeout=86400) +def comment_idlist(page=1, v=None, nsfw=False, sort="new", t="all"): + + posts = g.db.query(Submission).options(lazyload('*')) + cc_idlist = [x[0] for x in g.db.query(Submission.id).options(lazyload('*')).filter(Submission.club == True).all()] + + posts = posts.subquery() + + comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.parent_submission.notin_(cc_idlist)) + + if v and v.admin_level <= 3: + blocking = [x[0] for x in g.db.query( + UserBlock.target_id).filter_by( + user_id=v.id).all()] + blocked = [x[0] for x in g.db.query( + UserBlock.user_id).filter_by( + target_id=v.id).all()] + + comments = comments.filter( + Comment.author_id.notin_(blocking), + Comment.author_id.notin_(blocked) + ) + + if not v or not v.admin_level >= 3: + comments = comments.filter_by(is_banned=False).filter(Comment.deleted_utc == 0) + + now = int(time.time()) + if t == 'hour': + cutoff = now - 3600 + elif t == 'day': + cutoff = now - 86400 + elif t == 'week': + cutoff = now - 604800 + elif t == 'month': + cutoff = now - 2592000 + elif t == 'year': + cutoff = now - 31536000 + else: + cutoff = 0 + comments = comments.filter(Comment.created_utc >= cutoff) + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc.asc()) + elif sort == "controversial": + comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) + elif sort == "top": + comments = comments.order_by(Comment.downvotes - Comment.upvotes) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + comments = comments.offset(25 * (page - 1)).limit(26).all() + return [x[0] for x in comments] + +@app.get("/comments") +@auth_desired +def all_comments(v): + + + page = int(request.values.get("page", 1)) + + sort=request.values.get("sort", "new") + t=request.values.get("t", defaulttimefilter) + + idlist = comment_idlist(v=v, + page=page, + sort=sort, + t=t, + ) + + comments = get_comments(idlist, v=v) + + next_exists = len(idlist) > 25 + + idlist = idlist[:25] + + if request.headers.get("Authorization"): return {"data": [x.json for x in comments]} + else: return render_template("home_comments.html", v=v, sort=sort, t=t, page=page, comments=comments, standalone=True, next_exists=next_exists) diff --git a/files/routes/giphy.py b/files/routes/giphy.py old mode 100644 new mode 100755 index a222d92b7..5a40c4549 --- a/files/routes/giphy.py +++ b/files/routes/giphy.py @@ -1,22 +1,22 @@ -from flask import * -from os import environ -import requests - -from files.__main__ import app - -GIPHY_KEY = environ.get('GIPHY_KEY').rstrip() - - -@app.get("/giphy") -@app.get("/giphy") -def giphy(path=None): - - searchTerm = request.values.get("searchTerm", "").strip() - limit = int(request.values.get("limit", 48)) - if searchTerm and limit: - url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit={limit}" - elif searchTerm and not limit: - url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit=48" - else: - url = f"https://api.giphy.com/v1/gifs?api_key={GIPHY_KEY}&limit=48" - return jsonify(requests.get(url).json()) +from flask import * +from os import environ +import requests + +from files.__main__ import app + +GIPHY_KEY = environ.get('GIPHY_KEY').rstrip() + + +@app.get("/giphy") +@app.get("/giphy") +def giphy(path=None): + + searchTerm = request.values.get("searchTerm", "").strip() + limit = int(request.values.get("limit", 48)) + if searchTerm and limit: + url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit={limit}" + elif searchTerm and not limit: + url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit=48" + else: + url = f"https://api.giphy.com/v1/gifs?api_key={GIPHY_KEY}&limit=48" + return jsonify(requests.get(url).json()) diff --git a/files/routes/login.py b/files/routes/login.py old mode 100644 new mode 100755 index 92e1c9770..ff815eacf --- a/files/routes/login.py +++ b/files/routes/login.py @@ -1,563 +1,563 @@ -from urllib.parse import urlencode -from files.mail import * -from files.__main__ import app, limiter -from files.helpers.const import * -import requests - -valid_username_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$") -valid_password_regex = re.compile("^.{8,100}$") - - -@app.get("/login") -@auth_desired -def login_get(v): - - redir = request.values.get("redirect", "/").replace("/logged_out", "").strip() - if v: - return redirect(redir) - - return render_template("login.html", - failed=False, - redirect=redir) - - -def check_for_alts(current_id): - past_accs = set(session.get("history", [])) - past_accs.add(current_id) - session["history"] = list(past_accs) - - for past_id in session["history"]: - - if past_id == current_id: - continue - - check1 = g.db.query(Alt).options(lazyload('*')).filter_by( - user1=current_id, user2=past_id).first() - check2 = g.db.query(Alt).options(lazyload('*')).filter_by( - user1=past_id, user2=current_id).first() - - if not check1 and not check2: - - try: - new_alt = Alt(user1=past_id, user2=current_id) - g.db.add(new_alt) - g.db.flush() - except BaseException: - pass - - alts = g.db.query(Alt).options(lazyload('*')) - otheralts = alts.filter(or_(Alt.user1 == past_id, Alt.user2 == past_id, Alt.user1 == current_id, Alt.user2 == current_id)).all() - for a in otheralts: - existing = alts.filter_by(user1=a.user1, user2=past_id).first() - if not existing: - new_alt = Alt(user1=a.user1, user2=past_id) - g.db.add(new_alt) - g.db.flush() - - existing = alts.filter_by(user1=a.user1, user2=current_id).first() - if not existing: - new_alt = Alt(user1=a.user1, user2=current_id) - g.db.add(new_alt) - g.db.flush() - - existing = alts.filter_by(user1=a.user2, user2=past_id).first() - if not existing: - new_alt = Alt(user1=a.user2, user2=past_id) - g.db.add(new_alt) - g.db.flush() - - existing = alts.filter_by(user1=a.user2, user2=current_id).first() - if not existing: - new_alt = Alt(user1=a.user2, user2=current_id) - g.db.add(new_alt) - g.db.flush() - -# login post procedure - - -@app.post("/login") -@limiter.limit("1/second") -@limiter.limit("6/minute") -def login_post(): - - username = request.values.get("username") - - if not username: abort(400) - if "@" in username: - account = g.db.query(User).options(lazyload('*')).filter( - User.email.ilike(username)).first() - else: - account = get_user(username, graceful=True) - - if not account: - time.sleep(random.uniform(0, 2)) - return render_template("login.html", failed=True) - - - if request.values.get("password"): - - if not account.verifyPass(request.values.get("password")): - time.sleep(random.uniform(0, 2)) - return render_template("login.html", failed=True) - - if account.mfa_secret: - now = int(time.time()) - hash = generate_hash(f"{account.id}+{now}+2fachallenge") - return render_template("login_2fa.html", - v=account, - time=now, - hash=hash, - redirect=request.values.get("redirect", "/") - ) - elif request.values.get("2fa_token", "x"): - now = int(time.time()) - - if now - int(request.values.get("time")) > 600: - return redirect('/login') - - formhash = request.values.get("hash") - if not validate_hash(f"{account.id}+{request.values.get('time')}+2fachallenge", - formhash - ): - return redirect("/login") - - if not account.validate_2fa(request.values.get("2fa_token", "").strip()): - hash = generate_hash(f"{account.id}+{time}+2fachallenge") - return render_template("login_2fa.html", - v=account, - time=now, - hash=hash, - failed=True, - ) - - else: - abort(400) - - if account.is_banned and account.unban_utc > 0 and time.time() > account.unban_utc: - account.is_banned = 0 - account.unban_utc = 0 - g.db.add(account) - - session["user_id"] = account.id - session["session_id"] = token_hex(16) - session["login_nonce"] = account.login_nonce - session.permanent = True - - check_for_alts(account.id) - - - redir = request.values.get("redirect", "/").replace("/logged_out", "").strip() - - g.db.commit() - - return redirect(redir) - - -@app.get("/me") -@app.get("/@me") -@auth_required -def me(v): - if request.headers.get("Authorization"): return v.json - else: return redirect(v.url) - - -@app.post("/logout") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def logout(v): - - session.pop("user_id", None) - session.pop("session_id", None) - - return {"message": "Logout successful!"} - - -@app.get("/signup") -@auth_desired -def sign_up_get(v): - with open('./disablesignups', 'r') as f: - if f.read() == "yes": return "New account registration is currently closed. Please come back later.", 403 - - if v: return redirect("/") - - agent = request.headers.get("User-Agent", None) - if not agent: abort(403) - - ref = request.values.get("ref", None) - if ref: - ref_user = g.db.query(User).options(lazyload('*')).filter(User.username.ilike(ref)).first() - - else: - ref_user = None - - if ref_user and (ref_user.id in session.get("history", [])): - return render_template("sign_up_failed_ref.html") - - now = int(time.time()) - token = token_hex(16) - session["signup_token"] = token - - formkey_hashstr = str(now) + token + agent - - formkey = hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"), - msg=bytes(formkey_hashstr, "utf-16"), - digestmod='md5' - ).hexdigest() - - redir = request.values.get("redirect", "/").replace("/logged_out", "").strip() - - error = request.values.get("error", None) - - return render_template("sign_up.html", - formkey=formkey, - now=now, - redirect=redir, - ref_user=ref_user, - error=error, - hcaptcha=app.config["HCAPTCHA_SITEKEY"] - ) - - -@app.post("/signup") -@limiter.limit("1/second") -@limiter.limit("5/day") -@auth_desired -def sign_up_post(v): - with open('./disablesignups', 'r') as f: - if f.read() == "yes": return "New account registration is currently closed. Please come back later.", 403 - - if v: abort(403) - - agent = request.headers.get("User-Agent", None) - if not agent: abort(403) - - form_timestamp = request.values.get("now", '0') - form_formkey = request.values.get("formkey", "none") - - submitted_token = session.get("signup_token", "") - if not submitted_token: abort(400) - - correct_formkey_hashstr = form_timestamp + submitted_token + agent - - correct_formkey = hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"), - msg=bytes(correct_formkey_hashstr, "utf-16"), - digestmod='md5' - ).hexdigest() - - now = int(time.time()) - - username = request.values.get("username").strip() - - def new_signup(error): - - args = {"error": error} - if request.values.get("referred_by"): - user = g.db.query(User).options(lazyload('*')).filter_by( - id=request.values.get("referred_by")).first() - if user: - args["ref"] = user.username - - return redirect(f"/signup?{urlencode(args)}") - - if now - int(form_timestamp) < 5: - return new_signup("There was a problem. Please try again.") - - if not hmac.compare_digest(correct_formkey, form_formkey): - return new_signup("There was a problem. Please try again.") - - if not request.values.get( - "password") == request.values.get("password_confirm"): - return new_signup("Passwords did not match. Please try again.") - - if not re.fullmatch(valid_username_regex, username): - return new_signup("Invalid username") - - if not re.fullmatch(valid_password_regex, request.values.get("password")): - return new_signup("Password must be between 8 and 100 characters.") - - email = request.values.get("email") - email = email.strip() - if not email: email = None - - existing_account = get_user(username, graceful=True) - if existing_account and existing_account.reserved: - return redirect(existing_account.url) - - if existing_account or (email and g.db.query( - User).filter(User.email.ilike(email)).first()): - return new_signup( - "An account with that username or email already exists.") - - if app.config.get("HCAPTCHA_SITEKEY"): - token = request.values.get("h-captcha-response") - if not token: - return new_signup("Unable to verify captcha [1].") - - data = {"secret": app.config["HCAPTCHA_SECRET"], - "response": token, - "sitekey": app.config["HCAPTCHA_SITEKEY"]} - url = "https://hcaptcha.com/siteverify" - - x = requests.post(url, data=data) - - if not x.json()["success"]: - return new_signup("Unable to verify captcha [2].") - - session.pop("signup_token") - - ref_id = int(request.values.get("referred_by", 0)) - - if ref_id: - ref_user = g.db.query(User).options( - lazyload('*')).filter_by(id=ref_id).first() - if ref_user: - badge_types = g.db.query(BadgeDef).options(lazyload('*')).filter(BadgeDef.qualification_expr.isnot(None)).all() - for badge in badge_types: - if eval(badge.qualification_expr, {}, {'v': ref_user}): - if not ref_user.has_badge(badge.id): - new_badge = Badge(user_id=ref_user.id, badge_id=badge.id) - g.db.add(new_badge) - - g.db.add(ref_user) - - id_1 = g.db.query(User.id).options(lazyload('*')).filter_by(id=6).count() - users_count = g.db.query(User.id).count() #paranoid - if id_1 == 0 and users_count < 6: admin_level=6 - else: admin_level=0 - - new_user = User( - username=username, - original_username = username, - admin_level = admin_level, - password=request.values.get("password"), - email=email, - created_utc=int(time.time()), - referred_by=ref_id or None, - ban_evade = int(any([x.is_banned and not x.unban_utc for x in g.db.query(User).options(lazyload('*')).filter(User.id.in_(tuple(session.get("history", [])))).all() if x])), - agendaposter = any([x.agendaposter for x in g.db.query(User).options(lazyload('*')).filter(User.id.in_(tuple(session.get("history", [])))).all() if x]), - club_banned=any([x.club_banned for x in g.db.query(User).options(lazyload('*')).filter(User.id.in_(tuple(session.get("history", [])))).all() if x]) - ) - - g.db.add(new_user) - g.db.flush() - - - check_for_alts(new_user.id) - - if email: send_verification_email(new_user) - - if "rama" in request.host: send_notification(NOTIFICATIONS_ACCOUNT, new_user, "Dude bussy lmao") - - session["user_id"] = new_user.id - session["session_id"] = token_hex(16) - - g.db.commit() - - return redirect("/") - - -@app.get("/forgot") -def get_forgot(): - - return render_template("forgot_password.html", - ) - - -@app.post("/forgot") -@limiter.limit("1/second") -def post_forgot(): - - username = request.values.get("username").lstrip('@') - email = request.values.get("email",'').strip() - - email=email.replace("_","\_") - - user = g.db.query(User).options(lazyload('*')).filter( - User.username.ilike(username), - User.email.ilike(email)).first() - - if not user and email.endswith("@gmail.com"): - email=email.split('@')[0] - email=email.split('+')[0] - email=email.replace('.','') - email=f"{email}@gmail.com" - user = g.db.query(User).options(lazyload('*')).filter( - User.username.ilike(username), - User.email.ilike(email)).first() - - if user: - now = int(time.time()) - token = generate_hash(f"{user.id}+{now}+forgot+{user.login_nonce}") - url = f"https://{app.config['SERVER_NAME']}/reset?id={user.id}&time={now}&token={token}" - - send_mail(to_address=user.email, - subject="Password Reset Request", - html=render_template("email/password_reset.html", - action_url=url, - v=user) - ) - - return render_template("forgot_password.html", - msg="If the username and email matches an account, you will be sent a password reset email. You have ten minutes to complete the password reset process.") - - -@app.get("/reset") -def get_reset(): - - user_id = request.values.get("id") - timestamp = int(request.values.get("time",0)) - token = request.values.get("token") - - now = int(time.time()) - - if now - timestamp > 600: - return render_template("message.html", - title="Password reset link expired", - error="That password reset link has expired.") - - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - - if not validate_hash(f"{user_id}+{timestamp}+forgot+{user.login_nonce}", token): - abort(400) - - if not user: - abort(404) - - reset_token = generate_hash(f"{user.id}+{timestamp}+reset+{user.login_nonce}") - - return render_template("reset_password.html", - v=user, - token=reset_token, - time=timestamp, - ) - - -@app.post("/reset") -@limiter.limit("1/second") -@auth_desired -def post_reset(v): - if v: - return redirect('/') - - user_id = request.values.get("user_id") - timestamp = int(request.values.get("time")) - token = request.values.get("token") - - password = request.values.get("password") - confirm_password = request.values.get("confirm_password") - - now = int(time.time()) - - if now - timestamp > 600: - return render_template("message.html", - title="Password reset expired", - error="That password reset form has expired.") - - user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() - - if not validate_hash(f"{user_id}+{timestamp}+reset+{user.login_nonce}", token): - abort(400) - if not user: - abort(404) - - if not password == confirm_password: - return render_template("reset_password.html", - v=user, - token=token, - time=timestamp, - error="Passwords didn't match.") - - user.passhash = hash_password(password) - g.db.add(user) - - g.db.commit() - - return render_template("message_success.html", - title="Password reset successful!", - message="Login normally to access your account.") - -@app.get("/lost_2fa") -@auth_desired -def lost_2fa(v): - - return render_template( - "lost_2fa.html", - v=v - ) - -@app.post("/request_2fa_disable") -@limiter.limit("1/second") -@limiter.limit("6/minute") -def request_2fa_disable(): - - username=request.values.get("username") - user=get_user(username, graceful=True) - if not user or not user.email or not user.mfa_secret: - return render_template("message.html", - title="Removal request received", - message="If username, password, and email match, we will send you an email.") - - - email=request.values.get("email") - if email != user.email and email.endswith("@gmail.com"): - email=email.split('@')[0] - email=email.split('+')[0] - email=email.replace('.','') - email=f"{email}@gmail.com" - if email != user.email: - return render_template("message.html", - title="Removal request received", - message="If username, password, and email match, we will send you an email.") - - - password =request.values.get("password") - if not user.verifyPass(password): - return render_template("message.html", - title="Removal request received", - message="If username, password, and email match, we will send you an email.") - - valid=int(time.time()) - token=generate_hash(f"{user.id}+{user.username}+disable2fa+{valid}+{user.mfa_secret}+{user.login_nonce}") - - action_url=f"https://{app.config['SERVER_NAME']}/reset_2fa?id={user.id}&t={valid}&token={token}" - - send_mail(to_address=user.email, - subject="2FA Removal Request", - html=render_template("email/2fa_remove.html", - action_url=action_url, - v=user) - ) - - return render_template("message.html", - title="Removal request received", - message="If username, password, and email match, we will send you an email.") - -@app.get("/reset_2fa") -def reset_2fa(): - - now=int(time.time()) - t=int(request.values.get("t")) - - if now > t+3600*24: - return render_template("message.html", - title="Expired Link", - error="That link has expired.") - - token=request.values.get("token") - uid=request.values.get("id") - - user=get_account(uid) - - if not validate_hash(f"{user.id}+{user.username}+disable2fa+{t}+{user.mfa_secret}+{user.login_nonce}", token): - abort(403) - - user.mfa_secret=None - - g.db.add(user) - - g.db.commit() - - return render_template("message_success.html", - title="Two-factor authentication removed.", - message="Login normally to access your account.") +from urllib.parse import urlencode +from files.mail import * +from files.__main__ import app, limiter +from files.helpers.const import * +import requests + +valid_username_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$") +valid_password_regex = re.compile("^.{8,100}$") + + +@app.get("/login") +@auth_desired +def login_get(v): + + redir = request.values.get("redirect", "/").replace("/logged_out", "").strip() + if v: + return redirect(redir) + + return render_template("login.html", + failed=False, + redirect=redir) + + +def check_for_alts(current_id): + past_accs = set(session.get("history", [])) + past_accs.add(current_id) + session["history"] = list(past_accs) + + for past_id in session["history"]: + + if past_id == current_id: + continue + + check1 = g.db.query(Alt).options(lazyload('*')).filter_by( + user1=current_id, user2=past_id).first() + check2 = g.db.query(Alt).options(lazyload('*')).filter_by( + user1=past_id, user2=current_id).first() + + if not check1 and not check2: + + try: + new_alt = Alt(user1=past_id, user2=current_id) + g.db.add(new_alt) + g.db.flush() + except BaseException: + pass + + alts = g.db.query(Alt).options(lazyload('*')) + otheralts = alts.filter(or_(Alt.user1 == past_id, Alt.user2 == past_id, Alt.user1 == current_id, Alt.user2 == current_id)).all() + for a in otheralts: + existing = alts.filter_by(user1=a.user1, user2=past_id).first() + if not existing: + new_alt = Alt(user1=a.user1, user2=past_id) + g.db.add(new_alt) + g.db.flush() + + existing = alts.filter_by(user1=a.user1, user2=current_id).first() + if not existing: + new_alt = Alt(user1=a.user1, user2=current_id) + g.db.add(new_alt) + g.db.flush() + + existing = alts.filter_by(user1=a.user2, user2=past_id).first() + if not existing: + new_alt = Alt(user1=a.user2, user2=past_id) + g.db.add(new_alt) + g.db.flush() + + existing = alts.filter_by(user1=a.user2, user2=current_id).first() + if not existing: + new_alt = Alt(user1=a.user2, user2=current_id) + g.db.add(new_alt) + g.db.flush() + +# login post procedure + + +@app.post("/login") +@limiter.limit("1/second") +@limiter.limit("6/minute") +def login_post(): + + username = request.values.get("username") + + if not username: abort(400) + if "@" in username: + account = g.db.query(User).options(lazyload('*')).filter( + User.email.ilike(username)).first() + else: + account = get_user(username, graceful=True) + + if not account: + time.sleep(random.uniform(0, 2)) + return render_template("login.html", failed=True) + + + if request.values.get("password"): + + if not account.verifyPass(request.values.get("password")): + time.sleep(random.uniform(0, 2)) + return render_template("login.html", failed=True) + + if account.mfa_secret: + now = int(time.time()) + hash = generate_hash(f"{account.id}+{now}+2fachallenge") + return render_template("login_2fa.html", + v=account, + time=now, + hash=hash, + redirect=request.values.get("redirect", "/") + ) + elif request.values.get("2fa_token", "x"): + now = int(time.time()) + + if now - int(request.values.get("time")) > 600: + return redirect('/login') + + formhash = request.values.get("hash") + if not validate_hash(f"{account.id}+{request.values.get('time')}+2fachallenge", + formhash + ): + return redirect("/login") + + if not account.validate_2fa(request.values.get("2fa_token", "").strip()): + hash = generate_hash(f"{account.id}+{time}+2fachallenge") + return render_template("login_2fa.html", + v=account, + time=now, + hash=hash, + failed=True, + ) + + else: + abort(400) + + if account.is_banned and account.unban_utc > 0 and time.time() > account.unban_utc: + account.is_banned = 0 + account.unban_utc = 0 + g.db.add(account) + + session["user_id"] = account.id + session["session_id"] = token_hex(16) + session["login_nonce"] = account.login_nonce + session.permanent = True + + check_for_alts(account.id) + + + redir = request.values.get("redirect", "/").replace("/logged_out", "").strip() + + g.db.commit() + + return redirect(redir) + + +@app.get("/me") +@app.get("/@me") +@auth_required +def me(v): + if request.headers.get("Authorization"): return v.json + else: return redirect(v.url) + + +@app.post("/logout") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def logout(v): + + session.pop("user_id", None) + session.pop("session_id", None) + + return {"message": "Logout successful!"} + + +@app.get("/signup") +@auth_desired +def sign_up_get(v): + with open('./disablesignups', 'r') as f: + if f.read() == "yes": return "New account registration is currently closed. Please come back later.", 403 + + if v: return redirect("/") + + agent = request.headers.get("User-Agent", None) + if not agent: abort(403) + + ref = request.values.get("ref", None) + if ref: + ref_user = g.db.query(User).options(lazyload('*')).filter(User.username.ilike(ref)).first() + + else: + ref_user = None + + if ref_user and (ref_user.id in session.get("history", [])): + return render_template("sign_up_failed_ref.html") + + now = int(time.time()) + token = token_hex(16) + session["signup_token"] = token + + formkey_hashstr = str(now) + token + agent + + formkey = hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"), + msg=bytes(formkey_hashstr, "utf-16"), + digestmod='md5' + ).hexdigest() + + redir = request.values.get("redirect", "/").replace("/logged_out", "").strip() + + error = request.values.get("error", None) + + return render_template("sign_up.html", + formkey=formkey, + now=now, + redirect=redir, + ref_user=ref_user, + error=error, + hcaptcha=app.config["HCAPTCHA_SITEKEY"] + ) + + +@app.post("/signup") +@limiter.limit("1/second") +@limiter.limit("5/day") +@auth_desired +def sign_up_post(v): + with open('./disablesignups', 'r') as f: + if f.read() == "yes": return "New account registration is currently closed. Please come back later.", 403 + + if v: abort(403) + + agent = request.headers.get("User-Agent", None) + if not agent: abort(403) + + form_timestamp = request.values.get("now", '0') + form_formkey = request.values.get("formkey", "none") + + submitted_token = session.get("signup_token", "") + if not submitted_token: abort(400) + + correct_formkey_hashstr = form_timestamp + submitted_token + agent + + correct_formkey = hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"), + msg=bytes(correct_formkey_hashstr, "utf-16"), + digestmod='md5' + ).hexdigest() + + now = int(time.time()) + + username = request.values.get("username").strip() + + def new_signup(error): + + args = {"error": error} + if request.values.get("referred_by"): + user = g.db.query(User).options(lazyload('*')).filter_by( + id=request.values.get("referred_by")).first() + if user: + args["ref"] = user.username + + return redirect(f"/signup?{urlencode(args)}") + + if now - int(form_timestamp) < 5: + return new_signup("There was a problem. Please try again.") + + if not hmac.compare_digest(correct_formkey, form_formkey): + return new_signup("There was a problem. Please try again.") + + if not request.values.get( + "password") == request.values.get("password_confirm"): + return new_signup("Passwords did not match. Please try again.") + + if not re.fullmatch(valid_username_regex, username): + return new_signup("Invalid username") + + if not re.fullmatch(valid_password_regex, request.values.get("password")): + return new_signup("Password must be between 8 and 100 characters.") + + email = request.values.get("email") + email = email.strip() + if not email: email = None + + existing_account = get_user(username, graceful=True) + if existing_account and existing_account.reserved: + return redirect(existing_account.url) + + if existing_account or (email and g.db.query( + User).filter(User.email.ilike(email)).first()): + return new_signup( + "An account with that username or email already exists.") + + if app.config.get("HCAPTCHA_SITEKEY"): + token = request.values.get("h-captcha-response") + if not token: + return new_signup("Unable to verify captcha [1].") + + data = {"secret": app.config["HCAPTCHA_SECRET"], + "response": token, + "sitekey": app.config["HCAPTCHA_SITEKEY"]} + url = "https://hcaptcha.com/siteverify" + + x = requests.post(url, data=data) + + if not x.json()["success"]: + return new_signup("Unable to verify captcha [2].") + + session.pop("signup_token") + + ref_id = int(request.values.get("referred_by", 0)) + + if ref_id: + ref_user = g.db.query(User).options( + lazyload('*')).filter_by(id=ref_id).first() + if ref_user: + badge_types = g.db.query(BadgeDef).options(lazyload('*')).filter(BadgeDef.qualification_expr.isnot(None)).all() + for badge in badge_types: + if eval(badge.qualification_expr, {}, {'v': ref_user}): + if not ref_user.has_badge(badge.id): + new_badge = Badge(user_id=ref_user.id, badge_id=badge.id) + g.db.add(new_badge) + + g.db.add(ref_user) + + id_1 = g.db.query(User.id).options(lazyload('*')).filter_by(id=6).count() + users_count = g.db.query(User.id).count() #paranoid + if id_1 == 0 and users_count < 6: admin_level=6 + else: admin_level=0 + + new_user = User( + username=username, + original_username = username, + admin_level = admin_level, + password=request.values.get("password"), + email=email, + created_utc=int(time.time()), + referred_by=ref_id or None, + ban_evade = int(any([x.is_banned and not x.unban_utc for x in g.db.query(User).options(lazyload('*')).filter(User.id.in_(tuple(session.get("history", [])))).all() if x])), + agendaposter = any([x.agendaposter for x in g.db.query(User).options(lazyload('*')).filter(User.id.in_(tuple(session.get("history", [])))).all() if x]), + club_banned=any([x.club_banned for x in g.db.query(User).options(lazyload('*')).filter(User.id.in_(tuple(session.get("history", [])))).all() if x]) + ) + + g.db.add(new_user) + g.db.flush() + + + check_for_alts(new_user.id) + + if email: send_verification_email(new_user) + + if "rama" in request.host: send_notification(NOTIFICATIONS_ACCOUNT, new_user, "Dude bussy lmao") + + session["user_id"] = new_user.id + session["session_id"] = token_hex(16) + + g.db.commit() + + return redirect("/") + + +@app.get("/forgot") +def get_forgot(): + + return render_template("forgot_password.html", + ) + + +@app.post("/forgot") +@limiter.limit("1/second") +def post_forgot(): + + username = request.values.get("username").lstrip('@') + email = request.values.get("email",'').strip() + + email=email.replace("_","\_") + + user = g.db.query(User).options(lazyload('*')).filter( + User.username.ilike(username), + User.email.ilike(email)).first() + + if not user and email.endswith("@gmail.com"): + email=email.split('@')[0] + email=email.split('+')[0] + email=email.replace('.','') + email=f"{email}@gmail.com" + user = g.db.query(User).options(lazyload('*')).filter( + User.username.ilike(username), + User.email.ilike(email)).first() + + if user: + now = int(time.time()) + token = generate_hash(f"{user.id}+{now}+forgot+{user.login_nonce}") + url = f"https://{app.config['SERVER_NAME']}/reset?id={user.id}&time={now}&token={token}" + + send_mail(to_address=user.email, + subject="Password Reset Request", + html=render_template("email/password_reset.html", + action_url=url, + v=user) + ) + + return render_template("forgot_password.html", + msg="If the username and email matches an account, you will be sent a password reset email. You have ten minutes to complete the password reset process.") + + +@app.get("/reset") +def get_reset(): + + user_id = request.values.get("id") + timestamp = int(request.values.get("time",0)) + token = request.values.get("token") + + now = int(time.time()) + + if now - timestamp > 600: + return render_template("message.html", + title="Password reset link expired", + error="That password reset link has expired.") + + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + + if not validate_hash(f"{user_id}+{timestamp}+forgot+{user.login_nonce}", token): + abort(400) + + if not user: + abort(404) + + reset_token = generate_hash(f"{user.id}+{timestamp}+reset+{user.login_nonce}") + + return render_template("reset_password.html", + v=user, + token=reset_token, + time=timestamp, + ) + + +@app.post("/reset") +@limiter.limit("1/second") +@auth_desired +def post_reset(v): + if v: + return redirect('/') + + user_id = request.values.get("user_id") + timestamp = int(request.values.get("time")) + token = request.values.get("token") + + password = request.values.get("password") + confirm_password = request.values.get("confirm_password") + + now = int(time.time()) + + if now - timestamp > 600: + return render_template("message.html", + title="Password reset expired", + error="That password reset form has expired.") + + user = g.db.query(User).options(lazyload('*')).filter_by(id=user_id).first() + + if not validate_hash(f"{user_id}+{timestamp}+reset+{user.login_nonce}", token): + abort(400) + if not user: + abort(404) + + if not password == confirm_password: + return render_template("reset_password.html", + v=user, + token=token, + time=timestamp, + error="Passwords didn't match.") + + user.passhash = hash_password(password) + g.db.add(user) + + g.db.commit() + + return render_template("message_success.html", + title="Password reset successful!", + message="Login normally to access your account.") + +@app.get("/lost_2fa") +@auth_desired +def lost_2fa(v): + + return render_template( + "lost_2fa.html", + v=v + ) + +@app.post("/request_2fa_disable") +@limiter.limit("1/second") +@limiter.limit("6/minute") +def request_2fa_disable(): + + username=request.values.get("username") + user=get_user(username, graceful=True) + if not user or not user.email or not user.mfa_secret: + return render_template("message.html", + title="Removal request received", + message="If username, password, and email match, we will send you an email.") + + + email=request.values.get("email") + if email != user.email and email.endswith("@gmail.com"): + email=email.split('@')[0] + email=email.split('+')[0] + email=email.replace('.','') + email=f"{email}@gmail.com" + if email != user.email: + return render_template("message.html", + title="Removal request received", + message="If username, password, and email match, we will send you an email.") + + + password =request.values.get("password") + if not user.verifyPass(password): + return render_template("message.html", + title="Removal request received", + message="If username, password, and email match, we will send you an email.") + + valid=int(time.time()) + token=generate_hash(f"{user.id}+{user.username}+disable2fa+{valid}+{user.mfa_secret}+{user.login_nonce}") + + action_url=f"https://{app.config['SERVER_NAME']}/reset_2fa?id={user.id}&t={valid}&token={token}" + + send_mail(to_address=user.email, + subject="2FA Removal Request", + html=render_template("email/2fa_remove.html", + action_url=action_url, + v=user) + ) + + return render_template("message.html", + title="Removal request received", + message="If username, password, and email match, we will send you an email.") + +@app.get("/reset_2fa") +def reset_2fa(): + + now=int(time.time()) + t=int(request.values.get("t")) + + if now > t+3600*24: + return render_template("message.html", + title="Expired Link", + error="That link has expired.") + + token=request.values.get("token") + uid=request.values.get("id") + + user=get_account(uid) + + if not validate_hash(f"{user.id}+{user.username}+disable2fa+{t}+{user.mfa_secret}+{user.login_nonce}", token): + abort(403) + + user.mfa_secret=None + + g.db.add(user) + + g.db.commit() + + return render_template("message_success.html", + title="Two-factor authentication removed.", + message="Login normally to access your account.") diff --git a/files/routes/oauth.py b/files/routes/oauth.py old mode 100644 new mode 100755 index 124948696..b5b90cb9a --- a/files/routes/oauth.py +++ b/files/routes/oauth.py @@ -1,248 +1,248 @@ -from files.helpers.wrappers import * -from files.helpers.alerts import * -from files.helpers.get import * -from files.helpers.const import * -from files.classes import * -from flask import * -from files.__main__ import app, limiter -from sqlalchemy.orm import joinedload - -@app.get("/authorize") -@auth_required -def authorize_prompt(v): - client_id = request.values.get("client_id") - application = g.db.query(OauthApp).options(lazyload('*')).filter_by(client_id=client_id).first() - if not application: return {"oauth_error": "Invalid `client_id`"}, 401 - return render_template("oauth.html", v=v, application=application) - - -@app.post("/authorize") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def authorize(v): - - client_id = request.values.get("client_id") - application = g.db.query(OauthApp).options(lazyload('*')).filter_by(client_id=client_id).first() - if not application: return {"oauth_error": "Invalid `client_id`"}, 401 - access_token = secrets.token_urlsafe(128)[:128] - new_auth = ClientAuth( - oauth_client = application.id, - user_id = v.id, - access_token=access_token - ) - - g.db.add(new_auth) - - g.db.commit() - - return redirect(f"{application.redirect_uri}?token={access_token}") - - -@app.post("/api_keys") -@limiter.limit("1/second") -@is_not_banned -def request_api_keys(v): - - new_app = OauthApp( - app_name=request.values.get('name'), - redirect_uri=request.values.get('redirect_uri'), - author_id=v.id, - description=request.values.get("description")[:256] - ) - - g.db.add(new_app) - - send_admin(NOTIFICATIONS_ACCOUNT, f"{v.username} has requested API keys for `{request.values.get('name')}`. You can approve or deny the request [here](/admin/apps).") - - g.db.commit() - - return redirect('/settings/apps') - - -@app.post("/delete_app/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def delete_oauth_app(v, aid): - - aid = int(aid) - app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() - - for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): - g.db.delete(auth) - - g.db.delete(app) - - g.db.commit() - - return redirect('/apps') - - -@app.post("/edit_app/") -@limiter.limit("1/second") -@is_not_banned -@validate_formkey -def edit_oauth_app(v, aid): - - aid = int(aid) - app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() - - app.redirect_uri = request.values.get('redirect_uri') - app.app_name = request.values.get('name') - app.description = request.values.get("description")[:256] - - g.db.add(app) - - g.db.commit() - - return redirect('/settings/apps') - - -@app.post("/admin/app/approve/") -@limiter.limit("1/second") -@admin_level_required(3) -@validate_formkey -def admin_app_approve(v, aid): - - app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() - user = app.author - - app.client_id = secrets.token_urlsafe(64)[:64] - g.db.add(app) - - access_token = secrets.token_urlsafe(128)[:128] - new_auth = ClientAuth( - oauth_client = app.id, - user_id = user.id, - access_token=access_token - ) - - g.db.add(new_auth) - - send_notification(NOTIFICATIONS_ACCOUNT, user, f"Your application `{app.app_name}` has been approved. Here's your access token: `{access_token}`\nPlease check the guide [here](/api) if you don't know what to do next.") - - g.db.commit() - - return {"message": f"{app.app_name} approved"} - - -@app.post("/admin/app/revoke/") -@limiter.limit("1/second") -@admin_level_required(3) -@validate_formkey -def admin_app_revoke(v, aid): - - app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() - if app.id: - for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): g.db.delete(auth) - - send_notification(NOTIFICATIONS_ACCOUNT, app.author, f"Your application `{app.app_name}` has been revoked.") - - g.db.delete(app) - - g.db.commit() - - return {"message": f"App revoked"} - - -@app.post("/admin/app/reject/") -@limiter.limit("1/second") -@admin_level_required(3) -@validate_formkey -def admin_app_reject(v, aid): - - app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() - - for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): g.db.delete(auth) - - send_notification(NOTIFICATIONS_ACCOUNT, app.author, f"Your application `{app.app_name}` has been rejected.") - - g.db.delete(app) - - g.db.commit() - - return {"message": f"App rejected"} - - -@app.get("/admin/app/") -@admin_level_required(3) -def admin_app_id(v, aid): - - aid=aid - - oauth = g.db.query(OauthApp).options( - joinedload( - OauthApp.author)).filter_by( - id=aid).first() - - pids=oauth.idlist(page=int(request.values.get("page",1)), - ) - - next_exists=len(pids)==101 - pids=pids[:100] - - posts=get_posts(pids, v=v) - - return render_template("admin/app.html", - v=v, - app=oauth, - listing=posts, - next_exists=next_exists - ) - -@app.get("/admin/app//comments") -@admin_level_required(3) -def admin_app_id_comments(v, aid): - - aid=aid - - oauth = g.db.query(OauthApp).options( - joinedload( - OauthApp.author)).filter_by( - id=aid).first() - - cids=oauth.comments_idlist(page=int(request.values.get("page",1)), - ) - - next_exists=len(cids)==101 - cids=cids[:100] - - comments=get_comments(cids, v=v) - - - return render_template("admin/app.html", - v=v, - app=oauth, - comments=comments, - next_exists=next_exists, - standalone=True - ) - - -@app.get("/admin/apps") -@admin_level_required(3) -def admin_apps_list(v): - - apps = g.db.query(OauthApp).all() - - return render_template("admin/apps.html", v=v, apps=apps) - - -@app.post("/oauth/reroll/") -@limiter.limit("1/second") -@auth_required -def reroll_oauth_tokens(aid, v): - - aid = aid - - a = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() - - if a.author_id != v.id: abort(403) - - a.client_id = secrets.token_urlsafe(64)[:64] - g.db.add(a) - - g.db.commit() - - return {"message": "Client ID Rerolled", "id": a.client_id} +from files.helpers.wrappers import * +from files.helpers.alerts import * +from files.helpers.get import * +from files.helpers.const import * +from files.classes import * +from flask import * +from files.__main__ import app, limiter +from sqlalchemy.orm import joinedload + +@app.get("/authorize") +@auth_required +def authorize_prompt(v): + client_id = request.values.get("client_id") + application = g.db.query(OauthApp).options(lazyload('*')).filter_by(client_id=client_id).first() + if not application: return {"oauth_error": "Invalid `client_id`"}, 401 + return render_template("oauth.html", v=v, application=application) + + +@app.post("/authorize") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def authorize(v): + + client_id = request.values.get("client_id") + application = g.db.query(OauthApp).options(lazyload('*')).filter_by(client_id=client_id).first() + if not application: return {"oauth_error": "Invalid `client_id`"}, 401 + access_token = secrets.token_urlsafe(128)[:128] + new_auth = ClientAuth( + oauth_client = application.id, + user_id = v.id, + access_token=access_token + ) + + g.db.add(new_auth) + + g.db.commit() + + return redirect(f"{application.redirect_uri}?token={access_token}") + + +@app.post("/api_keys") +@limiter.limit("1/second") +@is_not_banned +def request_api_keys(v): + + new_app = OauthApp( + app_name=request.values.get('name'), + redirect_uri=request.values.get('redirect_uri'), + author_id=v.id, + description=request.values.get("description")[:256] + ) + + g.db.add(new_app) + + send_admin(NOTIFICATIONS_ACCOUNT, f"{v.username} has requested API keys for `{request.values.get('name')}`. You can approve or deny the request [here](/admin/apps).") + + g.db.commit() + + return redirect('/settings/apps') + + +@app.post("/delete_app/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def delete_oauth_app(v, aid): + + aid = int(aid) + app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() + + for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): + g.db.delete(auth) + + g.db.delete(app) + + g.db.commit() + + return redirect('/apps') + + +@app.post("/edit_app/") +@limiter.limit("1/second") +@is_not_banned +@validate_formkey +def edit_oauth_app(v, aid): + + aid = int(aid) + app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() + + app.redirect_uri = request.values.get('redirect_uri') + app.app_name = request.values.get('name') + app.description = request.values.get("description")[:256] + + g.db.add(app) + + g.db.commit() + + return redirect('/settings/apps') + + +@app.post("/admin/app/approve/") +@limiter.limit("1/second") +@admin_level_required(3) +@validate_formkey +def admin_app_approve(v, aid): + + app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() + user = app.author + + app.client_id = secrets.token_urlsafe(64)[:64] + g.db.add(app) + + access_token = secrets.token_urlsafe(128)[:128] + new_auth = ClientAuth( + oauth_client = app.id, + user_id = user.id, + access_token=access_token + ) + + g.db.add(new_auth) + + send_notification(NOTIFICATIONS_ACCOUNT, user, f"Your application `{app.app_name}` has been approved. Here's your access token: `{access_token}`\nPlease check the guide [here](/api) if you don't know what to do next.") + + g.db.commit() + + return {"message": f"{app.app_name} approved"} + + +@app.post("/admin/app/revoke/") +@limiter.limit("1/second") +@admin_level_required(3) +@validate_formkey +def admin_app_revoke(v, aid): + + app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() + if app.id: + for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): g.db.delete(auth) + + send_notification(NOTIFICATIONS_ACCOUNT, app.author, f"Your application `{app.app_name}` has been revoked.") + + g.db.delete(app) + + g.db.commit() + + return {"message": f"App revoked"} + + +@app.post("/admin/app/reject/") +@limiter.limit("1/second") +@admin_level_required(3) +@validate_formkey +def admin_app_reject(v, aid): + + app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() + + for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): g.db.delete(auth) + + send_notification(NOTIFICATIONS_ACCOUNT, app.author, f"Your application `{app.app_name}` has been rejected.") + + g.db.delete(app) + + g.db.commit() + + return {"message": f"App rejected"} + + +@app.get("/admin/app/") +@admin_level_required(3) +def admin_app_id(v, aid): + + aid=aid + + oauth = g.db.query(OauthApp).options( + joinedload( + OauthApp.author)).filter_by( + id=aid).first() + + pids=oauth.idlist(page=int(request.values.get("page",1)), + ) + + next_exists=len(pids)==101 + pids=pids[:100] + + posts=get_posts(pids, v=v) + + return render_template("admin/app.html", + v=v, + app=oauth, + listing=posts, + next_exists=next_exists + ) + +@app.get("/admin/app//comments") +@admin_level_required(3) +def admin_app_id_comments(v, aid): + + aid=aid + + oauth = g.db.query(OauthApp).options( + joinedload( + OauthApp.author)).filter_by( + id=aid).first() + + cids=oauth.comments_idlist(page=int(request.values.get("page",1)), + ) + + next_exists=len(cids)==101 + cids=cids[:100] + + comments=get_comments(cids, v=v) + + + return render_template("admin/app.html", + v=v, + app=oauth, + comments=comments, + next_exists=next_exists, + standalone=True + ) + + +@app.get("/admin/apps") +@admin_level_required(3) +def admin_apps_list(v): + + apps = g.db.query(OauthApp).all() + + return render_template("admin/apps.html", v=v, apps=apps) + + +@app.post("/oauth/reroll/") +@limiter.limit("1/second") +@auth_required +def reroll_oauth_tokens(aid, v): + + aid = aid + + a = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first() + + if a.author_id != v.id: abort(403) + + a.client_id = secrets.token_urlsafe(64)[:64] + g.db.add(a) + + g.db.commit() + + return {"message": "Client ID Rerolled", "id": a.client_id} diff --git a/files/routes/posts.py b/files/routes/posts.py old mode 100644 new mode 100755 index e805d3444..103288905 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -1,1003 +1,1003 @@ -import time -import mistletoe -import gevent -import requests -from files.helpers.wrappers import * -from files.helpers.sanitize import * -from files.helpers.filters import * -from files.helpers.markdown import * -from files.helpers.session import * -from files.helpers.alerts import send_notification -from files.helpers.discord import send_message -from files.helpers.const import * -from files.classes import * -from flask import * -from io import BytesIO -from files.__main__ import app, limiter, cache, db_session -from PIL import Image as PILimage -from .front import frontlist, changeloglist -from urllib.parse import ParseResult, urlunparse, urlparse, quote - -site = environ.get("DOMAIN").strip() -CATBOX_KEY = environ.get("CATBOX_KEY").strip() - -with open("snappy.txt", "r") as f: snappyquotes = f.read().split("{[para]}") - - -@app.post("/toggle_club/") -@auth_required -def toggle_club(pid, v): - - post = get_post(pid) - - if post.author_id != v.id or not v.paid_dues: abort(403) - - post.club = not post.club - g.db.add(post) - - if post.author_id!=v.id: - ma=ModAction( - kind="club" if post.club else "unclub", - user_id=v.id, - target_submission_id=post.id, - ) - g.db.add(ma) - - g.db.commit() - - if post.club: return {"message": "Post has been marked as club-only!"} - else: return {"message": "Post has been unmarked as club-only!"} - - -@app.post("/publish/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def publish(pid, v): - post = get_post(pid) - if not post.author_id == v.id: abort(403) - post.private = False - g.db.add(post) - - cache.delete_memoized(frontlist) - - for follow in v.followers: - user = get_account(follow.user_id) - send_notification(AUTOJANNY_ACCOUNT, user, f"@{v.username} has made a new post: [{post.title}](https://{site}{post.permalink})") - - g.db.commit() - - return {"message": "Post published!"} - -@app.get("/submit") -@auth_required -def submit_get(v): - - return render_template("submit.html", - v=v) - -@app.get("/post/") -@app.get("/post//") -@app.get("/logged_out/post/") -@app.get("/logged_out/post//") -@auth_desired -def post_id(pid, anything=None, v=None): - - if not v and not request.path.startswith('/logged_out') and not request.headers.get("Authorization"): return redirect(f"/logged_out{request.full_path}") - - if v and request.path.startswith('/logged_out'): v = None - - try: pid = int(pid) - except Exception as e: pass - - if v: defaultsortingcomments = v.defaultsortingcomments - else: defaultsortingcomments = "top" - sort=request.values.get("sort", defaultsortingcomments) - - try: pid = int(pid) - except: - try: pid = int(pid, 36) - except: abort(404) - - post = get_post(pid, v=v) - - if post.club and not (v and v.paid_dues): abort(403) - - if v: - votes = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id).subquery() - - blocking = v.blocking.subquery() - - blocked = v.blocked.subquery() - - comments = g.db.query( - Comment, - votes.c.vote_type, - blocking.c.id, - blocked.c.id, - ) - - if not (v and v.shadowbanned) and not (v and v.admin_level == 6): - shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] - comments = comments.filter(Comment.author_id.notin_(shadowbanned)) - - comments=comments.filter( - Comment.parent_submission == post.id, - Comment.author_id != AUTOPOLLER_ACCOUNT, - ).join( - votes, - votes.c.comment_id == Comment.id, - isouter=True - ).join( - blocking, - blocking.c.target_id == Comment.author_id, - isouter=True - ).join( - blocked, - blocked.c.user_id == Comment.author_id, - isouter=True - ) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc.asc()) - elif sort == "controversial": - comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) - elif sort == "top": - comments = comments.order_by(Comment.downvotes - Comment.upvotes) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - output = [] - for c in comments.all(): - comment = c[0] - comment.voted = c[1] or 0 - comment.is_blocking = c[2] or 0 - comment.is_blocked = c[3] or 0 - output.append(comment) - - post.preloaded_comments = output - - else: - shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] - comments = g.db.query(Comment).filter(Comment.parent_submission == post.id, Comment.author_id != AUTOPOLLER_ACCOUNT, Comment.author_id.notin_(shadowbanned)) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc.asc()) - elif sort == "controversial": - comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) - elif sort == "top": - comments = comments.order_by(Comment.downvotes - Comment.upvotes) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - post.preloaded_comments = comments.all() - - post.views += 1 - g.db.add(post) - if isinstance(session.get('over_18', 0), dict): session["over_18"] = 0 - if post.over_18 and not (v and v.over_18) and not session.get('over_18', 0) >= int(time.time()): - if request.headers.get("Authorization"): return {"error":"Must be 18+ to view"}, 451 - else: return render_template("errors/nsfw.html", v=v) - - g.db.commit() - if request.headers.get("Authorization"): return post.json - else: return post.rendered_page(v=v, sort=sort) - - -@app.post("/edit_post/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def edit_post(pid, v): - - p = get_post(pid) - - if not p.author_id == v.id: abort(403) - - title = request.values.get("title", "").strip() - body = request.values.get("body", "").strip() - - if title != p.title: - p.title = title - p.title_html = filter_title(title) - - if body != p.body: - for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE): - if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})') - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html = sanitize(body_md) - - bans = filter_comment_html(body_html) - if bans: - ban = bans[0] - reason = f"Remove the {ban.domain} link from your post and try again." - if ban.reason: - reason += f" {ban.reason}" - - return {"error": reason}, 403 - - p.body = body - p.body_html = body_html - - if "rama" in request.host and "ivermectin" in body_html.lower(): - - p.is_banned = True - p.ban_reason = "ToS Violation" - - g.db.add(p) - - body = VAXX_MSG.format(username=v.username) - - body_md = CustomRenderer().render(mistletoe.Document(body)) - - body_jannied_html = sanitize(body_md) - - - c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, - parent_submission=p.id, - level=1, - over_18=False, - is_bot=True, - app_id=None, - is_pinned=True, - distinguish_level=6, - body_html=body_jannied_html, - body=body - ) - - g.db.add(c_jannied) - g.db.flush() - - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - - if v.agendaposter and "trans lives matter" not in body_html.lower(): - - p.is_banned = True - p.ban_reason = "ToS Violation" - - g.db.add(p) - - body = AGENDAPOSTER_MSG.format(username=v.username) - - body_md = CustomRenderer().render(mistletoe.Document(body)) - - body_jannied_html = sanitize(body_md) - - c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, - parent_submission=p.id, - level=1, - over_18=False, - is_bot=True, - app_id=None, - is_pinned=True, - distinguish_level=6, - body_html=body_jannied_html, - body=body - ) - - g.db.add(c_jannied) - g.db.flush() - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - - notify_users = set() - - soup = BeautifulSoup(body_html, features="html.parser") - for mention in soup.find_all("a", href=re.compile("^/@(\w+)")): - username = mention["href"].split("@")[1] - user = g.db.query(User).options(lazyload('*')).filter_by(username=username).first() - if user and not v.any_block_exists(user) and user.id != v.id: notify_users.add(user) - - message = f"@{v.username} has mentioned you: https://{site}{p.permalink}" - for x in notify_users: - existing = g.db.query(Comment).options(lazyload('*')).filter(Comment.author_id == NOTIFICATIONS_ACCOUNT, Comment.body == message, Comment.notifiedto == x.id).first() - if not existing: send_notification(NOTIFICATIONS_ACCOUNT, x, message) - - - if title != p.title or body != p.body: - if int(time.time()) - p.created_utc > 60 * 3: p.edited_utc = int(time.time()) - g.db.add(p) - - g.db.commit() - - return redirect(p.permalink) - -@app.get("/submit/title") -@limiter.limit("6/minute") -@auth_required -def get_post_title(v): - - url = request.values.get("url", None) - if not url: return abort(400) - - headers = {"User-Agent": f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36"} - try: x = requests.get(url, headers=headers, timeout=5) - except BaseException: return {"error": "Could not reach page"}, 400 - - if not x.status_code == 200: return {"error": f"Page returned {x.status_code}"}, x.status_code - - try: - soup = BeautifulSoup(x.content, 'html.parser') - return {"url": url, "title": soup.find('title').string} - except BaseException: - return {"error": f"Could not find a title"}, 400 - -def archiveorg(url): - try: requests.get(f'https://web.archive.org/save/{url}', headers={'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'}, timeout=100) - except Exception as e: print(e) - -def filter_title(title): - title = title.strip() - title = title.replace("\n", "") - title = title.replace("\r", "") - title = title.replace("\t", "") - - title = bleach.clean(title, tags=[]) - - for i in re.finditer('(?', title) - - elif path.isfile(f'./files/assets/images/emojis/{emoji}.webp'): - title = re.sub(f'(?', title) - - if len(title) > 1500: abort(400) - else: return title - - - -def thumbnail_thread(pid): - - def expand_url(post_url, fragment_url): - - if fragment_url.startswith("https://"): - return fragment_url - elif fragment_url.startswith("http://"): - return f"https://{fragment_url.split('http://')[1]}" - elif fragment_url.startswith('//'): - return f"https:{fragment_url}" - elif fragment_url.startswith('/'): - parsed_url = urlparse(post_url) - return f"https://{parsed_url.netloc}{fragment_url}" - else: - return f"{post_url}{'/' if not post_url.endswith('/') else ''}{fragment_url}" - - db = db_session() - - post = db.query(Submission).filter_by(id=pid).first() - - if not post: - time.sleep(5) - post = db.query(Submission).filter_by(id=pid).first() - - fetch_url=post.url - - headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36"} - - try: - x=requests.get(fetch_url, headers=headers) - except: - db.close() - return - - if x.status_code != 200: - db.close() - return - - - - if x.headers.get("Content-Type","").startswith("text/html"): - soup=BeautifulSoup(x.content, 'html.parser') - - thumb_candidate_urls=[] - - meta_tags = [ - "ruqqus:thumbnail", - "twitter:image", - "og:image", - "thumbnail" - ] - - for tag_name in meta_tags: - - - tag = soup.find( - 'meta', - attrs={ - "name": tag_name, - "content": True - } - ) - if not tag: - tag = soup.find( - 'meta', - attrs={ - 'property': tag_name, - 'content': True - } - ) - if tag: - thumb_candidate_urls.append(expand_url(post.url, tag['content'])) - - for tag in soup.find_all("img", attrs={'src':True}): - thumb_candidate_urls.append(expand_url(post.url, tag['src'])) - - - for url in thumb_candidate_urls: - - try: - image_req=requests.get(url, headers=headers) - except: - continue - - if image_req.status_code >= 400: - continue - - if not image_req.headers.get("Content-Type","").startswith("image/"): - continue - - if image_req.headers.get("Content-Type","").startswith("image/svg"): - continue - - image = PILimage.open(BytesIO(image_req.content)) - if image.width < 30 or image.height < 30: - continue - - break - - else: - db.close() - return - - - - elif x.headers.get("Content-Type","").startswith("image/"): - image_req=x - image = PILimage.open(BytesIO(x.content)) - - else: - db.close() - return - - name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' - - with open(name, "wb") as file: - for chunk in image_req.iter_content(1024): - file.write(chunk) - - post.thumburl = "https://" + site + process_image(name, True) - db.add(post) - db.commit() - db.close() - return - - -@app.post("/submit") -@limiter.limit("1/second") -@limiter.limit("6/minute") -@is_not_banned -@validate_formkey -def submit_post(v): - if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 - - title = request.values.get("title", "").strip() - url = request.values.get("url", "").strip() - - if url: - if "/i.imgur.com/" in url: url = url.replace(".png", ".webp").replace(".jpg", ".webp").replace(".jpeg", ".webp") - elif "/media.giphy.com/" in url or "/c.tenor.com/" in url: url = url.replace(".gif", ".webp") - elif "/i.ibb.com/" in url: url = url.replace(".png", ".webp").replace(".jpg", ".webp").replace(".jpeg", ".webp").replace(".gif", ".webp") - - for rd in ["https://reddit.com/", "https://new.reddit.com/", "https://www.reddit.com/", "https://redd.it/"]: - url = url.replace(rd, "https://old.reddit.com/") - - url = url.replace("https://mobile.twitter.com", "https://twitter.com") - if url.startswith("https://streamable.com/") and not url.startswith("https://streamable.com/e/"): url = url.replace("https://streamable.com/", "https://streamable.com/e/") - - parsed_url = urlparse(url) - - domain = parsed_url.netloc - - qd = parse_qs(parsed_url.query) - filtered = dict((k, v) for k, v in qd.items() if not k.startswith('utm_')) - - new_url = ParseResult(scheme="https", - netloc=parsed_url.netloc, - path=parsed_url.path, - params=parsed_url.params, - query=urlencode(filtered, doseq=True), - fragment=parsed_url.fragment) - url = urlunparse(new_url) - - repost = g.db.query(Submission).options(lazyload('*')).filter( - Submission.url.ilike(f'{url}%'), - Submission.deleted_utc == 0, - Submission.is_banned == False - ).first() - - if repost: return redirect(repost.permalink) - - domain_obj = get_domain(domain) - if domain_obj: - if request.headers.get("Authorization"): return {"error":domain_obj.reason}, 400 - else: return render_template("submit.html", v=v, error=domain_obj.reason, title=title, url=url, body=request.values.get("body", "")), 400 - elif "twitter.com" in domain: - try: embed = requests.get("https://publish.twitter.com/oembed", params={"url":url, "omit_script":"t"}).json()["html"] - except: embed = None - elif "youtu" in domain: - try: - yt_id = re.match(re.compile("^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|shorts\/|\&v=)([^#\&\?]*).*"), url).group(2) - params = parse_qs(urlparse(url).query) - t = params.get('t', params.get('start', [0]))[0] - if t: embed = f"https://youtube.com/embed/{yt_id}?start={t}" - else: embed = f"https://youtube.com/embed/{yt_id}" - except: embed = None - elif app.config['SERVER_NAME'] in domain and "/post/" in url and "context" not in url: - id = url.split("/post/")[1] - if "/" in id: id = id.split("/")[0] - embed = id - else: embed = None - else: embed = None - - if not url and not request.values.get("body") and not request.files.get("file", None): - if request.headers.get("Authorization"): return {"error": "`url` or `body` parameter required."}, 400 - else: return render_template("submit.html", v=v, error="Please enter a url or some text.", title=title, url=url, body=request.values.get("body", "")), 400 - - if not title: - if request.headers.get("Authorization"): return {"error": "Please enter a better title"}, 400 - else: return render_template("submit.html", v=v, error="Please enter a better title.", title=title, url=url, body=request.values.get("body", "")), 400 - - - elif len(title) > 500: - if request.headers.get("Authorization"): return {"error": "500 character limit for titles"}, 400 - else: render_template("submit.html", v=v, error="500 character limit for titles.", title=title[:500], url=url, body=request.values.get("body", "")), 400 - - body = request.values.get("body", "").strip() - dup = g.db.query(Submission).options(lazyload('*')).filter( - Submission.author_id == v.id, - Submission.deleted_utc == 0, - Submission.title == title, - Submission.url == url, - Submission.body == body - ).first() - - if dup: return redirect(dup.permalink) - - now = int(time.time()) - cutoff = now - 60 * 60 * 24 - - - similar_posts = g.db.query(Submission).options( - lazyload('*') - ).filter( - Submission.author_id == v.id, - Submission.title.op('<->')(title) < app.config["SPAM_SIMILARITY_THRESHOLD"], - Submission.created_utc > cutoff - ).all() - - if url: - similar_urls = g.db.query(Submission).options( - lazyload('*') - ).filter( - Submission.author_id == v.id, - Submission.url.op('<->')(url) < app.config["SPAM_URL_SIMILARITY_THRESHOLD"], - Submission.created_utc > cutoff - ).all() - else: - similar_urls = [] - - threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] - if v.age >= (60 * 60 * 24 * 7): - threshold *= 3 - elif v.age >= (60 * 60 * 24): - threshold *= 2 - - if max(len(similar_urls), len(similar_posts)) >= threshold: - - text = "Your account has been suspended for 1 day for the following reason:\n\n> Too much spam!" - send_notification(NOTIFICATIONS_ACCOUNT, v, text) - - v.ban(reason="Spamming.", - days=1) - - for alt in v.alts: - if not alt.is_suspended: - alt.ban(reason="Spamming.", days=1) - - for post in similar_posts + similar_urls: - post.is_banned = True - post.is_pinned = False - post.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." - g.db.add(post) - ma=ModAction( - user_id=AUTOJANNY_ACCOUNT, - target_submission_id=post.id, - kind="ban_post", - note="spam" - ) - g.db.add(ma) - return redirect("/notifications") - - if len(str(body)) > 10000: - - if request.headers.get("Authorization"): return {"error":"10000 character limit for text body."}, 400 - else: return render_template("submit.html", v=v, error="10000 character limit for text body.", title=title, url=url, body=request.values.get("body", "")), 400 - - if len(url) > 2048: - - if request.headers.get("Authorization"): return {"error":"2048 character limit for URLs."}, 400 - else: return render_template("submit.html", v=v, error="2048 character limit for URLs.", title=title, url=url,body=request.values.get("body", "")), 400 - - for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE): - if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})') - body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body) - - options = [] - for i in re.finditer('\s*\$\$([^\$\n]+)\$\$\s*', body): - options.append(i.group(1)) - body = body.replace(i.group(0), "") - - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html = sanitize(body_md) - - - - if len(body_html) > 20000: abort(400) - - bans = filter_comment_html(body_html) - if bans: - ban = bans[0] - reason = f"Remove the {ban.domain} link from your post and try again." - if ban.reason: reason += f" {ban.reason}" - if request.headers.get("Authorization"): return {"error": reason}, 403 - else: return render_template("submit.html", v=v, error=reason, title=title, url=url, body=request.values.get("body", "")), 403 - - if v.paid_dues: club = bool(request.values.get("club","")) - else: club = False - - new_post = Submission( - private=bool(request.values.get("private","")), - club=club, - author_id=v.id, - over_18=bool(request.values.get("over_18","")), - app_id=v.client.application.id if v.client else None, - is_bot = request.headers.get("X-User-Type","").lower()=="bot", - url=url, - body=body, - body_html=body_html, - embed_url=embed, - title=title, - title_html=filter_title(title) - ) - - g.db.add(new_post) - g.db.flush() - - for option in options: - c = Comment(author_id=AUTOPOLLER_ACCOUNT, - parent_submission=new_post.id, - level=1, - body_html=filter_title(option), - ) - - g.db.add(c) - - vote = Vote(user_id=v.id, - vote_type=1, - submission_id=new_post.id - ) - g.db.add(vote) - g.db.flush() - - if request.files.get('file') and request.headers.get("cf-ipcountry") != "T1": - - file = request.files['file'] - - if not file.content_type.startswith(('image/', 'video/')): - if request.headers.get("Authorization"): return {"error": f"File type not allowed"}, 400 - else: return render_template("submit.html", v=v, error=f"File type not allowed.", title=title, body=request.values.get("body", "")), 400 - - if file.content_type.startswith('video/') and v.coins < app.config["VIDEO_COIN_REQUIREMENT"] and v.admin_level < 1: - if request.headers.get("Authorization"): - return { - "error": f"You need at least {app.config['VIDEO_COIN_REQUIREMENT']} coins to upload videos" - }, 403 - else: - return render_template( - "submit.html", - v=v, - error=f"You need at least {app.config['VIDEO_COIN_REQUIREMENT']} coins to upload videos.", - title=title, - body=request.values.get("body", "") - ), 403 - - if file.content_type.startswith('image/'): - name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' - file.save(name) - new_post.url = request.host_url[:-1] + process_image(name) - - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - new_post.url = requests.post('https://catbox.moe/user/api.php', data={'userhash':CATBOX_KEY, 'reqtype':'fileupload'}, files={'fileToUpload':f}).text - - g.db.add(new_post) - - g.db.flush() - - - - - if (new_post.url or request.files.get('file')) and (v.is_activated or request.headers.get('cf-ipcountry')!="T1"): - gevent.spawn( thumbnail_thread, new_post.id) - - notify_users = set() - - soup = BeautifulSoup(body_html, features="html.parser") - for mention in soup.find_all("a", href=re.compile("^/@(\w+)")): - username = mention["href"].split("@")[1] - user = g.db.query(User).options(lazyload('*')).filter_by(username=username).first() - if user and not v.any_block_exists(user) and user.id != v.id: notify_users.add(user) - - for x in notify_users: send_notification(NOTIFICATIONS_ACCOUNT, x, f"@{v.username} has mentioned you: https://{site}{new_post.permalink}") - - if not new_post.private: - for follow in v.followers: - user = get_account(follow.user_id) - send_notification(AUTOJANNY_ACCOUNT, user, f"@{v.username} has made a new post: [{title}](https://{site}{new_post.permalink})") - - g.db.add(new_post) - g.db.flush() - - - if "rama" in request.host and "ivermectin" in new_post.body_html.lower(): - - new_post.is_banned = True - new_post.ban_reason = "ToS Violation" - - g.db.add(new_post) - - - body = VAXX_MSG.format(username=v.username) - - body_md = CustomRenderer().render(mistletoe.Document(body)) - - body_jannied_html = sanitize(body_md) - - - c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, - parent_submission=new_post.id, - level=1, - over_18=False, - is_bot=True, - app_id=None, - is_pinned=True, - distinguish_level=6, - body_html=body_jannied_html, - body=body, - ) - - g.db.add(c_jannied) - g.db.flush() - - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - - if v.agendaposter and "trans lives matter" not in new_post.body_html.lower(): - - new_post.is_banned = True - new_post.ban_reason = "ToS Violation" - - g.db.add(new_post) - - body = AGENDAPOSTER_MSG.format(username=v.username) - - body_md = CustomRenderer().render(mistletoe.Document(body)) - - body_jannied_html = sanitize(body_md) - - - - c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, - parent_submission=new_post.id, - level=1, - over_18=False, - is_bot=True, - app_id=None, - is_pinned=True, - distinguish_level=6, - body_html=body_jannied_html, - body=body, - ) - - g.db.add(c_jannied) - g.db.flush() - - - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - if "rama" in request.host or new_post.url: - new_post.comment_count = 1 - g.db.add(new_post) - - if "rama" in request.host: - if v.id == 995: - if random.random() < 0.02: body = "i love you carp" - else: body = "fuck off carp" - elif v.id == 3833: - if random.random() < 0.5: body = "wow, this lawlzpost sucks!" - else: body = "wow, a good lawlzpost for once!" - else: body = random.choice(snappyquotes) - body += "\n\n---\n\n" - else: body = "" - if new_post.url: - body += f"Snapshots:\n\n* [reveddit.com](https://reveddit.com/{new_post.url})\n* [archive.org](https://web.archive.org/{new_post.url})\n* [archive.ph](https://archive.ph/?url={quote(new_post.url)}&run=1) (click to archive)" - gevent.spawn(archiveorg, new_post.url) - body_md = CustomRenderer().render(mistletoe.Document(body)) - body_html = sanitize(body_md) - - - c = Comment(author_id=261, - distinguish_level=6, - parent_submission=new_post.id, - level=1, - over_18=False, - is_bot=True, - app_id=None, - body_html=body_html, - body=body, - ) - - g.db.add(c) - g.db.flush() - - - n = Notification(comment_id=c.id, user_id=v.id) - g.db.add(n) - g.db.flush() - - v.post_count = v.submissions.filter_by(is_banned=False, deleted_utc=0).count() - g.db.add(v) - - cache.delete_memoized(frontlist) - cache.delete_memoized(User.userpagelisting) - if "[changelog]" in new_post.title or "(changelog)" in new_post.title: - send_message(f"https://{site}{new_post.permalink}") - cache.delete_memoized(changeloglist) - - g.db.commit() - - if request.headers.get("Authorization"): return new_post.json - else: return redirect(new_post.permalink) - - -@app.post("/delete_post/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def delete_post_pid(pid, v): - - post = get_post(pid) - if not post.author_id == v.id: - abort(403) - - post.deleted_utc = int(time.time()) - post.is_pinned = False - post.stickied = None - - g.db.add(post) - - cache.delete_memoized(frontlist) - - g.db.commit() - - return {"message": "Post deleted!"} - -@app.post("/undelete_post/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def undelete_post_pid(pid, v): - post = get_post(pid) - if not post.author_id == v.id: abort(403) - post.deleted_utc =0 - g.db.add(post) - - cache.delete_memoized(frontlist) - - g.db.commit() - - return {"message": "Post undeleted!"} - - -@app.post("/toggle_comment_nsfw/") -@auth_required -@validate_formkey -def toggle_comment_nsfw(cid, v): - - comment = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first() - if not comment.author_id == v.id and not v.admin_level >= 3: abort(403) - comment.over_18 = not comment.over_18 - g.db.add(comment) - g.db.flush() - - g.db.commit() - - if comment.over_18: return {"message": "Comment has been marked as +18!"} - else: return {"message": "Comment has been unmarked as +18!"} - -@app.post("/toggle_post_nsfw/") -@auth_required -@validate_formkey -def toggle_post_nsfw(pid, v): - - post = get_post(pid) - - if not post.author_id == v.id and not v.admin_level >= 3: - abort(403) - - post.over_18 = not post.over_18 - g.db.add(post) - - if post.author_id!=v.id: - ma=ModAction( - kind="set_nsfw" if post.over_18 else "unset_nsfw", - user_id=v.id, - target_submission_id=post.id, - ) - g.db.add(ma) - - g.db.commit() - - if post.over_18: return {"message": "Post has been marked as +18!"} - else: return {"message": "Post has been unmarked as +18!"} - -@app.post("/save_post/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def save_post(pid, v): - - post=get_post(pid) - - save = g.db.query(SaveRelationship).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id, type=1).first() - - if not save: - new_save=SaveRelationship(user_id=v.id, submission_id=post.id, type=1) - g.db.add(new_save) - g.db.commit() - - return {"message": "Post saved!"} - -@app.post("/unsave_post/") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def unsave_post(pid, v): - - post=get_post(pid) - - save = g.db.query(SaveRelationship).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id, type=1).first() - - if save: - g.db.delete(save) - g.db.commit() - - return {"message": "Post unsaved!"} +import time +import mistletoe +import gevent +import requests +from files.helpers.wrappers import * +from files.helpers.sanitize import * +from files.helpers.filters import * +from files.helpers.markdown import * +from files.helpers.session import * +from files.helpers.alerts import send_notification +from files.helpers.discord import send_message +from files.helpers.const import * +from files.classes import * +from flask import * +from io import BytesIO +from files.__main__ import app, limiter, cache, db_session +from PIL import Image as PILimage +from .front import frontlist, changeloglist +from urllib.parse import ParseResult, urlunparse, urlparse, quote + +site = environ.get("DOMAIN").strip() +CATBOX_KEY = environ.get("CATBOX_KEY").strip() + +with open("snappy.txt", "r") as f: snappyquotes = f.read().split("{[para]}") + + +@app.post("/toggle_club/") +@auth_required +def toggle_club(pid, v): + + post = get_post(pid) + + if post.author_id != v.id or not v.paid_dues: abort(403) + + post.club = not post.club + g.db.add(post) + + if post.author_id!=v.id: + ma=ModAction( + kind="club" if post.club else "unclub", + user_id=v.id, + target_submission_id=post.id, + ) + g.db.add(ma) + + g.db.commit() + + if post.club: return {"message": "Post has been marked as club-only!"} + else: return {"message": "Post has been unmarked as club-only!"} + + +@app.post("/publish/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def publish(pid, v): + post = get_post(pid) + if not post.author_id == v.id: abort(403) + post.private = False + g.db.add(post) + + cache.delete_memoized(frontlist) + + for follow in v.followers: + user = get_account(follow.user_id) + send_notification(AUTOJANNY_ACCOUNT, user, f"@{v.username} has made a new post: [{post.title}](https://{site}{post.permalink})") + + g.db.commit() + + return {"message": "Post published!"} + +@app.get("/submit") +@auth_required +def submit_get(v): + + return render_template("submit.html", + v=v) + +@app.get("/post/") +@app.get("/post//") +@app.get("/logged_out/post/") +@app.get("/logged_out/post//") +@auth_desired +def post_id(pid, anything=None, v=None): + + if not v and not request.path.startswith('/logged_out') and not request.headers.get("Authorization"): return redirect(f"/logged_out{request.full_path}") + + if v and request.path.startswith('/logged_out'): v = None + + try: pid = int(pid) + except Exception as e: pass + + if v: defaultsortingcomments = v.defaultsortingcomments + else: defaultsortingcomments = "top" + sort=request.values.get("sort", defaultsortingcomments) + + try: pid = int(pid) + except: + try: pid = int(pid, 36) + except: abort(404) + + post = get_post(pid, v=v) + + if post.club and not (v and v.paid_dues): abort(403) + + if v: + votes = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id).subquery() + + blocking = v.blocking.subquery() + + blocked = v.blocked.subquery() + + comments = g.db.query( + Comment, + votes.c.vote_type, + blocking.c.id, + blocked.c.id, + ) + + if not (v and v.shadowbanned) and not (v and v.admin_level == 6): + shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] + comments = comments.filter(Comment.author_id.notin_(shadowbanned)) + + comments=comments.filter( + Comment.parent_submission == post.id, + Comment.author_id != AUTOPOLLER_ACCOUNT, + ).join( + votes, + votes.c.comment_id == Comment.id, + isouter=True + ).join( + blocking, + blocking.c.target_id == Comment.author_id, + isouter=True + ).join( + blocked, + blocked.c.user_id == Comment.author_id, + isouter=True + ) + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc.asc()) + elif sort == "controversial": + comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) + elif sort == "top": + comments = comments.order_by(Comment.downvotes - Comment.upvotes) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + output = [] + for c in comments.all(): + comment = c[0] + comment.voted = c[1] or 0 + comment.is_blocking = c[2] or 0 + comment.is_blocked = c[3] or 0 + output.append(comment) + + post.preloaded_comments = output + + else: + shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()] + comments = g.db.query(Comment).filter(Comment.parent_submission == post.id, Comment.author_id != AUTOPOLLER_ACCOUNT, Comment.author_id.notin_(shadowbanned)) + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc.asc()) + elif sort == "controversial": + comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) + elif sort == "top": + comments = comments.order_by(Comment.downvotes - Comment.upvotes) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + post.preloaded_comments = comments.all() + + post.views += 1 + g.db.add(post) + if isinstance(session.get('over_18', 0), dict): session["over_18"] = 0 + if post.over_18 and not (v and v.over_18) and not session.get('over_18', 0) >= int(time.time()): + if request.headers.get("Authorization"): return {"error":"Must be 18+ to view"}, 451 + else: return render_template("errors/nsfw.html", v=v) + + g.db.commit() + if request.headers.get("Authorization"): return post.json + else: return post.rendered_page(v=v, sort=sort) + + +@app.post("/edit_post/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def edit_post(pid, v): + + p = get_post(pid) + + if not p.author_id == v.id: abort(403) + + title = request.values.get("title", "").strip() + body = request.values.get("body", "").strip() + + if title != p.title: + p.title = title + p.title_html = filter_title(title) + + if body != p.body: + for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE): + if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})') + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html = sanitize(body_md) + + bans = filter_comment_html(body_html) + if bans: + ban = bans[0] + reason = f"Remove the {ban.domain} link from your post and try again." + if ban.reason: + reason += f" {ban.reason}" + + return {"error": reason}, 403 + + p.body = body + p.body_html = body_html + + if "rama" in request.host and "ivermectin" in body_html.lower(): + + p.is_banned = True + p.ban_reason = "ToS Violation" + + g.db.add(p) + + body = VAXX_MSG.format(username=v.username) + + body_md = CustomRenderer().render(mistletoe.Document(body)) + + body_jannied_html = sanitize(body_md) + + + c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, + parent_submission=p.id, + level=1, + over_18=False, + is_bot=True, + app_id=None, + is_pinned=True, + distinguish_level=6, + body_html=body_jannied_html, + body=body + ) + + g.db.add(c_jannied) + g.db.flush() + + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + + if v.agendaposter and "trans lives matter" not in body_html.lower(): + + p.is_banned = True + p.ban_reason = "ToS Violation" + + g.db.add(p) + + body = AGENDAPOSTER_MSG.format(username=v.username) + + body_md = CustomRenderer().render(mistletoe.Document(body)) + + body_jannied_html = sanitize(body_md) + + c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, + parent_submission=p.id, + level=1, + over_18=False, + is_bot=True, + app_id=None, + is_pinned=True, + distinguish_level=6, + body_html=body_jannied_html, + body=body + ) + + g.db.add(c_jannied) + g.db.flush() + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + + notify_users = set() + + soup = BeautifulSoup(body_html, features="html.parser") + for mention in soup.find_all("a", href=re.compile("^/@(\w+)")): + username = mention["href"].split("@")[1] + user = g.db.query(User).options(lazyload('*')).filter_by(username=username).first() + if user and not v.any_block_exists(user) and user.id != v.id: notify_users.add(user) + + message = f"@{v.username} has mentioned you: https://{site}{p.permalink}" + for x in notify_users: + existing = g.db.query(Comment).options(lazyload('*')).filter(Comment.author_id == NOTIFICATIONS_ACCOUNT, Comment.body == message, Comment.notifiedto == x.id).first() + if not existing: send_notification(NOTIFICATIONS_ACCOUNT, x, message) + + + if title != p.title or body != p.body: + if int(time.time()) - p.created_utc > 60 * 3: p.edited_utc = int(time.time()) + g.db.add(p) + + g.db.commit() + + return redirect(p.permalink) + +@app.get("/submit/title") +@limiter.limit("6/minute") +@auth_required +def get_post_title(v): + + url = request.values.get("url", None) + if not url: return abort(400) + + headers = {"User-Agent": f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36"} + try: x = requests.get(url, headers=headers, timeout=5) + except BaseException: return {"error": "Could not reach page"}, 400 + + if not x.status_code == 200: return {"error": f"Page returned {x.status_code}"}, x.status_code + + try: + soup = BeautifulSoup(x.content, 'html.parser') + return {"url": url, "title": soup.find('title').string} + except BaseException: + return {"error": f"Could not find a title"}, 400 + +def archiveorg(url): + try: requests.get(f'https://web.archive.org/save/{url}', headers={'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'}, timeout=100) + except Exception as e: print(e) + +def filter_title(title): + title = title.strip() + title = title.replace("\n", "") + title = title.replace("\r", "") + title = title.replace("\t", "") + + title = bleach.clean(title, tags=[]) + + for i in re.finditer('(?', title) + + elif path.isfile(f'./files/assets/images/emojis/{emoji}.webp'): + title = re.sub(f'(?', title) + + if len(title) > 1500: abort(400) + else: return title + + + +def thumbnail_thread(pid): + + def expand_url(post_url, fragment_url): + + if fragment_url.startswith("https://"): + return fragment_url + elif fragment_url.startswith("http://"): + return f"https://{fragment_url.split('http://')[1]}" + elif fragment_url.startswith('//'): + return f"https:{fragment_url}" + elif fragment_url.startswith('/'): + parsed_url = urlparse(post_url) + return f"https://{parsed_url.netloc}{fragment_url}" + else: + return f"{post_url}{'/' if not post_url.endswith('/') else ''}{fragment_url}" + + db = db_session() + + post = db.query(Submission).filter_by(id=pid).first() + + if not post: + time.sleep(5) + post = db.query(Submission).filter_by(id=pid).first() + + fetch_url=post.url + + headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36"} + + try: + x=requests.get(fetch_url, headers=headers) + except: + db.close() + return + + if x.status_code != 200: + db.close() + return + + + + if x.headers.get("Content-Type","").startswith("text/html"): + soup=BeautifulSoup(x.content, 'html.parser') + + thumb_candidate_urls=[] + + meta_tags = [ + "ruqqus:thumbnail", + "twitter:image", + "og:image", + "thumbnail" + ] + + for tag_name in meta_tags: + + + tag = soup.find( + 'meta', + attrs={ + "name": tag_name, + "content": True + } + ) + if not tag: + tag = soup.find( + 'meta', + attrs={ + 'property': tag_name, + 'content': True + } + ) + if tag: + thumb_candidate_urls.append(expand_url(post.url, tag['content'])) + + for tag in soup.find_all("img", attrs={'src':True}): + thumb_candidate_urls.append(expand_url(post.url, tag['src'])) + + + for url in thumb_candidate_urls: + + try: + image_req=requests.get(url, headers=headers) + except: + continue + + if image_req.status_code >= 400: + continue + + if not image_req.headers.get("Content-Type","").startswith("image/"): + continue + + if image_req.headers.get("Content-Type","").startswith("image/svg"): + continue + + image = PILimage.open(BytesIO(image_req.content)) + if image.width < 30 or image.height < 30: + continue + + break + + else: + db.close() + return + + + + elif x.headers.get("Content-Type","").startswith("image/"): + image_req=x + image = PILimage.open(BytesIO(x.content)) + + else: + db.close() + return + + name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' + + with open(name, "wb") as file: + for chunk in image_req.iter_content(1024): + file.write(chunk) + + post.thumburl = "https://" + site + process_image(name, True) + db.add(post) + db.commit() + db.close() + return + + +@app.post("/submit") +@limiter.limit("1/second") +@limiter.limit("6/minute") +@is_not_banned +@validate_formkey +def submit_post(v): + if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 + + title = request.values.get("title", "").strip() + url = request.values.get("url", "").strip() + + if url: + if "/i.imgur.com/" in url: url = url.replace(".png", ".webp").replace(".jpg", ".webp").replace(".jpeg", ".webp") + elif "/media.giphy.com/" in url or "/c.tenor.com/" in url: url = url.replace(".gif", ".webp") + elif "/i.ibb.com/" in url: url = url.replace(".png", ".webp").replace(".jpg", ".webp").replace(".jpeg", ".webp").replace(".gif", ".webp") + + for rd in ["https://reddit.com/", "https://new.reddit.com/", "https://www.reddit.com/", "https://redd.it/"]: + url = url.replace(rd, "https://old.reddit.com/") + + url = url.replace("https://mobile.twitter.com", "https://twitter.com") + if url.startswith("https://streamable.com/") and not url.startswith("https://streamable.com/e/"): url = url.replace("https://streamable.com/", "https://streamable.com/e/") + + parsed_url = urlparse(url) + + domain = parsed_url.netloc + + qd = parse_qs(parsed_url.query) + filtered = dict((k, v) for k, v in qd.items() if not k.startswith('utm_')) + + new_url = ParseResult(scheme="https", + netloc=parsed_url.netloc, + path=parsed_url.path, + params=parsed_url.params, + query=urlencode(filtered, doseq=True), + fragment=parsed_url.fragment) + url = urlunparse(new_url) + + repost = g.db.query(Submission).options(lazyload('*')).filter( + Submission.url.ilike(f'{url}%'), + Submission.deleted_utc == 0, + Submission.is_banned == False + ).first() + + if repost: return redirect(repost.permalink) + + domain_obj = get_domain(domain) + if domain_obj: + if request.headers.get("Authorization"): return {"error":domain_obj.reason}, 400 + else: return render_template("submit.html", v=v, error=domain_obj.reason, title=title, url=url, body=request.values.get("body", "")), 400 + elif "twitter.com" in domain: + try: embed = requests.get("https://publish.twitter.com/oembed", params={"url":url, "omit_script":"t"}).json()["html"] + except: embed = None + elif "youtu" in domain: + try: + yt_id = re.match(re.compile("^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|shorts\/|\&v=)([^#\&\?]*).*"), url).group(2) + params = parse_qs(urlparse(url).query) + t = params.get('t', params.get('start', [0]))[0] + if t: embed = f"https://youtube.com/embed/{yt_id}?start={t}" + else: embed = f"https://youtube.com/embed/{yt_id}" + except: embed = None + elif app.config['SERVER_NAME'] in domain and "/post/" in url and "context" not in url: + id = url.split("/post/")[1] + if "/" in id: id = id.split("/")[0] + embed = id + else: embed = None + else: embed = None + + if not url and not request.values.get("body") and not request.files.get("file", None): + if request.headers.get("Authorization"): return {"error": "`url` or `body` parameter required."}, 400 + else: return render_template("submit.html", v=v, error="Please enter a url or some text.", title=title, url=url, body=request.values.get("body", "")), 400 + + if not title: + if request.headers.get("Authorization"): return {"error": "Please enter a better title"}, 400 + else: return render_template("submit.html", v=v, error="Please enter a better title.", title=title, url=url, body=request.values.get("body", "")), 400 + + + elif len(title) > 500: + if request.headers.get("Authorization"): return {"error": "500 character limit for titles"}, 400 + else: render_template("submit.html", v=v, error="500 character limit for titles.", title=title[:500], url=url, body=request.values.get("body", "")), 400 + + body = request.values.get("body", "").strip() + dup = g.db.query(Submission).options(lazyload('*')).filter( + Submission.author_id == v.id, + Submission.deleted_utc == 0, + Submission.title == title, + Submission.url == url, + Submission.body == body + ).first() + + if dup: return redirect(dup.permalink) + + now = int(time.time()) + cutoff = now - 60 * 60 * 24 + + + similar_posts = g.db.query(Submission).options( + lazyload('*') + ).filter( + Submission.author_id == v.id, + Submission.title.op('<->')(title) < app.config["SPAM_SIMILARITY_THRESHOLD"], + Submission.created_utc > cutoff + ).all() + + if url: + similar_urls = g.db.query(Submission).options( + lazyload('*') + ).filter( + Submission.author_id == v.id, + Submission.url.op('<->')(url) < app.config["SPAM_URL_SIMILARITY_THRESHOLD"], + Submission.created_utc > cutoff + ).all() + else: + similar_urls = [] + + threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] + if v.age >= (60 * 60 * 24 * 7): + threshold *= 3 + elif v.age >= (60 * 60 * 24): + threshold *= 2 + + if max(len(similar_urls), len(similar_posts)) >= threshold: + + text = "Your account has been suspended for 1 day for the following reason:\n\n> Too much spam!" + send_notification(NOTIFICATIONS_ACCOUNT, v, text) + + v.ban(reason="Spamming.", + days=1) + + for alt in v.alts: + if not alt.is_suspended: + alt.ban(reason="Spamming.", days=1) + + for post in similar_posts + similar_urls: + post.is_banned = True + post.is_pinned = False + post.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly." + g.db.add(post) + ma=ModAction( + user_id=AUTOJANNY_ACCOUNT, + target_submission_id=post.id, + kind="ban_post", + note="spam" + ) + g.db.add(ma) + return redirect("/notifications") + + if len(str(body)) > 10000: + + if request.headers.get("Authorization"): return {"error":"10000 character limit for text body."}, 400 + else: return render_template("submit.html", v=v, error="10000 character limit for text body.", title=title, url=url, body=request.values.get("body", "")), 400 + + if len(url) > 2048: + + if request.headers.get("Authorization"): return {"error":"2048 character limit for URLs."}, 400 + else: return render_template("submit.html", v=v, error="2048 character limit for URLs.", title=title, url=url,body=request.values.get("body", "")), 400 + + for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE): + if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})') + body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body) + + options = [] + for i in re.finditer('\s*\$\$([^\$\n]+)\$\$\s*', body): + options.append(i.group(1)) + body = body.replace(i.group(0), "") + + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html = sanitize(body_md) + + + + if len(body_html) > 20000: abort(400) + + bans = filter_comment_html(body_html) + if bans: + ban = bans[0] + reason = f"Remove the {ban.domain} link from your post and try again." + if ban.reason: reason += f" {ban.reason}" + if request.headers.get("Authorization"): return {"error": reason}, 403 + else: return render_template("submit.html", v=v, error=reason, title=title, url=url, body=request.values.get("body", "")), 403 + + if v.paid_dues: club = bool(request.values.get("club","")) + else: club = False + + new_post = Submission( + private=bool(request.values.get("private","")), + club=club, + author_id=v.id, + over_18=bool(request.values.get("over_18","")), + app_id=v.client.application.id if v.client else None, + is_bot = request.headers.get("X-User-Type","").lower()=="bot", + url=url, + body=body, + body_html=body_html, + embed_url=embed, + title=title, + title_html=filter_title(title) + ) + + g.db.add(new_post) + g.db.flush() + + for option in options: + c = Comment(author_id=AUTOPOLLER_ACCOUNT, + parent_submission=new_post.id, + level=1, + body_html=filter_title(option), + ) + + g.db.add(c) + + vote = Vote(user_id=v.id, + vote_type=1, + submission_id=new_post.id + ) + g.db.add(vote) + g.db.flush() + + if request.files.get('file') and request.headers.get("cf-ipcountry") != "T1": + + file = request.files['file'] + + if not file.content_type.startswith(('image/', 'video/')): + if request.headers.get("Authorization"): return {"error": f"File type not allowed"}, 400 + else: return render_template("submit.html", v=v, error=f"File type not allowed.", title=title, body=request.values.get("body", "")), 400 + + if file.content_type.startswith('video/') and v.coins < app.config["VIDEO_COIN_REQUIREMENT"] and v.admin_level < 1: + if request.headers.get("Authorization"): + return { + "error": f"You need at least {app.config['VIDEO_COIN_REQUIREMENT']} coins to upload videos" + }, 403 + else: + return render_template( + "submit.html", + v=v, + error=f"You need at least {app.config['VIDEO_COIN_REQUIREMENT']} coins to upload videos.", + title=title, + body=request.values.get("body", "") + ), 403 + + if file.content_type.startswith('image/'): + name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' + file.save(name) + new_post.url = request.host_url[:-1] + process_image(name) + + elif file.content_type.startswith('video/'): + file.save("video.mp4") + with open("video.mp4", 'rb') as f: + new_post.url = requests.post('https://catbox.moe/user/api.php', data={'userhash':CATBOX_KEY, 'reqtype':'fileupload'}, files={'fileToUpload':f}).text + + g.db.add(new_post) + + g.db.flush() + + + + + if (new_post.url or request.files.get('file')) and (v.is_activated or request.headers.get('cf-ipcountry')!="T1"): + gevent.spawn( thumbnail_thread, new_post.id) + + notify_users = set() + + soup = BeautifulSoup(body_html, features="html.parser") + for mention in soup.find_all("a", href=re.compile("^/@(\w+)")): + username = mention["href"].split("@")[1] + user = g.db.query(User).options(lazyload('*')).filter_by(username=username).first() + if user and not v.any_block_exists(user) and user.id != v.id: notify_users.add(user) + + for x in notify_users: send_notification(NOTIFICATIONS_ACCOUNT, x, f"@{v.username} has mentioned you: https://{site}{new_post.permalink}") + + if not new_post.private: + for follow in v.followers: + user = get_account(follow.user_id) + send_notification(AUTOJANNY_ACCOUNT, user, f"@{v.username} has made a new post: [{title}](https://{site}{new_post.permalink})") + + g.db.add(new_post) + g.db.flush() + + + if "rama" in request.host and "ivermectin" in new_post.body_html.lower(): + + new_post.is_banned = True + new_post.ban_reason = "ToS Violation" + + g.db.add(new_post) + + + body = VAXX_MSG.format(username=v.username) + + body_md = CustomRenderer().render(mistletoe.Document(body)) + + body_jannied_html = sanitize(body_md) + + + c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, + parent_submission=new_post.id, + level=1, + over_18=False, + is_bot=True, + app_id=None, + is_pinned=True, + distinguish_level=6, + body_html=body_jannied_html, + body=body, + ) + + g.db.add(c_jannied) + g.db.flush() + + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + + if v.agendaposter and "trans lives matter" not in new_post.body_html.lower(): + + new_post.is_banned = True + new_post.ban_reason = "ToS Violation" + + g.db.add(new_post) + + body = AGENDAPOSTER_MSG.format(username=v.username) + + body_md = CustomRenderer().render(mistletoe.Document(body)) + + body_jannied_html = sanitize(body_md) + + + + c_jannied = Comment(author_id=AUTOJANNY_ACCOUNT, + parent_submission=new_post.id, + level=1, + over_18=False, + is_bot=True, + app_id=None, + is_pinned=True, + distinguish_level=6, + body_html=body_jannied_html, + body=body, + ) + + g.db.add(c_jannied) + g.db.flush() + + + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + if "rama" in request.host or new_post.url: + new_post.comment_count = 1 + g.db.add(new_post) + + if "rama" in request.host: + if v.id == 995: + if random.random() < 0.02: body = "i love you carp" + else: body = "fuck off carp" + elif v.id == 3833: + if random.random() < 0.5: body = "wow, this lawlzpost sucks!" + else: body = "wow, a good lawlzpost for once!" + else: body = random.choice(snappyquotes) + body += "\n\n---\n\n" + else: body = "" + if new_post.url: + body += f"Snapshots:\n\n* [reveddit.com](https://reveddit.com/{new_post.url})\n* [archive.org](https://web.archive.org/{new_post.url})\n* [archive.ph](https://archive.ph/?url={quote(new_post.url)}&run=1) (click to archive)" + gevent.spawn(archiveorg, new_post.url) + body_md = CustomRenderer().render(mistletoe.Document(body)) + body_html = sanitize(body_md) + + + c = Comment(author_id=261, + distinguish_level=6, + parent_submission=new_post.id, + level=1, + over_18=False, + is_bot=True, + app_id=None, + body_html=body_html, + body=body, + ) + + g.db.add(c) + g.db.flush() + + + n = Notification(comment_id=c.id, user_id=v.id) + g.db.add(n) + g.db.flush() + + v.post_count = v.submissions.filter_by(is_banned=False, deleted_utc=0).count() + g.db.add(v) + + cache.delete_memoized(frontlist) + cache.delete_memoized(User.userpagelisting) + if "[changelog]" in new_post.title or "(changelog)" in new_post.title: + send_message(f"https://{site}{new_post.permalink}") + cache.delete_memoized(changeloglist) + + g.db.commit() + + if request.headers.get("Authorization"): return new_post.json + else: return redirect(new_post.permalink) + + +@app.post("/delete_post/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def delete_post_pid(pid, v): + + post = get_post(pid) + if not post.author_id == v.id: + abort(403) + + post.deleted_utc = int(time.time()) + post.is_pinned = False + post.stickied = None + + g.db.add(post) + + cache.delete_memoized(frontlist) + + g.db.commit() + + return {"message": "Post deleted!"} + +@app.post("/undelete_post/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def undelete_post_pid(pid, v): + post = get_post(pid) + if not post.author_id == v.id: abort(403) + post.deleted_utc =0 + g.db.add(post) + + cache.delete_memoized(frontlist) + + g.db.commit() + + return {"message": "Post undeleted!"} + + +@app.post("/toggle_comment_nsfw/") +@auth_required +@validate_formkey +def toggle_comment_nsfw(cid, v): + + comment = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first() + if not comment.author_id == v.id and not v.admin_level >= 3: abort(403) + comment.over_18 = not comment.over_18 + g.db.add(comment) + g.db.flush() + + g.db.commit() + + if comment.over_18: return {"message": "Comment has been marked as +18!"} + else: return {"message": "Comment has been unmarked as +18!"} + +@app.post("/toggle_post_nsfw/") +@auth_required +@validate_formkey +def toggle_post_nsfw(pid, v): + + post = get_post(pid) + + if not post.author_id == v.id and not v.admin_level >= 3: + abort(403) + + post.over_18 = not post.over_18 + g.db.add(post) + + if post.author_id!=v.id: + ma=ModAction( + kind="set_nsfw" if post.over_18 else "unset_nsfw", + user_id=v.id, + target_submission_id=post.id, + ) + g.db.add(ma) + + g.db.commit() + + if post.over_18: return {"message": "Post has been marked as +18!"} + else: return {"message": "Post has been unmarked as +18!"} + +@app.post("/save_post/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def save_post(pid, v): + + post=get_post(pid) + + save = g.db.query(SaveRelationship).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id, type=1).first() + + if not save: + new_save=SaveRelationship(user_id=v.id, submission_id=post.id, type=1) + g.db.add(new_save) + g.db.commit() + + return {"message": "Post saved!"} + +@app.post("/unsave_post/") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def unsave_post(pid, v): + + post=get_post(pid) + + save = g.db.query(SaveRelationship).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id, type=1).first() + + if save: + g.db.delete(save) + g.db.commit() + + return {"message": "Post unsaved!"} diff --git a/files/routes/reporting.py b/files/routes/reporting.py old mode 100644 new mode 100755 index 046c56402..f9d156c85 --- a/files/routes/reporting.py +++ b/files/routes/reporting.py @@ -1,90 +1,90 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from flask import g -from files.__main__ import app, limiter -from os import path - -@app.post("/flag/post/") -@limiter.limit("1/second") -@auth_desired -def api_flag_post(pid, v): - - post = get_post(pid) - - if v and not v.shadowbanned: - existing = g.db.query(Flag).options(lazyload('*')).filter_by(user_id=v.id, post_id=post.id).first() - - if existing: return "", 409 - - reason = request.values.get("reason", "").strip()[:100] - if "<" in reason: return {"error": f"Reasons can't contain <"} - - for i in re.finditer(':(.{1,30}?):', reason): - if path.isfile(f'./files/assets/images/emojis/{i.group(1)}.webp'): - reason = reason.replace(f':{i.group(1)}:', f'') - - flag = Flag(post_id=post.id, - user_id=v.id, - reason=reason, - ) - - - g.db.add(flag) - - g.db.commit() - - return {"message": "Post reported!"} - - -@app.post("/flag/comment/") -@limiter.limit("1/second") -@auth_desired -def api_flag_comment(cid, v): - - comment = get_comment(cid) - - if v and not v.shadowbanned: - existing = g.db.query(CommentFlag).options(lazyload('*')).filter_by( - user_id=v.id, comment_id=comment.id).first() - - if existing: return "", 409 - reason = request.values.get("reason", "").strip()[:100] - if "<" in reason: return {"error": f"Reasons can't contain <"} - - for i in re.finditer(':(.{1,30}?):', reason): - if path.isfile(f'./files/assets/images/emojis/{i.group(1)}.webp'): - reason = reason.replace(f':{i.group(1)}:', f'') - - flag = CommentFlag(comment_id=comment.id, - user_id=v.id, - reason=reason, - ) - - g.db.add(flag) - - g.db.commit() - - return {"message": "Comment reported!"} - - -@app.post('/del_report/') -@limiter.limit("1/second") -@auth_required -@validate_formkey -def remove_report(report_fn, v): - - if v.admin_level < 6: - return {"error": "go outside"}, 403 - - if report_fn.startswith('c'): - report = g.db.query(CommentFlag).options(lazyload('*')).filter_by(id=int(report_fn.lstrip('c'))).first() - elif report_fn.startswith('p'): - report = g.db.query(Flag).options(lazyload('*')).filter_by(id=int(report_fn.lstrip('p'))).first() - else: - return {"error": "Invalid report ID"}, 400 - - g.db.delete(report) - - g.db.commit() - +from files.helpers.wrappers import * +from files.helpers.get import * +from flask import g +from files.__main__ import app, limiter +from os import path + +@app.post("/flag/post/") +@limiter.limit("1/second") +@auth_desired +def api_flag_post(pid, v): + + post = get_post(pid) + + if v and not v.shadowbanned: + existing = g.db.query(Flag).options(lazyload('*')).filter_by(user_id=v.id, post_id=post.id).first() + + if existing: return "", 409 + + reason = request.values.get("reason", "").strip()[:100] + if "<" in reason: return {"error": f"Reasons can't contain <"} + + for i in re.finditer(':(.{1,30}?):', reason): + if path.isfile(f'./files/assets/images/emojis/{i.group(1)}.webp'): + reason = reason.replace(f':{i.group(1)}:', f'') + + flag = Flag(post_id=post.id, + user_id=v.id, + reason=reason, + ) + + + g.db.add(flag) + + g.db.commit() + + return {"message": "Post reported!"} + + +@app.post("/flag/comment/") +@limiter.limit("1/second") +@auth_desired +def api_flag_comment(cid, v): + + comment = get_comment(cid) + + if v and not v.shadowbanned: + existing = g.db.query(CommentFlag).options(lazyload('*')).filter_by( + user_id=v.id, comment_id=comment.id).first() + + if existing: return "", 409 + reason = request.values.get("reason", "").strip()[:100] + if "<" in reason: return {"error": f"Reasons can't contain <"} + + for i in re.finditer(':(.{1,30}?):', reason): + if path.isfile(f'./files/assets/images/emojis/{i.group(1)}.webp'): + reason = reason.replace(f':{i.group(1)}:', f'') + + flag = CommentFlag(comment_id=comment.id, + user_id=v.id, + reason=reason, + ) + + g.db.add(flag) + + g.db.commit() + + return {"message": "Comment reported!"} + + +@app.post('/del_report/') +@limiter.limit("1/second") +@auth_required +@validate_formkey +def remove_report(report_fn, v): + + if v.admin_level < 6: + return {"error": "go outside"}, 403 + + if report_fn.startswith('c'): + report = g.db.query(CommentFlag).options(lazyload('*')).filter_by(id=int(report_fn.lstrip('c'))).first() + elif report_fn.startswith('p'): + report = g.db.query(Flag).options(lazyload('*')).filter_by(id=int(report_fn.lstrip('p'))).first() + else: + return {"error": "Invalid report ID"}, 400 + + g.db.delete(report) + + g.db.commit() + return {"message": "Removed report"} \ No newline at end of file diff --git a/files/routes/search.py b/files/routes/search.py old mode 100644 new mode 100755 index 22bbbe99d..3a72e9371 --- a/files/routes/search.py +++ b/files/routes/search.py @@ -1,287 +1,287 @@ -from files.helpers.wrappers import * -import re -from sqlalchemy import * -from flask import * -from files.__main__ import app - - -query_regex=re.compile("(\w+):(\S+)") -valid_params=[ - 'author', - 'domain', - 'over18' -] - -def searchparse(text): - - - criteria = {x[0]:x[1] for x in query_regex.findall(text)} - - for x in criteria: - if x in valid_params: - text = text.replace(f"{x}:{criteria[x]}", "") - - text=text.strip() - - if text: - criteria['q']=text - - return criteria - - - - - -@app.get("/search/posts") -@auth_desired -def searchposts(v): - - - query = request.values.get("q", '').strip() - - page = max(1, int(request.values.get("page", 1))) - - sort = request.values.get("sort", "top").lower() - t = request.values.get('t', 'all').lower() - - criteria=searchparse(query) - - - - - - - - - - - - - posts = g.db.query(Submission.id).options(lazyload('*')) - - if not (v and v.admin_level == 6): posts = posts.filter(Submission.private == False) - - if 'q' in criteria: - words=criteria['q'].split() - words=[Submission.title.ilike('%'+x+'%') for x in words] - words=tuple(words) - posts=posts.filter(*words) - - if 'over18' in criteria: posts = posts.filter(Submission.over_18==True) - - if 'author' in criteria: posts = posts.filter(Submission.author_id == get_user(criteria['author']).id) - - if 'domain' in criteria: - domain=criteria['domain'] - posts=posts.filter( - or_( - Submission.url.ilike("https://"+domain+'/%'), - Submission.url.ilike("https://"+domain+'/%'), - Submission.url.ilike("https://"+domain), - Submission.url.ilike("https://"+domain), - Submission.url.ilike("https://www."+domain+'/%'), - Submission.url.ilike("https://www."+domain+'/%'), - Submission.url.ilike("https://www."+domain), - Submission.url.ilike("https://www."+domain), - Submission.url.ilike("https://old." + domain + '/%'), - Submission.url.ilike("https://old." + domain + '/%'), - Submission.url.ilike("https://old." + domain), - Submission.url.ilike("https://old." + domain) - ) - ) - - if not(v and v.admin_level >= 3): - posts = posts.filter( - Submission.deleted_utc == 0, - Submission.is_banned == False, - ) - - if v and v.admin_level >= 4: - pass - elif v: - blocking = [x[0] for x in g.db.query( - UserBlock.target_id).filter_by( - user_id=v.id).all()] - blocked = [x[0] for x in g.db.query( - UserBlock.user_id).filter_by( - target_id=v.id).all()] - - posts = posts.filter( - Submission.author_id.notin_(blocking), - Submission.author_id.notin_(blocked), - ) - - if t: - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - posts = posts.filter(Submission.created_utc >= cutoff) - - if sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc.asc()) - elif sort == "controversial": - posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc()) - - total = posts.count() - - posts = posts.offset(25 * (page - 1)).limit(26).all() - - ids = [x[0] for x in posts] - - - - - next_exists = (len(ids) > 25) - ids = ids[:25] - - posts = get_posts(ids, v=v) - - if v and v.admin_level>3 and "domain" in criteria: - domain=criteria['domain'] - domain_obj=get_domain(domain) - else: - domain=None - domain_obj=None - - if request.headers.get("Authorization"): return {"data":[x.json for x in posts]} - else: return render_template("search.html", - v=v, - query=query, - total=total, - page=page, - listing=posts, - sort=sort, - t=t, - next_exists=next_exists, - domain=domain, - domain_obj=domain_obj - ) - -@app.get("/search/comments") -@auth_desired -def searchcomments(v): - - - query = request.values.get("q", '').strip() - - try: page = max(1, int(request.values.get("page", 1))) - except: page = 1 - - sort = request.values.get("sort", "new").lower() - t = request.values.get('t', 'all').lower() - - criteria=searchparse(query) - - - - - - - comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.parent_submission != None) - - if 'q' in criteria: - words=criteria['q'].split() - words=[Comment.body.ilike('%'+x+'%') for x in words] - words=tuple(words) - comments=comments.filter(*words) - - if 'over18' in criteria: comments = comments.filter(Comment.over_18==True) - - if 'author' in criteria: comments = comments.filter(Comment.author_id == get_user(criteria['author']).id) - - if not(v and v.admin_level >= 3): - comments = comments.filter( - Comment.deleted_utc == 0, - Comment.is_banned == False) - - if t: - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - comments = comments.filter(Comment.created_utc >= cutoff) - - - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc.asc()) - elif sort == "controversial": - comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) - elif sort == "top": - comments = comments.order_by(Comment.downvotes - Comment.upvotes) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - total = comments.count() - - comments = comments.offset(25 * (page - 1)).limit(26).all() - ids = [x[0] for x in comments] - - - - - next_exists = (len(ids) > 25) - ids = ids[:25] - - comments = get_comments(ids, v=v) - - if request.headers.get("Authorization"): return [x.json for x in comments] - else: return render_template("search_comments.html", v=v, query=query, total=total, page=page, comments=comments, sort=sort, t=t, next_exists=next_exists) - - -@app.get("/search/users") -@auth_desired -def searchusers(v): - - - query = request.values.get("q", '').strip() - - page = max(1, int(request.values.get("page", 1))) - sort = request.values.get("sort", "new").lower() - t = request.values.get('t', 'all').lower() - term=query.lstrip('@') - term=term.replace('\\','') - term=term.replace('_','\_') - - users=g.db.query(User).options(lazyload('*')).filter(User.username.ilike(f'%{term}%')) - - users=users.order_by(User.username.ilike(term).desc(), User.stored_subscriber_count.desc()) - - total=users.count() - - users=[x for x in users.offset(25 * (page-1)).limit(26)] - next_exists=(len(users)>25) - users=users[:25] - - - if request.headers.get("Authorization"): return [x.json for x in users] +from files.helpers.wrappers import * +import re +from sqlalchemy import * +from flask import * +from files.__main__ import app + + +query_regex=re.compile("(\w+):(\S+)") +valid_params=[ + 'author', + 'domain', + 'over18' +] + +def searchparse(text): + + + criteria = {x[0]:x[1] for x in query_regex.findall(text)} + + for x in criteria: + if x in valid_params: + text = text.replace(f"{x}:{criteria[x]}", "") + + text=text.strip() + + if text: + criteria['q']=text + + return criteria + + + + + +@app.get("/search/posts") +@auth_desired +def searchposts(v): + + + query = request.values.get("q", '').strip() + + page = max(1, int(request.values.get("page", 1))) + + sort = request.values.get("sort", "top").lower() + t = request.values.get('t', 'all').lower() + + criteria=searchparse(query) + + + + + + + + + + + + + posts = g.db.query(Submission.id).options(lazyload('*')) + + if not (v and v.admin_level == 6): posts = posts.filter(Submission.private == False) + + if 'q' in criteria: + words=criteria['q'].split() + words=[Submission.title.ilike('%'+x+'%') for x in words] + words=tuple(words) + posts=posts.filter(*words) + + if 'over18' in criteria: posts = posts.filter(Submission.over_18==True) + + if 'author' in criteria: posts = posts.filter(Submission.author_id == get_user(criteria['author']).id) + + if 'domain' in criteria: + domain=criteria['domain'] + posts=posts.filter( + or_( + Submission.url.ilike("https://"+domain+'/%'), + Submission.url.ilike("https://"+domain+'/%'), + Submission.url.ilike("https://"+domain), + Submission.url.ilike("https://"+domain), + Submission.url.ilike("https://www."+domain+'/%'), + Submission.url.ilike("https://www."+domain+'/%'), + Submission.url.ilike("https://www."+domain), + Submission.url.ilike("https://www."+domain), + Submission.url.ilike("https://old." + domain + '/%'), + Submission.url.ilike("https://old." + domain + '/%'), + Submission.url.ilike("https://old." + domain), + Submission.url.ilike("https://old." + domain) + ) + ) + + if not(v and v.admin_level >= 3): + posts = posts.filter( + Submission.deleted_utc == 0, + Submission.is_banned == False, + ) + + if v and v.admin_level >= 4: + pass + elif v: + blocking = [x[0] for x in g.db.query( + UserBlock.target_id).filter_by( + user_id=v.id).all()] + blocked = [x[0] for x in g.db.query( + UserBlock.user_id).filter_by( + target_id=v.id).all()] + + posts = posts.filter( + Submission.author_id.notin_(blocking), + Submission.author_id.notin_(blocked), + ) + + if t: + now = int(time.time()) + if t == 'hour': + cutoff = now - 3600 + elif t == 'day': + cutoff = now - 86400 + elif t == 'week': + cutoff = now - 604800 + elif t == 'month': + cutoff = now - 2592000 + elif t == 'year': + cutoff = now - 31536000 + else: + cutoff = 0 + posts = posts.filter(Submission.created_utc >= cutoff) + + if sort == "new": + posts = posts.order_by(Submission.created_utc.desc()) + elif sort == "old": + posts = posts.order_by(Submission.created_utc.asc()) + elif sort == "controversial": + posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes) + elif sort == "top": + posts = posts.order_by(Submission.downvotes - Submission.upvotes) + elif sort == "bottom": + posts = posts.order_by(Submission.upvotes - Submission.downvotes) + elif sort == "comments": + posts = posts.order_by(Submission.comment_count.desc()) + + total = posts.count() + + posts = posts.offset(25 * (page - 1)).limit(26).all() + + ids = [x[0] for x in posts] + + + + + next_exists = (len(ids) > 25) + ids = ids[:25] + + posts = get_posts(ids, v=v) + + if v and v.admin_level>3 and "domain" in criteria: + domain=criteria['domain'] + domain_obj=get_domain(domain) + else: + domain=None + domain_obj=None + + if request.headers.get("Authorization"): return {"data":[x.json for x in posts]} + else: return render_template("search.html", + v=v, + query=query, + total=total, + page=page, + listing=posts, + sort=sort, + t=t, + next_exists=next_exists, + domain=domain, + domain_obj=domain_obj + ) + +@app.get("/search/comments") +@auth_desired +def searchcomments(v): + + + query = request.values.get("q", '').strip() + + try: page = max(1, int(request.values.get("page", 1))) + except: page = 1 + + sort = request.values.get("sort", "new").lower() + t = request.values.get('t', 'all').lower() + + criteria=searchparse(query) + + + + + + + comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.parent_submission != None) + + if 'q' in criteria: + words=criteria['q'].split() + words=[Comment.body.ilike('%'+x+'%') for x in words] + words=tuple(words) + comments=comments.filter(*words) + + if 'over18' in criteria: comments = comments.filter(Comment.over_18==True) + + if 'author' in criteria: comments = comments.filter(Comment.author_id == get_user(criteria['author']).id) + + if not(v and v.admin_level >= 3): + comments = comments.filter( + Comment.deleted_utc == 0, + Comment.is_banned == False) + + if t: + now = int(time.time()) + if t == 'hour': + cutoff = now - 3600 + elif t == 'day': + cutoff = now - 86400 + elif t == 'week': + cutoff = now - 604800 + elif t == 'month': + cutoff = now - 2592000 + elif t == 'year': + cutoff = now - 31536000 + else: + cutoff = 0 + comments = comments.filter(Comment.created_utc >= cutoff) + + + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc.asc()) + elif sort == "controversial": + comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) + elif sort == "top": + comments = comments.order_by(Comment.downvotes - Comment.upvotes) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + total = comments.count() + + comments = comments.offset(25 * (page - 1)).limit(26).all() + ids = [x[0] for x in comments] + + + + + next_exists = (len(ids) > 25) + ids = ids[:25] + + comments = get_comments(ids, v=v) + + if request.headers.get("Authorization"): return [x.json for x in comments] + else: return render_template("search_comments.html", v=v, query=query, total=total, page=page, comments=comments, sort=sort, t=t, next_exists=next_exists) + + +@app.get("/search/users") +@auth_desired +def searchusers(v): + + + query = request.values.get("q", '').strip() + + page = max(1, int(request.values.get("page", 1))) + sort = request.values.get("sort", "new").lower() + t = request.values.get('t', 'all').lower() + term=query.lstrip('@') + term=term.replace('\\','') + term=term.replace('_','\_') + + users=g.db.query(User).options(lazyload('*')).filter(User.username.ilike(f'%{term}%')) + + users=users.order_by(User.username.ilike(term).desc(), User.stored_subscriber_count.desc()) + + total=users.count() + + users=[x for x in users.offset(25 * (page-1)).limit(26)] + next_exists=(len(users)>25) + users=users[:25] + + + if request.headers.get("Authorization"): return [x.json for x in users] else: return render_template("search_users.html", v=v, query=query, total=total, page=page, users=users, sort=sort, t=t, next_exists=next_exists) \ No newline at end of file diff --git a/files/routes/settings.py b/files/routes/settings.py old mode 100644 new mode 100755 index 8f52c8ff2..fdbfab6fa --- a/files/routes/settings.py +++ b/files/routes/settings.py @@ -1,871 +1,871 @@ -from __future__ import unicode_literals -from files.helpers.alerts import * -from files.helpers.sanitize import * -from files.helpers.filters import filter_comment_html -from files.helpers.markdown import * -from files.helpers.discord import remove_user, set_nick -from files.helpers.const import * -from files.mail import * -from files.__main__ import app, cache, limiter -import youtube_dl -from .front import frontlist -import os -from .posts import filter_title -from files.helpers.discord import add_role -from shutil import copyfile -import requests - -valid_username_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$") -valid_password_regex = re.compile("^.{8,100}$") - -YOUTUBE_KEY = environ.get("YOUTUBE_KEY", "").strip() -COINS_NAME = environ.get("COINS_NAME").strip() -GUMROAD_TOKEN = environ.get("GUMROAD_TOKEN", "").strip() - -tiers={ - "(Paypig)": 1, - "(Renthog)": 2, - "(Landchad)": 3, - "(Terminally online turboautist)": 4, - "(Footpig)": 5, - } - -@app.post("/settings/removebackground") -@limiter.limit("1/second") -@auth_required -def removebackground(v): - v.background = None - g.db.add(v) - g.db.commit() - return {"message": "Background removed!"} - -@app.post("/settings/profile") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_profile_post(v): - if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 - - updated = False - - if request.values.get("background", v.background) != v.background: - updated = True - v.background = request.values.get("background", None) - - if request.values.get("slurreplacer", v.slurreplacer) != v.slurreplacer: - updated = True - v.slurreplacer = request.values.get("slurreplacer", None) == 'true' - - if request.values.get("hidevotedon", v.hidevotedon) != v.hidevotedon: - updated = True - v.hidevotedon = request.values.get("hidevotedon", None) == 'true' - - if request.values.get("cardview", v.cardview) != v.cardview: - updated = True - v.cardview = request.values.get("cardview", None) == 'true' - - if request.values.get("highlightcomments", v.highlightcomments) != v.highlightcomments: - updated = True - v.highlightcomments = request.values.get("highlightcomments", None) == 'true' - - if request.values.get("newtab", v.newtab) != v.newtab: - updated = True - v.newtab = request.values.get("newtab", None) == 'true' - - if request.values.get("newtabexternal", v.newtabexternal) != v.newtabexternal: - updated = True - v.newtabexternal = request.values.get("newtabexternal", None) == 'true' - - if request.values.get("oldreddit", v.oldreddit) != v.oldreddit: - updated = True - v.oldreddit = request.values.get("oldreddit", None) == 'true' - - if request.values.get("nitter", v.nitter) != v.nitter: - updated = True - v.nitter = request.values.get("nitter", None) == 'true' - - if request.values.get("controversial", v.controversial) != v.controversial: - updated = True - v.controversial = request.values.get("controversial", None) == 'true' - - if request.values.get("over18", v.over_18) != v.over_18: - updated = True - v.over_18 = request.values.get("over18", None) == 'true' - - if request.values.get("private", v.is_private) != v.is_private: - updated = True - v.is_private = request.values.get("private", None) == 'true' - - if request.values.get("nofollow", v.is_nofollow) != v.is_nofollow: - updated = True - v.is_nofollow = request.values.get("nofollow", None) == 'true' - - if request.values.get("bio"): - bio = request.values.get("bio")[:1500] - - for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', bio, re.MULTILINE): - if "wikipedia" not in i.group(1): bio = bio.replace(i.group(1), f'![]({i.group(1)})') - bio = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', bio) - - if request.files.get('file') and request.headers.get("cf-ipcountry") != "T1": - - file = request.files['file'] - if not file.content_type.startswith('image/'): - if request.headers.get("Authorization"): return {"error": f"Image files only"}, 400 - else: return render_template("settings_profile.html", v=v, error=f"Image files only."), 400 - - name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' - file.save(name) - url = request.host_url[:-1] + process_image(name) - - bio += f"\n\n![]({url})" - - bio_html = CustomRenderer().render(mistletoe.Document(bio)) - bio_html = sanitize(bio_html) - bans = filter_comment_html(bio_html) - - if len(bio_html) > 10000: - return render_template("settings_profile.html", - v=v, - error="Your bio is too long") - - if bans: - ban = bans[0] - reason = f"Remove the {ban.domain} link from your bio and try again." - if ban.reason: - reason += f" {ban.reason}" - - return {"error": reason}, 401 - - if len(bio_html) > 10000: abort(400) - - v.bio = bio[:1500] - v.bio_html=bio_html - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", - v=v, - msg="Your bio has been updated.") - - if request.values.get("filters"): - - filters=request.values.get("filters")[:1000].strip() - - if filters==v.custom_filter_list: - return render_template("settings_profile.html", - v=v, - error="You didn't change anything") - - v.custom_filter_list=filters - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", - v=v, - msg="Your custom filters have been updated.") - - - - frontsize = request.values.get("frontsize") - if frontsize: - if frontsize in ["25", "50", "100"]: - v.frontsize = int(frontsize) - updated = True - cache.delete_memoized(frontlist) - else: abort(400) - - defaultsortingcomments = request.values.get("defaultsortingcomments") - if defaultsortingcomments: - if defaultsortingcomments in ["new", "old", "controversial", "top", "bottom"]: - v.defaultsortingcomments = defaultsortingcomments - updated = True - else: abort(400) - - defaultsorting = request.values.get("defaultsorting") - if defaultsorting: - if defaultsorting in ["hot", "new", "old", "comments", "controversial", "top", "bottom"]: - v.defaultsorting = defaultsorting - updated = True - else: abort(400) - - defaulttime = request.values.get("defaulttime") - if defaulttime: - if defaulttime in ["hour", "day", "week", "month", "year", "all"]: - v.defaulttime = defaulttime - updated = True - else: abort(400) - - theme = request.values.get("theme") - if theme: - v.theme = theme - if theme == "win98": v.themecolor = "30409f" - updated = True - - quadrant = request.values.get("quadrant") - if quadrant and 'pcmemes.net' in request.host.lower(): - v.quadrant = quadrant - v.customtitle = quadrant - if quadrant=="Centrist": - v.namecolor = "7f8fa6" - v.titlecolor = "7f8fa6" - elif quadrant=="LibLeft": - v.namecolor = "62ca56" - v.titlecolor = "62ca56" - elif quadrant=="LibRight": - v.namecolor = "f8db58" - v.titlecolor = "f8db58" - elif quadrant=="AuthLeft": - v.namecolor = "ff0000" - v.titlecolor = "ff0000" - elif quadrant=="AuthRight": - v.namecolor = "2a96f3" - v.titlecolor = "2a96f3" - elif quadrant=="LibCenter": - v.namecolor = "add357" - v.titlecolor = "add357" - elif quadrant=="AuthCenter": - v.namecolor = "954b7a" - v.titlecolor = "954b7a" - elif quadrant=="Left": - v.namecolor = "b1652b" - v.titlecolor = "b1652b" - elif quadrant=="Right": - v.namecolor = "91b9A6" - v.titlecolor = "91b9A6" - - updated = True - - if updated: - g.db.add(v) - g.db.commit() - - return {"message": "Your settings have been updated."} - - else: - return {"error": "You didn't change anything."}, 400 - -@app.post("/changelogsub") -@auth_required -@validate_formkey -def changelogsub(v): - v.changelogsub = not v.changelogsub - g.db.add(v) - - cache.delete_memoized(frontlist) - - g.db.commit() - if v.changelogsub: return {"message": "You have subscribed to the changelog!"} - else: return {"message": "You have unsubscribed from the changelog!"} - -@app.post("/settings/namecolor") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def namecolor(v): - color = str(request.values.get("color", "")).strip() - if color.startswith('#'): color = color[1:] - if len(color) != 6: return render_template("settings_security.html", v=v, error="Invalid color code") - v.namecolor = color - g.db.add(v) - g.db.commit() - return redirect("/settings/profile") - -@app.post("/settings/themecolor") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def themecolor(v): - themecolor = str(request.values.get("themecolor", "")).strip() - if themecolor.startswith('#'): themecolor = themecolor[1:] - if len(themecolor) != 6: return render_template("settings_security.html", v=v, error="Invalid color code") - v.themecolor = themecolor - g.db.add(v) - g.db.commit() - return redirect("/settings/profile") - -@app.post("/settings/gumroad") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def gumroad(v): - if 'rama' in request.host: patron = 'Paypig' - else: patron = 'Patron' - - if not (v.email and v.is_activated): return {"error": f"You must have a verified email to verify {patron} status and claim your rewards"}, 400 - - data = { - 'access_token': GUMROAD_TOKEN, - 'email': v.email - } - response = requests.get('https://api.gumroad.com/v2/sales', data=data).json()["sales"] - - if len(response) == 0: return {"error": "Email not found"}, 404 - - response = response[0] - tier = tiers[response["variants_and_quantity"]] - if v.patron == tier: return {"error": f"{patron} rewards already claimed"}, 400 - - v.patron = tier - g.db.add(v) - - grant_awards = {} - if tier == 1: - if v.discord_id: add_role(v, "1") - grant_awards["shit"] = 2 - grant_awards["fireflies"] = 2 - elif tier == 2: - if v.discord_id: add_role(v, "2") - grant_awards["shit"] = 5 - grant_awards["fireflies"] = 5 - grant_awards["ban"] = 1 - elif tier == 3: - if v.discord_id: add_role(v, "3") - grant_awards["shit"] = 10 - grant_awards["fireflies"] = 10 - grant_awards["ban"] = 2 - elif tier == 4: - if v.discord_id: add_role(v, "4") - grant_awards["shit"] = 25 - grant_awards["fireflies"] = 25 - grant_awards["ban"] = 5 - elif tier == 5 or tier == 8: - if v.discord_id: add_role(v, "5") - grant_awards["shit"] = 50 - grant_awards["fireflies"] = 50 - grant_awards["ban"] = 10 - - thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id - - for name in grant_awards: - for count in range(grant_awards[name]): - - thing += 1 - - award = AwardRelationship( - id=thing, - user_id=v.id, - kind=name - ) - - g.db.add(award) - - if not v.has_badge(20+tier): - new_badge = Badge(badge_id=20+tier, - user_id=v.id, - ) - g.db.add(new_badge) - - g.db.commit() - - return {"message": f"{patron} rewards claimed!"} - -@app.post("/settings/titlecolor") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def titlecolor(v): - titlecolor = str(request.values.get("titlecolor", "")).strip() - if titlecolor.startswith('#'): titlecolor = titlecolor[1:] - if len(titlecolor) != 6: return render_template("settings_security.html", v=v, error="Invalid color code") - v.titlecolor = titlecolor - g.db.add(v) - g.db.commit() - - return redirect("/settings/profile") - -@app.post("/settings/security") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_security_post(v): - if request.values.get("new_password"): - if request.values.get( - "new_password") != request.values.get("cnf_password"): - return redirect("/settings/security?error=" + - escape("Passwords do not match.")) - - if not re.match(valid_password_regex, request.values.get("new_password")): - return redirect("/settings/security?error=" + - escape("Password must be between 8 and 100 characters.")) - - if not v.verifyPass(request.values.get("old_password")): - return render_template( - "settings_security.html", v=v, error="Incorrect password") - - v.passhash = v.hash_password(request.values.get("new_password")) - - g.db.add(v) - - g.db.commit() - - return redirect("/settings/security?msg=" + - escape("Your password has been changed.")) - - if request.values.get("new_email"): - - if not v.verifyPass(request.values.get('password')): - return redirect("/settings/security?error=" + - escape("Invalid password.")) - - new_email = request.values.get("new_email","").strip() - if new_email == v.email: - return redirect("/settings/security?error=That email is already yours!") - - existing = g.db.query(User).options(lazyload('*')).filter(User.id != v.id, - func.lower(User.email) == new_email.lower()).first() - if existing: - return redirect("/settings/security?error=" + - escape("That email address is already in use.")) - - url = f"https://{app.config['SERVER_NAME']}/activate" - - now = int(time.time()) - - token = generate_hash(f"{new_email}+{v.id}+{now}") - params = f"?email={quote(new_email)}&id={v.id}&time={now}&token={token}" - - link = url + params - - send_mail(to_address=new_email, - subject="Verify your email address.", - html=render_template("email/email_change.html", - action_url=link, - v=v) - ) - - return redirect("/settings/security?msg=" + escape( - "Check your email and click the verification link to complete the email change.")) - - if request.values.get("2fa_token", ""): - - if not v.verifyPass(request.values.get('password')): - return redirect("/settings/security?error=" + - escape("Invalid password or token.")) - - secret = request.values.get("2fa_secret") - x = pyotp.TOTP(secret) - if not x.verify(request.values.get("2fa_token"), valid_window=1): - return redirect("/settings/security?error=" + - escape("Invalid password or token.")) - - v.mfa_secret = secret - g.db.add(v) - - g.db.commit() - - return redirect("/settings/security?msg=" + - escape("Two-factor authentication enabled.")) - - if request.values.get("2fa_remove", ""): - - if not v.verifyPass(request.values.get('password')): - return redirect("/settings/security?error=" + - escape("Invalid password or token.")) - - token = request.values.get("2fa_remove") - - if not v.validate_2fa(token): - return redirect("/settings/security?error=" + - escape("Invalid password or token.")) - - v.mfa_secret = None - g.db.add(v) - - g.db.commit() - - return redirect("/settings/security?msg=" + - escape("Two-factor authentication disabled.")) - -@app.post("/settings/log_out_all_others") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_log_out_others(v): - - submitted_password = request.values.get("password", "").strip() - - if not v.verifyPass(submitted_password): return render_template("settings_security.html", v=v, error="Incorrect Password"), 401 - - v.login_nonce += 1 - - session["login_nonce"] = v.login_nonce - - g.db.add(v) - - g.db.commit() - - return render_template("settings_security.html", v=v, msg="All other devices have been logged out") - - -@app.post("/settings/images/profile") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_images_profile(v): - if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 - - if request.headers.get("cf-ipcountry") == "T1": return "Image uploads are not allowed through TOR.", 403 - - file = request.files["profile"] - - name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' - file.save(name) - highres = request.host_url[:-1] + process_image(name) - - if not highres: abort(400) - - name2 = name.replace('.gif', 'r.gif') - copyfile(name, name2) - imageurl = request.host_url[:-1] + process_image(name2, True) - - if not imageurl: abort(400) - - v.highres = highres - v.profileurl = imageurl - g.db.add(v) - - g.db.commit() - - return render_template("settings_profile.html", v=v, msg="Profile picture successfully updated.") - - -@app.post("/settings/images/banner") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_images_banner(v): - if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 - - if request.headers.get("cf-ipcountry") == "T1": return "Image uploads are not allowed through TOR.", 403 - - file = request.files["banner"] - - name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' - file.save(name) - imageurl = request.host_url[:-1] + process_image(name) - - if imageurl: - v.bannerurl = imageurl - g.db.add(v) - g.db.commit() - - return render_template("settings_profile.html", v=v, msg="Banner successfully updated.") - - -@app.post("/settings/delete/profile") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_delete_profile(v): - v.highres = None - v.profileurl = None - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", v=v, - msg="Profile picture successfully removed.") - -@app.post("/settings/delete/banner") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_delete_banner(v): - - v.bannerurl = None - g.db.add(v) - g.db.commit() - - return render_template("settings_profile.html", v=v, - msg="Banner successfully removed.") - - -@app.get("/settings/blocks") -@auth_required -def settings_blockedpage(v): - - return render_template("settings_blocks.html", v=v) - -@app.get("/settings/css") -@auth_required -def settings_css_get(v): - - return render_template("settings_css.html", v=v) - -@app.post("/settings/css") -@limiter.limit("1/second") -@auth_required -def settings_css(v): - css = request.values.get("css").strip().replace('\\', '').strip()[:4000] - - if not v.agendaposter: - v.css = css - else: - v.css = 'body *::before, body *::after { content: "Trans rights are human rights!"; }' - g.db.add(v) - g.db.commit() - - return render_template("settings_css.html", v=v) - -@app.get("/settings/profilecss") -@auth_required -def settings_profilecss_get(v): - - if v.coins < 1000 and not v.patron and v.admin_level < 6: return f"You must have +1000 {COINS_NAME} or be a patron to set profile css." - return render_template("settings_profilecss.html", v=v) - -@app.post("/settings/profilecss") -@limiter.limit("1/second") -@auth_required -def settings_profilecss(v): - if v.coins < 1000 and not v.patron: return f"You must have +1000 {COINS_NAME} or be a patron to set profile css." - profilecss = request.values.get("profilecss").strip().replace('\\', '').strip()[:4000] - v.profilecss = profilecss - g.db.add(v) - g.db.commit() - - return render_template("settings_profilecss.html", v=v) - -@app.post("/settings/block") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_block_user(v): - - user = get_user(request.values.get("username"), graceful=True) - - if not user: - return {"error": "That user doesn't exist."}, 404 - - if user.id == v.id: - return {"error": "You can't block yourself."}, 409 - - if v.has_block(user): - return {"error": f"You have already blocked @{user.username}."}, 409 - - if user.id == NOTIFICATIONS_ACCOUNT: - return {"error": "You can't block this user."}, 409 - - new_block = UserBlock(user_id=v.id, - target_id=user.id, - ) - g.db.add(new_block) - - - - existing = g.db.query(Notification).options(lazyload('*')).filter_by(blocksender=v.id, user_id=user.id).first() - if not existing: send_block_notif(v.id, user.id, f"@{v.username} has blocked you!") - - cache.delete_memoized(frontlist) - - g.db.commit() - - if v.admin_level == 1: return {"message": f"@{user.username} banned!"} - else: return {"message": f"@{user.username} blocked."} - - -@app.post("/settings/unblock") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_unblock_user(v): - - user = get_user(request.values.get("username")) - - x = v.has_block(user) - - if not x: abort(409) - - g.db.delete(x) - - - - existing = g.db.query(Notification).options(lazyload('*')).filter_by(unblocksender=v.id, user_id=user.id).first() - if not existing: send_unblock_notif(v.id, user.id, f"@{v.username} has unblocked you!") - - cache.delete_memoized(frontlist) - - g.db.commit() - - if v.admin_level == 1: return {"message": f"@{user.username} unbanned!"} - - return {"message": f"@{user.username} unblocked."} - - -@app.get("/settings/apps") -@auth_required -def settings_apps(v): - - return render_template("settings_apps.html", v=v) - - -@app.post("/settings/remove_discord") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_remove_discord(v): - - remove_user(v) - - v.discord_id=None - g.db.add(v) - - g.db.commit() - - return redirect("/settings/profile") - -@app.get("/settings/content") -@auth_required -def settings_content_get(v): - - return render_template("settings_filters.html", v=v) - -@app.post("/settings/name_change") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_name_change(v): - - new_name=request.values.get("name").strip() - - if new_name==v.username: - return render_template("settings_profile.html", - v=v, - error="You didn't change anything") - - if not re.match(valid_username_regex, new_name): - return render_template("settings_profile.html", - v=v, - error=f"This isn't a valid username.") - - name=new_name.replace('_','\_') - - x= g.db.query(User).options( - lazyload('*') - ).filter( - or_( - User.username.ilike(name), - User.original_username.ilike(name) - ) - ).first() - - if x and x.id != v.id: - return render_template("settings_profile.html", - v=v, - error=f"Username `{new_name}` is already in use.") - - v=g.db.query(User).with_for_update().options(lazyload('*')).filter_by(id=v.id).first() - - v.username=new_name - v.name_changed_utc=int(time.time()) - - set_nick(v, new_name) - - g.db.add(v) - - g.db.commit() - - return redirect("/settings/profile") - -@app.post("/settings/song_change") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_song_change(v): - song=request.values.get("song").strip() - - if song == "" and v.song and path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User.id).options(lazyload('*')).filter_by(song=v.song).count() == 1: - os.remove(f"/songs/{v.song}.mp3") - v.song = None - g.db.add(v) - g.db.commit() - return redirect("/settings/profile") - - song = song.replace("https://music.youtube.com", "https://youtube.com") - if song.startswith(("https://www.youtube.com/watch?v=", "https://youtube.com/watch?v=", "https://m.youtube.com/watch?v=")): - id = song.split("v=")[1] - elif song.startswith("https://youtu.be/"): - id = song.split("https://youtu.be/")[1] - else: - return render_template("settings_profile.html", - v=v, - error=f"Not a youtube link.") - - if "?" in id: id = id.split("?")[0] - if "&" in id: id = id.split("&")[0] - - if path.isfile(f'/songs/{id}.mp3'): - v.song = id - g.db.add(v) - g.db.commit() - return redirect("/settings/profile") - - - req = requests.get(f"https://www.googleapis.com/youtube/v3/videos?id={id}&key={YOUTUBE_KEY}&part=contentDetails").json() - duration = req['items'][0]['contentDetails']['duration'] - if "H" in duration: - return render_template("settings_profile.html", - v=v, - error=f"Duration of the video must not exceed 10 minutes.") - - if "M" in duration: - duration = int(duration.split("PT")[1].split("M")[0]) - if duration > 10: - return render_template("settings_profile.html", - v=v, - error=f"Duration of the video must not exceed 10 minutes.") - - - if v.song and path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User.id).options(lazyload('*')).filter_by(song=v.song).count() == 1: - os.remove(f"/songs/{v.song}.mp3") - - ydl_opts = { - 'outtmpl': '/songs/%(title)s.%(ext)s', - 'format': 'bestaudio/best', - 'postprocessors': [{ - 'key': 'FFmpegExtractAudio', - 'preferredcodec': 'mp3', - 'preferredquality': '192', - }], - } - - with youtube_dl.YoutubeDL(ydl_opts) as ydl: - try: ydl.download([f"https://youtube.com/watch?v={id}"]) - except Exception as e: - print(e) - return render_template("settings_profile.html", - v=v, - error=f"Age-restricted videos aren't allowed.") - - files = os.listdir("/songs/") - paths = [path.join("/songs/", basename) for basename in files] - songfile = max(paths, key=path.getctime) - os.rename(songfile, f"/songs/{id}.mp3") - - v.song = id - g.db.add(v) - - g.db.commit() - - return redirect("/settings/profile") - -@app.post("/settings/title_change") -@limiter.limit("1/second") -@auth_required -@validate_formkey -def settings_title_change(v): - - if v.flairchanged: abort(403) - - new_name=request.values.get("title").strip()[:100].replace("𒐪","") - - if new_name==v.customtitle: - return render_template("settings_profile.html", - v=v, - error="You didn't change anything") - - v.customtitleplain = new_name - - v.customtitle = filter_title(new_name) - - g.db.add(v) - g.db.commit() - +from __future__ import unicode_literals +from files.helpers.alerts import * +from files.helpers.sanitize import * +from files.helpers.filters import filter_comment_html +from files.helpers.markdown import * +from files.helpers.discord import remove_user, set_nick +from files.helpers.const import * +from files.mail import * +from files.__main__ import app, cache, limiter +import youtube_dl +from .front import frontlist +import os +from .posts import filter_title +from files.helpers.discord import add_role +from shutil import copyfile +import requests + +valid_username_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$") +valid_password_regex = re.compile("^.{8,100}$") + +YOUTUBE_KEY = environ.get("YOUTUBE_KEY", "").strip() +COINS_NAME = environ.get("COINS_NAME").strip() +GUMROAD_TOKEN = environ.get("GUMROAD_TOKEN", "").strip() + +tiers={ + "(Paypig)": 1, + "(Renthog)": 2, + "(Landchad)": 3, + "(Terminally online turboautist)": 4, + "(Footpig)": 5, + } + +@app.post("/settings/removebackground") +@limiter.limit("1/second") +@auth_required +def removebackground(v): + v.background = None + g.db.add(v) + g.db.commit() + return {"message": "Background removed!"} + +@app.post("/settings/profile") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_profile_post(v): + if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 + + updated = False + + if request.values.get("background", v.background) != v.background: + updated = True + v.background = request.values.get("background", None) + + if request.values.get("slurreplacer", v.slurreplacer) != v.slurreplacer: + updated = True + v.slurreplacer = request.values.get("slurreplacer", None) == 'true' + + if request.values.get("hidevotedon", v.hidevotedon) != v.hidevotedon: + updated = True + v.hidevotedon = request.values.get("hidevotedon", None) == 'true' + + if request.values.get("cardview", v.cardview) != v.cardview: + updated = True + v.cardview = request.values.get("cardview", None) == 'true' + + if request.values.get("highlightcomments", v.highlightcomments) != v.highlightcomments: + updated = True + v.highlightcomments = request.values.get("highlightcomments", None) == 'true' + + if request.values.get("newtab", v.newtab) != v.newtab: + updated = True + v.newtab = request.values.get("newtab", None) == 'true' + + if request.values.get("newtabexternal", v.newtabexternal) != v.newtabexternal: + updated = True + v.newtabexternal = request.values.get("newtabexternal", None) == 'true' + + if request.values.get("oldreddit", v.oldreddit) != v.oldreddit: + updated = True + v.oldreddit = request.values.get("oldreddit", None) == 'true' + + if request.values.get("nitter", v.nitter) != v.nitter: + updated = True + v.nitter = request.values.get("nitter", None) == 'true' + + if request.values.get("controversial", v.controversial) != v.controversial: + updated = True + v.controversial = request.values.get("controversial", None) == 'true' + + if request.values.get("over18", v.over_18) != v.over_18: + updated = True + v.over_18 = request.values.get("over18", None) == 'true' + + if request.values.get("private", v.is_private) != v.is_private: + updated = True + v.is_private = request.values.get("private", None) == 'true' + + if request.values.get("nofollow", v.is_nofollow) != v.is_nofollow: + updated = True + v.is_nofollow = request.values.get("nofollow", None) == 'true' + + if request.values.get("bio"): + bio = request.values.get("bio")[:1500] + + for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', bio, re.MULTILINE): + if "wikipedia" not in i.group(1): bio = bio.replace(i.group(1), f'![]({i.group(1)})') + bio = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', bio) + + if request.files.get('file') and request.headers.get("cf-ipcountry") != "T1": + + file = request.files['file'] + if not file.content_type.startswith('image/'): + if request.headers.get("Authorization"): return {"error": f"Image files only"}, 400 + else: return render_template("settings_profile.html", v=v, error=f"Image files only."), 400 + + name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' + file.save(name) + url = request.host_url[:-1] + process_image(name) + + bio += f"\n\n![]({url})" + + bio_html = CustomRenderer().render(mistletoe.Document(bio)) + bio_html = sanitize(bio_html) + bans = filter_comment_html(bio_html) + + if len(bio_html) > 10000: + return render_template("settings_profile.html", + v=v, + error="Your bio is too long") + + if bans: + ban = bans[0] + reason = f"Remove the {ban.domain} link from your bio and try again." + if ban.reason: + reason += f" {ban.reason}" + + return {"error": reason}, 401 + + if len(bio_html) > 10000: abort(400) + + v.bio = bio[:1500] + v.bio_html=bio_html + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", + v=v, + msg="Your bio has been updated.") + + if request.values.get("filters"): + + filters=request.values.get("filters")[:1000].strip() + + if filters==v.custom_filter_list: + return render_template("settings_profile.html", + v=v, + error="You didn't change anything") + + v.custom_filter_list=filters + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", + v=v, + msg="Your custom filters have been updated.") + + + + frontsize = request.values.get("frontsize") + if frontsize: + if frontsize in ["25", "50", "100"]: + v.frontsize = int(frontsize) + updated = True + cache.delete_memoized(frontlist) + else: abort(400) + + defaultsortingcomments = request.values.get("defaultsortingcomments") + if defaultsortingcomments: + if defaultsortingcomments in ["new", "old", "controversial", "top", "bottom"]: + v.defaultsortingcomments = defaultsortingcomments + updated = True + else: abort(400) + + defaultsorting = request.values.get("defaultsorting") + if defaultsorting: + if defaultsorting in ["hot", "new", "old", "comments", "controversial", "top", "bottom"]: + v.defaultsorting = defaultsorting + updated = True + else: abort(400) + + defaulttime = request.values.get("defaulttime") + if defaulttime: + if defaulttime in ["hour", "day", "week", "month", "year", "all"]: + v.defaulttime = defaulttime + updated = True + else: abort(400) + + theme = request.values.get("theme") + if theme: + v.theme = theme + if theme == "win98": v.themecolor = "30409f" + updated = True + + quadrant = request.values.get("quadrant") + if quadrant and 'pcmemes.net' in request.host.lower(): + v.quadrant = quadrant + v.customtitle = quadrant + if quadrant=="Centrist": + v.namecolor = "7f8fa6" + v.titlecolor = "7f8fa6" + elif quadrant=="LibLeft": + v.namecolor = "62ca56" + v.titlecolor = "62ca56" + elif quadrant=="LibRight": + v.namecolor = "f8db58" + v.titlecolor = "f8db58" + elif quadrant=="AuthLeft": + v.namecolor = "ff0000" + v.titlecolor = "ff0000" + elif quadrant=="AuthRight": + v.namecolor = "2a96f3" + v.titlecolor = "2a96f3" + elif quadrant=="LibCenter": + v.namecolor = "add357" + v.titlecolor = "add357" + elif quadrant=="AuthCenter": + v.namecolor = "954b7a" + v.titlecolor = "954b7a" + elif quadrant=="Left": + v.namecolor = "b1652b" + v.titlecolor = "b1652b" + elif quadrant=="Right": + v.namecolor = "91b9A6" + v.titlecolor = "91b9A6" + + updated = True + + if updated: + g.db.add(v) + g.db.commit() + + return {"message": "Your settings have been updated."} + + else: + return {"error": "You didn't change anything."}, 400 + +@app.post("/changelogsub") +@auth_required +@validate_formkey +def changelogsub(v): + v.changelogsub = not v.changelogsub + g.db.add(v) + + cache.delete_memoized(frontlist) + + g.db.commit() + if v.changelogsub: return {"message": "You have subscribed to the changelog!"} + else: return {"message": "You have unsubscribed from the changelog!"} + +@app.post("/settings/namecolor") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def namecolor(v): + color = str(request.values.get("color", "")).strip() + if color.startswith('#'): color = color[1:] + if len(color) != 6: return render_template("settings_security.html", v=v, error="Invalid color code") + v.namecolor = color + g.db.add(v) + g.db.commit() + return redirect("/settings/profile") + +@app.post("/settings/themecolor") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def themecolor(v): + themecolor = str(request.values.get("themecolor", "")).strip() + if themecolor.startswith('#'): themecolor = themecolor[1:] + if len(themecolor) != 6: return render_template("settings_security.html", v=v, error="Invalid color code") + v.themecolor = themecolor + g.db.add(v) + g.db.commit() + return redirect("/settings/profile") + +@app.post("/settings/gumroad") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def gumroad(v): + if 'rama' in request.host: patron = 'Paypig' + else: patron = 'Patron' + + if not (v.email and v.is_activated): return {"error": f"You must have a verified email to verify {patron} status and claim your rewards"}, 400 + + data = { + 'access_token': GUMROAD_TOKEN, + 'email': v.email + } + response = requests.get('https://api.gumroad.com/v2/sales', data=data).json()["sales"] + + if len(response) == 0: return {"error": "Email not found"}, 404 + + response = response[0] + tier = tiers[response["variants_and_quantity"]] + if v.patron == tier: return {"error": f"{patron} rewards already claimed"}, 400 + + v.patron = tier + g.db.add(v) + + grant_awards = {} + if tier == 1: + if v.discord_id: add_role(v, "1") + grant_awards["shit"] = 1 + grant_awards["fireflies"] = 1 + elif tier == 2: + if v.discord_id: add_role(v, "2") + grant_awards["shit"] = 2 + grant_awards["fireflies"] = 2 + grant_awards["ban"] = 1 + elif tier == 3: + if v.discord_id: add_role(v, "3") + grant_awards["shit"] = 5 + grant_awards["fireflies"] = 5 + grant_awards["ban"] = 2 + elif tier == 4: + if v.discord_id: add_role(v, "4") + grant_awards["shit"] = 10 + grant_awards["fireflies"] = 10 + grant_awards["ban"] = 5 + elif tier == 5 or tier == 8: + if v.discord_id: add_role(v, "5") + grant_awards["shit"] = 20 + grant_awards["fireflies"] = 20 + grant_awards["ban"] = 10 + + thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id + + for name in grant_awards: + for count in range(grant_awards[name]): + + thing += 1 + + award = AwardRelationship( + id=thing, + user_id=v.id, + kind=name + ) + + g.db.add(award) + + if not v.has_badge(20+tier): + new_badge = Badge(badge_id=20+tier, + user_id=v.id, + ) + g.db.add(new_badge) + + g.db.commit() + + return {"message": f"{patron} rewards claimed!"} + +@app.post("/settings/titlecolor") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def titlecolor(v): + titlecolor = str(request.values.get("titlecolor", "")).strip() + if titlecolor.startswith('#'): titlecolor = titlecolor[1:] + if len(titlecolor) != 6: return render_template("settings_security.html", v=v, error="Invalid color code") + v.titlecolor = titlecolor + g.db.add(v) + g.db.commit() + + return redirect("/settings/profile") + +@app.post("/settings/security") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_security_post(v): + if request.values.get("new_password"): + if request.values.get( + "new_password") != request.values.get("cnf_password"): + return redirect("/settings/security?error=" + + escape("Passwords do not match.")) + + if not re.match(valid_password_regex, request.values.get("new_password")): + return redirect("/settings/security?error=" + + escape("Password must be between 8 and 100 characters.")) + + if not v.verifyPass(request.values.get("old_password")): + return render_template( + "settings_security.html", v=v, error="Incorrect password") + + v.passhash = v.hash_password(request.values.get("new_password")) + + g.db.add(v) + + g.db.commit() + + return redirect("/settings/security?msg=" + + escape("Your password has been changed.")) + + if request.values.get("new_email"): + + if not v.verifyPass(request.values.get('password')): + return redirect("/settings/security?error=" + + escape("Invalid password.")) + + new_email = request.values.get("new_email","").strip() + if new_email == v.email: + return redirect("/settings/security?error=That email is already yours!") + + existing = g.db.query(User).options(lazyload('*')).filter(User.id != v.id, + func.lower(User.email) == new_email.lower()).first() + if existing: + return redirect("/settings/security?error=" + + escape("That email address is already in use.")) + + url = f"https://{app.config['SERVER_NAME']}/activate" + + now = int(time.time()) + + token = generate_hash(f"{new_email}+{v.id}+{now}") + params = f"?email={quote(new_email)}&id={v.id}&time={now}&token={token}" + + link = url + params + + send_mail(to_address=new_email, + subject="Verify your email address.", + html=render_template("email/email_change.html", + action_url=link, + v=v) + ) + + return redirect("/settings/security?msg=" + escape( + "Check your email and click the verification link to complete the email change.")) + + if request.values.get("2fa_token", ""): + + if not v.verifyPass(request.values.get('password')): + return redirect("/settings/security?error=" + + escape("Invalid password or token.")) + + secret = request.values.get("2fa_secret") + x = pyotp.TOTP(secret) + if not x.verify(request.values.get("2fa_token"), valid_window=1): + return redirect("/settings/security?error=" + + escape("Invalid password or token.")) + + v.mfa_secret = secret + g.db.add(v) + + g.db.commit() + + return redirect("/settings/security?msg=" + + escape("Two-factor authentication enabled.")) + + if request.values.get("2fa_remove", ""): + + if not v.verifyPass(request.values.get('password')): + return redirect("/settings/security?error=" + + escape("Invalid password or token.")) + + token = request.values.get("2fa_remove") + + if not v.validate_2fa(token): + return redirect("/settings/security?error=" + + escape("Invalid password or token.")) + + v.mfa_secret = None + g.db.add(v) + + g.db.commit() + + return redirect("/settings/security?msg=" + + escape("Two-factor authentication disabled.")) + +@app.post("/settings/log_out_all_others") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_log_out_others(v): + + submitted_password = request.values.get("password", "").strip() + + if not v.verifyPass(submitted_password): return render_template("settings_security.html", v=v, error="Incorrect Password"), 401 + + v.login_nonce += 1 + + session["login_nonce"] = v.login_nonce + + g.db.add(v) + + g.db.commit() + + return render_template("settings_security.html", v=v, msg="All other devices have been logged out") + + +@app.post("/settings/images/profile") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_images_profile(v): + if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 + + if request.headers.get("cf-ipcountry") == "T1": return "Image uploads are not allowed through TOR.", 403 + + file = request.files["profile"] + + name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' + file.save(name) + highres = request.host_url[:-1] + process_image(name) + + if not highres: abort(400) + + name2 = name.replace('.gif', 'r.gif') + copyfile(name, name2) + imageurl = request.host_url[:-1] + process_image(name2, True) + + if not imageurl: abort(400) + + v.highres = highres + v.profileurl = imageurl + g.db.add(v) + + g.db.commit() + + return render_template("settings_profile.html", v=v, msg="Profile picture successfully updated.") + + +@app.post("/settings/images/banner") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_images_banner(v): + if request.content_length > 4 * 1024 * 1024: return "Max file size is 4 MB.", 413 + + if request.headers.get("cf-ipcountry") == "T1": return "Image uploads are not allowed through TOR.", 403 + + file = request.files["banner"] + + name = f'/images/{int(time.time())}{secrets.token_urlsafe(2)}.gif' + file.save(name) + imageurl = request.host_url[:-1] + process_image(name) + + if imageurl: + v.bannerurl = imageurl + g.db.add(v) + g.db.commit() + + return render_template("settings_profile.html", v=v, msg="Banner successfully updated.") + + +@app.post("/settings/delete/profile") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_delete_profile(v): + v.highres = None + v.profileurl = None + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", v=v, + msg="Profile picture successfully removed.") + +@app.post("/settings/delete/banner") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_delete_banner(v): + + v.bannerurl = None + g.db.add(v) + g.db.commit() + + return render_template("settings_profile.html", v=v, + msg="Banner successfully removed.") + + +@app.get("/settings/blocks") +@auth_required +def settings_blockedpage(v): + + return render_template("settings_blocks.html", v=v) + +@app.get("/settings/css") +@auth_required +def settings_css_get(v): + + return render_template("settings_css.html", v=v) + +@app.post("/settings/css") +@limiter.limit("1/second") +@auth_required +def settings_css(v): + css = request.values.get("css").strip().replace('\\', '').strip()[:4000] + + if not v.agendaposter: + v.css = css + else: + v.css = 'body *::before, body *::after { content: "Trans rights are human rights!"; }' + g.db.add(v) + g.db.commit() + + return render_template("settings_css.html", v=v) + +@app.get("/settings/profilecss") +@auth_required +def settings_profilecss_get(v): + + if v.coins < 1000 and not v.patron and v.admin_level < 6: return f"You must have +1000 {COINS_NAME} or be a patron to set profile css." + return render_template("settings_profilecss.html", v=v) + +@app.post("/settings/profilecss") +@limiter.limit("1/second") +@auth_required +def settings_profilecss(v): + if v.coins < 1000 and not v.patron: return f"You must have +1000 {COINS_NAME} or be a patron to set profile css." + profilecss = request.values.get("profilecss").strip().replace('\\', '').strip()[:4000] + v.profilecss = profilecss + g.db.add(v) + g.db.commit() + + return render_template("settings_profilecss.html", v=v) + +@app.post("/settings/block") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_block_user(v): + + user = get_user(request.values.get("username"), graceful=True) + + if not user: + return {"error": "That user doesn't exist."}, 404 + + if user.id == v.id: + return {"error": "You can't block yourself."}, 409 + + if v.has_block(user): + return {"error": f"You have already blocked @{user.username}."}, 409 + + if user.id == NOTIFICATIONS_ACCOUNT: + return {"error": "You can't block this user."}, 409 + + new_block = UserBlock(user_id=v.id, + target_id=user.id, + ) + g.db.add(new_block) + + + + existing = g.db.query(Notification).options(lazyload('*')).filter_by(blocksender=v.id, user_id=user.id).first() + if not existing: send_block_notif(v.id, user.id, f"@{v.username} has blocked you!") + + cache.delete_memoized(frontlist) + + g.db.commit() + + if v.admin_level == 1: return {"message": f"@{user.username} banned!"} + else: return {"message": f"@{user.username} blocked."} + + +@app.post("/settings/unblock") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_unblock_user(v): + + user = get_user(request.values.get("username")) + + x = v.has_block(user) + + if not x: abort(409) + + g.db.delete(x) + + + + existing = g.db.query(Notification).options(lazyload('*')).filter_by(unblocksender=v.id, user_id=user.id).first() + if not existing: send_unblock_notif(v.id, user.id, f"@{v.username} has unblocked you!") + + cache.delete_memoized(frontlist) + + g.db.commit() + + if v.admin_level == 1: return {"message": f"@{user.username} unbanned!"} + + return {"message": f"@{user.username} unblocked."} + + +@app.get("/settings/apps") +@auth_required +def settings_apps(v): + + return render_template("settings_apps.html", v=v) + + +@app.post("/settings/remove_discord") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_remove_discord(v): + + remove_user(v) + + v.discord_id=None + g.db.add(v) + + g.db.commit() + + return redirect("/settings/profile") + +@app.get("/settings/content") +@auth_required +def settings_content_get(v): + + return render_template("settings_filters.html", v=v) + +@app.post("/settings/name_change") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_name_change(v): + + new_name=request.values.get("name").strip() + + if new_name==v.username: + return render_template("settings_profile.html", + v=v, + error="You didn't change anything") + + if not re.match(valid_username_regex, new_name): + return render_template("settings_profile.html", + v=v, + error=f"This isn't a valid username.") + + name=new_name.replace('_','\_') + + x= g.db.query(User).options( + lazyload('*') + ).filter( + or_( + User.username.ilike(name), + User.original_username.ilike(name) + ) + ).first() + + if x and x.id != v.id: + return render_template("settings_profile.html", + v=v, + error=f"Username `{new_name}` is already in use.") + + v=g.db.query(User).with_for_update().options(lazyload('*')).filter_by(id=v.id).first() + + v.username=new_name + v.name_changed_utc=int(time.time()) + + set_nick(v, new_name) + + g.db.add(v) + + g.db.commit() + + return redirect("/settings/profile") + +@app.post("/settings/song_change") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_song_change(v): + song=request.values.get("song").strip() + + if song == "" and v.song and path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User.id).options(lazyload('*')).filter_by(song=v.song).count() == 1: + os.remove(f"/songs/{v.song}.mp3") + v.song = None + g.db.add(v) + g.db.commit() + return redirect("/settings/profile") + + song = song.replace("https://music.youtube.com", "https://youtube.com") + if song.startswith(("https://www.youtube.com/watch?v=", "https://youtube.com/watch?v=", "https://m.youtube.com/watch?v=")): + id = song.split("v=")[1] + elif song.startswith("https://youtu.be/"): + id = song.split("https://youtu.be/")[1] + else: + return render_template("settings_profile.html", + v=v, + error=f"Not a youtube link.") + + if "?" in id: id = id.split("?")[0] + if "&" in id: id = id.split("&")[0] + + if path.isfile(f'/songs/{id}.mp3'): + v.song = id + g.db.add(v) + g.db.commit() + return redirect("/settings/profile") + + + req = requests.get(f"https://www.googleapis.com/youtube/v3/videos?id={id}&key={YOUTUBE_KEY}&part=contentDetails").json() + duration = req['items'][0]['contentDetails']['duration'] + if "H" in duration: + return render_template("settings_profile.html", + v=v, + error=f"Duration of the video must not exceed 10 minutes.") + + if "M" in duration: + duration = int(duration.split("PT")[1].split("M")[0]) + if duration > 10: + return render_template("settings_profile.html", + v=v, + error=f"Duration of the video must not exceed 10 minutes.") + + + if v.song and path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User.id).options(lazyload('*')).filter_by(song=v.song).count() == 1: + os.remove(f"/songs/{v.song}.mp3") + + ydl_opts = { + 'outtmpl': '/songs/%(title)s.%(ext)s', + 'format': 'bestaudio/best', + 'postprocessors': [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192', + }], + } + + with youtube_dl.YoutubeDL(ydl_opts) as ydl: + try: ydl.download([f"https://youtube.com/watch?v={id}"]) + except Exception as e: + print(e) + return render_template("settings_profile.html", + v=v, + error=f"Age-restricted videos aren't allowed.") + + files = os.listdir("/songs/") + paths = [path.join("/songs/", basename) for basename in files] + songfile = max(paths, key=path.getctime) + os.rename(songfile, f"/songs/{id}.mp3") + + v.song = id + g.db.add(v) + + g.db.commit() + + return redirect("/settings/profile") + +@app.post("/settings/title_change") +@limiter.limit("1/second") +@auth_required +@validate_formkey +def settings_title_change(v): + + if v.flairchanged: abort(403) + + new_name=request.values.get("title").strip()[:100].replace("𒐪","") + + if new_name==v.customtitle: + return render_template("settings_profile.html", + v=v, + error="You didn't change anything") + + v.customtitleplain = new_name + + v.customtitle = filter_title(new_name) + + g.db.add(v) + g.db.commit() + return redirect("/settings/profile") \ No newline at end of file diff --git a/files/routes/static.py b/files/routes/static.py old mode 100644 new mode 100755 index c4b20cb97..0e5d6f1df --- a/files/routes/static.py +++ b/files/routes/static.py @@ -1,341 +1,341 @@ -from files.mail import * -from files.__main__ import app, limiter, mail -from files.helpers.alerts import * -from files.classes.award import AWARDS -from sqlalchemy import func -from os import path -import calendar -import matplotlib.pyplot as plt - -site = environ.get("DOMAIN").strip() -site_name = environ.get("SITE_NAME").strip() - -@app.get('/rules') -@auth_desired -def static_rules(v): - - if not path.exists(f'./{site_name} rules.html'): - if v and v.admin_level == 6: - return render_template('norules.html', v=v) - else: - abort(404) - - with open(f'./{site_name} rules.html', 'r') as f: - rules = f.read() - - return render_template('rules.html', rules=rules, v=v) - - -@app.get("/stats") -@auth_required -def participation_stats(v): - - now = int(time.time()) - - day = now - 86400 - - data = {"valid_users": g.db.query(User.id).count(), - "private_users": g.db.query(User.id).options(lazyload('*')).filter_by(is_private=True).count(), - "banned_users": g.db.query(User.id).options(lazyload('*')).filter(User.is_banned > 0).count(), - "verified_email_users": g.db.query(User.id).options(lazyload('*')).filter_by(is_activated=True).count(), - "total_coins": g.db.query(func.sum(User.coins)).scalar(), - "signups_last_24h": g.db.query(User.id).options(lazyload('*')).filter(User.created_utc > day).count(), - "total_posts": g.db.query(Submission.id).count(), - "posting_users": g.db.query(Submission.author_id).distinct().count(), - "listed_posts": g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False).filter(Submission.deleted_utc == 0).count(), - "removed_posts": g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=True).count(), - "deleted_posts": g.db.query(Submission.id).options(lazyload('*')).filter(Submission.deleted_utc > 0).count(), - "posts_last_24h": g.db.query(Submission.id).options(lazyload('*')).filter(Submission.created_utc > day).count(), - "total_comments": g.db.query(Comment.id).count(), - "commenting_users": g.db.query(Comment.author_id).distinct().count(), - "removed_comments": g.db.query(Comment.id).options(lazyload('*')).filter_by(is_banned=True).count(), - "deleted_comments": g.db.query(Comment.id).options(lazyload('*')).filter(Comment.deleted_utc>0).count(), - "comments_last_24h": g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc > day).count(), - "post_votes": g.db.query(Vote.id).count(), - "post_voting_users": g.db.query(Vote.user_id).distinct().count(), - "comment_votes": g.db.query(CommentVote.id).count(), - "comment_voting_users": g.db.query(CommentVote.user_id).distinct().count(), - "total_awards": g.db.query(AwardRelationship.id).count(), - "awards_given": g.db.query(AwardRelationship.id).options(lazyload('*')).filter(or_(AwardRelationship.submission_id != None, AwardRelationship.comment_id != None)).count() - } - - - return render_template("admin/content_stats.html", v=v, title="Content Statistics", data=data) - - -@app.get("/chart") -@auth_required -def chart(v): - file = cached_chart() - return send_file(f"../{file}") - - -@cache.memoize(timeout=86400) -def cached_chart(): - days = int(request.values.get("days", 25)) - - now = time.gmtime() - midnight_this_morning = time.struct_time((now.tm_year, - now.tm_mon, - now.tm_mday, - 0, - 0, - 0, - now.tm_wday, - now.tm_yday, - 0) - ) - today_cutoff = calendar.timegm(midnight_this_morning) - - day = 3600 * 24 - - day_cutoffs = [today_cutoff - day * i for i in range(days)] - day_cutoffs.insert(0, calendar.timegm(now)) - - daily_times = [time.strftime("%d", time.gmtime(day_cutoffs[i + 1])) for i in range(len(day_cutoffs) - 1)][2:][::-1] - - daily_signups = [g.db.query(User.id).options(lazyload('*')).filter(User.created_utc < day_cutoffs[i], User.created_utc > day_cutoffs[i + 1]).count() for i in range(len(day_cutoffs) - 1)][2:][::-1] - - post_stats = [g.db.query(Submission.id).options(lazyload('*')).filter(Submission.created_utc < day_cutoffs[i], Submission.created_utc > day_cutoffs[i + 1], Submission.is_banned == False).count() for i in range(len(day_cutoffs) - 1)][2:][::-1] - - comment_stats = [g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id != 1).count() for i in range(len(day_cutoffs) - 1)][2:][::-1] - - signup_chart = plt.subplot2grid((20, 4), (0, 0), rowspan=5, colspan=4) - posts_chart = plt.subplot2grid((20, 4), (7, 0), rowspan=5, colspan=4) - comments_chart = plt.subplot2grid((20, 4), (14, 0), rowspan=5, colspan=4) - - signup_chart.grid(), posts_chart.grid(), comments_chart.grid() - - signup_chart.plot( - daily_times, - daily_signups, - color='red') - posts_chart.plot( - daily_times, - post_stats, - color='green') - comments_chart.plot( - daily_times, - comment_stats, - color='gold') - - signup_chart.set_ylabel("Signups") - posts_chart.set_ylabel("Posts") - comments_chart.set_ylabel("Comments") - comments_chart.set_xlabel("Time (UTC)") - - signup_chart.legend(loc='upper left', frameon=True) - posts_chart.legend(loc='upper left', frameon=True) - comments_chart.legend(loc='upper left', frameon=True) - - file = "chart.png" - plt.savefig(file) - plt.clf() - return file - - -@app.get("/patrons") -@app.get("/paypigs") -@auth_desired -def patrons(v): - query = g.db.query( - User.id, User.username, User.patron, User.namecolor, - AwardRelationship.kind.label('last_award_kind'), func.count(AwardRelationship.id).label('last_award_count') - ).filter(AwardRelationship.submission_id==None, AwardRelationship.comment_id==None, User.patron > 0) \ - .group_by(User.username, User.patron, User.id, User.namecolor, AwardRelationship.kind) \ - .order_by(User.patron.desc(), AwardRelationship.kind.desc()) \ - .join(User).all() - - result = {} - for row in (r._asdict() for r in query): - user_id = row['id'] - if user_id not in result: - result[user_id] = row - result[user_id]['awards'] = {} - - kind = row['last_award_kind'] - if kind in AWARDS.keys(): - result[user_id]['awards'][kind] = (AWARDS[kind], row['last_award_count']) - - return render_template("patrons.html", v=v, result=result) - -@app.get("/admins") -@auth_desired -def admins(v): - admins = g.db.query(User).options(lazyload('*')).filter_by(admin_level=6).order_by(User.coins.desc()).all() - return render_template("admins.html", v=v, admins=admins) - - -@app.get("/log") -@auth_desired -def log(v): - - page=int(request.args.get("page",1)) - - if v and v.admin_level == 6: actions = g.db.query(ModAction).order_by(ModAction.id.desc()).offset(25 * (page - 1)).limit(26).all() - else: actions=g.db.query(ModAction).filter(ModAction.kind!="shadowban", ModAction.kind!="unshadowban", ModAction.kind!="club", ModAction.kind!="unclub").order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all() - - next_exists=len(actions)>25 - actions=actions[:25] - - return render_template("log.html", v=v, actions=actions, next_exists=next_exists, page=page) - -@app.get("/log/") -@auth_desired -def log_item(id, v): - - try: id = int(id) - except: - try: id = int(id, 36) - except: abort(404) - - action=g.db.query(ModAction).options(lazyload('*')).filter_by(id=id).first() - - if not action: - abort(404) - - if request.path != action.permalink: - return redirect(action.permalink) - - return render_template("log.html", - v=v, - actions=[action], - next_exists=False, - page=1, - action=action - ) - -@app.get("/assets/favicon.ico") -def favicon(): - return send_file(f"./assets/images/{site_name}/icon.webp") - -@app.get("/api") -@auth_desired -def api(v): - return render_template("api.html", v=v) - -@app.get("/contact") -@auth_required -def contact(v): - - return render_template("contact.html", v=v) - -@app.post("/contact") -@limiter.limit("1/second") -@auth_required -def submit_contact(v): - message = f'This message has been sent automatically to all admins via https://{site}/contact, user email is "{v.email}"\n\nMessage:\n\n' + request.values.get("message", "") - send_admin(v.id, message) - g.db.commit() - return render_template("contact.html", v=v, msg="Your message has been sent.") - -@app.get('/archives') -def archivesindex(): - return redirect("/archives/index.html") - -@app.get('/archives/') -def archives(path): - resp = make_response(send_from_directory('/archives', path)) - if request.path.endswith('.css'): resp.headers.add("Content-Type", "text/css") - return resp - -@app.get('/assets/') -@limiter.exempt -def static_service(path): - resp = make_response(send_from_directory('./assets', path)) - if request.path.endswith('.webp') or request.path.endswith('.gif') or request.path.endswith('.ttf') or request.path.endswith('.woff') or request.path.endswith('.woff2'): - resp.headers.remove("Cache-Control") - resp.headers.add("Cache-Control", "public, max-age=2628000") - - return resp - -@app.get('/images/') -@app.get('/hostedimages/') -@limiter.exempt -def images(path): - resp = make_response(send_from_directory('/images', path)) - resp.headers.remove("Cache-Control") - resp.headers.add("Cache-Control", "public, max-age=2628000") - return resp - -@app.get("/robots.txt") -def robots_txt(): - return send_file("./assets/robots.txt") - -@app.get("/settings") -@auth_required -def settings(v): - - - return redirect("/settings/profile") - - -@app.get("/settings/profile") -@auth_required -def settings_profile(v): - - - return render_template("settings_profile.html", - v=v) - -@app.get("/badges") -@auth_desired -def badges(v): - - - badges = g.db.query(BadgeDef).all() - return render_template("badges.html", v=v, badges=badges) - -@app.get("/blocks") -@auth_desired -def blocks(v): - - - blocks=g.db.query(UserBlock).all() - users = [] - targets = [] - for x in blocks: - users.append(get_account(x.user_id)) - targets.append(get_account(x.target_id)) - - return render_template("blocks.html", v=v, users=users, targets=targets) - -@app.get("/banned") -@auth_desired -def banned(v): - - - users = [x for x in g.db.query(User).options(lazyload('*')).filter(User.is_banned > 0, User.unban_utc == 0).all()] - return render_template("banned.html", v=v, users=users) - -@app.get("/formatting") -@auth_desired -def formatting(v): - - - return render_template("formatting.html", v=v) - -@app.get("/service-worker.js") -def serviceworker(): - with open("files/assets/js/service-worker.js", "r") as f: return Response(f.read(), mimetype='application/javascript') - -@app.get("/settings/security") -@auth_required -def settings_security(v): - - - return render_template("settings_security.html", - v=v, - mfa_secret=pyotp.random_base32() if not v.mfa_secret else None, - error=request.values.get("error") or None, - msg=request.values.get("msg") or None - ) - -@app.post("/dismiss_mobile_tip") -@limiter.limit("1/second") -def dismiss_mobile_tip(): - - session["tooltip_last_dismissed"]=int(time.time()) - session.modified=True - - return "", 204 +from files.mail import * +from files.__main__ import app, limiter, mail +from files.helpers.alerts import * +from files.classes.award import AWARDS +from sqlalchemy import func +from os import path +import calendar +import matplotlib.pyplot as plt + +site = environ.get("DOMAIN").strip() +site_name = environ.get("SITE_NAME").strip() + +@app.get('/rules') +@auth_desired +def static_rules(v): + + if not path.exists(f'./{site_name} rules.html'): + if v and v.admin_level == 6: + return render_template('norules.html', v=v) + else: + abort(404) + + with open(f'./{site_name} rules.html', 'r') as f: + rules = f.read() + + return render_template('rules.html', rules=rules, v=v) + + +@app.get("/stats") +@auth_required +def participation_stats(v): + + now = int(time.time()) + + day = now - 86400 + + data = {"valid_users": g.db.query(User.id).count(), + "private_users": g.db.query(User.id).options(lazyload('*')).filter_by(is_private=True).count(), + "banned_users": g.db.query(User.id).options(lazyload('*')).filter(User.is_banned > 0).count(), + "verified_email_users": g.db.query(User.id).options(lazyload('*')).filter_by(is_activated=True).count(), + "total_coins": g.db.query(func.sum(User.coins)).scalar(), + "signups_last_24h": g.db.query(User.id).options(lazyload('*')).filter(User.created_utc > day).count(), + "total_posts": g.db.query(Submission.id).count(), + "posting_users": g.db.query(Submission.author_id).distinct().count(), + "listed_posts": g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False).filter(Submission.deleted_utc == 0).count(), + "removed_posts": g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=True).count(), + "deleted_posts": g.db.query(Submission.id).options(lazyload('*')).filter(Submission.deleted_utc > 0).count(), + "posts_last_24h": g.db.query(Submission.id).options(lazyload('*')).filter(Submission.created_utc > day).count(), + "total_comments": g.db.query(Comment.id).count(), + "commenting_users": g.db.query(Comment.author_id).distinct().count(), + "removed_comments": g.db.query(Comment.id).options(lazyload('*')).filter_by(is_banned=True).count(), + "deleted_comments": g.db.query(Comment.id).options(lazyload('*')).filter(Comment.deleted_utc>0).count(), + "comments_last_24h": g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc > day).count(), + "post_votes": g.db.query(Vote.id).count(), + "post_voting_users": g.db.query(Vote.user_id).distinct().count(), + "comment_votes": g.db.query(CommentVote.id).count(), + "comment_voting_users": g.db.query(CommentVote.user_id).distinct().count(), + "total_awards": g.db.query(AwardRelationship.id).count(), + "awards_given": g.db.query(AwardRelationship.id).options(lazyload('*')).filter(or_(AwardRelationship.submission_id != None, AwardRelationship.comment_id != None)).count() + } + + + return render_template("admin/content_stats.html", v=v, title="Content Statistics", data=data) + + +@app.get("/chart") +@auth_required +def chart(v): + file = cached_chart() + return send_file(f"../{file}") + + +@cache.memoize(timeout=86400) +def cached_chart(): + days = int(request.values.get("days", 25)) + + now = time.gmtime() + midnight_this_morning = time.struct_time((now.tm_year, + now.tm_mon, + now.tm_mday, + 0, + 0, + 0, + now.tm_wday, + now.tm_yday, + 0) + ) + today_cutoff = calendar.timegm(midnight_this_morning) + + day = 3600 * 24 + + day_cutoffs = [today_cutoff - day * i for i in range(days)] + day_cutoffs.insert(0, calendar.timegm(now)) + + daily_times = [time.strftime("%d", time.gmtime(day_cutoffs[i + 1])) for i in range(len(day_cutoffs) - 1)][2:][::-1] + + daily_signups = [g.db.query(User.id).options(lazyload('*')).filter(User.created_utc < day_cutoffs[i], User.created_utc > day_cutoffs[i + 1]).count() for i in range(len(day_cutoffs) - 1)][2:][::-1] + + post_stats = [g.db.query(Submission.id).options(lazyload('*')).filter(Submission.created_utc < day_cutoffs[i], Submission.created_utc > day_cutoffs[i + 1], Submission.is_banned == False).count() for i in range(len(day_cutoffs) - 1)][2:][::-1] + + comment_stats = [g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id != 1).count() for i in range(len(day_cutoffs) - 1)][2:][::-1] + + signup_chart = plt.subplot2grid((20, 4), (0, 0), rowspan=5, colspan=4) + posts_chart = plt.subplot2grid((20, 4), (7, 0), rowspan=5, colspan=4) + comments_chart = plt.subplot2grid((20, 4), (14, 0), rowspan=5, colspan=4) + + signup_chart.grid(), posts_chart.grid(), comments_chart.grid() + + signup_chart.plot( + daily_times, + daily_signups, + color='red') + posts_chart.plot( + daily_times, + post_stats, + color='green') + comments_chart.plot( + daily_times, + comment_stats, + color='gold') + + signup_chart.set_ylabel("Signups") + posts_chart.set_ylabel("Posts") + comments_chart.set_ylabel("Comments") + comments_chart.set_xlabel("Time (UTC)") + + signup_chart.legend(loc='upper left', frameon=True) + posts_chart.legend(loc='upper left', frameon=True) + comments_chart.legend(loc='upper left', frameon=True) + + file = "chart.png" + plt.savefig(file) + plt.clf() + return file + + +@app.get("/patrons") +@app.get("/paypigs") +@auth_desired +def patrons(v): + query = g.db.query( + User.id, User.username, User.patron, User.namecolor, + AwardRelationship.kind.label('last_award_kind'), func.count(AwardRelationship.id).label('last_award_count') + ).filter(AwardRelationship.submission_id==None, AwardRelationship.comment_id==None, User.patron > 0) \ + .group_by(User.username, User.patron, User.id, User.namecolor, AwardRelationship.kind) \ + .order_by(User.patron.desc(), AwardRelationship.kind.desc()) \ + .join(User).all() + + result = {} + for row in (r._asdict() for r in query): + user_id = row['id'] + if user_id not in result: + result[user_id] = row + result[user_id]['awards'] = {} + + kind = row['last_award_kind'] + if kind in AWARDS.keys(): + result[user_id]['awards'][kind] = (AWARDS[kind], row['last_award_count']) + + return render_template("patrons.html", v=v, result=result) + +@app.get("/admins") +@auth_desired +def admins(v): + admins = g.db.query(User).options(lazyload('*')).filter_by(admin_level=6).order_by(User.coins.desc()).all() + return render_template("admins.html", v=v, admins=admins) + + +@app.get("/log") +@auth_desired +def log(v): + + page=int(request.args.get("page",1)) + + if v and v.admin_level == 6: actions = g.db.query(ModAction).order_by(ModAction.id.desc()).offset(25 * (page - 1)).limit(26).all() + else: actions=g.db.query(ModAction).filter(ModAction.kind!="shadowban", ModAction.kind!="unshadowban", ModAction.kind!="club", ModAction.kind!="unclub").order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all() + + next_exists=len(actions)>25 + actions=actions[:25] + + return render_template("log.html", v=v, actions=actions, next_exists=next_exists, page=page) + +@app.get("/log/") +@auth_desired +def log_item(id, v): + + try: id = int(id) + except: + try: id = int(id, 36) + except: abort(404) + + action=g.db.query(ModAction).options(lazyload('*')).filter_by(id=id).first() + + if not action: + abort(404) + + if request.path != action.permalink: + return redirect(action.permalink) + + return render_template("log.html", + v=v, + actions=[action], + next_exists=False, + page=1, + action=action + ) + +@app.get("/assets/favicon.ico") +def favicon(): + return send_file(f"./assets/images/{site_name}/icon.webp") + +@app.get("/api") +@auth_desired +def api(v): + return render_template("api.html", v=v) + +@app.get("/contact") +@auth_required +def contact(v): + + return render_template("contact.html", v=v) + +@app.post("/contact") +@limiter.limit("1/second") +@auth_required +def submit_contact(v): + message = f'This message has been sent automatically to all admins via https://{site}/contact, user email is "{v.email}"\n\nMessage:\n\n' + request.values.get("message", "") + send_admin(v.id, message) + g.db.commit() + return render_template("contact.html", v=v, msg="Your message has been sent.") + +@app.get('/archives') +def archivesindex(): + return redirect("/archives/index.html") + +@app.get('/archives/') +def archives(path): + resp = make_response(send_from_directory('/archives', path)) + if request.path.endswith('.css'): resp.headers.add("Content-Type", "text/css") + return resp + +@app.get('/assets/') +@limiter.exempt +def static_service(path): + resp = make_response(send_from_directory('./assets', path)) + if request.path.endswith('.webp') or request.path.endswith('.gif') or request.path.endswith('.ttf') or request.path.endswith('.woff') or request.path.endswith('.woff2'): + resp.headers.remove("Cache-Control") + resp.headers.add("Cache-Control", "public, max-age=2628000") + + return resp + +@app.get('/images/') +@app.get('/hostedimages/') +@limiter.exempt +def images(path): + resp = make_response(send_from_directory('/images', path)) + resp.headers.remove("Cache-Control") + resp.headers.add("Cache-Control", "public, max-age=2628000") + return resp + +@app.get("/robots.txt") +def robots_txt(): + return send_file("./assets/robots.txt") + +@app.get("/settings") +@auth_required +def settings(v): + + + return redirect("/settings/profile") + + +@app.get("/settings/profile") +@auth_required +def settings_profile(v): + + + return render_template("settings_profile.html", + v=v) + +@app.get("/badges") +@auth_desired +def badges(v): + + + badges = g.db.query(BadgeDef).all() + return render_template("badges.html", v=v, badges=badges) + +@app.get("/blocks") +@auth_desired +def blocks(v): + + + blocks=g.db.query(UserBlock).all() + users = [] + targets = [] + for x in blocks: + users.append(get_account(x.user_id)) + targets.append(get_account(x.target_id)) + + return render_template("blocks.html", v=v, users=users, targets=targets) + +@app.get("/banned") +@auth_desired +def banned(v): + + + users = [x for x in g.db.query(User).options(lazyload('*')).filter(User.is_banned > 0, User.unban_utc == 0).all()] + return render_template("banned.html", v=v, users=users) + +@app.get("/formatting") +@auth_desired +def formatting(v): + + + return render_template("formatting.html", v=v) + +@app.get("/service-worker.js") +def serviceworker(): + with open("files/assets/js/service-worker.js", "r") as f: return Response(f.read(), mimetype='application/javascript') + +@app.get("/settings/security") +@auth_required +def settings_security(v): + + + return render_template("settings_security.html", + v=v, + mfa_secret=pyotp.random_base32() if not v.mfa_secret else None, + error=request.values.get("error") or None, + msg=request.values.get("msg") or None + ) + +@app.post("/dismiss_mobile_tip") +@limiter.limit("1/second") +def dismiss_mobile_tip(): + + session["tooltip_last_dismissed"]=int(time.time()) + session.modified=True + + return "", 204 diff --git a/files/routes/users.py b/files/routes/users.py old mode 100644 new mode 100755 index a13eac7a3..d04f471ec --- a/files/routes/users.py +++ b/files/routes/users.py @@ -1,745 +1,745 @@ -import qrcode -import io -import time -import math -from files.classes.user import ViewerRelationship -from files.helpers.alerts import * -from files.helpers.sanitize import * -from files.helpers.markdown import * -from files.helpers.const import * -from files.mail import * -from flask import * -from files.__main__ import app, limiter -from pusher_push_notifications import PushNotifications - -site = environ.get("DOMAIN").strip() - -beams_client = PushNotifications( - instance_id=PUSHER_INSTANCE_ID, - secret_key=PUSHER_KEY, -) - - -@app.post("/pay_rent") -@limiter.limit("1/second") -@auth_required -def pay_rent(v): - if v.coins < 500: return "You must have more than 500 coins." - v.coins -= 500 - v.rent_utc = int(time.time()) - g.db.add(v) - u = get_account(253) - u.coins += 500 - g.db.add(u) - send_notification(NOTIFICATIONS_ACCOUNT, u, f"@{v.username} has paid rent!") - g.db.commit() - return {"message": "Rent paid!"} - - -@app.post("/steal") -@limiter.limit("1/second") -@is_not_banned -def steal(v): - if int(time.time()) - v.created_utc < 604800: - return "You must have an account older than 1 week in order to attempt stealing." - if v.coins < 5000: - return "You must have more than 5000 coins in order to attempt stealing." - u = get_account(253) - if random.randint(1, 10) < 5: - v.coins += 700 - v.steal_utc = int(time.time()) - g.db.add(v) - u.coins -= 700 - g.db.add(u) - send_notification(NOTIFICATIONS_ACCOUNT, u, f"Some [grubby little rentoid](/@{v.username}) has absconded with 700 of your hard-earned dramacoins to fuel his Funko Pop addiction. Stop being so trusting.") - send_notification(NOTIFICATIONS_ACCOUNT, v, f"You have successfully shorted your heroic landlord 700 dramacoins in rent. You're slightly less materially poor, but somehow even moreso morally. Are you proud of yourself?") - g.db.commit() - return {"message": "Attempt successful!"} - - else: - if random.random() < 0.15: - send_notification(NOTIFICATIONS_ACCOUNT, u, f"You caught [this sniveling little renthog](/@{v.username}) trying to rob you. After beating him within an inch of his life, you sold his Nintendo Switch for 500 dramacoins and called the cops. He was sentenced to one (1) day in renthog prison.") - send_notification(NOTIFICATIONS_ACCOUNT, v, f"The ever-vigilant landchad has caught you trying to steal his hard-earned rent money. The police take you away and laugh as you impotently stutter A-ACAB :sob: You are fined 500 dramacoins and sentenced to one (1) day in renthog prison.") - v.ban(days=1, reason="Jailed thief") - v.fail_utc = int(time.time()) - else: - send_notification(NOTIFICATIONS_ACCOUNT, u, f"You caught [this sniveling little renthog](/@{v.username}) trying to rob you. After beating him within an inch of his life, you showed mercy in exchange for a 500 dramacoin tip. This time.") - send_notification(NOTIFICATIONS_ACCOUNT, v, f"The ever-vigilant landchad has caught you trying to steal his hard-earned rent money. You were able to convince him to spare your life with a 500 dramacoin tip. This time.") - v.fail2_utc = int(time.time()) - v.coins -= 500 - g.db.add(v) - u.coins += 500 - g.db.add(u) - g.db.commit() - return {"message": "Attempt failed!"} - - -@app.get("/rentoids") -@auth_desired -def rentoids(v): - users = g.db.query(User).options(lazyload('*')).filter(User.rent_utc > 0).all() - return render_template("rentoids.html", v=v, users=users) - - -@app.get("/thiefs") -@auth_desired -def thiefs(v): - successful = g.db.query(User).options(lazyload('*')).filter(User.steal_utc > 0).all() - failed = g.db.query(User).options(lazyload('*')).filter(User.fail_utc > 0).all() - failed2 = g.db.query(User).options(lazyload('*')).filter(User.fail2_utc > 0).all() - return render_template("thiefs.html", v=v, successful=successful, failed=failed, failed2=failed2) - - -@app.post("/@/suicide") -@limiter.limit("1/second") -@auth_required -def suicide(v, username): - t = int(time.time()) - if v.admin_level == 0 and t - v.suicide_utc < 86400: return {"message": "You're on 1-day cooldown!"} - user = get_user(username) - suicide = f"Hi there,\n\nA [concerned user]({v.url}) reached out to us about you.\n\nWhen you're in the middle of something painful, it may feel like you don't have a lot of options. But whatever you're going through, you deserve help and there are people who are here for you.\n\nThere are resources available in your area that are free, confidential, and available 24/7:\n\n- Call, Text, or Chat with Canada's [Crisis Services Canada](https://www.crisisservicescanada.ca/en/)\n- Call, Email, or Visit the UK's [Samaritans](https://www.samaritans.org/)\n- Text CHAT to America's [Crisis Text Line](https://www.crisistextline.org/) at 741741.\nIf you don't see a resource in your area above, the moderators at r/SuicideWatch keep a comprehensive list of resources and hotlines for people organized by location. Find Someone Now\n\nIf you think you may be depressed or struggling in another way, don't ignore it or brush it aside. Take yourself and your feelings seriously, and reach out to someone.\n\nIt may not feel like it, but you have options. There are people available to listen to you, and ways to move forward.\n\nYour fellow users care about you and there are people who want to help." - send_notification(NOTIFICATIONS_ACCOUNT, user, suicide) - v.suicide_utc = t - g.db.add(v) - g.db.commit() - return {"message": "Help message sent!"} - - -@app.get("/@/coins") -@auth_required -def get_coins(v, username): - user = get_user(username) - if user != None: return {"coins": user.coins}, 200 - else: return {"error": "invalid_user"}, 404 - -@app.post("/@/transfer_coins") -@limiter.limit("1/second") -@is_not_banned -@validate_formkey -def transfer_coins(v, username): - TAX_RECEIVER_ID = 747 - TAX_RATE = 0.01 - - receiver = g.db.query(User).filter_by(username=username).first() - tax_receiver = g.db.query(User).filter_by(id=TAX_RECEIVER_ID).first() - - if receiver is None: return {"error": "That user doesn't exist."}, 404 - - if receiver.id != v.id: - amount = request.values.get("amount", "").strip() - amount = int(amount) if amount.isdigit() else None - - if amount is None or amount <= 0: return {"error": f"Invalid amount of {app.config['COINS_NAME']}."}, 400 - if v.coins < amount: return {"error": f"You don't have enough {app.config['COINS_NAME']}"}, 400 - if amount < 100: return {"error": f"You have to gift at least 100 {app.config['COINS_NAME']}."}, 400 - - tax = math.ceil(amount*TAX_RATE) - v.coins -= amount - receiver.coins += amount-tax - tax_receiver.coins += tax - g.db.add(receiver) - g.db.add(tax_receiver) - g.db.add(v) - - transfer_message = f"🤑 [@{v.username}]({v.url}) has gifted you {amount} {app.config['COINS_NAME']}!" - send_notification(NOTIFICATIONS_ACCOUNT, receiver, transfer_message) - - log_message = f"[@{v.username}]({v.url}) has transferred {amount} {app.config['COINS_NAME']} to [@{receiver.username}]({receiver.url})" - send_notification(NOTIFICATIONS_ACCOUNT, TAX_RECEIVER_ID, log_message) - - g.db.commit() - - return {"message": f"{amount-tax} {app.config['COINS_NAME']} transferred!"}, 200 - - return {"message": f"You can't transfer {app.config['COINS_NAME']} to yourself!"}, 400 - - -@app.get("/leaderboard") -@auth_desired -def leaderboard(v): - users = g.db.query(User).options(lazyload('*')) - users1 = users.order_by(User.coins.desc()).limit(25).all() - users2 = users.order_by(User.stored_subscriber_count.desc()).limit(10).all() - users3 = users.order_by(User.post_count.desc()).limit(10).all() - users4 = users.order_by(User.comment_count.desc()).limit(10).all() - users5 = users.order_by(User.received_award_count.desc()).limit(10).all() - if 'pcmemes.net' in request.host: - users6 = users.order_by(User.basedcount.desc()).limit(10).all() - return render_template("leaderboard.html", v=v, users1=users1, users2=users2, users3=users3, users4=users4, users5=users5, users6=users6) - return render_template("leaderboard.html", v=v, users1=users1, users2=users2, users3=users3, users4=users4, users5=users5) - - -@app.get("/@/css") -def get_css(username): - user = get_user(username) - if user.css: css = user.css - else: css = "" - resp=make_response(css) - resp.headers.add("Content-Type", "text/css") - return resp - -@app.get("/@/profilecss") -def get_profilecss(username): - user = get_user(username) - if user.profilecss: profilecss = user.profilecss - else: profilecss = "" - resp=make_response(profilecss) - resp.headers.add("Content-Type", "text/css") - return resp - -@app.get("/songs/") -def songs(id): - try: id = int(id) - except: return "", 400 - user = g.db.query(User).options(lazyload('*')).filter_by(id=id).first() - if user and user.song: return redirect(f"/song/{user.song}.mp3") - else: abort(404) - -@app.get("/song/") -def song(song): - resp = make_response(send_from_directory('/songs/', song)) - resp.headers.remove("Cache-Control") - resp.headers.add("Cache-Control", "public, max-age=2628000") - return resp - -@app.post("/subscribe/") -@limiter.limit("1/second") -@auth_required -def subscribe(v, post_id): - new_sub = Subscription(user_id=v.id, submission_id=post_id) - g.db.add(new_sub) - g.db.commit() - return {"message": "Post subscribed!"} - -@app.post("/unsubscribe/") -@limiter.limit("1/second") -@auth_required -def unsubscribe(v, post_id): - sub=g.db.query(Subscription).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post_id).first() - if sub: - g.db.delete(sub) - g.db.commit() - return {"message": "Post unsubscribed!"} - -@app.post("/@/message") -@limiter.limit("1/second") -@limiter.limit("10/hour") -@auth_required -def message2(v, username): - - user = get_user(username, v=v) - if hasattr(user, 'is_blocking') and user.is_blocking: return {"error": "You're blocking this user."}, 403 - - if v.admin_level <= 1: - if hasattr(user, 'is_blocked') and user.is_blocked: return {"error": "This user is blocking you."}, 403 - - message = request.values.get("message", "").strip()[:1000].strip() - - existing = g.db.query(Comment).options(lazyload('*')).filter(Comment.author_id == v.id, - Comment.sentto == user.id, - Comment.body == message, - ).first() - if existing: return redirect('/notifications?messages=true') - - text = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', message) - - text_html = Renderer().render(mistletoe.Document(text)) - - text_html = sanitize(text_html, True) - - new_comment = Comment(author_id=v.id, - parent_submission=None, - level=1, - sentto=user.id, - body=text, - body_html=text_html, - ) - g.db.add(new_comment) - - g.db.flush() - - - notif = Notification(comment_id=new_comment.id, user_id=user.id) - g.db.add(notif) - - - try: - beams_client.publish_to_interests( - interests=[str(user.id)], - publish_body={ - 'web': { - 'notification': { - 'title': f'New message from @{v.username}', - 'body': message, - 'deep_link': f'https://{site}/notifications', - }, - }, - }, - ) - except Exception as e: - print(e) - - g.db.commit() - - return redirect(f"/@{username}") - - -@app.post("/reply") -@limiter.limit("1/second") -@limiter.limit("6/minute") -@auth_required -def messagereply(v): - - message = request.values.get("body", "").strip()[:1000].strip() - id = int(request.values.get("parent_id")) - parent = get_comment(id, v=v) - user = parent.author - message = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', message) - - text_html = Renderer().render(mistletoe.Document(message)) - text_html = sanitize(text_html, True) - new_comment = Comment(author_id=v.id, - parent_submission=None, - parent_comment_id=id, - level=parent.level + 1, - sentto=user.id, - body=message, - body_html=text_html, - ) - g.db.add(new_comment) - g.db.flush() - - notif = Notification(comment_id=new_comment.id, user_id=user.id) - g.db.add(notif) - - g.db.commit() - - return render_template("comments.html", v=v, comments=[new_comment]) - -@app.get("/2faqr/") -@auth_required -def mfa_qr(secret, v): - x = pyotp.TOTP(secret) - qr = qrcode.QRCode( - error_correction=qrcode.constants.ERROR_CORRECT_L - ) - qr.add_data(x.provisioning_uri(v.username, issuer_name=app.config["SITE_NAME"])) - img = qr.make_image(fill_color="#000000", back_color="white") - - mem = io.BytesIO() - - img.save(mem, format="PNG") - mem.seek(0, 0) - return send_file(mem, mimetype="image/png", as_attachment=False) - - -@app.get("/is_available/") -@auth_desired -def api_is_available(name, v): - - name=name.strip() - - if len(name)<3 or len(name)>25: - return {name:False} - - name=name.replace('_','\_') - - x= g.db.query(User).options( - lazyload('*') - ).filter( - or_( - User.username.ilike(name), - User.original_username.ilike(name) - ) - ).first() - - if x: - return {name: False} - else: - return {name: True} - - -@app.get("/id/") -def user_id(id): - - user = get_account(int(id)) - return redirect(user.url) - -@app.get("/u/") -def redditor_moment_redirect(username): - return redirect(f"/@{username}") - -@app.get("/@/followers") -@auth_required -def followers(username, v): - - - u = get_user(username, v=v) - users = [x.user for x in u.followers] - return render_template("followers.html", v=v, u=u, users=users) - -@app.get("/views") -@auth_required -def visitors(v): - if v.admin_level < 1 and not v.patron: return render_template("errors/patron.html", v=v) - viewers=sorted(v.viewers, key = lambda x: x.last_view_utc, reverse=True) - return render_template("viewers.html", v=v, viewers=viewers) - - -@app.get("/@") -@app.get("/logged_out/@") -@auth_desired -def u_username(username, v=None): - - - if not v and not request.path.startswith('/logged_out'): return redirect(f"/logged_out{request.full_path}") - - if v and request.path.startswith('/logged_out'): v = None - - - - u = get_user(username, v=v) - - - if username != u.username: - return redirect(request.path.replace(username, u.username)) - - if u.reserved: - if request.headers.get("Authorization"): return {"error": f"That username is reserved for: {u.reserved}"} - else: return render_template("userpage_reserved.html", u=u, v=v) - - if v and u.id != v.id: - view = g.db.query(ViewerRelationship).options(lazyload('*')).filter( - and_( - ViewerRelationship.viewer_id == v.id, - ViewerRelationship.user_id == u.id - ) - ).first() - - if view: - view.last_view_utc = g.timestamp - else: - view = ViewerRelationship(user_id = u.id, - viewer_id = v.id) - - g.db.add(view) - g.db.commit() - - - if u.is_private and (not v or (v.id != u.id and v.admin_level < 3)): - - if v and u.id == 253: - if int(time.time()) - v.rent_utc > 600: - if request.headers.get("Authorization"): return {"error": "That userpage is private"} - else: return render_template("userpage_private.html", time=int(time.time()), u=u, v=v) - else: - if request.headers.get("Authorization"): return {"error": "That userpage is private"} - else: return render_template("userpage_private.html", time=int(time.time()), u=u, v=v) - - - if hasattr(u, 'is_blocking') and u.is_blocking and (not v or v.admin_level < 3): - if request.headers.get("Authorization"): return {"error": f"You are blocking @{u.username}."} - else: return render_template("userpage_blocking.html", u=u, v=v) - - - if hasattr(u, 'is_blocked') and u.is_blocked and (not v or v.admin_level < 3): - if request.headers.get("Authorization"): return {"error": "This person is blocking you."} - else: return render_template("userpage_blocked.html", u=u, v=v) - - - sort = request.values.get("sort", "new") - t = request.values.get("t", "all") - page = int(request.values.get("page", "1")) - page = max(page, 1) - - ids = u.userpagelisting(v=v, page=page, sort=sort, t=t) - - next_exists = (len(ids) > 25) - ids = ids[:25] - - # If page 1, check for sticky - if page == 1: - sticky = [] - sticky = g.db.query(Submission).options(lazyload('*')).filter_by(is_pinned=True, author_id=u.id).all() - if sticky: - for p in sticky: - ids = [p.id] + ids - - listing = get_posts(ids, v=v) - - if u.unban_utc: - if request.headers.get("Authorization"): {"data": [x.json for x in listing]} - else: return render_template("userpage.html", - unban=u.unban_string, - u=u, - v=v, - listing=listing, - page=page, - sort=sort, - t=t, - next_exists=next_exists, - is_following=(v and u.has_follower(v))) - - - - if request.headers.get("Authorization"): return {"data": [x.json for x in listing]} - else: return render_template("userpage.html", - u=u, - v=v, - listing=listing, - page=page, - sort=sort, - t=t, - next_exists=next_exists, - is_following=(v and u.has_follower(v))) - - -@app.get("/@/comments") -@app.get("/logged_out/@/comments") -@auth_desired -def u_username_comments(username, v=None): - - - if not v and not request.path.startswith('/logged_out'): return redirect(f"/logged_out{request.full_path}") - - if v and request.path.startswith('/logged_out'): v = None - - - - user = get_user(username, v=v) - - - if username != user.username: return redirect(f'{user.url}/comments') - - u = user - - if u.reserved: - if request.headers.get("Authorization"): return {"error": f"That username is reserved for: {u.reserved}"} - else: return render_template("userpage_reserved.html", - u=u, - v=v) - - - if u.is_private and (not v or (v.id != u.id and v.admin_level < 3)): - if v and u.id == 253: - if int(time.time()) - v.rent_utc > 600: - if request.headers.get("Authorization"): return {"error": "That userpage is private"} - else: return render_template("userpage_private.html", time=int(time.time()), u=u, v=v) - else: - if request.headers.get("Authorization"): return {"error": "That userpage is private"} - else: return render_template("userpage_private.html", time=int(time.time()), u=u, v=v) - - if hasattr(u, 'is_blocking') and u.is_blocking and (not v or v.admin_level < 3): - if request.headers.get("Authorization"): return {"error": f"You are blocking @{u.username}."} - else: return render_template("userpage_blocking.html", - u=u, - v=v) - - if hasattr(u, 'is_blocked') and u.is_blocked and (not v or v.admin_level < 3): - if request.headers.get("Authorization"): return {"error": "This person is blocking you."} - else: return render_template("userpage_blocked.html", - u=u, - v=v) - - - page = int(request.values.get("page", "1")) - sort=request.values.get("sort","new") - t=request.values.get("t","all") - - - comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.author_id == u.id, Comment.parent_submission != None) - - if (not v) or (v.id != u.id and v.admin_level == 0): - comments = comments.filter(Comment.deleted_utc == 0) - comments = comments.filter(Comment.is_banned == False) - - now = int(time.time()) - if t == 'hour': - cutoff = now - 3600 - elif t == 'day': - cutoff = now - 86400 - elif t == 'week': - cutoff = now - 604800 - elif t == 'month': - cutoff = now - 2592000 - elif t == 'year': - cutoff = now - 31536000 - else: - cutoff = 0 - comments = comments.filter(Comment.created_utc >= cutoff) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc.asc()) - elif sort == "controversial": - comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) - elif sort == "top": - comments = comments.order_by(Comment.downvotes - Comment.upvotes) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - comments = comments.offset(25 * (page - 1)).limit(26).all() - ids = [x.id for x in comments] - - next_exists = (len(ids) > 25) - ids = ids[:25] - - listing = get_comments(ids, v=v) - - is_following = (v and user.has_follower(v)) - - if request.headers.get("Authorization"): return {"data": [c.json for c in listing]} - else: return render_template("userpage_comments.html", u=user, v=v, listing=listing, page=page, sort=sort, t=t,next_exists=next_exists, is_following=is_following, standalone=True) - - -@app.get("/@/info") -@auth_desired -def u_username_info(username, v=None): - - user=get_user(username, v=v) - - if hasattr(user, 'is_blocking') and user.is_blocking: - return {"error": "You're blocking this user."}, 401 - elif hasattr(user, 'is_blocked') and user.is_blocked: - return {"error": "This user is blocking you."}, 403 - - return user.json - - -@app.post("/follow/") -@limiter.limit("1/second") -@auth_required -def follow_user(username, v): - - target = get_user(username) - - if target.id==v.id: return {"error": "You can't follow yourself!"}, 400 - - if g.db.query(Follow).options(lazyload('*')).filter_by(user_id=v.id, target_id=target.id).first(): return {"message": "User followed!"} - - new_follow = Follow(user_id=v.id, target_id=target.id) - g.db.add(new_follow) - - g.db.flush() - target.stored_subscriber_count = g.db.query(Follow.id).options(lazyload('*')).filter_by(target_id=target.id).count() - g.db.add(target) - - existing = g.db.query(Notification).options(lazyload('*')).filter_by(followsender=v.id, user_id=target.id).first() - if not existing: send_follow_notif(v.id, target.id, f"@{v.username} has followed you!") - - g.db.commit() - - return {"message": "User followed!"} - -@app.post("/unfollow/") -@limiter.limit("1/second") -@auth_required -def unfollow_user(username, v): - - target = get_user(username) - - if target.id == 995: abort(403) - - follow = g.db.query(Follow).options(lazyload('*')).filter_by(user_id=v.id, target_id=target.id).first() - - if not follow: return {"message": "User unfollowed!"} - - g.db.delete(follow) - - g.db.flush() - target.stored_subscriber_count = g.db.query(Follow.id).options(lazyload('*')).filter_by(target_id=target.id).count() - g.db.add(target) - - existing = g.db.query(Notification).options(lazyload('*')).filter_by(unfollowsender=v.id, user_id=target.id).first() - if not existing: send_unfollow_notif(v.id, target.id, f"@{v.username} has unfollowed you!") - - g.db.commit() - - return {"message": "User unfollowed!"} - -@app.post("/remove_follow/") -@limiter.limit("1/second") -@auth_required -def remove_follow(username, v): - target = get_user(username) - - follow = g.db.query(Follow).options(lazyload('*')).filter_by(user_id=target.id, target_id=v.id).first() - - if not follow: return {"message": "Follower removed!"} - - g.db.delete(follow) - - g.db.flush() - v.stored_subscriber_count = g.db.query(Follow.id).options(lazyload('*')).filter_by(target_id=v.id).count() - g.db.add(v) - - existing = g.db.query(Notification).options(lazyload('*')).filter_by(removefollowsender=v.id, user_id=target.id).first() - if not existing: send_unfollow_notif(v.id, target.id, f"@{v.username} has removed your follow!") - - g.db.commit() - - return {"message": "Follower removed!"} - - -@app.get("/uid//pic/profile") -@limiter.exempt -def user_profile_uid(id): - try: id = int(id) - except: - try: id = int(id, 36) - except: abort(404) - x=get_account(id) - return redirect(x.profile_url) - - -@app.get("/@/saved/posts") -@auth_required -def saved_posts(v, username): - - page=int(request.values.get("page",1)) - - ids=v.saved_idlist(page=page) - - next_exists=len(ids)>25 - - ids=ids[:25] - - listing = get_posts(ids, v=v) - - if request.headers.get("Authorization"): return {"data": [x.json for x in listing]} - else: return render_template("userpage.html", - u=v, - v=v, - listing=listing, - page=page, - next_exists=next_exists, - ) - - -@app.get("/@/saved/comments") -@auth_required -def saved_comments(v, username): - - page=int(request.values.get("page",1)) - - firstrange = 25 * (page - 1) - secondrange = firstrange+26 - - ids=v.saved_comment_idlist()[firstrange:secondrange] - - next_exists=len(ids) > 25 - - ids=ids[:25] - - listing = get_comments(ids, v=v) - - - if request.headers.get("Authorization"): return {"data": [x.json for x in listing]} - else: return render_template("userpage_comments.html", - u=v, - v=v, - listing=listing, - page=page, - next_exists=next_exists, - standalone=True) +import qrcode +import io +import time +import math +from files.classes.user import ViewerRelationship +from files.helpers.alerts import * +from files.helpers.sanitize import * +from files.helpers.markdown import * +from files.helpers.const import * +from files.mail import * +from flask import * +from files.__main__ import app, limiter +from pusher_push_notifications import PushNotifications + +site = environ.get("DOMAIN").strip() + +beams_client = PushNotifications( + instance_id=PUSHER_INSTANCE_ID, + secret_key=PUSHER_KEY, +) + + +@app.post("/pay_rent") +@limiter.limit("1/second") +@auth_required +def pay_rent(v): + if v.coins < 500: return "You must have more than 500 coins." + v.coins -= 500 + v.rent_utc = int(time.time()) + g.db.add(v) + u = get_account(253) + u.coins += 500 + g.db.add(u) + send_notification(NOTIFICATIONS_ACCOUNT, u, f"@{v.username} has paid rent!") + g.db.commit() + return {"message": "Rent paid!"} + + +@app.post("/steal") +@limiter.limit("1/second") +@is_not_banned +def steal(v): + if int(time.time()) - v.created_utc < 604800: + return "You must have an account older than 1 week in order to attempt stealing." + if v.coins < 5000: + return "You must have more than 5000 coins in order to attempt stealing." + u = get_account(253) + if random.randint(1, 10) < 5: + v.coins += 700 + v.steal_utc = int(time.time()) + g.db.add(v) + u.coins -= 700 + g.db.add(u) + send_notification(NOTIFICATIONS_ACCOUNT, u, f"Some [grubby little rentoid](/@{v.username}) has absconded with 700 of your hard-earned dramacoins to fuel his Funko Pop addiction. Stop being so trusting.") + send_notification(NOTIFICATIONS_ACCOUNT, v, f"You have successfully shorted your heroic landlord 700 dramacoins in rent. You're slightly less materially poor, but somehow even moreso morally. Are you proud of yourself?") + g.db.commit() + return {"message": "Attempt successful!"} + + else: + if random.random() < 0.15: + send_notification(NOTIFICATIONS_ACCOUNT, u, f"You caught [this sniveling little renthog](/@{v.username}) trying to rob you. After beating him within an inch of his life, you sold his Nintendo Switch for 500 dramacoins and called the cops. He was sentenced to one (1) day in renthog prison.") + send_notification(NOTIFICATIONS_ACCOUNT, v, f"The ever-vigilant landchad has caught you trying to steal his hard-earned rent money. The police take you away and laugh as you impotently stutter A-ACAB :sob: You are fined 500 dramacoins and sentenced to one (1) day in renthog prison.") + v.ban(days=1, reason="Jailed thief") + v.fail_utc = int(time.time()) + else: + send_notification(NOTIFICATIONS_ACCOUNT, u, f"You caught [this sniveling little renthog](/@{v.username}) trying to rob you. After beating him within an inch of his life, you showed mercy in exchange for a 500 dramacoin tip. This time.") + send_notification(NOTIFICATIONS_ACCOUNT, v, f"The ever-vigilant landchad has caught you trying to steal his hard-earned rent money. You were able to convince him to spare your life with a 500 dramacoin tip. This time.") + v.fail2_utc = int(time.time()) + v.coins -= 500 + g.db.add(v) + u.coins += 500 + g.db.add(u) + g.db.commit() + return {"message": "Attempt failed!"} + + +@app.get("/rentoids") +@auth_desired +def rentoids(v): + users = g.db.query(User).options(lazyload('*')).filter(User.rent_utc > 0).all() + return render_template("rentoids.html", v=v, users=users) + + +@app.get("/thiefs") +@auth_desired +def thiefs(v): + successful = g.db.query(User).options(lazyload('*')).filter(User.steal_utc > 0).all() + failed = g.db.query(User).options(lazyload('*')).filter(User.fail_utc > 0).all() + failed2 = g.db.query(User).options(lazyload('*')).filter(User.fail2_utc > 0).all() + return render_template("thiefs.html", v=v, successful=successful, failed=failed, failed2=failed2) + + +@app.post("/@/suicide") +@limiter.limit("1/second") +@auth_required +def suicide(v, username): + t = int(time.time()) + if v.admin_level == 0 and t - v.suicide_utc < 86400: return {"message": "You're on 1-day cooldown!"} + user = get_user(username) + suicide = f"Hi there,\n\nA [concerned user]({v.url}) reached out to us about you.\n\nWhen you're in the middle of something painful, it may feel like you don't have a lot of options. But whatever you're going through, you deserve help and there are people who are here for you.\n\nThere are resources available in your area that are free, confidential, and available 24/7:\n\n- Call, Text, or Chat with Canada's [Crisis Services Canada](https://www.crisisservicescanada.ca/en/)\n- Call, Email, or Visit the UK's [Samaritans](https://www.samaritans.org/)\n- Text CHAT to America's [Crisis Text Line](https://www.crisistextline.org/) at 741741.\nIf you don't see a resource in your area above, the moderators at r/SuicideWatch keep a comprehensive list of resources and hotlines for people organized by location. Find Someone Now\n\nIf you think you may be depressed or struggling in another way, don't ignore it or brush it aside. Take yourself and your feelings seriously, and reach out to someone.\n\nIt may not feel like it, but you have options. There are people available to listen to you, and ways to move forward.\n\nYour fellow users care about you and there are people who want to help." + send_notification(NOTIFICATIONS_ACCOUNT, user, suicide) + v.suicide_utc = t + g.db.add(v) + g.db.commit() + return {"message": "Help message sent!"} + + +@app.get("/@/coins") +@auth_required +def get_coins(v, username): + user = get_user(username) + if user != None: return {"coins": user.coins}, 200 + else: return {"error": "invalid_user"}, 404 + +@app.post("/@/transfer_coins") +@limiter.limit("1/second") +@is_not_banned +@validate_formkey +def transfer_coins(v, username): + TAX_RECEIVER_ID = 747 + TAX_RATE = 0.01 + + receiver = g.db.query(User).filter_by(username=username).first() + tax_receiver = g.db.query(User).filter_by(id=TAX_RECEIVER_ID).first() + + if receiver is None: return {"error": "That user doesn't exist."}, 404 + + if receiver.id != v.id: + amount = request.values.get("amount", "").strip() + amount = int(amount) if amount.isdigit() else None + + if amount is None or amount <= 0: return {"error": f"Invalid amount of {app.config['COINS_NAME']}."}, 400 + if v.coins < amount: return {"error": f"You don't have enough {app.config['COINS_NAME']}"}, 400 + if amount < 100: return {"error": f"You have to gift at least 100 {app.config['COINS_NAME']}."}, 400 + + tax = math.ceil(amount*TAX_RATE) + v.coins -= amount + receiver.coins += amount-tax + tax_receiver.coins += tax + g.db.add(receiver) + g.db.add(tax_receiver) + g.db.add(v) + + transfer_message = f"🤑 [@{v.username}]({v.url}) has gifted you {amount} {app.config['COINS_NAME']}!" + send_notification(NOTIFICATIONS_ACCOUNT, receiver, transfer_message) + + log_message = f"[@{v.username}]({v.url}) has transferred {amount} {app.config['COINS_NAME']} to [@{receiver.username}]({receiver.url})" + send_notification(NOTIFICATIONS_ACCOUNT, TAX_RECEIVER_ID, log_message) + + g.db.commit() + + return {"message": f"{amount-tax} {app.config['COINS_NAME']} transferred!"}, 200 + + return {"message": f"You can't transfer {app.config['COINS_NAME']} to yourself!"}, 400 + + +@app.get("/leaderboard") +@auth_desired +def leaderboard(v): + users = g.db.query(User).options(lazyload('*')) + users1 = users.order_by(User.coins.desc()).limit(25).all() + users2 = users.order_by(User.stored_subscriber_count.desc()).limit(10).all() + users3 = users.order_by(User.post_count.desc()).limit(10).all() + users4 = users.order_by(User.comment_count.desc()).limit(10).all() + users5 = users.order_by(User.received_award_count.desc()).limit(10).all() + if 'pcmemes.net' in request.host: + users6 = users.order_by(User.basedcount.desc()).limit(10).all() + return render_template("leaderboard.html", v=v, users1=users1, users2=users2, users3=users3, users4=users4, users5=users5, users6=users6) + return render_template("leaderboard.html", v=v, users1=users1, users2=users2, users3=users3, users4=users4, users5=users5) + + +@app.get("/@/css") +def get_css(username): + user = get_user(username) + if user.css: css = user.css + else: css = "" + resp=make_response(css) + resp.headers.add("Content-Type", "text/css") + return resp + +@app.get("/@/profilecss") +def get_profilecss(username): + user = get_user(username) + if user.profilecss: profilecss = user.profilecss + else: profilecss = "" + resp=make_response(profilecss) + resp.headers.add("Content-Type", "text/css") + return resp + +@app.get("/songs/") +def songs(id): + try: id = int(id) + except: return "", 400 + user = g.db.query(User).options(lazyload('*')).filter_by(id=id).first() + if user and user.song: return redirect(f"/song/{user.song}.mp3") + else: abort(404) + +@app.get("/song/") +def song(song): + resp = make_response(send_from_directory('/songs/', song)) + resp.headers.remove("Cache-Control") + resp.headers.add("Cache-Control", "public, max-age=2628000") + return resp + +@app.post("/subscribe/") +@limiter.limit("1/second") +@auth_required +def subscribe(v, post_id): + new_sub = Subscription(user_id=v.id, submission_id=post_id) + g.db.add(new_sub) + g.db.commit() + return {"message": "Post subscribed!"} + +@app.post("/unsubscribe/") +@limiter.limit("1/second") +@auth_required +def unsubscribe(v, post_id): + sub=g.db.query(Subscription).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post_id).first() + if sub: + g.db.delete(sub) + g.db.commit() + return {"message": "Post unsubscribed!"} + +@app.post("/@/message") +@limiter.limit("1/second") +@limiter.limit("10/hour") +@auth_required +def message2(v, username): + + user = get_user(username, v=v) + if hasattr(user, 'is_blocking') and user.is_blocking: return {"error": "You're blocking this user."}, 403 + + if v.admin_level <= 1: + if hasattr(user, 'is_blocked') and user.is_blocked: return {"error": "This user is blocking you."}, 403 + + message = request.values.get("message", "").strip()[:1000].strip() + + existing = g.db.query(Comment).options(lazyload('*')).filter(Comment.author_id == v.id, + Comment.sentto == user.id, + Comment.body == message, + ).first() + if existing: return redirect('/notifications?messages=true') + + text = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', message) + + text_html = Renderer().render(mistletoe.Document(text)) + + text_html = sanitize(text_html, True) + + new_comment = Comment(author_id=v.id, + parent_submission=None, + level=1, + sentto=user.id, + body=text, + body_html=text_html, + ) + g.db.add(new_comment) + + g.db.flush() + + + notif = Notification(comment_id=new_comment.id, user_id=user.id) + g.db.add(notif) + + + try: + beams_client.publish_to_interests( + interests=[str(user.id)], + publish_body={ + 'web': { + 'notification': { + 'title': f'New message from @{v.username}', + 'body': message, + 'deep_link': f'https://{site}/notifications', + }, + }, + }, + ) + except Exception as e: + print(e) + + g.db.commit() + + return redirect(f"/@{username}") + + +@app.post("/reply") +@limiter.limit("1/second") +@limiter.limit("6/minute") +@auth_required +def messagereply(v): + + message = request.values.get("body", "").strip()[:1000].strip() + id = int(request.values.get("parent_id")) + parent = get_comment(id, v=v) + user = parent.author + message = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', message) + + text_html = Renderer().render(mistletoe.Document(message)) + text_html = sanitize(text_html, True) + new_comment = Comment(author_id=v.id, + parent_submission=None, + parent_comment_id=id, + level=parent.level + 1, + sentto=user.id, + body=message, + body_html=text_html, + ) + g.db.add(new_comment) + g.db.flush() + + notif = Notification(comment_id=new_comment.id, user_id=user.id) + g.db.add(notif) + + g.db.commit() + + return render_template("comments.html", v=v, comments=[new_comment]) + +@app.get("/2faqr/") +@auth_required +def mfa_qr(secret, v): + x = pyotp.TOTP(secret) + qr = qrcode.QRCode( + error_correction=qrcode.constants.ERROR_CORRECT_L + ) + qr.add_data(x.provisioning_uri(v.username, issuer_name=app.config["SITE_NAME"])) + img = qr.make_image(fill_color="#000000", back_color="white") + + mem = io.BytesIO() + + img.save(mem, format="PNG") + mem.seek(0, 0) + return send_file(mem, mimetype="image/png", as_attachment=False) + + +@app.get("/is_available/") +@auth_desired +def api_is_available(name, v): + + name=name.strip() + + if len(name)<3 or len(name)>25: + return {name:False} + + name=name.replace('_','\_') + + x= g.db.query(User).options( + lazyload('*') + ).filter( + or_( + User.username.ilike(name), + User.original_username.ilike(name) + ) + ).first() + + if x: + return {name: False} + else: + return {name: True} + + +@app.get("/id/") +def user_id(id): + + user = get_account(int(id)) + return redirect(user.url) + +@app.get("/u/") +def redditor_moment_redirect(username): + return redirect(f"/@{username}") + +@app.get("/@/followers") +@auth_required +def followers(username, v): + + + u = get_user(username, v=v) + users = [x.user for x in u.followers] + return render_template("followers.html", v=v, u=u, users=users) + +@app.get("/views") +@auth_required +def visitors(v): + if v.admin_level < 1 and not v.patron: return render_template("errors/patron.html", v=v) + viewers=sorted(v.viewers, key = lambda x: x.last_view_utc, reverse=True) + return render_template("viewers.html", v=v, viewers=viewers) + + +@app.get("/@") +@app.get("/logged_out/@") +@auth_desired +def u_username(username, v=None): + + + if not v and not request.path.startswith('/logged_out'): return redirect(f"/logged_out{request.full_path}") + + if v and request.path.startswith('/logged_out'): v = None + + + + u = get_user(username, v=v) + + + if username != u.username: + return redirect(request.path.replace(username, u.username)) + + if u.reserved: + if request.headers.get("Authorization"): return {"error": f"That username is reserved for: {u.reserved}"} + else: return render_template("userpage_reserved.html", u=u, v=v) + + if v and u.id != v.id: + view = g.db.query(ViewerRelationship).options(lazyload('*')).filter( + and_( + ViewerRelationship.viewer_id == v.id, + ViewerRelationship.user_id == u.id + ) + ).first() + + if view: + view.last_view_utc = g.timestamp + else: + view = ViewerRelationship(user_id = u.id, + viewer_id = v.id) + + g.db.add(view) + g.db.commit() + + + if u.is_private and (not v or (v.id != u.id and v.admin_level < 3)): + + if v and u.id == 253: + if int(time.time()) - v.rent_utc > 600: + if request.headers.get("Authorization"): return {"error": "That userpage is private"} + else: return render_template("userpage_private.html", time=int(time.time()), u=u, v=v) + else: + if request.headers.get("Authorization"): return {"error": "That userpage is private"} + else: return render_template("userpage_private.html", time=int(time.time()), u=u, v=v) + + + if hasattr(u, 'is_blocking') and u.is_blocking and (not v or v.admin_level < 3): + if request.headers.get("Authorization"): return {"error": f"You are blocking @{u.username}."} + else: return render_template("userpage_blocking.html", u=u, v=v) + + + if hasattr(u, 'is_blocked') and u.is_blocked and (not v or v.admin_level < 3): + if request.headers.get("Authorization"): return {"error": "This person is blocking you."} + else: return render_template("userpage_blocked.html", u=u, v=v) + + + sort = request.values.get("sort", "new") + t = request.values.get("t", "all") + page = int(request.values.get("page", "1")) + page = max(page, 1) + + ids = u.userpagelisting(v=v, page=page, sort=sort, t=t) + + next_exists = (len(ids) > 25) + ids = ids[:25] + + # If page 1, check for sticky + if page == 1: + sticky = [] + sticky = g.db.query(Submission).options(lazyload('*')).filter_by(is_pinned=True, author_id=u.id).all() + if sticky: + for p in sticky: + ids = [p.id] + ids + + listing = get_posts(ids, v=v) + + if u.unban_utc: + if request.headers.get("Authorization"): {"data": [x.json for x in listing]} + else: return render_template("userpage.html", + unban=u.unban_string, + u=u, + v=v, + listing=listing, + page=page, + sort=sort, + t=t, + next_exists=next_exists, + is_following=(v and u.has_follower(v))) + + + + if request.headers.get("Authorization"): return {"data": [x.json for x in listing]} + else: return render_template("userpage.html", + u=u, + v=v, + listing=listing, + page=page, + sort=sort, + t=t, + next_exists=next_exists, + is_following=(v and u.has_follower(v))) + + +@app.get("/@/comments") +@app.get("/logged_out/@/comments") +@auth_desired +def u_username_comments(username, v=None): + + + if not v and not request.path.startswith('/logged_out'): return redirect(f"/logged_out{request.full_path}") + + if v and request.path.startswith('/logged_out'): v = None + + + + user = get_user(username, v=v) + + + if username != user.username: return redirect(f'{user.url}/comments') + + u = user + + if u.reserved: + if request.headers.get("Authorization"): return {"error": f"That username is reserved for: {u.reserved}"} + else: return render_template("userpage_reserved.html", + u=u, + v=v) + + + if u.is_private and (not v or (v.id != u.id and v.admin_level < 3)): + if v and u.id == 253: + if int(time.time()) - v.rent_utc > 600: + if request.headers.get("Authorization"): return {"error": "That userpage is private"} + else: return render_template("userpage_private.html", time=int(time.time()), u=u, v=v) + else: + if request.headers.get("Authorization"): return {"error": "That userpage is private"} + else: return render_template("userpage_private.html", time=int(time.time()), u=u, v=v) + + if hasattr(u, 'is_blocking') and u.is_blocking and (not v or v.admin_level < 3): + if request.headers.get("Authorization"): return {"error": f"You are blocking @{u.username}."} + else: return render_template("userpage_blocking.html", + u=u, + v=v) + + if hasattr(u, 'is_blocked') and u.is_blocked and (not v or v.admin_level < 3): + if request.headers.get("Authorization"): return {"error": "This person is blocking you."} + else: return render_template("userpage_blocked.html", + u=u, + v=v) + + + page = int(request.values.get("page", "1")) + sort=request.values.get("sort","new") + t=request.values.get("t","all") + + + comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.author_id == u.id, Comment.parent_submission != None) + + if (not v) or (v.id != u.id and v.admin_level == 0): + comments = comments.filter(Comment.deleted_utc == 0) + comments = comments.filter(Comment.is_banned == False) + + now = int(time.time()) + if t == 'hour': + cutoff = now - 3600 + elif t == 'day': + cutoff = now - 86400 + elif t == 'week': + cutoff = now - 604800 + elif t == 'month': + cutoff = now - 2592000 + elif t == 'year': + cutoff = now - 31536000 + else: + cutoff = 0 + comments = comments.filter(Comment.created_utc >= cutoff) + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc.asc()) + elif sort == "controversial": + comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes) + elif sort == "top": + comments = comments.order_by(Comment.downvotes - Comment.upvotes) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + comments = comments.offset(25 * (page - 1)).limit(26).all() + ids = [x.id for x in comments] + + next_exists = (len(ids) > 25) + ids = ids[:25] + + listing = get_comments(ids, v=v) + + is_following = (v and user.has_follower(v)) + + if request.headers.get("Authorization"): return {"data": [c.json for c in listing]} + else: return render_template("userpage_comments.html", u=user, v=v, listing=listing, page=page, sort=sort, t=t,next_exists=next_exists, is_following=is_following, standalone=True) + + +@app.get("/@/info") +@auth_desired +def u_username_info(username, v=None): + + user=get_user(username, v=v) + + if hasattr(user, 'is_blocking') and user.is_blocking: + return {"error": "You're blocking this user."}, 401 + elif hasattr(user, 'is_blocked') and user.is_blocked: + return {"error": "This user is blocking you."}, 403 + + return user.json + + +@app.post("/follow/") +@limiter.limit("1/second") +@auth_required +def follow_user(username, v): + + target = get_user(username) + + if target.id==v.id: return {"error": "You can't follow yourself!"}, 400 + + if g.db.query(Follow).options(lazyload('*')).filter_by(user_id=v.id, target_id=target.id).first(): return {"message": "User followed!"} + + new_follow = Follow(user_id=v.id, target_id=target.id) + g.db.add(new_follow) + + g.db.flush() + target.stored_subscriber_count = g.db.query(Follow.id).options(lazyload('*')).filter_by(target_id=target.id).count() + g.db.add(target) + + existing = g.db.query(Notification).options(lazyload('*')).filter_by(followsender=v.id, user_id=target.id).first() + if not existing: send_follow_notif(v.id, target.id, f"@{v.username} has followed you!") + + g.db.commit() + + return {"message": "User followed!"} + +@app.post("/unfollow/") +@limiter.limit("1/second") +@auth_required +def unfollow_user(username, v): + + target = get_user(username) + + if target.id == 995: abort(403) + + follow = g.db.query(Follow).options(lazyload('*')).filter_by(user_id=v.id, target_id=target.id).first() + + if not follow: return {"message": "User unfollowed!"} + + g.db.delete(follow) + + g.db.flush() + target.stored_subscriber_count = g.db.query(Follow.id).options(lazyload('*')).filter_by(target_id=target.id).count() + g.db.add(target) + + existing = g.db.query(Notification).options(lazyload('*')).filter_by(unfollowsender=v.id, user_id=target.id).first() + if not existing: send_unfollow_notif(v.id, target.id, f"@{v.username} has unfollowed you!") + + g.db.commit() + + return {"message": "User unfollowed!"} + +@app.post("/remove_follow/") +@limiter.limit("1/second") +@auth_required +def remove_follow(username, v): + target = get_user(username) + + follow = g.db.query(Follow).options(lazyload('*')).filter_by(user_id=target.id, target_id=v.id).first() + + if not follow: return {"message": "Follower removed!"} + + g.db.delete(follow) + + g.db.flush() + v.stored_subscriber_count = g.db.query(Follow.id).options(lazyload('*')).filter_by(target_id=v.id).count() + g.db.add(v) + + existing = g.db.query(Notification).options(lazyload('*')).filter_by(removefollowsender=v.id, user_id=target.id).first() + if not existing: send_unfollow_notif(v.id, target.id, f"@{v.username} has removed your follow!") + + g.db.commit() + + return {"message": "Follower removed!"} + + +@app.get("/uid//pic/profile") +@limiter.exempt +def user_profile_uid(id): + try: id = int(id) + except: + try: id = int(id, 36) + except: abort(404) + x=get_account(id) + return redirect(x.profile_url) + + +@app.get("/@/saved/posts") +@auth_required +def saved_posts(v, username): + + page=int(request.values.get("page",1)) + + ids=v.saved_idlist(page=page) + + next_exists=len(ids)>25 + + ids=ids[:25] + + listing = get_posts(ids, v=v) + + if request.headers.get("Authorization"): return {"data": [x.json for x in listing]} + else: return render_template("userpage.html", + u=v, + v=v, + listing=listing, + page=page, + next_exists=next_exists, + ) + + +@app.get("/@/saved/comments") +@auth_required +def saved_comments(v, username): + + page=int(request.values.get("page",1)) + + firstrange = 25 * (page - 1) + secondrange = firstrange+26 + + ids=v.saved_comment_idlist()[firstrange:secondrange] + + next_exists=len(ids) > 25 + + ids=ids[:25] + + listing = get_comments(ids, v=v) + + + if request.headers.get("Authorization"): return {"data": [x.json for x in listing]} + else: return render_template("userpage_comments.html", + u=v, + v=v, + listing=listing, + page=page, + next_exists=next_exists, + standalone=True) diff --git a/files/routes/votes.py b/files/routes/votes.py old mode 100644 new mode 100755 index 32d879772..18dd13044 --- a/files/routes/votes.py +++ b/files/routes/votes.py @@ -1,199 +1,199 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from files.classes import * -from flask import * -from files.__main__ import app, limiter -from sqlalchemy.orm import joinedload - - -@app.get("/votes") -@auth_desired -def admin_vote_info_get(v): - - - link = request.values.get("link") - if not link: return render_template("votes.html", v=v) - - try: - if "t2_" in link: thing = get_post(int(link.split("t2_")[1]), v=v) - elif "t3_" in link: thing = get_comment(int(link.split("t3_")[1]), v=v) - else: abort(400) - except: abort(400) - - if isinstance(thing, Submission): - - ups = g.db.query(Vote - ).options(joinedload(Vote.user) - ).filter_by(submission_id=thing.id, vote_type=1 - ).order_by(Vote.id).all() - - downs = g.db.query(Vote - ).options(joinedload(Vote.user) - ).filter_by(submission_id=thing.id, vote_type=-1 - ).order_by(Vote.id).all() - - elif isinstance(thing, Comment): - - ups = g.db.query(CommentVote - ).options(joinedload(CommentVote.user) - ).filter_by(comment_id=thing.id, vote_type=1 - ).order_by(CommentVote.id).all() - - downs = g.db.query(CommentVote - ).options(joinedload(CommentVote.user) - ).filter_by(comment_id=thing.id, vote_type=-1 - ).order_by(CommentVote.id).all() - - else: abort(400) - - return render_template("votes.html", - v=v, - thing=thing, - ups=ups, - downs=downs,) - - - -@app.post("/vote/post//") -@auth_required -@validate_formkey -def api_vote_post(post_id, new, v): - - if new not in ["-1", "0", "1"]: abort(400) - - if request.headers.get("X-User-Type","") == "Bot": abort(403) - - new = int(new) - - post = get_post(post_id) - - existing = g.db.query(Vote).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id).first() - - if existing and existing.vote_type == new: return "", 204 - - if existing: - if existing.vote_type == 0 and new != 0: - post.author.coins += 1 - post.author.truecoins += 1 - g.db.add(post.author) - existing.vote_type = new - g.db.add(existing) - elif existing.vote_type != 0 and new == 0: - post.author.coins -= 1 - post.author.truecoins -= 1 - g.db.add(post.author) - g.db.delete(existing) - else: - existing.vote_type = new - g.db.add(existing) - elif new != 0: - post.author.coins += 1 - post.author.truecoins += 1 - g.db.add(post.author) - vote = Vote(user_id=v.id, - vote_type=new, - submission_id=post_id, - app_id=v.client.application.id if v.client else None - ) - g.db.add(vote) - - try: - g.db.flush() - post.upvotes = g.db.query(Vote.id).options(lazyload('*')).filter_by(submission_id=post.id, vote_type=1).count() - post.downvotes = g.db.query(Vote.id).options(lazyload('*')).filter_by(submission_id=post.id, vote_type=-1).count() - g.db.add(post) - g.db.commit() - except: g.db.rollback() - return "", 204 - -@app.post("/vote/comment//") -@auth_required -@validate_formkey -def api_vote_comment(comment_id, new, v): - - if new not in ["-1", "0", "1"]: abort(400) - - if request.headers.get("X-User-Type","") == "Bot": abort(403) - - new = int(new) - - try: comment_id = int(comment_id) - except: - try: comment_id = int(comment_id, 36) - except: abort(404) - - comment = get_comment(comment_id) - - existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first() - - if existing and existing.vote_type == new: return "", 204 - - if existing: - if existing.vote_type == 0 and new != 0: - comment.author.coins += 1 - comment.author.truecoins += 1 - g.db.add(comment.author) - existing.vote_type = new - g.db.add(existing) - elif existing.vote_type != 0 and new == 0: - comment.author.coins -= 1 - comment.author.truecoins -= 1 - g.db.add(comment.author) - g.db.delete(existing) - else: - existing.vote_type = new - g.db.add(existing) - elif new != 0: - comment.author.coins += 1 - comment.author.truecoins += 1 - g.db.add(comment.author) - vote = CommentVote(user_id=v.id, - vote_type=new, - comment_id=comment_id, - app_id=v.client.application.id if v.client else None - ) - - g.db.add(vote) - - try: - g.db.flush() - comment.upvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=1).count() - comment.downvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=-1).count() - g.db.add(comment) - g.db.commit() - except: g.db.rollback() - return "", 204 - - -@app.post("/vote/poll/") -@auth_required -def api_vote_poll(comment_id, v): - - vote = request.values.get("vote") - if vote == "true": new = 1 - elif vote == "false": new = 0 - else: abort(400) - - comment_id = int(comment_id) - comment = get_comment(comment_id) - - existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first() - - if existing and existing.vote_type == new: return "", 204 - - if existing: - if new == 1: - existing.vote_type = new - g.db.add(existing) - else: g.db.delete(existing) - elif new == 1: - vote = CommentVote(user_id=v.id, vote_type=new, comment_id=comment.id) - g.db.add(vote) - - try: - g.db.flush() - comment.upvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=1).count() - g.db.add(comment) - g.db.commit() - except: g.db.rollback() +from files.helpers.wrappers import * +from files.helpers.get import * +from files.classes import * +from flask import * +from files.__main__ import app, limiter +from sqlalchemy.orm import joinedload + + +@app.get("/votes") +@auth_desired +def admin_vote_info_get(v): + + + link = request.values.get("link") + if not link: return render_template("votes.html", v=v) + + try: + if "t2_" in link: thing = get_post(int(link.split("t2_")[1]), v=v) + elif "t3_" in link: thing = get_comment(int(link.split("t3_")[1]), v=v) + else: abort(400) + except: abort(400) + + if isinstance(thing, Submission): + + ups = g.db.query(Vote + ).options(joinedload(Vote.user) + ).filter_by(submission_id=thing.id, vote_type=1 + ).order_by(Vote.id).all() + + downs = g.db.query(Vote + ).options(joinedload(Vote.user) + ).filter_by(submission_id=thing.id, vote_type=-1 + ).order_by(Vote.id).all() + + elif isinstance(thing, Comment): + + ups = g.db.query(CommentVote + ).options(joinedload(CommentVote.user) + ).filter_by(comment_id=thing.id, vote_type=1 + ).order_by(CommentVote.id).all() + + downs = g.db.query(CommentVote + ).options(joinedload(CommentVote.user) + ).filter_by(comment_id=thing.id, vote_type=-1 + ).order_by(CommentVote.id).all() + + else: abort(400) + + return render_template("votes.html", + v=v, + thing=thing, + ups=ups, + downs=downs,) + + + +@app.post("/vote/post//") +@auth_required +@validate_formkey +def api_vote_post(post_id, new, v): + + if new not in ["-1", "0", "1"]: abort(400) + + if request.headers.get("X-User-Type","") == "Bot": abort(403) + + new = int(new) + + post = get_post(post_id) + + existing = g.db.query(Vote).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id).first() + + if existing and existing.vote_type == new: return "", 204 + + if existing: + if existing.vote_type == 0 and new != 0: + post.author.coins += 1 + post.author.truecoins += 1 + g.db.add(post.author) + existing.vote_type = new + g.db.add(existing) + elif existing.vote_type != 0 and new == 0: + post.author.coins -= 1 + post.author.truecoins -= 1 + g.db.add(post.author) + g.db.delete(existing) + else: + existing.vote_type = new + g.db.add(existing) + elif new != 0: + post.author.coins += 1 + post.author.truecoins += 1 + g.db.add(post.author) + vote = Vote(user_id=v.id, + vote_type=new, + submission_id=post_id, + app_id=v.client.application.id if v.client else None + ) + g.db.add(vote) + + try: + g.db.flush() + post.upvotes = g.db.query(Vote.id).options(lazyload('*')).filter_by(submission_id=post.id, vote_type=1).count() + post.downvotes = g.db.query(Vote.id).options(lazyload('*')).filter_by(submission_id=post.id, vote_type=-1).count() + g.db.add(post) + g.db.commit() + except: g.db.rollback() + return "", 204 + +@app.post("/vote/comment//") +@auth_required +@validate_formkey +def api_vote_comment(comment_id, new, v): + + if new not in ["-1", "0", "1"]: abort(400) + + if request.headers.get("X-User-Type","") == "Bot": abort(403) + + new = int(new) + + try: comment_id = int(comment_id) + except: + try: comment_id = int(comment_id, 36) + except: abort(404) + + comment = get_comment(comment_id) + + existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first() + + if existing and existing.vote_type == new: return "", 204 + + if existing: + if existing.vote_type == 0 and new != 0: + comment.author.coins += 1 + comment.author.truecoins += 1 + g.db.add(comment.author) + existing.vote_type = new + g.db.add(existing) + elif existing.vote_type != 0 and new == 0: + comment.author.coins -= 1 + comment.author.truecoins -= 1 + g.db.add(comment.author) + g.db.delete(existing) + else: + existing.vote_type = new + g.db.add(existing) + elif new != 0: + comment.author.coins += 1 + comment.author.truecoins += 1 + g.db.add(comment.author) + vote = CommentVote(user_id=v.id, + vote_type=new, + comment_id=comment_id, + app_id=v.client.application.id if v.client else None + ) + + g.db.add(vote) + + try: + g.db.flush() + comment.upvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=1).count() + comment.downvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=-1).count() + g.db.add(comment) + g.db.commit() + except: g.db.rollback() + return "", 204 + + +@app.post("/vote/poll/") +@auth_required +def api_vote_poll(comment_id, v): + + vote = request.values.get("vote") + if vote == "true": new = 1 + elif vote == "false": new = 0 + else: abort(400) + + comment_id = int(comment_id) + comment = get_comment(comment_id) + + existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first() + + if existing and existing.vote_type == new: return "", 204 + + if existing: + if new == 1: + existing.vote_type = new + g.db.add(existing) + else: g.db.delete(existing) + elif new == 1: + vote = CommentVote(user_id=v.id, vote_type=new, comment_id=comment.id) + g.db.add(vote) + + try: + g.db.flush() + comment.upvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=1).count() + g.db.add(comment) + g.db.commit() + except: g.db.rollback() return "", 204 \ No newline at end of file diff --git a/files/templates/admin/admin_home.html b/files/templates/admin/admin_home.html old mode 100644 new mode 100755 index ccf02398b..c49eea823 --- a/files/templates/admin/admin_home.html +++ b/files/templates/admin/admin_home.html @@ -1,61 +1,61 @@ -{% extends "default.html" %} - -{% block title %} -{{'SITE_NAME' | app_config}} - -{% endblock %} - -{% block content %} -

-

-

 Admin Tools

- -

Content

- - -

Users

- - -

Safety

- - -

Grant

- - -

API Access Control

- - -

Statistics

- - -

Configuration

- - -
- - -
- +{% extends "default.html" %} + +{% block title %} +{{'SITE_NAME' | app_config}} + +{% endblock %} + +{% block content %} +

+

+

 Admin Tools

+ +

Content

+ + +

Users

+ + +

Safety

+ + +

Grant

+ + +

API Access Control

+ + +

Statistics

+ + +

Configuration

+ + +
+ + +
+ {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/alt_votes.html b/files/templates/admin/alt_votes.html old mode 100644 new mode 100755 index 04de672ca..9e9ad185d --- a/files/templates/admin/alt_votes.html +++ b/files/templates/admin/alt_votes.html @@ -1,88 +1,88 @@ -{% extends "default.html" %} - -{% block title %} -{{'SITE_NAME' | app_config}} - -{% endblock %} - -{% block content %} -
-
-
-
-
-
Vote Info
- -
- - - - -
- -{% if u1 and u2 %} - - -

Analysis

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@{{u1.username}} only(% unique)Both@{{u2.username}} only (% unique)
Post Upvotes{{data['u1_only_post_ups']}} ({{data['u1_post_ups_unique']}}%){{data['both_post_ups']}}{{data['u2_only_post_ups']}} ({{data['u2_post_ups_unique']}}%)
Post Downvotes{{data['u1_only_post_downs']}} ({{data['u1_post_downs_unique']}}%){{data['both_post_downs']}}{{data['u2_only_post_downs']}} ({{data['u2_post_downs_unique']}}%)
Comment Upvotes{{data['u1_only_comment_ups']}} ({{data['u1_comment_ups_unique']}}%){{data['both_comment_ups']}}{{data['u2_only_comment_ups']}} ({{data['u2_comment_ups_unique']}}%)
Comment Downvotes{{data['u1_only_comment_downs']}} ({{data['u1_comment_downs_unique']}}%){{data['both_comment_downs']}}{{data['u2_only_comment_downs']}} ({{data['u2_comment_downs_unique']}}%)
- -

Link Accounts

- -{% if u2 in u1.alts %} -

Accounts are known alts of eachother.

-{% else %} - -

Two accounts controlled by different people should have most uniqueness percentages at or above 70-80%

-

A sockpuppet account will have its uniqueness percentages significantly lower.

- -Link Accounts -
- - - - -
- -{% endif %} - -{% endif %} - - +{% extends "default.html" %} + +{% block title %} +{{'SITE_NAME' | app_config}} + +{% endblock %} + +{% block content %} +
+
+
+
+
+
Vote Info
+ +
+ + + + +
+ +{% if u1 and u2 %} + + +

Analysis

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@{{u1.username}} only(% unique)Both@{{u2.username}} only (% unique)
Post Upvotes{{data['u1_only_post_ups']}} ({{data['u1_post_ups_unique']}}%){{data['both_post_ups']}}{{data['u2_only_post_ups']}} ({{data['u2_post_ups_unique']}}%)
Post Downvotes{{data['u1_only_post_downs']}} ({{data['u1_post_downs_unique']}}%){{data['both_post_downs']}}{{data['u2_only_post_downs']}} ({{data['u2_post_downs_unique']}}%)
Comment Upvotes{{data['u1_only_comment_ups']}} ({{data['u1_comment_ups_unique']}}%){{data['both_comment_ups']}}{{data['u2_only_comment_ups']}} ({{data['u2_comment_ups_unique']}}%)
Comment Downvotes{{data['u1_only_comment_downs']}} ({{data['u1_comment_downs_unique']}}%){{data['both_comment_downs']}}{{data['u2_only_comment_downs']}} ({{data['u2_comment_downs_unique']}}%)
+ +

Link Accounts

+ +{% if u2 in u1.alts %} +

Accounts are known alts of eachother.

+{% else %} + +

Two accounts controlled by different people should have most uniqueness percentages at or above 70-80%

+

A sockpuppet account will have its uniqueness percentages significantly lower.

+ +Link Accounts +
+ + + + +
+ +{% endif %} + +{% endif %} + + {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/app.html b/files/templates/admin/app.html old mode 100644 new mode 100755 index 6c9573e75..59da65439 --- a/files/templates/admin/app.html +++ b/files/templates/admin/app.html @@ -1,72 +1,72 @@ -{% extends "default.html" %} - -{% block title %} -API App Administration - -{% endblock %} - -{% block content %} - -
-
-
- -
-
-
- -
-
- - - - - - - - - - - -
-
- -
- -
- -{% if listing %} - {% include "submission_listing.html" %} -{% elif comments %} - {% include "comments.html" %} -{% endif %} - -
-
- - - - - - +{% extends "default.html" %} + +{% block title %} +API App Administration + +{% endblock %} + +{% block content %} + +
+
+
+ +
+
+
+ +
+
+ + + + + + + + + + + +
+
+ +
+ +
+ +{% if listing %} + {% include "submission_listing.html" %} +{% elif comments %} + {% include "comments.html" %} +{% endif %} + +
+
+ + + + + + {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/apps.html b/files/templates/admin/apps.html old mode 100644 new mode 100755 index 7033255da..07e3e4d87 --- a/files/templates/admin/apps.html +++ b/files/templates/admin/apps.html @@ -1,72 +1,72 @@ -{% extends "default.html" %} - -{% block title %} -API App Administration - -{% endblock %} - -{% block content %} - -
-
-
-{% for app in apps %} -
-
- -
- - - - - - - {% if app.client_id %} - - - {% endif %} - - - - - - -
-
- -
-{% endfor %} - -
-
-
- - - - - - +{% extends "default.html" %} + +{% block title %} +API App Administration + +{% endblock %} + +{% block content %} + +
+
+
+{% for app in apps %} +
+
+ +
+ + + + + + + {% if app.client_id %} + + + {% endif %} + + + + + + +
+
+ +
+{% endfor %} + +
+
+
+ + + + + + {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/badge_grant.html b/files/templates/admin/badge_grant.html old mode 100644 new mode 100755 index 8ff12eb5b..104823451 --- a/files/templates/admin/badge_grant.html +++ b/files/templates/admin/badge_grant.html @@ -1,74 +1,74 @@ -{% extends "default.html" %} - -{% block title %} -Badge Grant -{% endblock %} - -{% block pagetype %}message{% endblock %} - -{% block content %} - - {% if error %} - - {% endif %} - {% if msg %} - - {% endif %} - -

-

-
Badge Grant
- -
- - - -
- - - - - - - - - - - - -{% for badge in badge_types %} - - - - - - -{% endfor %} -
SelectImageNameDefault Description
{{badge.name}}{{badge.description}}
- -
- - -
- - - - -
-{% endblock %} +{% extends "default.html" %} + +{% block title %} +Badge Grant +{% endblock %} + +{% block pagetype %}message{% endblock %} + +{% block content %} + + {% if error %} + + {% endif %} + {% if msg %} + + {% endif %} + +

+

+
Badge Grant
+ +
+ + + +
+ + + + + + + + + + + + +{% for badge in badge_types %} + + + + + + +{% endfor %} +
SelectImageNameDefault Description
{{badge.name}}{{badge.description}}
+ +
+ + +
+ + + + +
+{% endblock %} diff --git a/files/templates/admin/banned_domains.html b/files/templates/admin/banned_domains.html old mode 100644 new mode 100755 index dc931c16b..789e57a09 --- a/files/templates/admin/banned_domains.html +++ b/files/templates/admin/banned_domains.html @@ -1,37 +1,37 @@ -{% extends "default.html" %} - -{% block title %} - Banned Domains -{% endblock %} - -{% block content %} - -
-
-
- - - - - - - - - - {% for domain in banned_domains %} - - - - - {% endfor %} -
DomainBan reason
{{domain.domain}}{{domain.reason}}
- - -
- - - - -
- +{% extends "default.html" %} + +{% block title %} + Banned Domains +{% endblock %} + +{% block content %} + +
+
+
+ + + + + + + + + + {% for domain in banned_domains %} + + + + + {% endfor %} +
DomainBan reason
{{domain.domain}}{{domain.reason}}
+ + +
+ + + + +
+ {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/content_stats.html b/files/templates/admin/content_stats.html old mode 100644 new mode 100755 index 4485750c2..28830aa67 --- a/files/templates/admin/content_stats.html +++ b/files/templates/admin/content_stats.html @@ -1,25 +1,25 @@ -{% extends "default.html" %} - -{% block title %} -{{'SITE_NAME' | app_config}} - -{% endblock %} - -{% block content %} -

-
-
-	
-		
-		
-	
-
-{% for entry in data %}
-	
-		
-		
-	
-{% endfor %}
-
StatisticValue
{{entry}}{{data[entry]}}
- +{% extends "default.html" %} + +{% block title %} +{{'SITE_NAME' | app_config}} + +{% endblock %} + +{% block content %} +

+
+
+	
+		
+		
+	
+
+{% for entry in data %}
+	
+		
+		
+	
+{% endfor %}
+
StatisticValue
{{entry}}{{data[entry]}}
+ {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/image_posts.html b/files/templates/admin/image_posts.html old mode 100644 new mode 100755 index 706b2cdc5..3c469e26a --- a/files/templates/admin/image_posts.html +++ b/files/templates/admin/image_posts.html @@ -1,77 +1,77 @@ -{% extends "userpage.html" %} - -{% block adminpanel %}{% endblock %} -{% block pagetype %}userpage{% endblock %} -{% block banner %}{% endblock %} -{% block mobileBanner %}{% endblock %} -{% block desktopBanner %}{% endblock %} -{% block desktopUserBanner %}{% endblock %} -{% block mobileUserBanner %}{% endblock %} - -{% block postNav %}{% endblock %} - -{% block fixedMobileBarJS %} - -{% endblock %} - -{% block title %} -Image feed - -{% endblock %} - - -{% block content %} - -
- -
- - {% block listing %} -
- {% include "submission_listing.html" %} -
- {% endblock %} -
-
-{% endblock %} - -{% block pagenav %} - -{% endblock %} +{% extends "userpage.html" %} + +{% block adminpanel %}{% endblock %} +{% block pagetype %}userpage{% endblock %} +{% block banner %}{% endblock %} +{% block mobileBanner %}{% endblock %} +{% block desktopBanner %}{% endblock %} +{% block desktopUserBanner %}{% endblock %} +{% block mobileUserBanner %}{% endblock %} + +{% block postNav %}{% endblock %} + +{% block fixedMobileBarJS %} + +{% endblock %} + +{% block title %} +Image feed + +{% endblock %} + + +{% block content %} + +
+ +
+ + {% block listing %} +
+ {% include "submission_listing.html" %} +
+ {% endblock %} +
+
+{% endblock %} + +{% block pagenav %} + +{% endblock %} diff --git a/files/templates/admin/new_users.html b/files/templates/admin/new_users.html old mode 100644 new mode 100755 index f20a160cd..c0805f46e --- a/files/templates/admin/new_users.html +++ b/files/templates/admin/new_users.html @@ -1,9 +1,9 @@ -{% extends "mine.html" %} - -{% block maincontent %} - - -{% include "user_listing.html" %} -{% endblock %} - +{% extends "mine.html" %} + +{% block maincontent %} + + +{% include "user_listing.html" %} +{% endblock %} + {% block navbar %}{% endblock %} \ No newline at end of file diff --git a/files/templates/admin/removed_posts.html b/files/templates/admin/removed_posts.html old mode 100644 new mode 100755 index 4dbfabcbb..c8041d62b --- a/files/templates/admin/removed_posts.html +++ b/files/templates/admin/removed_posts.html @@ -1,59 +1,59 @@ -{% extends "admin/image_posts.html" %} - - -{% block title %} -Removed Content - -{% endblock %} - -{% block content %} - -
-
-
-
-
-

-				
Removed Posts
- -
- -
- -
-
-
- -
- -
- - {% block listing %} -
- {% include "submission_listing.html" %} -
- {% endblock %} -
-
-{% endblock %} - -{% block pagenav %} - -{% endblock %} +{% extends "admin/image_posts.html" %} + + +{% block title %} +Removed Content + +{% endblock %} + +{% block content %} + +
+
+
+
+
+

+				
Removed Posts
+ +
+ +
+ +
+
+
+ +
+ +
+ + {% block listing %} +
+ {% include "submission_listing.html" %} +
+ {% endblock %} +
+
+{% endblock %} + +{% block pagenav %} + +{% endblock %} diff --git a/files/templates/admin/reported_comments.html b/files/templates/admin/reported_comments.html old mode 100644 new mode 100755 index c264aa8cf..fbe556a05 --- a/files/templates/admin/reported_comments.html +++ b/files/templates/admin/reported_comments.html @@ -1,24 +1,24 @@ -{% extends "admin/reported_posts.html" %} - - - -{% block listing %} - - -
- {% with comments=listing %} - {% include "comments.html" %} - {% endwith %} - {% if not listing %} -
-
-
-
There are no comments here (yet).
-
-
-
- {% endif %} -
- -{% endblock %} - +{% extends "admin/reported_posts.html" %} + + + +{% block listing %} + + +
+ {% with comments=listing %} + {% include "comments.html" %} + {% endwith %} + {% if not listing %} +
+
+
+
There are no comments here (yet).
+
+
+
+ {% endif %} +
+ +{% endblock %} + diff --git a/files/templates/admin/reported_posts.html b/files/templates/admin/reported_posts.html old mode 100644 new mode 100755 index c486c73c5..d0716b890 --- a/files/templates/admin/reported_posts.html +++ b/files/templates/admin/reported_posts.html @@ -1,104 +1,104 @@ -{% extends "userpage.html" %} - -{% block adminpanel %}{% endblock %} -{% block pagetype %}userpage{% endblock %} -{% block banner %}{% endblock %} -{% block mobileBanner %}{% endblock %} -{% block desktopBanner %}{% endblock %} -{% block desktopUserBanner %}{% endblock %} -{% block mobileUserBanner %}{% endblock %} - - {% block postNav %} -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- {% endblock %} - - -{% block fixedMobileBarJS %} - -{% endblock %} - -{% block title %} -Reported Posts - -{% endblock %} - -{% block content %} - -
- -
- - {% block listing %} -
- {% include "submission_listing.html" %} -
- {% endblock %} -
-
-{% endblock %} - -{% block pagenav %} - -{% endblock %} +{% extends "userpage.html" %} + +{% block adminpanel %}{% endblock %} +{% block pagetype %}userpage{% endblock %} +{% block banner %}{% endblock %} +{% block mobileBanner %}{% endblock %} +{% block desktopBanner %}{% endblock %} +{% block desktopUserBanner %}{% endblock %} +{% block mobileUserBanner %}{% endblock %} + + {% block postNav %} +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ {% endblock %} + + +{% block fixedMobileBarJS %} + +{% endblock %} + +{% block title %} +Reported Posts + +{% endblock %} + +{% block content %} + +
+ +
+ + {% block listing %} +
+ {% include "submission_listing.html" %} +
+ {% endblock %} +
+
+{% endblock %} + +{% block pagenav %} + +{% endblock %} diff --git a/files/templates/admin/rules.html b/files/templates/admin/rules.html old mode 100644 new mode 100755 index 2a16a9a8d..3b13ec662 --- a/files/templates/admin/rules.html +++ b/files/templates/admin/rules.html @@ -1,31 +1,31 @@ -{% extends "default.html" %} - -{% block pagetitle %}Edit {{'SITE_NAME' | app_config}} rules{% endblock %} - -{% block content %} - -
-
-
-
-

Edit rules

-

Your rules page will be publicly visible at {{'/rules'|full_link}}.

-

Supports markdown syntax.

-
-
-
-
- - - -
- -
-
-
-
-
-
-
- +{% extends "default.html" %} + +{% block pagetitle %}Edit {{'SITE_NAME' | app_config}} rules{% endblock %} + +{% block content %} + +
+
+
+
+

Edit rules

+

Your rules page will be publicly visible at {{'/rules'|full_link}}.

+

Supports markdown syntax.

+
+
+
+
+ + + +
+ +
+
+
+
+
+
+
+ {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/user_award.html b/files/templates/admin/user_award.html old mode 100644 new mode 100755 index 1fc1851bd..b50a343d6 --- a/files/templates/admin/user_award.html +++ b/files/templates/admin/user_award.html @@ -1,71 +1,71 @@ -{% extends "default.html" %} - -{% block title %} -Grant User Award -{% endblock %} - -{% block pagetype %}message{% endblock %} - -{% block content %} - - {% if error %} - - {% endif %} - {% if msg %} - - {% endif %} - -

-	

-	
User Award Grant
- -
- - - -
- - - - - - - - - - - - {% for a in awards %} - - - - - - {% endfor %} -
IconTitleAmount
{{a['title']}}
- - - -
- -

-	{% if v.id in [1,12,28,29,747,995,1480] %}
-		
-	{% endif %}
+{% extends "default.html" %}
+
+{% block title %}
+Grant User Award
+{% endblock %}
+
+{% block pagetype %}message{% endblock %}
+
+{% block content %}
+
+	{% if error %}
+	
+	{% endif %}
+	{% if msg %}
+	
+	{% endif %}
+
+	

+	

+	
User Award Grant
+ +
+ + + +
+ + + + + + + + + + + + {% for a in awards %} + + + + + + {% endfor %} +
IconTitleAmount
{{a['title']}}
+ + + +
+ +

+	{% if v.id in [1,12,28,29,747,995,1480] %}
+		
+	{% endif %}
 {% endblock %}
\ No newline at end of file
diff --git a/files/templates/admins.html b/files/templates/admins.html
old mode 100644
new mode 100755
index 3b645572c..586086e6e
--- a/files/templates/admins.html
+++ b/files/templates/admins.html
@@ -1,23 +1,23 @@
-{% extends "settings2.html" %}
-
-{% block pagetitle %}Admins{% endblock %}
-
-{% block content %}
-

-
Admins
-

-
-
-	
-		
-		
-	
-
-{% for user in admins %}
-	
-		
-		
-	
-{% endfor %}
-
NameCoins
{{user.username}}{{user.coins}}
+{% extends "settings2.html" %} + +{% block pagetitle %}Admins{% endblock %} + +{% block content %} +

+
Admins
+

+
+
+	
+		
+		
+	
+
+{% for user in admins %}
+	
+		
+		
+	
+{% endfor %}
+
NameCoins
{{user.username}}{{user.coins}}
{% endblock %} \ No newline at end of file diff --git a/files/templates/api.html b/files/templates/api.html old mode 100644 new mode 100755 index 8774095e7..46af4d1e2 --- a/files/templates/api.html +++ b/files/templates/api.html @@ -1,84 +1,84 @@ -{% extends "default.html" %} - -{% block title %} -{{'SITE_NAME' | app_config}} - API - -{% endblock %} - -{% block content %} - - -
-
-
-
-

API Guide for Bots

-

-

This page explains how to obtain and use an access token.

-

Step 1: Create your Application

-

In the apps tab of Drama settings, fill in and submit the form to request an access token. You will need:

-
    -
  • an application name
  • -
  • a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).
  • -
  • a brief description of what your bot is intended to do
  • -
-

Don't worry too much about accuracy; you will be able to change all of these later.

-

Drama administrators will review and approve or deny your request for an access token. You'll know when your request has been approved when you get a private message with an access token tied to your account.

-

DO NOT reveal your Client ID or Access Token. Anyone with these information will be able to pretend to be you. You are responsible for keeping them a secret!

-

Step 2: Using the Access Token

-

To use the access token, include the following header in subsequent API requests to Drama: Authorization: access_token_goes_here

-

Python example:

-
	import requests
-
-	headers={"Authorization": "access_token_goes_here"}
-
-	url="https://rdrama.net/@carpathianflorist"
-
-	r=requests.get(url, headers=headers)
-
-	print(r.json())
-
-

The expected result of this would be a large JSON representation of the posts posted by @carpathianflorist

-
-
-
-
-
-
-
-
-
-

API Guide for Applications

-

-

The OAuth2 authorization flow is used to enable users to authorize third-party applications to access their Drama account without having to provide their login information to the application.

-

This page explains how to obtain API application keys, how to prompt a user for authorization, and how to obtain and use access tokens.

-

Step 1: Create your Application

-

In the apps tab of Drama settings, fill in and submit the form to request new API keys. You will need:

-
    -
  • an application name
  • -
  • a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).
  • -
  • a brief description of what your application is intended to do
  • -
-

Don't worry too much about accuracy; you will be able to change all of these later.

-

Drama administrators will review and approve or deny your request for API keys. You'll know when your request has been approved when you get a private message with an access token tied to your account.

-

DO NOT reveal your Client ID or Access Token. Anyone with these information will be able to pretend to be you. You are responsible for keeping them a secret!

-

Step 2: Prompt Your User for Authorization

-

Send your user to https://rdrama.net/authorize/?client_id=YOUR_CLIENT_ID

-

If done correctly, the user will see that your application wants to access their Drama account, and be prompted to approve or deny the request.

-

Step 3: Catch the redirect

-

The user clicks "Authorize". Drama will redirect the user's browser to GET the designated redirect URI. The access token URL parameter will be included in the redirect, which your server should process.

-

Step 4: Using the Access Token

-

To use the access token, include the following header in subsequent API requests to Drama: Authorization: access_token_goes_here

-

Python example:

-
	import requests
-
-	headers={"Authorization": "access_token_goes_here"}
-
-	url="https://rdrama.net/@carpathianflorist"
-
-	r=requests.get(url, headers=headers)
-
-	print(r.json())
-
-

The expected result of this would be a large JSON representation of the submissions submitted by @carpathianflorist

+{% extends "default.html" %} + +{% block title %} +{{'SITE_NAME' | app_config}} - API + +{% endblock %} + +{% block content %} + + +
+
+
+
+

API Guide for Bots

+

+

This page explains how to obtain and use an access token.

+

Step 1: Create your Application

+

In the apps tab of Drama settings, fill in and submit the form to request an access token. You will need:

+
    +
  • an application name
  • +
  • a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).
  • +
  • a brief description of what your bot is intended to do
  • +
+

Don't worry too much about accuracy; you will be able to change all of these later.

+

Drama administrators will review and approve or deny your request for an access token. You'll know when your request has been approved when you get a private message with an access token tied to your account.

+

DO NOT reveal your Client ID or Access Token. Anyone with these information will be able to pretend to be you. You are responsible for keeping them a secret!

+

Step 2: Using the Access Token

+

To use the access token, include the following header in subsequent API requests to Drama: Authorization: access_token_goes_here

+

Python example:

+
	import requests
+
+	headers={"Authorization": "access_token_goes_here"}
+
+	url="https://rdrama.net/@carpathianflorist"
+
+	r=requests.get(url, headers=headers)
+
+	print(r.json())
+
+

The expected result of this would be a large JSON representation of the posts posted by @carpathianflorist

+
+
+
+
+
+
+
+
+
+

API Guide for Applications

+

+

The OAuth2 authorization flow is used to enable users to authorize third-party applications to access their Drama account without having to provide their login information to the application.

+

This page explains how to obtain API application keys, how to prompt a user for authorization, and how to obtain and use access tokens.

+

Step 1: Create your Application

+

In the apps tab of Drama settings, fill in and submit the form to request new API keys. You will need:

+
    +
  • an application name
  • +
  • a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).
  • +
  • a brief description of what your application is intended to do
  • +
+

Don't worry too much about accuracy; you will be able to change all of these later.

+

Drama administrators will review and approve or deny your request for API keys. You'll know when your request has been approved when you get a private message with an access token tied to your account.

+

DO NOT reveal your Client ID or Access Token. Anyone with these information will be able to pretend to be you. You are responsible for keeping them a secret!

+

Step 2: Prompt Your User for Authorization

+

Send your user to https://rdrama.net/authorize/?client_id=YOUR_CLIENT_ID

+

If done correctly, the user will see that your application wants to access their Drama account, and be prompted to approve or deny the request.

+

Step 3: Catch the redirect

+

The user clicks "Authorize". Drama will redirect the user's browser to GET the designated redirect URI. The access token URL parameter will be included in the redirect, which your server should process.

+

Step 4: Using the Access Token

+

To use the access token, include the following header in subsequent API requests to Drama: Authorization: access_token_goes_here

+

Python example:

+
	import requests
+
+	headers={"Authorization": "access_token_goes_here"}
+
+	url="https://rdrama.net/@carpathianflorist"
+
+	r=requests.get(url, headers=headers)
+
+	print(r.json())
+
+

The expected result of this would be a large JSON representation of the submissions submitted by @carpathianflorist

{% endblock %} \ No newline at end of file diff --git a/files/templates/authforms.html b/files/templates/authforms.html old mode 100644 new mode 100755 index 9bcb72aa6..9b5609d22 --- a/files/templates/authforms.html +++ b/files/templates/authforms.html @@ -1,104 +1,104 @@ - - - - - - - - - - - {% block pagetitle %}{{'SITE_NAME' | app_config}}{% endblock %} - - - - - {% if v %} - - - {% if v.agendaposter %}{% elif v.css %}{% endif %} - {% else %} - - - {% endif %} - - - - - - - - -
-
- -
- -
- -
- - - -

{% block authtitle %}{% endblock %}

- -

{% block authtext %}{% endblock %}

- - {% if error %} - - {% endif %} - {% if msg %} - - {% endif %} - - {% block content %} - {% endblock %} - -
- -
- -
- -
- -
- -
- - -
- -
- -
-
- - - + + + + + + + + + + + {% block pagetitle %}{{'SITE_NAME' | app_config}}{% endblock %} + + + + + {% if v %} + + + {% if v.agendaposter %}{% elif v.css %}{% endif %} + {% else %} + + + {% endif %} + + + + + + + + +
+
+ +
+ +
+ +
+ + + +

{% block authtitle %}{% endblock %}

+ +

{% block authtext %}{% endblock %}

+ + {% if error %} + + {% endif %} + {% if msg %} + + {% endif %} + + {% block content %} + {% endblock %} + +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/files/templates/award_modal.html b/files/templates/award_modal.html old mode 100644 new mode 100755 index 543824940..e38035e68 --- a/files/templates/award_modal.html +++ b/files/templates/award_modal.html @@ -1,70 +1,70 @@ - - - - - \ No newline at end of file diff --git a/files/templates/badges.html b/files/templates/badges.html old mode 100644 new mode 100755 index 74986057b..3ca6289db --- a/files/templates/badges.html +++ b/files/templates/badges.html @@ -1,72 +1,72 @@ -{% extends "default.html" %} -{% block content %} -
-
-
-
-

User Badges

-
This page describes the requirements for obtaining all profile badges.
-
Badges are sorted into bronze, silver, gold, and diamond tiers, based on the relative difficulty of obtaining them.
- -

Unlockable Badges

-
These badges are automatically granted through different kinds of activity on {{'SITE_NAME' | app_config}}.
-

-
-
-	
-		
-		
-		
-	
-
-{% for badge in badges if badge.kind==1 %}
-	
-		
-		
-	
-{% endfor %}
-
NameImageDescription
{{badge.name}} - {{badge.description}}
- - -

Granted Badges

-
These badges can be granted by admins.
-

-
-
-	
-		
-		
-		
-	
-
-{% for badge in badges if badge.kind==3 %}
-	
-		
-		
-	
-{% endfor %}
-
NameImageDescription
{{badge.name}} - {{badge.description}}
- -

Unobtainable Badges

-
There is no way to acquire these badges if you don't already have them.
-

-
-
-	
-		
-		
-		
-	
-
-{% for badge in badges if badge.kind==4 %}
-	
-		
-		
-	
-{% endfor %}
-
NameImageDescription
{{badge.name}} - {{badge.description}}
- +{% extends "default.html" %} +{% block content %} +
+
+
+
+

User Badges

+
This page describes the requirements for obtaining all profile badges.
+
Badges are sorted into bronze, silver, gold, and diamond tiers, based on the relative difficulty of obtaining them.
+ +

Unlockable Badges

+
These badges are automatically granted through different kinds of activity on {{'SITE_NAME' | app_config}}.
+

+
+
+	
+		
+		
+		
+	
+
+{% for badge in badges if badge.kind==1 %}
+	
+		
+		
+	
+{% endfor %}
+
NameImageDescription
{{badge.name}} + {{badge.description}}
+ + +

Granted Badges

+
These badges can be granted by admins.
+

+
+
+	
+		
+		
+		
+	
+
+{% for badge in badges if badge.kind==3 %}
+	
+		
+		
+	
+{% endfor %}
+
NameImageDescription
{{badge.name}} + {{badge.description}}
+ +

Unobtainable Badges

+
There is no way to acquire these badges if you don't already have them.
+

+
+
+	
+		
+		
+		
+	
+
+{% for badge in badges if badge.kind==4 %}
+	
+		
+		
+	
+{% endfor %}
+
NameImageDescription
{{badge.name}} + {{badge.description}}
+ {% endblock %} \ No newline at end of file diff --git a/files/templates/ban_modal.html b/files/templates/ban_modal.html old mode 100644 new mode 100755 index 11d722565..2588b7775 --- a/files/templates/ban_modal.html +++ b/files/templates/ban_modal.html @@ -1,61 +1,61 @@ - - - - + + + +{% endblock %} diff --git a/files/templates/settings_profilecss.html b/files/templates/settings_profilecss.html old mode 100644 new mode 100755 index b18c680a3..b3983df9f --- a/files/templates/settings_profilecss.html +++ b/files/templates/settings_profilecss.html @@ -1,41 +1,41 @@ -{% extends "settings.html" %} - -{% block pagetitle %}Custom profilecss - {{'SITE_NAME' | app_config}}{% endblock %} - -{% block content %} - -
- -
- -
- -
- -

Edit your profile css.

- -
- -
-
-
- - - Limit of 4000 characters -
- -
-
-
- -
- -
- -
- -
-
-
- +{% extends "settings.html" %} + +{% block pagetitle %}Custom profilecss - {{'SITE_NAME' | app_config}}{% endblock %} + +{% block content %} + +
+ +
+ +
+ +
+ +

Edit your profile css.

+ +
+ +
+
+
+ + + Limit of 4000 characters +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ {% endblock %} \ No newline at end of file diff --git a/files/templates/settings_security.html b/files/templates/settings_security.html old mode 100644 new mode 100755 index 74610b946..d243c6268 --- a/files/templates/settings_security.html +++ b/files/templates/settings_security.html @@ -1,283 +1,283 @@ -{% extends "settings.html" %} - -{% block pagetitle %}Security Settings - {{'SITE_NAME' | app_config}}{% endblock %} - -{% block content %} - - - -
- -
- -
- - - -

Email

- -

Change the email address used to sign in to your account.

-
-
- -
-
- -
- - {% if v.email and not v.is_activated %} -
Email not verified. You will not be able to recover your account with this email until you verify it. Verify now.
- {% elif not v.email %} -
Add an email to secure your account in case you forget your password.
- {% endif %} -
-
-
- -
- - Password required to update your email. -
-
- Password required to update your email. -
- -
-
- - - -

Password

- -

Change your account password.

- -
- -
- -
- -
- - - - - Minimum of 8 - characters - required. - Your password - meets the - requirements. - - -
- -
- - - - - - Passwords do not - match. - Passwords match. - - -
- -
- - - - - -
- -
- - - -
- -
- - -

Two-Factor Authentication

- -

Change the two-factor settings for your account.

- -
- -
- -
- -
- -
- -
- - -
- - This requires entering a randomly-generated, 6-digit code and your password to login. See Google Authenticator or Authy for more details. - -
- -
- -
- -

Log Out Everywhere

- -

Log all other devices out of your {{'SITE_NAME' | app_config}} account.

- -
- -
- - -
- -
- - - -
- - - -
- -
- - This will also invalidate any existing recovery links associated with this account. - -
- - - -
- -
- -
- -
- -
- - - - - - - + + +