diff --git a/.gitattributes b/.gitattributes index cfef720ed..8dc698e6b 100644 --- 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/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index b7ad2f868..000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,70 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '18 19 * * 1' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript', 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 87af76111..d5f6da0a6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/ossar.yml b/.github/workflows/ossar.yml index 4cfa76d5b..726eb0c3f 100644 --- a/.github/workflows/ossar.yml +++ b/.github/workflows/ossar.yml @@ -22,6 +22,8 @@ jobs: # OSSAR runs on windows-latest. # ubuntu-latest and macos-latest support coming soon runs-on: windows-latest + permissions: + security-events: write steps: - name: Checkout repository @@ -44,6 +46,6 @@ jobs: # Upload results to the Security tab - name: Upload OSSAR results - uses: github/codeql-action/upload-sarif@v1 + uses: github/codeql-action/upload-sarif@v2 with: - sarif_file: ${{ steps.ossar.outputs.sarifFile }} + sarif_file: ${{ steps.ossar.outputs.sarifFile }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..b4eacda94 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,14 @@ +name: "run_tests.py" + +on: [push, pull_request] + +jobs: + analyze: + runs-on: ubuntu-20.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: run_tests.py + run: | + ./run_tests.py \ No newline at end of file diff --git a/.gitignore b/.gitignore index 62ed1d6db..5b21b7455 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ image.* video.mp4 -video.webm +unsanitized.mp4 cache/ __pycache__/ .idea/ diff --git a/Dockerfile b/Dockerfile index 8b2276f66..a6e59a7a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,17 @@ -FROM ubuntu:20.04 - -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt update && apt -y upgrade && apt install -y supervisor python3-pip libenchant1c2a ffmpeg - -COPY supervisord.conf /etc/supervisord.conf - -COPY requirements.txt /etc/requirements.txt - -RUN pip3 install -r /etc/requirements.txt - -RUN mkdir /images && mkdir /songs - -EXPOSE 80/tcp - -CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ] +FROM ubuntu:20.04 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt update && apt -y upgrade && apt install -y supervisor python3-pip libenchant1c2a ffmpeg + +COPY supervisord.conf /etc/supervisord.conf + +COPY requirements.txt /etc/requirements.txt + +RUN pip3 install -r /etc/requirements.txt + +RUN mkdir /images && mkdir /songs + +EXPOSE 80/tcp + +CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ] diff --git a/LICENSE b/LICENSE index 1c66220d2..6f0c5d239 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,661 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/dependabot.yml b/dependabot.yml index b4c618375..fa05fac46 100644 --- 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/docker-compose.yml b/docker-compose.yml index 691c16797..ac99b2d20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ version: '2.3' services: files: + container_name: "rDrama" build: context: . volumes: diff --git a/files/__main__.py b/files/__main__.py index 09e06c488..313a1fec9 100644 --- a/files/__main__.py +++ b/files/__main__.py @@ -1,123 +1,127 @@ -import gevent.monkey -gevent.monkey.patch_all() -from os import environ, path -import secrets -from flask import * -from flask_caching import Cache -from flask_limiter import Limiter -from flask_compress import Compress -from flask_mail import Mail -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, scoped_session -from sqlalchemy import * -import gevent -import redis -import time -from sys import stdout, argv -import faulthandler -import json - -app = Flask(__name__, template_folder='templates') -app.url_map.strict_slashes = False -app.jinja_env.cache = {} -app.jinja_env.auto_reload = True -faulthandler.enable() - -app.config["SITE_NAME"]=environ.get("SITE_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", "postgresql://postgres@localhost:5432") -app.config['SECRET_KEY'] = environ.get('MASTER_KEY') -app.config["SERVER_NAME"] = environ.get("DOMAIN").strip() -app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3153600 -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"] = True -app.config["SESSION_COOKIE_SAMESITE"] = "Lax" -app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 365 -app.config["DEFAULT_COLOR"] = environ.get("DEFAULT_COLOR", "ff0000").strip() -app.config["DEFAULT_THEME"] = environ.get("DEFAULT_THEME", "midnight").strip() -app.config["FORCE_HTTPS"] = 1 -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_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", 0.1)) -app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", 10)) -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", 10)) -app.config["CACHE_TYPE"] = "RedisCache" -app.config["CACHE_REDIS_URL"] = environ.get("REDIS_URL", "redis://localhost") -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() -app.config['DESCRIPTION'] = environ.get("DESCRIPTION", "rdrama.net caters to drama in all forms such as: Real life, videos, photos, gossip, rumors, news sites, Reddit, and Beyond™. There isn't drama we won't touch, and we want it all!").strip() -app.config['SETTINGS'] = {} - -r=redis.Redis(host=environ.get("REDIS_URL", "redis://localhost"), decode_responses=True, ssl_cert_reqs=None) - -def get_CF(): - with app.app_context(): - return request.headers.get('CF-Connecting-IP') - -limiter = Limiter( - app, - key_func=get_CF, - default_limits=["3/second;30/minute;200/hour;1000/day"], - application_limits=["10/second;200/minute;5000/hour;10000/day"], - storage_uri=environ.get("REDIS_URL", "redis://localhost") -) - -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(): - - with open('site_settings.json', 'r') as f: - app.config['SETTINGS'] = json.load(f) - - if request.host != app.config["SERVER_NAME"]: return {"error":"Unauthorized host provided."}, 401 - if request.headers.get("CF-Worker"): return {"error":"Cloudflare workers are not allowed to access this website."}, 401 - - if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"): abort(503) - - g.db = db_session() - - ua = request.headers.get("User-Agent","").lower() - - if '; wv) ' in ua: g.webview = True - else: g.webview = False - - if 'iphone' in ua or 'ipad' in ua or 'ipod' in ua or 'mac os' in ua or ' firefox/' in ua: g.inferior_browser = True - else: g.inferior_browser = False - - g.timestamp = int(time.time()) - - -@app.teardown_appcontext -def teardown_request(error): - if hasattr(g, 'db') and g.db: - g.db.close() - stdout.flush() - -@app.after_request -def after_request(response): - response.headers.add("Strict-Transport-Security", "max-age=31536000") - response.headers.add("X-Frame-Options", "deny") - return response - -if "load_chat" in argv: - from files.routes.chat import * -else: +import gevent.monkey +gevent.monkey.patch_all() +from os import environ, path +import secrets +from flask import * +from flask_caching import Cache +from flask_limiter import Limiter +from flask_compress import Compress +from flask_mail import Mail +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy import * +import gevent +import redis +import time +from sys import stdout, argv +import faulthandler +import json + +app = Flask(__name__, template_folder='templates') +app.url_map.strict_slashes = False +app.jinja_env.cache = {} +app.jinja_env.auto_reload = True +faulthandler.enable() + +app.config["SITE_NAME"]=environ.get("SITE_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", "postgresql://postgres@localhost:5432") +app.config['SECRET_KEY'] = environ.get('MASTER_KEY') +app.config["SERVER_NAME"] = environ.get("DOMAIN").strip() +app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3153600 +app.config["SESSION_COOKIE_NAME"] = "session_" + environ.get("SITE_NAME").strip().lower() +app.config["VERSION"] = "1.0.0" +app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 +app.config["SESSION_COOKIE_SECURE"] = True +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" +app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 365 +app.config['SESSION_REFRESH_EACH_REQUEST'] = False +app.config["DEFAULT_COLOR"] = environ.get("DEFAULT_COLOR", "ff0000").strip() +app.config["DEFAULT_THEME"] = environ.get("DEFAULT_THEME", "midnight").strip() +app.config["FORCE_HTTPS"] = 1 +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_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", 0.1)) +app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", 10)) +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", 10)) +app.config["CACHE_TYPE"] = "RedisCache" +app.config["CACHE_REDIS_URL"] = environ.get("REDIS_URL", "redis://localhost") +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() +app.config['DESCRIPTION'] = environ.get("DESCRIPTION", "rdrama.net caters to drama in all forms such as: Real life, videos, photos, gossip, rumors, news sites, Reddit, and Beyond™. There isn't drama we won't touch, and we want it all!").strip() +app.config['SETTINGS'] = {} + +r=redis.Redis(host=environ.get("REDIS_URL", "redis://localhost"), decode_responses=True, ssl_cert_reqs=None) + +def get_CF(): + with app.app_context(): + return request.headers.get('CF-Connecting-IP') + +limiter = Limiter( + app, + key_func=get_CF, + default_limits=["3/second;30/minute;200/hour;1000/day"], + application_limits=["10/second;200/minute;5000/hour;10000/day"], + storage_uri=environ.get("REDIS_URL", "redis://localhost") +) + +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(): + + with open('site_settings.json', 'r') as f: + app.config['SETTINGS'] = json.load(f) + + if request.host != app.config["SERVER_NAME"]: return {"error":"Unauthorized host provided."}, 401 + if request.headers.get("CF-Worker"): return {"error":"Cloudflare workers are not allowed to access this website."}, 401 + + if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"): abort(503) + + g.db = db_session() + + ua = request.headers.get("User-Agent","").lower() + + if '; wv) ' in ua: g.webview = True + else: g.webview = False + + if 'iphone' in ua or 'ipad' in ua or 'ipod' in ua or 'mac os' in ua or ' firefox/' in ua: g.inferior_browser = True + else: g.inferior_browser = False + + g.timestamp = int(time.time()) + + +@app.teardown_appcontext +def teardown_request(error): + if hasattr(g, 'db') and g.db: + g.db.close() + stdout.flush() + +@app.after_request +def after_request(response): + response.headers.add("Strict-Transport-Security", "max-age=31536000") + response.headers.add("X-Frame-Options", "deny") + return response + +if app.config["SERVER_NAME"] == 'localhost': + from files.routes import * + # from files.routes.chat import * +elif "load_chat" in argv: + from files.routes.chat import * +else: from files.routes import * \ No newline at end of file diff --git a/files/classes/__init__.py b/files/classes/__init__.py index 15c93d1ea..5c62a23f5 100644 --- a/files/classes/__init__.py +++ b/files/classes/__init__.py @@ -1,21 +1,21 @@ -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 * -from .marsey import * -from .sub_block import * -from .saves import * -from .views import * -from .notifications 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 * +from .marsey import * +from .sub_block import * +from .saves import * +from .views import * +from .notifications import * from .follows import * \ No newline at end of file diff --git a/files/classes/alts.py b/files/classes/alts.py index 7ee7561a8..5e9bf1d8a 100644 --- a/files/classes/alts.py +++ b/files/classes/alts.py @@ -1,14 +1,14 @@ -from sqlalchemy import * -from files.__main__ import Base - - -class Alt(Base): - __tablename__ = "alts" - - user1 = Column(Integer, ForeignKey("users.id"), primary_key=True) - user2 = Column(Integer, ForeignKey("users.id"), primary_key=True) - is_manual = Column(Boolean, default=False) - - def __repr__(self): - - return f"" +from sqlalchemy import * +from files.__main__ import Base + + +class Alt(Base): + __tablename__ = "alts" + + user1 = Column(Integer, ForeignKey("users.id"), primary_key=True) + user2 = Column(Integer, ForeignKey("users.id"), primary_key=True) + is_manual = Column(Boolean, default=False) + + def __repr__(self): + + return f"" diff --git a/files/classes/award.py b/files/classes/award.py index bb646860a..f7fdf6e0d 100644 --- a/files/classes/award.py +++ b/files/classes/award.py @@ -1,36 +1,36 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from os import environ -from files.helpers.lazy import lazy -from files.helpers.const import * - -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 +from files.helpers.const import * + +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 index b3e70bda0..58d3332b7 100644 --- a/files/classes/badges.py +++ b/files/classes/badges.py @@ -1,73 +1,73 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base, app -from os import environ -from files.helpers.lazy import lazy -from files.helpers.const import * -from datetime import datetime -from json import loads - -class BadgeDef(Base): - __tablename__ = "badge_defs" - - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(String) - description = Column(String) - - def __repr__(self): - return f"" - - -class Badge(Base): - - __tablename__ = "badges" - - user_id = Column(Integer, ForeignKey('users.id'), primary_key=True) - badge_id = Column(Integer, ForeignKey('badge_defs.id'), primary_key=True) - description = Column(String) - url = Column(String) - - user = relationship("User", viewonly=True) - badge = relationship("BadgeDef", primaryjoin="foreign(Badge.badge_id) == remote(BadgeDef.id)", viewonly=True) - - def __repr__(self): - return f"" - - @property - @lazy - def text(self): - if self.name == "Chud": - ti = self.user.agendaposter - if ti: text = self.badge.description + " until " + datetime.utcfromtimestamp(ti).strftime('%Y-%m-%d %H:%M:%S') - else: text = self.badge.description + " permanently" - elif self.badge_id in {94,95,96,97,98,109}: - if self.badge_id == 94: ti = self.user.progressivestack - elif self.badge_id == 95: ti = self.user.bird - elif self.badge_id == 96: ti = self.user.flairchanged - elif self.badge_id == 97: ti = self.user.longpost - elif self.badge_id == 98: ti = self.user.marseyawarded - elif self.badge_id == 109: ti = self.user.rehab - text = self.badge.description + " until " + datetime.utcfromtimestamp(ti).strftime('%Y-%m-%d %H:%M:%S') - elif self.description: text = self.description - elif self.badge.description: text = self.badge.description - else: return self.name - return f'{self.name} - {text}' - - @property - @lazy - def name(self): - return self.badge.name - - @property - @lazy - def path(self): - return f"/assets/images/badges/{self.badge_id}.webp" - - @property - @lazy - def json(self): - return {'text': self.text, - 'name': self.name, - 'url': self.url, - 'icon_url':self.path - } +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base, app +from os import environ +from files.helpers.lazy import lazy +from files.helpers.const import * +from datetime import datetime +from json import loads + +class BadgeDef(Base): + __tablename__ = "badge_defs" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String) + description = Column(String) + + def __repr__(self): + return f"" + + +class Badge(Base): + + __tablename__ = "badges" + + user_id = Column(Integer, ForeignKey('users.id'), primary_key=True) + badge_id = Column(Integer, ForeignKey('badge_defs.id'), primary_key=True) + description = Column(String) + url = Column(String) + + user = relationship("User", viewonly=True) + badge = relationship("BadgeDef", primaryjoin="foreign(Badge.badge_id) == remote(BadgeDef.id)", viewonly=True) + + def __repr__(self): + return f"" + + @property + @lazy + def text(self): + if self.name == "Chud": + ti = self.user.agendaposter + if ti: text = self.badge.description + " until " + datetime.utcfromtimestamp(ti).strftime('%Y-%m-%d %H:%M:%S') + else: text = self.badge.description + " permanently" + elif self.badge_id in {94,95,96,97,98,109}: + if self.badge_id == 94: ti = self.user.progressivestack + elif self.badge_id == 95: ti = self.user.bird + elif self.badge_id == 96: ti = self.user.flairchanged + elif self.badge_id == 97: ti = self.user.longpost + elif self.badge_id == 98: ti = self.user.marseyawarded + elif self.badge_id == 109: ti = self.user.rehab + text = self.badge.description + " until " + datetime.utcfromtimestamp(ti).strftime('%Y-%m-%d %H:%M:%S') + elif self.description: text = self.description + elif self.badge.description: text = self.badge.description + else: return self.name + return f'{self.name} - {text}' + + @property + @lazy + def name(self): + return self.badge.name + + @property + @lazy + def path(self): + return f"/assets/images/badges/{self.badge_id}.webp" + + @property + @lazy + def json(self): + return {'text': self.text, + 'name': self.name, + 'url': self.url, + 'icon_url':self.path + } diff --git a/files/classes/clients.py b/files/classes/clients.py index d16309895..2df38f4ce 100644 --- a/files/classes/clients.py +++ b/files/classes/clients.py @@ -1,84 +1,84 @@ -from flask import * -from sqlalchemy import * -from sqlalchemy.orm import relationship -from .submission import Submission -from .comment import Comment -from files.__main__ import Base -from files.helpers.lazy import lazy -from files.helpers.const import * -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).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).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" - - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - oauth_client = Column(Integer, ForeignKey("oauth_apps.id"), primary_key=True) - access_token = Column(String) - - 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 +from .submission import Submission +from .comment import Comment +from files.__main__ import Base +from files.helpers.lazy import lazy +from files.helpers.const import * +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).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).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" + + user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) + oauth_client = Column(Integer, ForeignKey("oauth_apps.id"), primary_key=True) + access_token = Column(String) + + 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 index 158929196..cb532df6b 100644 --- a/files/classes/comment.py +++ b/files/classes/comment.py @@ -392,10 +392,11 @@ class Comment(Base): if not self.total_poll_voted(v): body += ' d-none' body += f'"> - {c.upvotes} votes' - curr = self.total_choice_voted(v) - if curr: curr = " value=" + str(curr[0].comment_id) - else: curr = '' - body += f'' + if self.choices: + curr = self.total_choice_voted(v) + if curr: curr = " value=" + str(curr[0].comment_id) + else: curr = '' + body += f'' for c in self.choices: body += 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))) - - @lazy - def realreason(self, v): - return censor_slurs(self.reason, v) - - -class CommentFlag(Base): - - __tablename__ = "commentflags" - - comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True) - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - reason = Column(String) - created_utc = Column(Integer) - - user = relationship("User", primaryjoin = "CommentFlag.user_id == User.id", uselist = False, 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"" - - @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))) - - @lazy - def realreason(self, v): +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base +from files.helpers.lazy import lazy +from files.helpers.const import * +import time + +class Flag(Base): + + __tablename__ = "flags" + + post_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) + reason = Column(String) + created_utc = Column(Integer) + + user = relationship("User", primaryjoin = "Flag.user_id == User.id", uselist = False, 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"" + + @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))) + + @lazy + def realreason(self, v): + return censor_slurs(self.reason, v) + + +class CommentFlag(Base): + + __tablename__ = "commentflags" + + comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) + reason = Column(String) + created_utc = Column(Integer) + + user = relationship("User", primaryjoin = "CommentFlag.user_id == User.id", uselist = False, 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"" + + @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))) + + @lazy + def realreason(self, v): return censor_slurs(self.reason, v) \ No newline at end of file diff --git a/files/classes/mod_logs.py b/files/classes/mod_logs.py index 0f972b70c..6adf58e7b 100644 --- a/files/classes/mod_logs.py +++ b/files/classes/mod_logs.py @@ -1,424 +1,428 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -import time -from files.helpers.lazy import lazy -from os import environ -from copy import deepcopy -from files.helpers.const import * - -class ModAction(Base): - __tablename__ = "modactions" - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.id")) - kind = Column(String) - target_user_id = Column(Integer, ForeignKey("users.id")) - target_submission_id = Column(Integer, ForeignKey("submissions.id")) - target_comment_id = Column(Integer, ForeignKey("comments.id")) - _note=Column(String) - created_utc = Column(Integer) - - 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()) - 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, cc=CC_TITLE) - - 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: - if self.target_post.club: return f'{CC} ONLY' - return f'{self.target_post.title_html}' - 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 = { - 'agendaposter': { - "str": 'set chud theme on {self.target_link}', - "icon": 'fa-snooze', - "color": 'bg-danger' - }, - 'approve_app': { - "str": 'approved an application by {self.target_link}', - "icon": 'fa-robot', - "color": 'bg-success' - }, - 'badge_grant': { - "str": 'granted badge to {self.target_link}', - "icon": 'fa-badge', - "color": 'bg-success' - }, - 'badge_remove': { - "str": 'removed badge from {self.target_link}', - "icon": 'fa-badge', - "color": 'bg-danger' - }, - 'ban_comment': { - "str": 'removed {self.target_link}', - "icon": 'fa-comment', - "color": 'bg-danger' - }, - 'ban_domain': { - "str": 'banned a domain', - "icon": 'fa-globe', - "color": 'bg-danger' - }, - 'ban_post': { - "str": 'removed post {self.target_link}', - "icon": 'fa-feather-alt', - "color": 'bg-danger' - }, - 'ban_user': { - "str": 'banned user {self.target_link}', - "icon": 'fa-user-slash', - "color": 'bg-danger' - }, - 'change_sidebar': { - "str": 'changed the sidebar', - "icon": 'fa-columns', - "color": 'bg-primary' - }, - 'check': { - "str": 'gave {self.target_link} a checkmark', - "icon": 'fa-badge-check', - "color": 'bg-success' - }, - 'club_allow': { - "str": 'allowed user {self.target_link} into the {cc}', - "icon": 'fa-golf-club', - "color": 'bg-success' - }, - 'club_ban': { - "str": 'disallowed user {self.target_link} from the {cc}', - "icon": 'fa-golf-club', - "color": 'bg-danger' - }, - 'delete_report': { - "str": 'deleted report on {self.target_link}', - "icon": 'fa-flag', - "color": 'bg-danger' - }, - 'disable_Bots': { - "str": 'disabled Bots', - "icon": 'fa-robot', - "color": 'bg-danger' - }, - 'disable_Fart mode': { - "str": 'disabled fart mode', - "icon": 'fa-gas-pump-slash', - "color": 'bg-danger' - }, - 'disable_Read-only mode': { - "str": 'disabled readonly mode', - "icon": 'fa-book', - "color": 'bg-danger' - }, - 'disable_Signups': { - "str": 'disabled Signups', - "icon": 'fa-users', - "color": 'bg-danger' - }, - 'disable_under_attack': { - "str": 'disabled under attack mode', - "icon": 'fa-shield', - "color": 'bg-muted' - }, - 'distinguish_comment': { - "str": 'distinguished {self.target_link}', - "icon": 'fa-crown', - "color": 'bg-success' - }, - 'distinguish_post': { - "str": 'distinguished {self.target_link}', - "icon": 'fa-crown', - "color": 'bg-success' - }, - 'distribute': { - "str": 'distributed bet winnings to voters on {self.target_link}', - "icon": 'fa-dollar-sign', - "color": 'bg-success' - }, - 'dump_cache': { - "str": 'dumped cache', - "icon": 'fa-trash-alt', - "color": 'bg-muted' - }, - 'edit_post': { - "str": 'edited {self.target_link}', - "icon": 'fa-edit', - "color": 'bg-primary' - }, - 'enable_Bots': { - "str": 'enabled Bots', - "icon": 'fa-robot', - "color": 'bg-success' - }, - 'enable_Fart mode': { - "str": 'enabled fart mode', - "icon": 'fa-gas-pump', - "color": 'bg-success' - }, - 'enable_Read-only mode': { - "str": 'enabled readonly mode', - "icon": 'fa-book', - "color": 'bg-success' - }, - 'enable_Signups': { - "str": 'enabled Signups', - "icon": 'fa-users', - "color": 'bg-success' - }, - 'enable_under_attack': { - "str": 'enabled under attack mode', - "icon": 'fa-shield', - "color": 'bg-success' - }, - 'flair_post': { - "str": 'set a flair on {self.target_link}', - "icon": 'fa-tag', - "color": 'bg-primary' - }, - 'grant_awards': { - "str": 'granted awards to {self.target_link}', - "icon": 'fa-gift', - "color": 'bg-primary' - }, - 'link_accounts': { - "str": 'linked {self.target_link}', - "icon": 'fa-link', - "color": 'bg-success' - }, - 'make_admin': { - "str": 'made {self.target_link} admin', - "icon": 'fa-user-crown', - "color": 'bg-success' - }, - 'make_meme_admin': { - "str": 'made {self.target_link} meme admin', - "icon": 'fa-user-crown', - "color": 'bg-success' - }, - 'monthly': { - "str": 'distributed monthly marseybux', - "icon": 'fa-sack-dollar', - "color": 'bg-success' - }, - 'move_hole': { - "str": 'moved {self.target_link} to /h/{self.target_post.sub}', - "icon": 'fa-manhole', - "color": 'bg-primary' - }, - 'nuke_user': { - "str": 'removed all content of {self.target_link}', - "icon": 'fa-radiation-alt', - "color": 'bg-danger' - }, - 'pin_comment': { - "str": 'pinned a {self.target_link}', - "icon": 'fa-thumbtack fa-rotate--45', - "color": 'bg-success' - }, - 'pin_post': { - "str": 'pinned post {self.target_link}', - "icon": 'fa-thumbtack fa-rotate--45', - "color": 'bg-success' - }, - 'purge_cache': { - "str": 'purged cache', - "icon": 'fa-memory', - "color": 'bg-muted' - }, - 'reject_app': { - "str": 'rejected an application request by {self.target_link}', - "icon": 'fa-robot', - "color": 'bg-muted' - }, - 'remove_admin': { - "str": 'removed {self.target_link} as admin', - "icon": 'fa-user-crown', - "color": 'bg-danger' - }, - 'remove_meme_admin': { - "str": 'removed {self.target_link} as meme admin', - "icon": 'fa-user-crown', - "color": 'bg-danger' - }, - 'revert': { - "str": 'reverted {self.target_link} mod actions', - "icon": 'fa-history', - "color": 'bg-danger' - }, - 'revoke_app': { - "str": 'revoked an application by {self.target_link}', - "icon": 'fa-robot', - "color": 'bg-muted' - }, - 'set_flair_locked': { - "str": "set {self.target_link}'s flair (locked)", - "icon": 'fa-award', - "color": 'bg-primary' - }, - 'set_flair_notlocked': { - "str": "set {self.target_link}'s flair (not locked)", - "icon": 'fa-award', - "color": 'bg-primary' - }, - 'set_nsfw': { - "str": 'set nsfw on post {self.target_link}', - "icon": 'fa-eye-evil', - "color": 'bg-danger' - }, - 'shadowban': { - "str": 'shadowbanned {self.target_link}', - "icon": 'fa-eye-slash', - "color": 'bg-danger' - }, - 'unagendaposter': { - "str": 'removed chud theme from {self.target_link}', - "icon": 'fa-snooze', - "color": 'bg-success' - }, - 'unban_comment': { - "str": 'reinstated {self.target_link}', - "icon": 'fa-comment', - "color": 'bg-success' - }, - 'unban_domain': { - "str": 'unbanned a domain', - "icon": 'fa-globe', - "color": 'bg-success' - }, - 'unban_post': { - "str": 'reinstated post {self.target_link}', - "icon": 'fa-feather-alt', - "color": 'bg-success' - }, - 'unban_user': { - "str": 'unbanned user {self.target_link}', - "icon": 'fa-user', - "color": 'bg-success' - }, - 'uncheck': { - "str": 'removed checkmark from {self.target_link}', - "icon": 'fa-badge-check', - "color": 'bg-muted' - }, - 'undistinguish_comment': { - "str": 'un-distinguished {self.target_link}', - "icon": 'fa-crown', - "color": 'bg-muted' - }, - 'undistinguish_post': { - "str": 'un-distinguished {self.target_link}', - "icon": 'fa-crown', - "color": 'bg-muted' - }, - 'unnuke_user': { - "str": 'approved all content of {self.target_link}', - "icon": 'fa-radiation-alt', - "color": 'bg-success' - }, - 'unpin_comment': { - "str": 'un-pinned a {self.target_link}', - "icon": 'fa-thumbtack fa-rotate--45', - "color": 'bg-muted' - }, - 'unpin_post': { - "str": 'un-pinned post {self.target_link}', - "icon": 'fa-thumbtack fa-rotate--45', - "color": 'bg-muted' - }, - 'unset_nsfw': { - "str": 'un-set nsfw on post {self.target_link}', - "icon": 'fa-eye-evil', - "color": 'bg-success' - }, - 'unshadowban': { - "str": 'unshadowbanned {self.target_link}', - "icon": 'fa-eye', - "color": 'bg-success' - } -} - -ACTIONTYPES2 = deepcopy(ACTIONTYPES) -ACTIONTYPES2.pop("shadowban") -ACTIONTYPES2.pop("unshadowban") -ACTIONTYPES2.pop("flair_post") +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base +import time +from files.helpers.lazy import lazy +from os import environ +from copy import deepcopy +from files.helpers.const import * + +class ModAction(Base): + __tablename__ = "modactions" + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + kind = Column(String) + target_user_id = Column(Integer, ForeignKey("users.id")) + target_submission_id = Column(Integer, ForeignKey("submissions.id")) + target_comment_id = Column(Integer, ForeignKey("comments.id")) + _note=Column(String) + created_utc = Column(Integer) + + 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()) + 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 + @lazy + def created_string(self): + return time.strftime('%d %b %Y %H:%M:%S UTC', time.gmtime(self.created_utc)) + + @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, cc=CC_TITLE) + + 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: + if self.target_post.club: return f'{CC} ONLY' + return f'{self.target_post.title_html}' + 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 = { + 'agendaposter': { + "str": 'set chud theme on {self.target_link}', + "icon": 'fa-snooze', + "color": 'bg-danger' + }, + 'approve_app': { + "str": 'approved an application by {self.target_link}', + "icon": 'fa-robot', + "color": 'bg-success' + }, + 'badge_grant': { + "str": 'granted badge to {self.target_link}', + "icon": 'fa-badge', + "color": 'bg-success' + }, + 'badge_remove': { + "str": 'removed badge from {self.target_link}', + "icon": 'fa-badge', + "color": 'bg-danger' + }, + 'ban_comment': { + "str": 'removed {self.target_link}', + "icon": 'fa-comment', + "color": 'bg-danger' + }, + 'ban_domain': { + "str": 'banned a domain', + "icon": 'fa-globe', + "color": 'bg-danger' + }, + 'ban_post': { + "str": 'removed post {self.target_link}', + "icon": 'fa-feather-alt', + "color": 'bg-danger' + }, + 'ban_user': { + "str": 'banned user {self.target_link}', + "icon": 'fa-user-slash', + "color": 'bg-danger' + }, + 'change_sidebar': { + "str": 'changed the sidebar', + "icon": 'fa-columns', + "color": 'bg-primary' + }, + 'check': { + "str": 'gave {self.target_link} a checkmark', + "icon": 'fa-badge-check', + "color": 'bg-success' + }, + 'club_allow': { + "str": 'allowed user {self.target_link} into the {cc}', + "icon": 'fa-golf-club', + "color": 'bg-success' + }, + 'club_ban': { + "str": 'disallowed user {self.target_link} from the {cc}', + "icon": 'fa-golf-club', + "color": 'bg-danger' + }, + 'delete_report': { + "str": 'deleted report on {self.target_link}', + "icon": 'fa-flag', + "color": 'bg-danger' + }, + 'disable_Bots': { + "str": 'disabled Bots', + "icon": 'fa-robot', + "color": 'bg-danger' + }, + 'disable_Fart mode': { + "str": 'disabled fart mode', + "icon": 'fa-gas-pump-slash', + "color": 'bg-danger' + }, + 'disable_Read-only mode': { + "str": 'disabled readonly mode', + "icon": 'fa-book', + "color": 'bg-danger' + }, + 'disable_Signups': { + "str": 'disabled Signups', + "icon": 'fa-users', + "color": 'bg-danger' + }, + 'disable_under_attack': { + "str": 'disabled under attack mode', + "icon": 'fa-shield', + "color": 'bg-muted' + }, + 'distinguish_comment': { + "str": 'distinguished {self.target_link}', + "icon": 'fa-crown', + "color": 'bg-success' + }, + 'distinguish_post': { + "str": 'distinguished {self.target_link}', + "icon": 'fa-crown', + "color": 'bg-success' + }, + 'distribute': { + "str": 'distributed bet winnings to voters on {self.target_link}', + "icon": 'fa-dollar-sign', + "color": 'bg-success' + }, + 'dump_cache': { + "str": 'dumped cache', + "icon": 'fa-trash-alt', + "color": 'bg-muted' + }, + 'edit_post': { + "str": 'edited {self.target_link}', + "icon": 'fa-edit', + "color": 'bg-primary' + }, + 'enable_Bots': { + "str": 'enabled Bots', + "icon": 'fa-robot', + "color": 'bg-success' + }, + 'enable_Fart mode': { + "str": 'enabled fart mode', + "icon": 'fa-gas-pump', + "color": 'bg-success' + }, + 'enable_Read-only mode': { + "str": 'enabled readonly mode', + "icon": 'fa-book', + "color": 'bg-success' + }, + 'enable_Signups': { + "str": 'enabled Signups', + "icon": 'fa-users', + "color": 'bg-success' + }, + 'enable_under_attack': { + "str": 'enabled under attack mode', + "icon": 'fa-shield', + "color": 'bg-success' + }, + 'flair_post': { + "str": 'set a flair on {self.target_link}', + "icon": 'fa-tag', + "color": 'bg-primary' + }, + 'grant_awards': { + "str": 'granted awards to {self.target_link}', + "icon": 'fa-gift', + "color": 'bg-primary' + }, + 'link_accounts': { + "str": 'linked {self.target_link}', + "icon": 'fa-link', + "color": 'bg-success' + }, + 'make_admin': { + "str": 'made {self.target_link} admin', + "icon": 'fa-user-crown', + "color": 'bg-success' + }, + 'make_meme_admin': { + "str": 'made {self.target_link} meme admin', + "icon": 'fa-user-crown', + "color": 'bg-success' + }, + 'monthly': { + "str": 'distributed monthly marseybux', + "icon": 'fa-sack-dollar', + "color": 'bg-success' + }, + 'move_hole': { + "str": 'moved {self.target_link} to /h/{self.target_post.sub}', + "icon": 'fa-manhole', + "color": 'bg-primary' + }, + 'nuke_user': { + "str": 'removed all content of {self.target_link}', + "icon": 'fa-radiation-alt', + "color": 'bg-danger' + }, + 'pin_comment': { + "str": 'pinned a {self.target_link}', + "icon": 'fa-thumbtack fa-rotate--45', + "color": 'bg-success' + }, + 'pin_post': { + "str": 'pinned post {self.target_link}', + "icon": 'fa-thumbtack fa-rotate--45', + "color": 'bg-success' + }, + 'purge_cache': { + "str": 'purged cache', + "icon": 'fa-memory', + "color": 'bg-muted' + }, + 'reject_app': { + "str": 'rejected an application request by {self.target_link}', + "icon": 'fa-robot', + "color": 'bg-muted' + }, + 'remove_admin': { + "str": 'removed {self.target_link} as admin', + "icon": 'fa-user-crown', + "color": 'bg-danger' + }, + 'remove_meme_admin': { + "str": 'removed {self.target_link} as meme admin', + "icon": 'fa-user-crown', + "color": 'bg-danger' + }, + 'revert': { + "str": 'reverted {self.target_link} mod actions', + "icon": 'fa-history', + "color": 'bg-danger' + }, + 'revoke_app': { + "str": 'revoked an application by {self.target_link}', + "icon": 'fa-robot', + "color": 'bg-muted' + }, + 'set_flair_locked': { + "str": "set {self.target_link}'s flair (locked)", + "icon": 'fa-award', + "color": 'bg-primary' + }, + 'set_flair_notlocked': { + "str": "set {self.target_link}'s flair (not locked)", + "icon": 'fa-award', + "color": 'bg-primary' + }, + 'set_nsfw': { + "str": 'set nsfw on post {self.target_link}', + "icon": 'fa-eye-evil', + "color": 'bg-danger' + }, + 'shadowban': { + "str": 'shadowbanned {self.target_link}', + "icon": 'fa-eye-slash', + "color": 'bg-danger' + }, + 'unagendaposter': { + "str": 'removed chud theme from {self.target_link}', + "icon": 'fa-snooze', + "color": 'bg-success' + }, + 'unban_comment': { + "str": 'reinstated {self.target_link}', + "icon": 'fa-comment', + "color": 'bg-success' + }, + 'unban_domain': { + "str": 'unbanned a domain', + "icon": 'fa-globe', + "color": 'bg-success' + }, + 'unban_post': { + "str": 'reinstated post {self.target_link}', + "icon": 'fa-feather-alt', + "color": 'bg-success' + }, + 'unban_user': { + "str": 'unbanned user {self.target_link}', + "icon": 'fa-user', + "color": 'bg-success' + }, + 'uncheck': { + "str": 'removed checkmark from {self.target_link}', + "icon": 'fa-badge-check', + "color": 'bg-muted' + }, + 'undistinguish_comment': { + "str": 'un-distinguished {self.target_link}', + "icon": 'fa-crown', + "color": 'bg-muted' + }, + 'undistinguish_post': { + "str": 'un-distinguished {self.target_link}', + "icon": 'fa-crown', + "color": 'bg-muted' + }, + 'unnuke_user': { + "str": 'approved all content of {self.target_link}', + "icon": 'fa-radiation-alt', + "color": 'bg-success' + }, + 'unpin_comment': { + "str": 'un-pinned a {self.target_link}', + "icon": 'fa-thumbtack fa-rotate--45', + "color": 'bg-muted' + }, + 'unpin_post': { + "str": 'un-pinned post {self.target_link}', + "icon": 'fa-thumbtack fa-rotate--45', + "color": 'bg-muted' + }, + 'unset_nsfw': { + "str": 'un-set nsfw on post {self.target_link}', + "icon": 'fa-eye-evil', + "color": 'bg-success' + }, + 'unshadowban': { + "str": 'unshadowbanned {self.target_link}', + "icon": 'fa-eye', + "color": 'bg-success' + } +} + +ACTIONTYPES2 = deepcopy(ACTIONTYPES) +ACTIONTYPES2.pop("shadowban") +ACTIONTYPES2.pop("unshadowban") +ACTIONTYPES2.pop("flair_post") ACTIONTYPES2.pop("edit_post") \ No newline at end of file diff --git a/files/classes/submission.py b/files/classes/submission.py index ed9e0e860..84daea59d 100644 --- a/files/classes/submission.py +++ b/files/classes/submission.py @@ -165,8 +165,6 @@ class Submission(Base): @lazy def edited_string(self): - if not self.edited_utc: return "never" - age = int(time.time()) - self.edited_utc if age < 60: @@ -184,11 +182,13 @@ class Submission(Base): now = time.gmtime() ctd = time.gmtime(self.edited_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 = now.tm_year - ctd.tm_year + years = int(months / 12) return f"{years}yr ago" @@ -452,7 +452,7 @@ class Submission(Base): def realtitle(self, v): if self.club and not (v and (v.paid_dues or v.id == self.author_id)): if v: return random.choice(TROLLTITLES).format(username=v.username) - elif SITE == 'cringetopia.org': return f'Please make an account to see this post' + elif dues == -2: return f'Please make an account to see this post' else: return f'{CC} MEMBERS ONLY' elif self.title_html: title = self.title_html else: title = self.title diff --git a/files/classes/subscriptions.py b/files/classes/subscriptions.py index 2d81c9ee6..1d4935a85 100644 --- a/files/classes/subscriptions.py +++ b/files/classes/subscriptions.py @@ -1,16 +1,16 @@ -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base - -class Subscription(Base): - __tablename__ = "subscriptions" - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True) - - user = relationship("User", uselist=False, 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" + user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) + submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True) + + user = relationship("User", uselist=False, 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 index 83c58f984..dfb4348ac 100644 --- a/files/classes/user.py +++ b/files/classes/user.py @@ -26,6 +26,9 @@ defaulttheme = environ.get("DEFAULT_THEME", "midnight").strip() defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip() cardview = bool(int(environ.get("CARD_VIEW", 1))) +if SITE_NAME in ('Cringetopia', 'WPD'): patron_default = 7 +else: patron_default = 0 + class User(Base): __tablename__ = "users" @@ -48,7 +51,7 @@ class User(Base): profileurl = Column(String) bannerurl = Column(String) house = Column(String) - patron = Column(Integer, default=0) + patron = Column(Integer, default=patron_default) patron_utc = Column(Integer, default=0) verified = Column(String) verifiedcolor = Column(String) @@ -181,6 +184,21 @@ class User(Base): return time.strftime("%d %b %Y", time.gmtime(self.created_utc)) + + @property + @lazy + def is_cakeday(self): + if time.time() - self.created_utc > 363 * 86400: + date = time.strftime("%d %b", time.gmtime(self.created_utc)) + now = time.strftime("%d %b", time.gmtime()) + if date == now: + if not self.has_badge(134): + new_badge = Badge(badge_id=134, user_id=self.id) + g.db.add(new_badge) + g.db.commit() + return True + return False + @property @lazy def discount(self): @@ -190,6 +208,7 @@ class User(Base): elif self.patron == 4: discount = 0.75 elif self.patron == 5: discount = 0.70 elif self.patron == 6: discount = 0.65 + elif self.patron == 7: discount = 0.60 else: discount = 1 for badge in discounts: @@ -306,7 +325,7 @@ class User(Base): @property @lazy def follow_count(self): - return g.db.query(Follow.target_id).filter_by(user_id=self.id).count() + return g.db.query(Follow).filter_by(user_id=self.id).count() @property @lazy @@ -406,7 +425,7 @@ class User(Base): @lazy def modaction_num(self): if self.admin_level < 2: return 0 - return g.db.query(ModAction.id).filter_by(user_id=self.id).count() + return g.db.query(ModAction).filter_by(user_id=self.id).count() @property @lazy @@ -421,12 +440,12 @@ class User(Base): @property @lazy def post_notifications_count(self): - return g.db.query(Notification.user_id).join(Comment).filter(Notification.user_id == self.id, Notification.read == False, Comment.author_id == AUTOJANNY_ID).count() + return g.db.query(Notification).join(Comment).filter(Notification.user_id == self.id, Notification.read == False, Comment.author_id == AUTOJANNY_ID).count() @property @lazy def reddit_notifications_count(self): - return g.db.query(Notification.user_id).join(Comment).filter(Notification.user_id == self.id, Notification.read == False, Comment.is_banned == False, Comment.deleted_utc == 0, Comment.body_html.like('%

New site mention: " \ No newline at end of file diff --git a/files/classes/votes.py b/files/classes/votes.py index 65ad90ca3..407c9319b 100644 --- a/files/classes/votes.py +++ b/files/classes/votes.py @@ -1,87 +1,87 @@ -from flask import * -from sqlalchemy import * -from sqlalchemy.orm import relationship -from files.__main__ import Base -from files.helpers.lazy import lazy -import time - -class Vote(Base): - - __tablename__ = "votes" - - submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True) - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - vote_type = Column(Integer) - app_id = Column(Integer, ForeignKey("oauth_apps.id")) - real = Column(Boolean, default=True) - created_utc = Column(Integer) - - user = relationship("User", lazy="subquery", viewonly=True) - post = relationship("Submission", lazy="subquery", 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"" - - @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" - - comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True) - user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - vote_type = Column(Integer) - app_id = Column(Integer, ForeignKey("oauth_apps.id")) - real = Column(Boolean, default=True) - created_utc = Column(Integer) - - user = relationship("User", lazy="subquery") - comment = relationship("Comment", lazy="subquery", 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"" - - @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 +import time + +class Vote(Base): + + __tablename__ = "votes" + + submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) + vote_type = Column(Integer) + app_id = Column(Integer, ForeignKey("oauth_apps.id")) + real = Column(Boolean, default=True) + created_utc = Column(Integer) + + user = relationship("User", lazy="subquery", viewonly=True) + post = relationship("Submission", lazy="subquery", 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"" + + @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" + + comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) + vote_type = Column(Integer) + app_id = Column(Integer, ForeignKey("oauth_apps.id")) + real = Column(Boolean, default=True) + created_utc = Column(Integer) + + user = relationship("User", lazy="subquery") + comment = relationship("Comment", lazy="subquery", 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"" + + @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 index 4db12f83f..43bf01f67 100644 --- a/files/helpers/alerts.py +++ b/files/helpers/alerts.py @@ -1,100 +1,112 @@ -from files.classes import * -from flask import g -from .sanitize import * -from .const import * - -def create_comment(text_html, autojanny=False): - if autojanny: author_id = AUTOJANNY_ID - else: author_id = NOTIFICATIONS_ID - - new_comment = Comment(author_id=author_id, - parent_submission=None, - body_html=text_html, - distinguish_level=6) - g.db.add(new_comment) - g.db.flush() - - new_comment.top_comment_id = new_comment.id - - return new_comment.id - -def send_repeatable_notification(uid, text, autojanny=False): - - if autojanny: author_id = AUTOJANNY_ID - else: author_id = NOTIFICATIONS_ID - - text_html = sanitize(text) - - existing_comment = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).first() - - if existing_comment: - cid = existing_comment[0] - existing_notif = g.db.query(Notification.user_id).filter_by(user_id=uid, comment_id=cid).one_or_none() - if existing_notif: cid = create_comment(text_html, autojanny) - else: cid = create_comment(text_html, autojanny) - - notif = Notification(comment_id=cid, user_id=uid) - g.db.add(notif) - - -def send_notification(uid, text, autojanny=False): - - cid = notif_comment(text, autojanny) - add_notif(cid, uid) - - -def notif_comment(text, autojanny=False): - - if autojanny: - author_id = AUTOJANNY_ID - alert = True - else: - author_id = NOTIFICATIONS_ID - alert = False - - text_html = sanitize(text, alert=alert) - - existing = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).one_or_none() - - if existing: return existing[0] - else: return create_comment(text_html, autojanny) - - -def notif_comment2(p): - - search_html = f'% has mentioned you: %' - - existing = g.db.query(Comment.id).filter(Comment.author_id == NOTIFICATIONS_ID, Comment.parent_submission == None, Comment.body_html.like(search_html)).first() - - if existing: return existing[0] - else: - text = f"@{p.author.username} has mentioned you: [{p.title}](/post/{p.id})" - if p.sub: text += f" in /h/{p.sub}" - text_html = sanitize(text, alert=True) - return create_comment(text_html) - - -def add_notif(cid, uid): - existing = g.db.query(Notification.user_id).filter_by(comment_id=cid, user_id=uid).one_or_none() - if not existing: - notif = Notification(comment_id=cid, user_id=uid) - g.db.add(notif) - - -def NOTIFY_USERS(text, v): - notify_users = set() - for word, id in NOTIFIED_USERS.items(): - if id == 0 or v.id == id: continue - if word in text.lower() and id not in notify_users: notify_users.add(id) - - captured = [] - for i in mention_regex.finditer(text): - if v.username.lower() == i.group(2).lower(): continue - - if i.group(0) in captured: continue - captured.append(i.group(0)) - - user = get_user(i.group(2), graceful=True) - if user and v.id != user.id and not v.any_block_exists(user): notify_users.add(user.id) - +from files.classes import * +from flask import g +from .sanitize import * +from .const import * + +def create_comment(text_html, autojanny=False): + if autojanny: author_id = AUTOJANNY_ID + else: author_id = NOTIFICATIONS_ID + + new_comment = Comment(author_id=author_id, + parent_submission=None, + body_html=text_html, + distinguish_level=6) + g.db.add(new_comment) + g.db.flush() + + new_comment.top_comment_id = new_comment.id + + return new_comment.id + +def send_repeatable_notification(uid, text, autojanny=False): + + if autojanny: author_id = AUTOJANNY_ID + else: author_id = NOTIFICATIONS_ID + + text_html = sanitize(text) + + existing_comment = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).first() + + if existing_comment: + cid = existing_comment[0] + existing_notif = g.db.query(Notification.user_id).filter_by(user_id=uid, comment_id=cid).one_or_none() + if existing_notif: cid = create_comment(text_html, autojanny) + else: cid = create_comment(text_html, autojanny) + + notif = Notification(comment_id=cid, user_id=uid) + g.db.add(notif) + + +def send_notification(uid, text, autojanny=False): + + cid = notif_comment(text, autojanny) + add_notif(cid, uid) + + +def notif_comment(text, autojanny=False): + + if autojanny: + author_id = AUTOJANNY_ID + alert = True + else: + author_id = NOTIFICATIONS_ID + alert = False + + text_html = sanitize(text, alert=alert) + + try: existing = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).one_or_none() + except: + existing = g.db.query(Comment).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).all() + + + notifs = g.db.query(Notification).filter(Notification.comment_id.in_([x.id for x in existing])).all() + for c in notifs: g.db.delete(c) + g.db.flush() + + + for c in existing: g.db.delete(c) + g.db.flush() + existing = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).one_or_none() + + if existing: return existing[0] + else: return create_comment(text_html, autojanny) + + +def notif_comment2(p): + + search_html = f'% has mentioned you: %' + + existing = g.db.query(Comment.id).filter(Comment.author_id == NOTIFICATIONS_ID, Comment.parent_submission == None, Comment.body_html.like(search_html)).first() + + if existing: return existing[0] + else: + text = f"@{p.author.username} has mentioned you: [{p.title}](/post/{p.id})" + if p.sub: text += f" in /h/{p.sub}" + text_html = sanitize(text, alert=True) + return create_comment(text_html) + + +def add_notif(cid, uid): + existing = g.db.query(Notification.user_id).filter_by(comment_id=cid, user_id=uid).one_or_none() + if not existing: + notif = Notification(comment_id=cid, user_id=uid) + g.db.add(notif) + + +def NOTIFY_USERS(text, v): + notify_users = set() + for word, id in NOTIFIED_USERS.items(): + if id == 0 or v.id == id: continue + if word in text.lower() and id not in notify_users: notify_users.add(id) + + captured = [] + for i in mention_regex.finditer(text): + if v.username.lower() == i.group(2).lower(): continue + + if i.group(0) in captured: continue + captured.append(i.group(0)) + + user = get_user(i.group(2), graceful=True) + if user and v.id != user.id and not v.any_block_exists(user): notify_users.add(user.id) + return notify_users \ No newline at end of file diff --git a/files/helpers/blackjack.py b/files/helpers/blackjack.py index 63d1d0b02..6b08d90d5 100644 --- a/files/helpers/blackjack.py +++ b/files/helpers/blackjack.py @@ -6,7 +6,7 @@ deck_count = 4 ranks = ("2", "3", "4", "5", "6", "7", "8", "9", "X", "J", "Q", "K", "A") suits = ("♠️", "♥️", "♣️", "♦️") coins_command_word = "!blackjack" -marseybucks_command_word = "!blackjackmb" +marseybux_command_word = "!blackjackmb" minimum_bet = 100 maximum_bet = INFINITY @@ -51,7 +51,7 @@ def format_all(player_hand, dealer_hand, deck, status, wager, kind, is_insured=0 def check_for_blackjack_commands(in_text, from_user, from_comment): - for command_word in (coins_command_word, marseybucks_command_word): + for command_word in (coins_command_word, marseybux_command_word): currency_prop = "coins" if command_word == coins_command_word else "procoins" currency_value = getattr(from_user, currency_prop, 0) @@ -107,7 +107,8 @@ def player_stayed(from_comment): deck = deck.split("/") if dealer_value == 21 and is_insured == "1": - from_comment.author.coins += int(wager) + currency_value = getattr(from_comment.author, kind, 0) + setattr(from_comment.author, kind, currency_value + int(wager)) else: while dealer_value < 17 and dealer_value != -1: next = deck.pop(0) @@ -126,12 +127,13 @@ def player_doubled_down(from_comment): # When doubling down, the player receives one additional card (a "hit") and their initial bet is doubled. player_hand, dealer_hand, deck, status, wager, kind, is_insured = from_comment.blackjack_result.split("_") wager_value = int(wager) + currency_value = getattr(from_comment.author, kind, 0) # Gotsta have enough coins - if (from_comment.author.coins < wager_value): return + if (currency_value < wager_value): return # Double the initial wager - from_comment.author.coins -= wager_value + setattr(from_comment.author, kind, currency_value - wager_value) wager_value *= 2 # Apply the changes to the stored hand. @@ -148,12 +150,13 @@ def player_bought_insurance(from_comment): player_hand, dealer_hand, deck, status, wager, kind, is_insured = from_comment.blackjack_result.split("_") wager_value = int(wager) insurance_cost = wager_value / 2 + currency_value = getattr(from_comment.author, kind, 0) # Gotsta have enough coins - if (from_comment.author.coins < insurance_cost): return + if (currency_value < insurance_cost): return # Charge for (and grant) insurance - from_comment.author.coins -= insurance_cost + setattr(from_comment.author, kind, currency_value - insurance_cost) is_insured = 1 # Apply the changes to the stored hand. diff --git a/files/helpers/const.py b/files/helpers/const.py index 62dc65cba..a7da7a0e3 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -26,29 +26,23 @@ AJ_REPLACEMENTS = { ' YOUR ': " YOU'RE ", ' TO ': " TOO ", + + 'anybody': 'anypony', + 'everybody': 'everypony', + + 'Anybody': 'Anypony', + 'Everybody': 'Everypony', + + 'ANYBODY': 'ANYPONY', + 'EVERYBODY': 'EVERYPONY', } -if SITE_NAME == 'Cringetopia': - SLURS = { - "retarded": "neurodivergent", - "retard": "neurodivergent", - "faggotry": "cute twinkry", - "faggot": "cute twink", - "n1gger": "🏀", - "nlgger": "🏀", - "nigger": "🏀", - "uss liberty incident": "tragic accident aboard the USS Liberty", - "lavon affair": "Lavon Misunderstanding", - "i hate marsey": "i love marsey", - "autistic": "neurodivergent", - "holohoax": "i tried to claim the Holocaust didn't happen because I am a pencil-dicked imbecile and the word filter caught me lol", - "i hate carp": "i love Carp", - "heil hitler": "hello kitty", - - " fag ": " cute twink ", - } -else: +if SITE_NAME == 'rDrama': SLURS = { + "california": "commiefornia", + "hollywood": "hollyweird", + "tiananmen square": "tiananmen square didn't happen (but it should have)", + "dasha": "beautiful angelic perfect Dasha/future Mrs. Carp", "retarded": "r-slurred", "retard": "r-slur", "gayfag": "gaystrag", @@ -56,8 +50,8 @@ else: "richfag": "richstrag", "newfag": "newstrag", "oldfag": "oldstrag", - "faggotry": "cute twinkry", "faggot": "cute twink", + "fag": "cute twink", "pedophile": "libertarian", "kill yourself": "keep yourself safe", "n1gger": "BIPOC", @@ -104,15 +98,32 @@ else: "elon musk": "rocket daddy", "fake and gay": "fake and straight", - " rapist ": " male feminist ", - " pedo ": " libertarian ", + " rapist": " male feminist", + " kys ": " keep yourself safe ", - " fag ": " cute twink ", + " pedo ": " libertarian ", + " pedos ": " libertarians ", + } +else: + SLURS = { + "retarded": "neurodivergent", + "retard": "neurodivergent", + "faggot": "cute twink", + "fag": "cute twink", + "n1gger": "🏀", + "nlgger": "🏀", + "nigger": "🏀", + "uss liberty incident": "tragic accident aboard the USS Liberty", + "lavon affair": "Lavon Misunderstanding", + "i hate marsey": "i love marsey", + "autistic": "neurodivergent", + "holohoax": "i tried to claim the Holocaust didn't happen because I am a pencil-dicked imbecile and the word filter caught me lol", + "i hate carp": "i love Carp", + "heil hitler": "hello kitty", } single_words = "|".join([slur.lower() for slur in SLURS.keys()]) - 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.", ':#marseywoah:', '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.", f'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 {SITE} was the best option?', "I don't have enough spoons to read this shit", "All those words won't bring daddy back.", 'OUT!', "Damn, you're really mad over this, but thanks for the effort you put into typing that all out! Sadly I won't read it all.", "Jesse what the fuck are you talking about??", "▼you're fucking bananas if you think I'm reading all that, take my downvote and shut up idiot", "Are you feeling okay bud?") AGENDAPOSTER_PHRASE = 'trans lives matter' @@ -130,6 +141,7 @@ if SITE in {'rdrama.net','devrama.xyz'}: AUTOCHOICE_ID = 9167 BASEDBOT_ID = 0 + SCHIZO_ID = 8494 A_ID = 1230 KIPPY_ID = 7150 GIFT_NOTIF_ID = 995 @@ -173,6 +185,7 @@ elif SITE == "pcmemes.net": AUTOCHOICE_ID = 2072 BASEDBOT_ID = 800 + SCHIZO_ID = 0 A_ID = 0 KIPPY_ID = 1592 PIZZASHILL_ID = 0 @@ -205,6 +218,7 @@ elif SITE == 'cringetopia.org': AUTOCHOICE_ID = 8 BASEDBOT_ID = 0 + SCHIZO_ID = 0 A_ID = 0 KIPPY_ID = 0 GIFT_NOTIF_ID = 43 @@ -248,6 +262,7 @@ else: AUTOCHOICE_ID = 8 BASEDBOT_ID = 0 + SCHIZO_ID = 0 A_ID = 0 KIPPY_ID = 0 GIFT_NOTIF_ID = 9 @@ -613,6 +628,14 @@ AWARDS = { "color": "text-gold", "price": 50000 }, + "checkmark": { + "kind": "checkmark", + "title": "Checkmark", + "description": "Gives the recipient a checkmark.", + "icon": "fas fa-badge-check", + "color": "checkmark", + "price": 100000 + }, "firework": { "kind": "firework", "title": "Fireworks", @@ -671,7 +694,7 @@ if SITE_NAME == 'PCM': AWARDS2 = deepcopy(AWARDS) for k, val in AWARDS.items(): if val['description'] == '' and not (k == 'ghost' and SITE_NAME == 'PCM'): AWARDS2.pop(k) - if SITE == 'pcmemes.net' and k in ('ban','pizzashill','marsey','bird','grass','chud'): AWARDS2.pop(k) + if SITE == 'pcmemes.net' and k in ('ban','pizzashill','marsey','bird','grass','chud','unblockable'): AWARDS2.pop(k) AWARDS3 = {} @@ -704,10 +727,14 @@ NOTIFIED_USERS = { 'kippy': KIPPY_ID, 'the_homocracy': HOMO_ID, 'soren': SOREN_ID, + 'schizocel': SCHIZO_ID, + 'scitzocel': SCHIZO_ID } FORTUNE_REPLIES = ('Your fortune: Allah Wills It','Your fortune: Inshallah, Only Good Things Shall Come To Pass','Your fortune: Allah Smiles At You This Day','Your fortune: Your Bussy Is In For A Blasting','Your fortune: You Will Be Propositioned By A High-Tier Twink','Your fortune: Repent, You Have Displeased Allah And His Vengeance Is Nigh','Your fortune: Reply Hazy, Try Again','Your fortune: lmao you just lost 100 coins','Your fortune: Yikes 😬','Your fortune: You Will Be Blessed With Many Black Bulls','Your fortune: NEETmax, The Day Is Lost If You Venture Outside','Your fortune: A Taste Of Jannah Awaits You Today','Your fortune: Watch Your Back','Your fortune: Outlook good','Your fortune: Godly Luck','Your fortune: Good Luck','Your fortune: Bad Luck','Your fortune: Good news will come to you by mail','Your fortune: Very Bad Luck','Your fortune: キタ━━━━━━(゚∀゚)━━━━━━ !!!!','Your fortune: Better not tell you now','Your fortune: You will meet a dark handsome stranger','Your fortune: ( ´_ゝ`)フーン','Your fortune: Excellent Luck','Your fortune: Average Luck') +FACTCHECK_REPLIES = ('Factcheck: This claim has been confirmed as correct by experts. ','Factcheck: This claim has been classified as misogynistic.','Factcheck: This claim is currently being debunked.','Factcheck: This claim is 100% true.','Factcheck: This claim hurts trans lives.','Factcheck: [REDACTED].','Factcheck: This claim is both true and false.','Factcheck: You really believe that shit? Lmao dumbass nigga 🤣','Factcheck: None of this is real.','Factcheck: Yes.','Factcheck: This claim has not been approved by experts.','Factcheck: This claim is a gross exageration of reality.','Factcheck: WARNING! THIS CLAIM HAS BEEN CLASSIFIED AS DANGEROUS. PLEASE REMAIN STILL, AN AGENT WILL COME TO MEET YOU SHORTLY.') + if SITE_NAME == 'rDrama': patron = 'Paypig' else: patron = 'Patron' @@ -808,6 +835,7 @@ slur_regex = re.compile(f"({single_words})(?![^<]*>)", flags=re.I|re.A) slur_regex_upper = re.compile(f"({single_words.upper()})(?![^<]*>)", flags=re.A) torture_regex = re.compile('(^|\s)(i|me) ', flags=re.I|re.A) torture_regex2 = re.compile("(^|\s)i'm ", flags=re.I|re.A) +torture_regex_exclude = re.compile('^\s*>', flags=re.A) def sub_matcher(match): return SLURS[match.group(0).lower()] @@ -822,15 +850,21 @@ def censor_slurs(body, logged_user): return body def torture_ap(body, username): - for k, l in AJ_REPLACEMENTS.items(): - body = body.replace(k, l) - body = torture_regex.sub(rf'\1@{username} ', body) - body = torture_regex2.sub(rf'\1@{username} is ', body) - return body + lines = body.splitlines(keepends=True) + + for i in range(len(lines)): + if torture_regex_exclude.match(lines[i]): + continue + for k, l in AJ_REPLACEMENTS.items(): + lines[i] = lines[i].replace(k, l) + lines[i] = torture_regex.sub(rf'\1@{username} ', lines[i]) + lines[i] = torture_regex2.sub(rf'\1@{username} is ', lines[i]) + + return ''.join(lines) YOUTUBE_KEY = environ.get("YOUTUBE_KEY", "").strip() -ADMIGGERS = (37696,37697,37749,37833,37838) +ADMIGGERS = (37696,37697,37749,37833,37838,39413) proxies = {"http":"http://127.0.0.1:18080","https":"http://127.0.0.1:18080"} @@ -840,9 +874,9 @@ approved_embed_hosts = [ 'rdrama.net', 'pcmemes.net', 'cringetopia.org', + 'watchpeopledie.co', 'devrama.xyz', 'imgur.com', - 'ibb.co', 'lain.la', 'pngfind.com', 'kym-cdn.com', @@ -887,7 +921,13 @@ approved_embed_hosts = [ 'githubusercontent.com', 'unilad.co.uk', 'grrrgraphics.com', - 'redditmedia.com' + 'redditmedia.com', + 'deviantart.com', + 'deviantart.net', + 'googleapis.com', + 'bing.com', + 'typekit.net', + 'postimg.cc' ] hosts = "|".join(approved_embed_hosts).replace('.','\.') @@ -906,8 +946,13 @@ yt_id_regex = re.compile('[a-z0-9-_]{5,20}', flags=re.I|re.A) image_regex = re.compile("(^|\s)(https:\/\/[\w\-.#&/=\?@%;+]{5,250}(\.png|\.jpg|\.jpeg|\.gif|\.webp|maxwidth=9999|fidelity=high))($|\s)", flags=re.I|re.A) +link_fix_regex = re.compile("(?!.*(http|\/))(.*\[[^\]]+\]\()([^)]+\))", flags=re.A) + +css_regex = re.compile('''url\(['"]?(.*?)['"]?\)''', flags=re.I|re.A) +css_regex2 = re.compile('''['"](http.*?)['"]''', flags=re.I|re.A) + procoins_li = (0,2500,5000,10000,25000,50000,125000,250000) linefeeds_regex = re.compile("([^\n])\n([^\n])", flags=re.A) -def make_name(*args, **kwargs): return request.base_url \ No newline at end of file +def make_name(*args, **kwargs): return request.base_url diff --git a/files/helpers/discord.py b/files/helpers/discord.py index 695b635a8..e8ac3df00 100644 --- a/files/helpers/discord.py +++ b/files/helpers/discord.py @@ -1,66 +1,60 @@ -from os import environ -import requests -import threading -from .const import * - -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() - -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, timeout=5) - -@discord_wrap -def remove_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.delete(url, headers=headers, timeout=5) - -@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, timeout=5) - -@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, timeout=5) - -def send_discord_message(message): - headers = {"Authorization": f"Bot {BOT_TOKEN}"} - data={"content": message} - requests.post("https://discordapp.com/api/channels/924485611715452940/messages", headers=headers, data=data, timeout=5) - requests.post("https://discordapp.com/api/channels/924486091795484732/messages", headers=headers, data=data, timeout=5) - - -def send_cringetopia_message(message): - headers = {"Authorization": f"Bot {BOT_TOKEN}"} - data={"content": message} - requests.post("https://discordapp.com/api/channels/965264044531527740/messages", headers=headers, data=data, timeout=5) \ No newline at end of file +from os import environ +import requests +import threading +from .const import * + +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() + +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, timeout=5) + +@discord_wrap +def remove_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.delete(url, headers=headers, timeout=5) + +@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, timeout=5) + +@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, timeout=5) + +def send_discord_message(message): + headers = {"Authorization": f"Bot {BOT_TOKEN}"} + data={"content": message} + requests.post("https://discordapp.com/api/channels/924485611715452940/messages", headers=headers, data=data, timeout=5) + requests.post("https://discordapp.com/api/channels/924486091795484732/messages", headers=headers, data=data, timeout=5) \ No newline at end of file diff --git a/files/helpers/get.py b/files/helpers/get.py index 8c411d975..1118d33e4 100644 --- a/files/helpers/get.py +++ b/files/helpers/get.py @@ -1,289 +1,289 @@ -from files.classes import * -from flask import g - - -def get_id(username, v=None, graceful=False): - - username = username.replace('\\', '').replace('_', '\_').replace('%', '').strip() - - user = g.db.query( - User.id - ).filter( - or_( - User.username.ilike(username), - User.original_username.ilike(username) - ) - ).one_or_none() - - if not user: - if not graceful: - abort(404) - else: - return None - - return user[0] - - -def get_user(username, v=None, graceful=False): - - if not username: - if not graceful: abort(404) - else: return None - - username = username.replace('\\', '').replace('_', '\_').replace('%', '').strip() - - user = g.db.query( - User - ).filter( - or_( - User.username.ilike(username), - User.original_username.ilike(username) - ) - ).one_or_none() - - if not user: - if not graceful: abort(404) - else: return None - - if v: - block = g.db.query(UserBlock).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): - - try: id = int(id) - except: abort(404) - - user = g.db.query(User).filter_by(id = id).one_or_none() - - if not user: abort(404) - - if v: - block = g.db.query(UserBlock).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).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.target_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.one_or_none() - - if not items: - if graceful: return None - else: 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).one_or_none() - if not items: - if graceful: return None - else: abort(404) - x=items - - return x - - -def get_posts(pids, v=None): - - if not pids: - return [] - - if v: - vt = g.db.query(Vote).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.target_id, - blocked.c.target_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,).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).filter(Comment.id == i).one_or_none() - - if not comment and not graceful: abort(404) - - block = g.db.query(UserBlock).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).filter_by(user_id=v.id, comment_id=comment.id) - vt = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none() - 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).filter(Comment.id == i).one_or_none() - if not comment and not graceful:abort(404) - - return comment - - -def get_comments(cids, v=None, load_parent=False): - - if not cids: return [] - - if v: - votes = g.db.query(CommentVote).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.target_id, - blocked.c.target_id, - ).filter(Comment.id.in_(cids)) - - if not (v and (v.shadowbanned or v.admin_level > 2)): - comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) - - 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: - output = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.id.in_(cids)).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) - - doms = [x for x in g.db.query(BannedDomain).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_id(username, v=None, graceful=False): + + username = username.replace('\\', '').replace('_', '\_').replace('%', '').strip() + + user = g.db.query( + User.id + ).filter( + or_( + User.username.ilike(username), + User.original_username.ilike(username) + ) + ).one_or_none() + + if not user: + if not graceful: + abort(404) + else: + return None + + return user[0] + + +def get_user(username, v=None, graceful=False): + + if not username: + if not graceful: abort(404) + else: return None + + username = username.replace('\\', '').replace('_', '\_').replace('%', '').strip() + + user = g.db.query( + User + ).filter( + or_( + User.username.ilike(username), + User.original_username.ilike(username) + ) + ).one_or_none() + + if not user: + if not graceful: abort(404) + else: return None + + if v: + block = g.db.query(UserBlock).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): + + try: id = int(id) + except: abort(404) + + user = g.db.query(User).filter_by(id = id).one_or_none() + + if not user: abort(404) + + if v: + block = g.db.query(UserBlock).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).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.target_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.one_or_none() + + if not items: + if graceful: return None + else: 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).one_or_none() + if not items: + if graceful: return None + else: abort(404) + x=items + + return x + + +def get_posts(pids, v=None): + + if not pids: + return [] + + if v: + vt = g.db.query(Vote).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.target_id, + blocked.c.target_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,).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).filter(Comment.id == i).one_or_none() + + if not comment and not graceful: abort(404) + + block = g.db.query(UserBlock).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).filter_by(user_id=v.id, comment_id=comment.id) + vt = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none() + 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).filter(Comment.id == i).one_or_none() + if not comment and not graceful:abort(404) + + return comment + + +def get_comments(cids, v=None, load_parent=False): + + if not cids: return [] + + if v: + votes = g.db.query(CommentVote).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.target_id, + blocked.c.target_id, + ).filter(Comment.id.in_(cids)) + + if not (v and (v.shadowbanned or v.admin_level > 2)): + comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) + + 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: + output = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.id.in_(cids)).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) + + doms = [x for x in g.db.query(BannedDomain).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 index e22abe92a..cda0292c6 100644 --- a/files/helpers/images.py +++ b/files/helpers/images.py @@ -1,28 +1,35 @@ -from PIL import Image, ImageOps -from PIL.ImageSequence import Iterator -from webptools import gifwebp -import subprocess - -def process_image(filename=None, resize=0): - - i = Image.open(filename) - - if resize and i.width > resize: - try: subprocess.call(["convert", filename, "-coalesce", "-resize", f"{resize}>", filename]) - except: pass - elif i.format.lower() != "webp": - - exif = i.getexif() - for k in exif.keys(): - if k != 0x0112: - exif[k] = None - del exif[k] - i.info["exif"] = exif.tobytes() - - if i.format.lower() == "gif": - gifwebp(input_image=filename, output_image=filename, option="-mixed -metadata none -f 100 -mt -m 6") - else: - i = ImageOps.exif_transpose(i) - i.save(filename, format="WEBP", method=6) - +from PIL import Image, ImageOps +from PIL.ImageSequence import Iterator +from webptools import gifwebp +import subprocess +import os +from flask import abort + +def process_image(patron, filename=None, resize=0): + size = os.stat(filename).st_size + + if size > 16 * 1024 * 1024 or not patron and size > 8 * 1024 * 1024: + os.remove(filename) + abort(413) + + i = Image.open(filename) + + if resize and i.width > resize: + try: subprocess.call(["convert", filename, "-coalesce", "-resize", f"{resize}>", filename]) + except: pass + elif i.format.lower() != "webp": + + exif = i.getexif() + for k in exif.keys(): + if k != 0x0112: + exif[k] = None + del exif[k] + i.info["exif"] = exif.tobytes() + + if i.format.lower() == "gif": + gifwebp(input_image=filename, output_image=filename, option="-mixed -metadata none -f 100 -mt -m 6") + else: + i = ImageOps.exif_transpose(i) + i.save(filename, format="WEBP", method=6) + return filename \ No newline at end of file diff --git a/files/helpers/jinja2.py b/files/helpers/jinja2.py index c2786f67f..1cec597c8 100644 --- a/files/helpers/jinja2.py +++ b/files/helpers/jinja2.py @@ -3,6 +3,7 @@ from .get import * from os import listdir, environ from .const import * import time +from datetime import datetime @app.template_filter("post_embed") def post_embed(id, v): @@ -49,4 +50,4 @@ def timestamp(timestamp): @app.context_processor def inject_constants(): - return {"environ":environ, "SITE":SITE, "SITE_NAME":SITE_NAME, "SITE_FULL":SITE_FULL, "AUTOJANNY_ID":AUTOJANNY_ID, "NOTIFICATIONS_ID":NOTIFICATIONS_ID, "PUSHER_ID":PUSHER_ID, "CC":CC, "CC_TITLE":CC_TITLE, "listdir":listdir, "MOOSE_ID":MOOSE_ID, "AEVANN_ID":AEVANN_ID, "PIZZASHILL_ID":PIZZASHILL_ID, "config":app.config.get, "DEFAULT_COLOR":DEFAULT_COLOR, "COLORS":COLORS, "ADMIGGERS":ADMIGGERS} \ No newline at end of file + return {"environ":environ, "SITE":SITE, "SITE_NAME":SITE_NAME, "SITE_FULL":SITE_FULL, "AUTOJANNY_ID":AUTOJANNY_ID, "NOTIFICATIONS_ID":NOTIFICATIONS_ID, "PUSHER_ID":PUSHER_ID, "CC":CC, "CC_TITLE":CC_TITLE, "listdir":listdir, "MOOSE_ID":MOOSE_ID, "AEVANN_ID":AEVANN_ID, "PIZZASHILL_ID":PIZZASHILL_ID, "config":app.config.get, "DEFAULT_COLOR":DEFAULT_COLOR, "COLORS":COLORS, "ADMIGGERS":ADMIGGERS, "datetime":datetime} \ No newline at end of file diff --git a/files/helpers/lazy.py b/files/helpers/lazy.py index 825d5564b..e91cf2b63 100644 --- 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/sanitize.py b/files/helpers/sanitize.py index fc1ffcc52..5c11581d6 100644 --- a/files/helpers/sanitize.py +++ b/files/helpers/sanitize.py @@ -1,328 +1,357 @@ -import bleach -from bs4 import BeautifulSoup -from bleach.linkifier import LinkifyFilter, build_url_re -from functools import partial -from .get import * -from os import path, environ -import re -from mistletoe import markdown -from json import loads, dump -from random import random, choice -import signal -import time -import requests - -TLDS = ('ac','ad','ae','aero','af','ag','ai','al','am','an','ao','aq','ar','arpa','as','asia','at','au','aw','ax','az','ba','bb','bd','be','bf','bg','bh','bi','biz','bj','bm','bn','bo','br','bs','bt','bv','bw','by','bz','ca','cafe','cat','cc','cd','cf','cg','ch','ci','ck','cl','club','cm','cn','co','com','coop','cr','cu','cv','cx','cy','cz','de','dj','dk','dm','do','dz','ec','edu','ee','eg','er','es','et','eu','fi','fj','fk','fm','fo','fr','ga','gb','gd','ge','gf','gg','gh','gi','gl','gm','gn','gov','gp','gq','gr','gs','gt','gu','gw','gy','hk','hm','hn','hr','ht','hu','id','ie','il','im','in','info','int','io','iq','ir','is','it','je','jm','jo','jobs','jp','ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz','la','lb','lc','li','lk','lr','ls','lt','lu','lv','ly','ma','mc','md','me','mg','mh','mil','mk','ml','mm','mn','mo','mobi','mp','mq','mr','ms','mt','mu','museum','mv','mw','mx','my','mz','na','name','nc','ne','net','nf','ng','ni','nl','no','np','nr','nu','nz','om','org','pa','pe','pf','pg','ph','pk','pl','pm','pn','post','pr','pro','ps','pt','pw','py','qa','re','ro','rs','ru','rw','sa','sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so','social','sr','ss','st','su','sv','sx','sy','sz','tc','td','tel','tf','tg','th','tj','tk','tl','tm','tn','to','tp','tr','travel','tt','tv','tw','tz','ua','ug','uk','us','uy','uz','va','vc','ve','vg','vi','vn','vu','wf','win','ws','xn','xxx','xyz','ye','yt','yu','za','zm','zw') - -allowed_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','span','ruby','rp','rt','spoiler','img','lite-youtube','video','source') - -def allowed_attributes(tag, name, value): - - if name == 'style': return True - - if tag == 'marquee': - if name in ['direction', 'behavior', 'scrollamount']: return True - if name in {'height', 'width'}: - try: value = int(value.replace('px', '')) - except: return False - if 0 < value <= 250: return True - return False - - if tag == 'a': - if name == 'href': return True - if name == 'rel' and value == 'nofollow noopener noreferrer': return True - if name == 'target' and value == '_blank': return True - return False - - if tag == 'img': - if name in ['src','data-src']: - if value.startswith('/') or value.startswith(f'{SITE_FULL}/') or embed_fullmatch_regex.fullmatch(value): return True - else: return False - - if name == 'loading' and value == 'lazy': return True - if name == 'referrpolicy' and value == 'no-referrer': return True - if name == 'data-bs-toggle' and value == 'tooltip': return True - if name in ['alt','title','g','b','pat']: return True - if name == 'class' and value == 'pat-hand': return True - return False - - if tag == 'lite-youtube': - if name == 'params' and value.startswith('autoplay=1&modestbranding=1'): return True - if name == 'videoid': return True - return False - - if tag == 'video': - if name == 'controls' and value == '': return True - if name == 'preload' and value == 'none': return True - return False - - if tag == 'source': - if name == 'src' and embed_fullmatch_regex.fullmatch(value): return True - return False - - if tag == 'p': - if name == 'class' and value == 'mb-0': return True - return False - - if tag == 'span': - if name == 'class' and value in ['pat-container', 'pat-hand']: return True - if name == 'data-bs-toggle' and value == 'tooltip': return True - if name == 'title': return True - if name == 'alt': return True - return False - - -url_re = build_url_re(tlds=TLDS, protocols=['http', 'https']) - -def callback(attrs, new=False): - href = attrs[(None, "href")] - - if not href.startswith('/') and not href.startswith(f'{SITE_FULL}/'): - attrs[(None, "target")] = "_blank" - attrs[(None, "rel")] = "nofollow noopener noreferrer" - - return attrs - - -def handler(signum, frame): - print("Timeout!") - raise Exception("Timeout") - -def render_emoji(html, regexp, edit, marseys_used=set(), b=False): - emojis = list(regexp.finditer(html)) - captured = set() - - for i in emojis: - if i.group(0) in captured: continue - captured.add(i.group(0)) - - emoji = i.group(1).lower() - attrs = '' - if b: attrs += ' b' - if not edit and len(emojis) <= 20 and random() < 0.0025 and ('marsey' in emoji or emoji in marseys_const2): attrs += ' g' - - old = emoji - emoji = emoji.replace('!','').replace('#','') - if emoji == 'marseyrandom': emoji = choice(marseys_const2) - - emoji_partial_pat = ':{0}:' - emoji_partial = ':{0}:' - emoji_html = None - - if emoji.endswith('pat'): - if path.isfile(f"files/assets/images/emojis/{emoji.replace('pat','')}.webp"): - attrs += ' pat' - emoji_html = f'{emoji_partial_pat.format(old, f"/e/{emoji[:-3]}.webp", attrs)}' - elif emoji.startswith('@'): - if u := get_user(emoji[1:-3], graceful=True): - attrs += ' pat' - emoji_html = f'{emoji_partial_pat.format(old, f"/pp/{u.id}", attrs)}' - elif path.isfile(f'files/assets/images/emojis/{emoji}.webp'): - emoji_html = emoji_partial.format(old, f'/e/{emoji}.webp', attrs) - - - if emoji_html: - html = re.sub(f'(?\1', sanitized) - - sanitized = sanitized.replace('‎','').replace('​','').replace("\ufeff", "").replace("𒐪","") - - if alert: - captured = [] - for i in mention_regex2.finditer(sanitized): - if i.group(0) in captured: continue - captured.append(i.group(0)) - - u = get_user(i.group(1), graceful=True) - if u: - sanitized = sanitized.replace(i.group(0), f'''

@{u.username}''') - else: - sanitized = reddit_regex.sub(r'\1/\2', sanitized) - - sanitized = sub_regex.sub(r'\1/\2', sanitized) - - captured = [] - for i in mention_regex.finditer(sanitized): - if i.group(0) in captured: continue - captured.append(i.group(0)) - - u = get_user(i.group(2), graceful=True) - - if u and (not (g.v and g.v.any_block_exists(u)) or g.v.admin_level > 1): - sanitized = sanitized.replace(i.group(0), f'''{i.group(1)}@{u.username}''') - - - sanitized = imgur_regex.sub(r'\1_d.webp?maxwidth=9999&fidelity=high', sanitized) - - soup = BeautifulSoup(sanitized, 'lxml') - - for tag in soup.find_all("img"): - if tag.get("src") and not tag["src"].startswith('/pp/'): - tag["loading"] = "lazy" - tag["data-src"] = tag["src"] - tag["src"] = "/assets/images/loading.webp" - tag['alt'] = f'![]({tag["data-src"]})' - tag['referrerpolicy'] = "no-referrer" - - for tag in soup.find_all("a"): - if tag.get("href") and fishylinks_regex.fullmatch(str(tag.string)): - tag.string = tag["href"] - - - sanitized = str(soup) - - sanitized = spoiler_regex.sub(r'\1', sanitized) - - marseys_used = set() - - emojis = list(emoji_regex.finditer(sanitized)) - if len(emojis) > 20: edit = True - - captured = [] - for i in emojis: - if i.group(0) in captured: continue - captured.append(i.group(0)) - - old = i.group(0) - if 'marseylong1' in old or 'marseylong2' in old or 'marseyllama1' in old or 'marseyllama2' in old: new = old.lower().replace(">", " class='mb-0'>") - else: new = old.lower() - - new = render_emoji(new, emoji_regex2, edit, marseys_used, True) - - sanitized = sanitized.replace(old, new) - - emojis = list(emoji_regex2.finditer(sanitized)) - if len(emojis) > 20: edit = True - - sanitized = render_emoji(sanitized, emoji_regex2, edit, marseys_used) - - for rd in ["://reddit.com", "://new.reddit.com", "://www.reddit.com", "://redd.it", "://libredd.it", "://teddit.net"]: - sanitized = sanitized.replace(rd, "://old.reddit.com") - - sanitized = sanitized.replace("nitter.net", "twitter.com").replace("old.reddit.com/gallery", "reddit.com/gallery").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").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("m.wikipedia.org", "wikipedia.org").replace("https://m.youtube", "https://youtube").replace("https://www.youtube", "https://youtube").replace("https://www.twitter", "https://twitter").replace("https://www.instagram", "https://instagram").replace("https://www.tiktok", "https://tiktok") - - - if "https://youtube.com/watch?v=" in sanitized: sanitized = sanitized.replace("?t=", "&t=") - - captured = [] - for i in youtube_regex.finditer(sanitized): - if i.group(0) in captured: continue - captured.append(i.group(0)) - - params = parse_qs(urlparse(i.group(2).replace('&','&')).query) - t = params.get('t', params.get('start', [0]))[0] - if isinstance(t, str): t = t.replace('s','') - - htmlsource = f'{i.group(1)}' - - sanitized = sanitized.replace(i.group(0), htmlsource) - - sanitized = video_sub_regex.sub(r'\1', sanitized) - - if comment: - for marsey in g.db.query(Marsey).filter(Marsey.name.in_(marseys_used)).all(): - marsey.count += 1 - g.db.add(marsey) - - if '#fortune' in sanitized: - sanitized = sanitized.replace('#fortune', '') - sanitized += '\n\n

' + choice(FORTUNE_REPLIES) + '

' - - sanitized = sanitized.replace('&','&') - sanitized = utm_regex.sub('', sanitized) - sanitized = utm_regex2.sub('', sanitized) - - - sanitized = sanitized.replace('','').replace('','') - - - - sanitized = bleach.Cleaner(tags=allowed_tags, - attributes=allowed_attributes, - protocols=['http', 'https'], - styles=['color', 'background-color', 'font-weight', 'text-align'], - filters=[partial(LinkifyFilter, skip_tags=["pre"], parse_email=False, callbacks=[callback], url_re=url_re)] - ).clean(sanitized) - - - - soup = BeautifulSoup(sanitized, 'lxml') - - links = soup.find_all("a") - - domain_list = set() - - for link in links: - - href = link.get("href") - if not href: continue - - url = urlparse(href) - domain = url.netloc - url_path = url.path - domain_list.add(domain+url_path) - - 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 = g.db.query(BannedDomain.domain).filter(BannedDomain.domain.in_(list(domain_list))).all() - - if bans: abort(403, description=f"Remove the banned domains {bans} and try again!") - - - signal.alarm(0) - - return sanitized - - - - - -def allowed_attributes_emojis(tag, name, value): - - if tag == 'img': - if name == 'loading' and value == 'lazy': return True - if name == 'data-bs-toggle' and value == 'tooltip': return True - if name in ['src','alt','title','g']: return True - return False - - -def filter_emojis_only(title, edit=False, graceful=False): - - signal.signal(signal.SIGALRM, handler) - signal.alarm(1) - - title = title.replace('‎','').replace('​','').replace("\ufeff", "").replace("𒐪","").replace("\n", "").replace("\r", "").replace("\t", "").replace("&", "&").replace('<','<').replace('>','>').replace('"', '"').replace("'", "'").strip() - - title = render_emoji(title, emoji_regex3, edit) - - title = strikethrough_regex.sub(r'\1', title) - - sanitized = bleach.clean(title, tags=['img','del'], attributes=allowed_attributes_emojis, protocols=['http','https']) - - signal.alarm(0) - - if len(title) > 1500 and not graceful: abort(400) - else: return title +import bleach +from bs4 import BeautifulSoup +from bleach.linkifier import LinkifyFilter, build_url_re +from functools import partial +from .get import * +from os import path, environ +import re +from mistletoe import markdown +from json import loads, dump +from random import random, choice +import signal +import time +import requests + +TLDS = ('ac','ad','ae','aero','af','ag','ai','al','am','an','ao','aq','ar','arpa','as','asia','at','au','aw','ax','az','ba','bb','bd','be','bf','bg','bh','bi','biz','bj','bm','bn','bo','br','bs','bt','bv','bw','by','bz','ca','cafe','cat','cc','cd','cf','cg','ch','ci','ck','cl','club','cm','cn','co','com','coop','cr','cu','cv','cx','cy','cz','de','dj','dk','dm','do','dz','ec','edu','ee','eg','er','es','et','eu','fi','fj','fk','fm','fo','fr','ga','gb','gd','ge','gf','gg','gh','gi','gl','gm','gn','gov','gp','gq','gr','gs','gt','gu','gw','gy','hk','hm','hn','hr','ht','hu','id','ie','il','im','in','info','int','io','iq','ir','is','it','je','jm','jo','jobs','jp','ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz','la','lb','lc','li','lk','lr','ls','lt','lu','lv','ly','ma','mc','md','me','mg','mh','mil','mk','ml','mm','mn','mo','mobi','mp','mq','mr','ms','mt','mu','museum','mv','mw','mx','my','mz','na','name','nc','ne','net','nf','ng','ni','nl','no','np','nr','nu','nz','om','org','pa','pe','pf','pg','ph','pk','pl','pm','pn','post','pr','pro','ps','pt','pw','py','qa','re','ro','rs','ru','rw','sa','sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so','social','sr','ss','st','su','sv','sx','sy','sz','tc','td','tel','tf','tg','th','tj','tk','tl','tm','tn','to','tp','tr','travel','tt','tv','tw','tz','ua','ug','uk','us','uy','uz','va','vc','ve','vg','vi','vn','vu','wf','win','ws','xn','xxx','xyz','ye','yt','yu','za','zm','zw') + +allowed_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','span','ruby','rp','rt','spoiler','img','lite-youtube','video','source','audio') + +def allowed_attributes(tag, name, value): + + if name == 'style': return True + + if tag == 'marquee': + if name in ['direction', 'behavior', 'scrollamount']: return True + if name in {'height', 'width'}: + try: value = int(value.replace('px', '')) + except: return False + if 0 < value <= 250: return True + return False + + if tag == 'a': + if name == 'href': return True + if name == 'rel' and value == 'nofollow noopener noreferrer': return True + if name == 'target' and value == '_blank': return True + return False + + if tag == 'img': + if name in ['src','data-src']: + if value.startswith('/') or value.startswith(f'{SITE_FULL}/') or embed_fullmatch_regex.fullmatch(value): return True + else: return False + + if name == 'loading' and value == 'lazy': return True + if name == 'data-bs-toggle' and value == 'tooltip': return True + if name in ['g','b'] and not value: return True + if name in ['alt','title']: return True + if name == 'referrpolicy' and value == 'no-referrer': return True + return False + + if tag == 'lite-youtube': + if name == 'params' and value.startswith('autoplay=1&modestbranding=1'): return True + if name == 'videoid': return True + return False + + if tag == 'video': + if name == 'controls' and value == '': return True + if name == 'preload' and value == 'none': return True + return False + + if tag == 'source': + if name == 'src' and embed_fullmatch_regex.fullmatch(value): return True + return False + + if tag == 'audio': + if name == 'controls' and value == '': return True + if name == 'preload' and value == 'none': return True + if name == 'src' and embed_fullmatch_regex.fullmatch(value): return True + return False + + if tag == 'p': + if name == 'class' and value == 'mb-0': return True + return False + + if tag == 'span': + if name == 'data-bs-toggle' and value == 'tooltip': return True + if name == 'title': return True + if name == 'alt': return True + return False + + +url_re = build_url_re(tlds=TLDS, protocols=['http', 'https']) + +def callback(attrs, new=False): + if (None, "href") not in attrs: + return # Incorrect tag + + href = attrs[(None, "href")] + + # \ in href right after / makes most browsers ditch site hostname and allows for a host injection bypassing the check, see cool + if "\\" in href: + attrs["_text"] = href # Laugh at this user + del attrs[(None, "href")] # Make unclickable and reset harmful payload + return attrs + + if not href.startswith('/') and not href.startswith(f'{SITE_FULL}/'): + attrs[(None, "target")] = "_blank" + attrs[(None, "rel")] = "nofollow noopener noreferrer" + + return attrs + + +def handler(signum, frame): + print("Timeout!") + raise Exception("Timeout") + +def render_emoji(html, regexp, edit, marseys_used=set(), b=False): + emojis = list(regexp.finditer(html)) + captured = set() + + for i in emojis: + if i.group(0) in captured: continue + captured.add(i.group(0)) + + emoji = i.group(1).lower() + attrs = '' + if b: attrs += ' b' + if not edit and len(emojis) <= 20 and random() < 0.0025 and ('marsey' in emoji or emoji in marseys_const2): attrs += ' g' + + old = emoji + emoji = emoji.replace('!','').replace('#','') + if emoji == 'marseyrandom': emoji = choice(marseys_const2) + + emoji_partial_pat = ':{0}:' + emoji_partial = ':{0}:' + emoji_html = None + + if emoji.endswith('pat'): + if path.isfile(f"files/assets/images/emojis/{emoji.replace('pat','')}.webp"): + emoji_html = f'{emoji_partial_pat.format(old, f"/e/{emoji[:-3]}.webp", attrs)}' + elif emoji.startswith('@'): + if u := get_user(emoji[1:-3], graceful=True): + emoji_html = f'{emoji_partial_pat.format(old, f"/pp/{u.id}", attrs)}' + elif path.isfile(f'files/assets/images/emojis/{emoji}.webp'): + emoji_html = emoji_partial.format(old, f'/e/{emoji}.webp', attrs) + + + if emoji_html: + marseys_used.add(emoji) + html = re.sub(f'(?'): + sanitized = linefeeds_regex.sub(r'\1\n\n\2', sanitized) + + sanitized = image_regex.sub(r'\1![](\2)\4', sanitized) + + sanitized = image_check_regex.sub(r'\1', sanitized) + + sanitized = link_fix_regex.sub(r'\2https://\3', sanitized) + + sanitized = markdown(sanitized) + + sanitized = strikethrough_regex.sub(r'\1', sanitized) + + sanitized = sanitized.replace('‎','').replace('​','').replace("\ufeff", "").replace("𒐪","") + + if alert: + captured = [] + for i in mention_regex2.finditer(sanitized): + if i.group(0) in captured: continue + captured.append(i.group(0)) + + u = get_user(i.group(1), graceful=True) + if u: + sanitized = sanitized.replace(i.group(0), f'''

@{u.username}''') + else: + sanitized = reddit_regex.sub(r'\1/\2', sanitized) + + sanitized = sub_regex.sub(r'\1/\2', sanitized) + + captured = [] + for i in mention_regex.finditer(sanitized): + if i.group(0) in captured: continue + captured.append(i.group(0)) + + u = get_user(i.group(2), graceful=True) + + if u and (not (g.v and g.v.any_block_exists(u)) or g.v.admin_level > 1): + sanitized = sanitized.replace(i.group(0), f'''{i.group(1)}@{u.username}''') + + + sanitized = imgur_regex.sub(r'\1_d.webp?maxwidth=9999&fidelity=high', sanitized) + + soup = BeautifulSoup(sanitized, 'lxml') + + for tag in soup.find_all("img"): + if tag.get("src") and not tag["src"].startswith('/pp/'): + tag["loading"] = "lazy" + tag["data-src"] = tag["src"] + tag["src"] = "/assets/images/loading.webp" + tag['alt'] = f'![]({tag["data-src"]})' + tag['referrerpolicy'] = "no-referrer" + + for tag in soup.find_all("a"): + if tag.get("href") and fishylinks_regex.fullmatch(str(tag.string)): + tag.string = tag["href"] + + + sanitized = str(soup) + + sanitized = spoiler_regex.sub(r'\1', sanitized) + + marseys_used = set() + + emojis = list(emoji_regex.finditer(sanitized)) + if len(emojis) > 20: edit = True + + captured = [] + for i in emojis: + if i.group(0) in captured: continue + captured.append(i.group(0)) + + old = i.group(0) + if 'marseylong1' in old or 'marseylong2' in old or 'marseyllama1' in old or 'marseyllama2' in old: new = old.lower().replace(">", " class='mb-0'>") + else: new = old.lower() + + new = render_emoji(new, emoji_regex2, edit, marseys_used, True) + + sanitized = sanitized.replace(old, new) + + emojis = list(emoji_regex2.finditer(sanitized)) + if len(emojis) > 20: edit = True + + sanitized = render_emoji(sanitized, emoji_regex2, edit, marseys_used) + + for rd in ["://reddit.com", "://new.reddit.com", "://www.reddit.com", "://redd.it", "://libredd.it", "://teddit.net"]: + sanitized = sanitized.replace(rd, "://old.reddit.com") + + sanitized = sanitized.replace("nitter.net", "twitter.com").replace("old.reddit.com/gallery", "reddit.com/gallery").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").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("m.wikipedia.org", "wikipedia.org").replace("https://m.youtube", "https://youtube").replace("https://www.youtube", "https://youtube").replace("https://www.twitter", "https://twitter").replace("https://www.instagram", "https://instagram").replace("https://www.tiktok", "https://tiktok") + + + if "https://youtube.com/watch?v=" in sanitized: sanitized = sanitized.replace("?t=", "&t=") + + captured = [] + for i in youtube_regex.finditer(sanitized): + if i.group(0) in captured: continue + captured.append(i.group(0)) + + params = parse_qs(urlparse(i.group(2).replace('&','&')).query) + t = params.get('t', params.get('start', [0]))[0] + if isinstance(t, str): t = t.replace('s','') + + htmlsource = f'{i.group(1)}' + + sanitized = sanitized.replace(i.group(0), htmlsource) + + sanitized = video_sub_regex.sub(r'\1', sanitized) + + if comment: + for marsey in g.db.query(Marsey).filter(Marsey.name.in_(marseys_used)).all(): + marsey.count += 1 + g.db.add(marsey) + + if '#fortune' in sanitized: + sanitized = sanitized.replace('#fortune', '') + sanitized += '\n\n

' + choice(FORTUNE_REPLIES) + '

' + + if '#factcheck' in sanitized: + sanitized = sanitized.replace('#factcheck', '') + sanitized += '\n\n

' + choice(FACTCHECK_REPLIES) + '

' + + sanitized = sanitized.replace('

', '') + sanitized = sanitized.replace('&','&') + sanitized = utm_regex.sub('', sanitized) + sanitized = utm_regex2.sub('', sanitized) + + + sanitized = sanitized.replace('','').replace('','') + + + + sanitized = bleach.Cleaner(tags=allowed_tags, + attributes=allowed_attributes, + protocols=['http', 'https'], + styles=['color', 'background-color', 'font-weight', 'text-align'], + filters=[partial(LinkifyFilter, skip_tags=["pre"], parse_email=False, callbacks=[callback], url_re=url_re)] + ).clean(sanitized) + + + + soup = BeautifulSoup(sanitized, 'lxml') + + links = soup.find_all("a") + + domain_list = set() + + for link in links: + + href = link.get("href") + if not href: continue + + url = urlparse(href) + domain = url.netloc + url_path = url.path + domain_list.add(domain+url_path) + + 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 = g.db.query(BannedDomain.domain).filter(BannedDomain.domain.in_(list(domain_list))).all() + + if bans: abort(403, description=f"Remove the banned domains {bans} and try again!") + + + signal.alarm(0) + + return sanitized + + + + + +def allowed_attributes_emojis(tag, name, value): + + if tag == 'img': + if name == 'src' and value.startswith('/'): return True + if name == 'loading' and value == 'lazy': return True + if name == 'data-bs-toggle' and value == 'tooltip': return True + if name == 'g' and not value: return True + if name in ['alt','title']: return True + + if tag == 'span': + if name == 'data-bs-toggle' and value == 'tooltip': return True + if name == 'title': return True + if name == 'alt': return True + return False + return False + + +def filter_emojis_only(title, edit=False, graceful=False): + + signal.signal(signal.SIGALRM, handler) + signal.alarm(1) + + title = title.replace('‎','').replace('​','').replace("\ufeff", "").replace("𒐪","").replace("\n", "").replace("\r", "").replace("\t", "").replace("&", "&").replace('<','<').replace('>','>').replace('"', '"').replace("'", "'").strip() + + title = render_emoji(title, emoji_regex3, edit) + + title = strikethrough_regex.sub(r'\1', title) + + title = bleach.clean(title, tags=['img','del','span'], attributes=allowed_attributes_emojis, protocols=['http','https']) + + signal.alarm(0) + + if len(title) > 1500 and not graceful: abort(400) + else: return title diff --git a/files/helpers/security.py b/files/helpers/security.py index 42102d713..f921b805d 100644 --- 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/wrappers.py b/files/helpers/wrappers.py index 8981a9689..cb0257852 100644 --- a/files/helpers/wrappers.py +++ b/files/helpers/wrappers.py @@ -1,118 +1,114 @@ -from .get import * -from .alerts import * -from files.helpers.const import * -from files.__main__ import db_session -from random import randint - -def get_logged_in_user(): - if not (hasattr(g, 'db') and g.db): g.db = db_session() - - v = None - - token = request.headers.get("Authorization","").strip() - if token: - client = g.db.query(ClientAuth).filter(ClientAuth.access_token == token).one_or_none() - if client: - v = client.user - v.client = client - else: - lo_user = session.get("lo_user") - if lo_user: - id = int(lo_user) - v = g.db.query(User).filter_by(id=id).one_or_none() - if v: - nonce = session.get("login_nonce", 0) - if nonce < v.login_nonce or v.id != id: abort(401) - - if request.method != "GET": - submitted_key = request.values.get("formkey") - if not submitted_key: abort(401) - if not v.validate_formkey(submitted_key): abort(401) - - v.client = None - - - if request.method.lower() != "get" and app.config['SETTINGS']['Read-only mode'] and not (v and v.admin_level): - abort(403) - - if v and v.patron: - if request.content_length and request.content_length > 16 * 1024 * 1024: abort(413) - elif request.content_length and request.content_length > 8 * 1024 * 1024: abort(413) - - return v - -def check_ban_evade(v): - if v and not v.patron and v.admin_level < 2 and v.ban_evade and not v.unban_utc: - v.shadowbanned = "AutoJanny" - g.db.add(v) - g.db.commit() - -def auth_desired(f): - def wrapper(*args, **kwargs): - - v = get_logged_in_user() - - check_ban_evade(v) - - g.v = v - return make_response(f(*args, v=v, **kwargs)) - - 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 - return make_response(f(*args, v=v, **kwargs)) - - wrapper.__name__ = f.__name__ - return wrapper - - -def is_not_permabanned(f): - - def wrapper(*args, **kwargs): - - v = get_logged_in_user() - - if not v: abort(401) - - check_ban_evade(v) - - if v.is_banned and v.unban_utc == 0: - return {"error": "Interal server error"}, 500 - - g.v = v - return make_response(f(*args, v=v, **kwargs)) - - 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 - return make_response(f(*args, v=v, **kwargs)) - - wrapper.__name__ = f.__name__ - return wrapper - +from .get import * +from .alerts import * +from files.helpers.const import * +from files.__main__ import db_session +from random import randint + +def get_logged_in_user(): + if not (hasattr(g, 'db') and g.db): g.db = db_session() + + v = None + + token = request.headers.get("Authorization","").strip() + if token: + client = g.db.query(ClientAuth).filter(ClientAuth.access_token == token).one_or_none() + if client: + v = client.user + v.client = client + else: + lo_user = session.get("lo_user") + if lo_user: + id = int(lo_user) + v = g.db.query(User).filter_by(id=id).one_or_none() + if v: + nonce = session.get("login_nonce", 0) + if nonce < v.login_nonce or v.id != id: abort(401) + + if request.method != "GET": + submitted_key = request.values.get("formkey") + if not submitted_key: abort(401) + if not v.validate_formkey(submitted_key): abort(401) + + v.client = None + + + if request.method.lower() != "get" and app.config['SETTINGS']['Read-only mode'] and not (v and v.admin_level): + abort(403) + + return v + +def check_ban_evade(v): + if v and not v.patron and v.admin_level < 2 and v.ban_evade and not v.unban_utc: + v.shadowbanned = "AutoJanny" + g.db.add(v) + g.db.commit() + +def auth_desired(f): + def wrapper(*args, **kwargs): + + v = get_logged_in_user() + + check_ban_evade(v) + + g.v = v + return make_response(f(*args, v=v, **kwargs)) + + 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 + return make_response(f(*args, v=v, **kwargs)) + + wrapper.__name__ = f.__name__ + return wrapper + + +def is_not_permabanned(f): + + def wrapper(*args, **kwargs): + + v = get_logged_in_user() + + if not v: abort(401) + + check_ban_evade(v) + + if v.is_banned and v.unban_utc == 0: + return {"error": "Interal server error"}, 500 + + g.v = v + return make_response(f(*args, v=v, **kwargs)) + + 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 + return make_response(f(*args, v=v, **kwargs)) + + wrapper.__name__ = f.__name__ + return wrapper + return wrapper_maker \ No newline at end of file diff --git a/files/mail/__init__.py b/files/mail/__init__.py index fba1477ed..c2e118cc2 100644 --- a/files/mail/__init__.py +++ b/files/mail/__init__.py @@ -1,94 +1,94 @@ -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.helpers.const import * -from files.classes import * -from files.__main__ import app, mail, limiter -from flask_mail import Message - -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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def api_verify_email(v): - - send_verification_email(v) - - return {"message": "Email has been sent (ETA ~5 minutes)"} - - -@app.get("/activate") -@auth_required -def activate(v): - - email = request.values.get("email", "").strip().lower() - - if not email_regex.fullmatch(email): - return render_template("message.html", v=v, title="Invalid email.", error="Invalid email."), 400 - - - 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).filter_by(id=id).one_or_none() - 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.flush() - send_notification(user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({mail_badge.path})\n\n{mail_badge.name}") - - - 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.helpers.const import * +from files.classes import * +from files.__main__ import app, mail, limiter +from flask_mail import Message + +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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def api_verify_email(v): + + send_verification_email(v) + + return {"message": "Email has been sent (ETA ~5 minutes)"} + + +@app.get("/activate") +@auth_required +def activate(v): + + email = request.values.get("email", "").strip().lower() + + if not email_regex.fullmatch(email): + return render_template("message.html", v=v, title="Invalid email.", error="Invalid email."), 400 + + + 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).filter_by(id=id).one_or_none() + 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.flush() + send_notification(user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({mail_badge.path})\n\n{mail_badge.name}") + + + 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 index e9121b46a..6d73bd1fc 100644 --- a/files/routes/__init__.py +++ b/files/routes/__init__.py @@ -1,18 +1,18 @@ -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 * +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 * from .subs import * \ No newline at end of file diff --git a/files/routes/admin.py b/files/routes/admin.py index d1c2f5858..01e086918 100644 --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -1,1678 +1,1686 @@ -import time -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.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 -from datetime import datetime -import requests -from urllib.parse import quote, urlencode - -GUMROAD_ID = environ.get("GUMROAD_ID", "tfcvri").strip() -GUMROAD_TOKEN = environ.get("GUMROAD_TOKEN", "").strip() - -month = datetime.now().strftime('%B') - - -@app.get('/admin/merge//') -@admin_level_required(3) -def merge(v, id1, id2): - if v.id != AEVANN_ID: abort(403) - - if time.time() - session.get('verified', 0) > 3: - session.pop("session_id", None) - session.pop("lo_user", None) - path = request.path - qs = urlencode(dict(request.values)) - argval = quote(f"{path}?{qs}", safe='') - return redirect(f"/login?redirect={argval}") - - user1 = get_account(id1) - user2 = get_account(id2) - - awards = g.db.query(AwardRelationship).filter_by(user_id=user2.id) - comments = g.db.query(Comment).filter_by(author_id=user2.id) - submissions = g.db.query(Submission).filter_by(author_id=user2.id) - badges = g.db.query(Badge).filter_by(user_id=user2.id) - mods = g.db.query(Mod).filter_by(user_id=user2.id) - exiles = g.db.query(Exile).filter_by(user_id=user2.id) - - for award in awards: - award.user_id = user1.id - g.db.add(award) - for comment in comments: - comment.author_id = user1.id - g.db.add(comment) - for submission in submissions: - submission.author_id = user1.id - g.db.add(submission) - for badge in badges: - if not user1.has_badge(badge.badge_id): - badge.user_id = user1.id - g.db.add(badge) - g.db.flush() - for mod in mods: - if not user1.mods(mod.sub): - mod.user_id = user1.id - g.db.add(mod) - g.db.flush() - for exile in exiles: - if not user1.exiled_from(exile.sub): - exile.user_id = user1.id - g.db.add(exile) - g.db.flush() - - for kind in ('comment_count', 'post_count', 'winnings', 'received_award_count', 'coins_spent', 'lootboxes_bought', 'coins', 'truecoins', 'procoins', 'subs_created'): - amount = getattr(user1, kind) + getattr(user2, kind) - setattr(user1, kind, amount) - setattr(user2, kind, 0) - - g.db.add(user1) - g.db.add(user2) - g.db.commit() - cache.clear() - return redirect(user1.url) - - -@app.get('/admin/merge_all/') -@admin_level_required(3) -def merge_all(v, id): - if v.id != AEVANN_ID: abort(403) - - if time.time() - session.get('verified', 0) > 3: - session.pop("session_id", None) - session.pop("lo_user", None) - path = request.path - qs = urlencode(dict(request.values)) - argval = quote(f"{path}?{qs}", safe='') - return redirect(f"/login?redirect={argval}") - - user = get_account(id) - - alt_ids = [x.id for x in user.alts_unique] - - things = g.db.query(AwardRelationship).filter(AwardRelationship.user_id.in_(alt_ids)).all() + g.db.query(Mod).filter(Mod.user_id.in_(alt_ids)).all() + g.db.query(Exile).filter(Exile.user_id.in_(alt_ids)).all() - for thing in things: - thing.user_id = user.id - g.db.add(thing) - - things = g.db.query(Submission).filter(Submission.author_id.in_(alt_ids)).all() + g.db.query(Comment).filter(Comment.author_id.in_(alt_ids)).all() - for thing in things: - thing.author_id = user.id - g.db.add(thing) - - - badges = g.db.query(Badge).filter(Badge.user_id.in_(alt_ids)).all() - for badge in badges: - if not user.has_badge(badge.badge_id): - badge.user_id = user.id - g.db.add(badge) - g.db.flush() - - for alt in user.alts_unique: - for kind in ('comment_count', 'post_count', 'winnings', 'received_award_count', 'coins_spent', 'lootboxes_bought', 'coins', 'truecoins', 'procoins', 'subs_created'): - amount = getattr(user, kind) + getattr(alt, kind) - setattr(user, kind, amount) - setattr(alt, kind, 0) - g.db.add(alt) - - g.db.add(user) - g.db.commit() - cache.clear() - return redirect(user.url) - - - -if SITE_NAME == 'PCM': - @app.get('/admin/sidebar') - @admin_level_required(3) - def get_sidebar(v): - - try: - with open(f'files/templates/sidebar_{SITE_NAME}.html', 'r', encoding="utf-8") as f: sidebar = f.read() - except: - sidebar = None - - return render_template('admin/sidebar.html', v=v, sidebar=sidebar) - - - @app.post('/admin/sidebar') - @limiter.limit("1/second;30/minute;200/hour;1000/day") - @admin_level_required(3) - def post_sidebar(v): - - text = request.values.get('sidebar', '').strip() - - with open(f'files/templates/sidebar_{SITE_NAME}.html', 'w+', encoding="utf-8") as f: f.write(text) - - with open(f'files/templates/sidebar_{SITE_NAME}.html', 'r', encoding="utf-8") as f: sidebar = f.read() - - ma = ModAction( - kind="change_sidebar", - user_id=v.id, - ) - g.db.add(ma) - - g.db.commit() - - return render_template('admin/sidebar.html', v=v, sidebar=sidebar, msg='Sidebar edited successfully!') - - -@app.post("/@/make_admin") -@admin_level_required(3) -def make_admin(v, username): - user = get_user(username) - if not user: abort(404) - user.admin_level = 2 - g.db.add(user) - - ma = ModAction( - kind="make_admin", - user_id=v.id, - target_user_id=user.id - ) - g.db.add(ma) - - g.db.commit() - return {"message": "User has been made admin!"} - - -@app.post("/@/remove_admin") -@admin_level_required(3) -def remove_admin(v, username): - user = get_user(username) - if not user: abort(404) - user.admin_level = 0 - g.db.add(user) - - ma = ModAction( - kind="remove_admin", - user_id=v.id, - target_user_id=user.id - ) - g.db.add(ma) - - g.db.commit() - return {"message": "Admin removed!"} - -@app.post("/distribute/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(3) -def distribute(v, comment): - autobetter = g.db.query(User).filter_by(id=AUTOBETTER_ID).one_or_none() - if autobetter.coins == 0: return {"error": "@AutoBetter has 0 coins"} - - try: comment = int(comment) - except: abort(400) - post = g.db.query(Comment.parent_submission).filter_by(id=comment).one_or_none()[0] - post = g.db.query(Submission).filter_by(id=post).one_or_none() - - pool = 0 - for option in post.bet_options: pool += option.upvotes - pool *= 200 - - autobetter.coins -= pool - if autobetter.coins < 0: autobetter.coins = 0 - g.db.add(autobetter) - - votes = g.db.query(CommentVote).filter_by(comment_id=comment) - coinsperperson = int(pool / votes.count()) - - cid = notif_comment(f"You won {coinsperperson} coins betting on [{post.title}]({post.shortlink}) :marseyparty:") - for vote in votes: - u = vote.user - u.coins += coinsperperson - add_notif(cid, u.id) - - cid = notif_comment(f"You lost the 200 coins you bet on [{post.title}]({post.shortlink}) :marseylaugh:") - cids = [x.id for x in post.bet_options] - cids.remove(comment) - votes = g.db.query(CommentVote).filter(CommentVote.comment_id.in_(cids)).all() - for vote in votes: add_notif(cid, vote.user.id) - - post.body += '\n\nclosed' - g.db.add(post) - - ma = ModAction( - kind="distribute", - user_id=v.id, - target_comment_id=comment - ) - g.db.add(ma) - - g.db.commit() - return {"message": f"Each winner has received {coinsperperson} coins!"} - -@app.post("/@/revert_actions") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(3) -def revert_actions(v, username): - user = get_user(username) - if not user: abort(404) - - ma = ModAction( - kind="revert", - user_id=v.id, - target_user_id=user.id - ) - g.db.add(ma) - - cutoff = int(time.time()) - 86400 - - posts = [x[0] for x in g.db.query(ModAction.target_submission_id).filter(ModAction.user_id == user.id, ModAction.created_utc > cutoff, ModAction.kind == 'ban_post').all()] - posts = g.db.query(Submission).filter(Submission.id.in_(posts)).all() - - comments = [x[0] for x in g.db.query(ModAction.target_comment_id).filter(ModAction.user_id == user.id, ModAction.created_utc > cutoff, ModAction.kind == 'ban_comment').all()] - comments = g.db.query(Comment).filter(Comment.id.in_(comments)).all() - - for item in posts + comments: - item.is_banned = False - item.ban_reason = None - g.db.add(item) - - users = (x[0] for x in g.db.query(ModAction.target_user_id).filter(ModAction.user_id == user.id, ModAction.created_utc > cutoff, ModAction.kind.in_(('shadowban', 'ban_user'))).all()) - users = g.db.query(User).filter(User.id.in_(users)).all() - - for user in users: - user.shadowbanned = None - user.is_banned = 0 - user.unban_utc = 0 - user.ban_evade = 0 - send_repeatable_notification(user.id, f"@{v.username} has unbanned you!") - g.db.add(user) - for u in user.alts: - u.shadowbanned = None - u.is_banned = 0 - u.unban_utc = 0 - u.ban_evade = 0 - send_repeatable_notification(u.id, f"@{v.username} has unbanned you!") - g.db.add(u) - - g.db.commit() - return {"message": "Admin actions reverted!"} - -@app.post("/@/club_allow") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -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 - g.db.add(u) - - for x in u.alts_unique: - x.club_allowed = True - 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 {CC_TITLE}!"} - -@app.post("/@/club_ban") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -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_allowed = False - - for x in u.alts_unique: - 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 {CC_TITLE}. Deserved."} - - -@app.post("/@/make_meme_admin") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(3) -def make_meme_admin(v, username): - user = get_user(username) - if not user: abort(404) - user.admin_level = 1 - g.db.add(user) - - ma = ModAction( - kind="make_meme_admin", - user_id=v.id, - target_user_id=user.id - ) - g.db.add(ma) - - g.db.commit() - return {"message": "User has been made meme admin!"} - - -@app.post("/@/remove_meme_admin") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(3) -def remove_meme_admin(v, username): - user = get_user(username) - if not user: abort(404) - user.admin_level = 0 - g.db.add(user) - - ma = ModAction( - kind="remove_meme_admin", - user_id=v.id, - target_user_id=user.id - ) - g.db.add(ma) - - g.db.commit() - return {"message": "Meme admin removed!"} - - -@app.post("/admin/monthly") -@limiter.limit("1/day") -@admin_level_required(3) -def monthly(v): - if SITE_NAME == 'rDrama' and v.id != AEVANN_ID: abort (403) - - data = {'access_token': GUMROAD_TOKEN} - - emails = [x['email'] for x in requests.get(f'https://api.gumroad.com/v2/products/{GUMROAD_ID}/subscribers', data=data, timeout=5).json()["subscribers"]] - - for u in g.db.query(User).filter(User.patron > 0, User.patron_utc == 0).all(): - if u.patron > 4 or u.email and u.email.lower() in emails: - procoins = procoins_li[u.patron] - u.procoins += procoins - g.db.add(u) - send_repeatable_notification(u.id, f"@{v.username} has given you {procoins} Marseybux for the month of {month}! You can use them to buy awards in the [shop](/shop).") - elif u.patron == 1 and u.admin_level > 0: - procoins = 2500 - u.procoins += procoins - g.db.add(u) - send_repeatable_notification(u.id, f"@{v.username} has given you {procoins} Marseybux for the month of {month}! You can use them to buy awards in the [shop](/shop).") - else: print(u.username) - - if request.host == 'pcmemes.net': - u = g.db.query(User).filter_by(id=KIPPY_ID).one() - u.procoins += 50000 - g.db.add(u) - - if request.host == 'rdrama.net': - u = g.db.query(User).filter_by(id=A_ID).one() - u.procoins += 25000 - g.db.add(u) - - ma = ModAction( - kind="monthly", - user_id=v.id - ) - g.db.add(ma) - - g.db.commit() - - return {"message": "Monthly coins granted"} - - -@app.get("/admin/shadowbanned") -@auth_required -def shadowbanned(v): - if not (v and v.admin_level > 1): abort(404) - users = [x for x in g.db.query(User).filter(User.shadowbanned != None).order_by(User.shadowbanned).all()] - return render_template("shadowbanned.html", v=v, users=users) - - -@app.get("/admin/image_posts") -@admin_level_required(2) -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(2) -def reported_posts(v): - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Submission).filter_by( - is_approved=None, - is_banned=False - ).join(Submission.reports).order_by(Submission.id.desc()).offset(25 * (page - 1)).limit(26) - - listing = [p.id for p in listing] - 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(2) -def reported_comments(v): - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Comment - ).filter_by( - is_approved=None, - is_banned=False - ).join(Comment.reports).order_by(Comment.id.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [c.id for c in listing] - 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(2) -def admin_home(v): - if CF_ZONE == 'blahblahblah': response = 'high' - else: response = requests.get(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/settings/security_level', headers=CF_HEADERS, timeout=5).json()['result']['value'] - under_attack = response == 'under_attack' - - return render_template("admin/admin_home.html", v=v, under_attack=under_attack, site_settings=app.config['SETTINGS']) - - -@app.post("/admin/site_settings/") -@admin_level_required(3) -def change_settings(v, setting): - site_settings = app.config['SETTINGS'] - site_settings[setting] = not site_settings[setting] - with open("site_settings.json", "w") as f: - json.dump(site_settings, f) - - if site_settings[setting]: word = 'enable' - else: word = 'disable' - - body = f"@{v.username} has {word}d `{setting}` in the [admin dashboard](/admin)!" - - body_html = sanitize(body) - - new_comment = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=None, - level=1, - body_html=body_html, - sentto=2, - distinguish_level=6 - ) - g.db.add(new_comment) - g.db.flush() - - new_comment.top_comment_id = new_comment.id - - for admin in g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all(): - notif = Notification(comment_id=new_comment.id, user_id=admin.id) - g.db.add(notif) - - ma = ModAction( - kind=f"{word}_{setting}", - user_id=v.id, - ) - g.db.add(ma) - - g.db.commit() - - return {'message': f"{setting} {word}d successfully!"} - - -@app.post("/admin/purge_cache") -@admin_level_required(3) -def purge_cache(v): - cache.clear() - response = str(requests.post(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache', headers=CF_HEADERS, data='{"purge_everything":true}', timeout=5)) - - ma = ModAction( - kind="purge_cache", - user_id=v.id - ) - g.db.add(ma) - - if response == "": return {"message": "Cache purged!"} - return {"error": "Failed to purge cache."} - - -@app.post("/admin/under_attack") -@admin_level_required(3) -def under_attack(v): - response = requests.get(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/settings/security_level', headers=CF_HEADERS, timeout=5).json()['result']['value'] - - if response == 'under_attack': - ma = ModAction( - kind="disable_under_attack", - user_id=v.id, - ) - g.db.add(ma) - g.db.commit() - - response = str(requests.patch(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/settings/security_level', headers=CF_HEADERS, data='{"value":"medium"}', timeout=5)) - if response == "": return {"message": "Under attack mode disabled!"} - return {"error": "Failed to disable under attack mode."} - else: - ma = ModAction( - kind="enable_under_attack", - user_id=v.id, - ) - g.db.add(ma) - g.db.commit() - - response = str(requests.patch(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/settings/security_level', headers=CF_HEADERS, data='{"value":"under_attack"}', timeout=5)) - if response == "": return {"message": "Under attack mode enabled!"} - return {"error": "Failed to enable under attack mode."} - -@app.get("/admin/badge_grant") -@admin_level_required(2) -def badge_grant_get(v): - badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() - return render_template("admin/badge_grant.html", v=v, badge_types=badges) - - -@app.post("/admin/badge_grant") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def badge_grant_post(v): - badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() - - user = get_user(request.values.get("username").strip(), graceful=True) - if not user: - return render_template("admin/badge_grant.html", v=v, badge_types=badges, error="User not found.") - - try: badge_id = int(request.values.get("badge_id")) - except: abort(400) - - if badge_id in {16,17,94,95,96,97,98,109} and v.id != AEVANN_ID: - abort(403) - - if user.has_badge(badge_id): - return render_template("admin/badge_grant.html", v=v, badge_types=badges, error="User already has that badge.") - - 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() - - if v.id != user.id: - text = f"@{v.username} has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}" - send_notification(user.id, text) - - ma = ModAction( - kind="badge_grant", - user_id=v.id, - target_user_id=user.id, - _note=new_badge.name - ) - g.db.add(ma) - - g.db.commit() - return render_template("admin/badge_grant.html", v=v, badge_types=badges, msg="Badge granted!") - - - -@app.get("/admin/badge_remove") -@admin_level_required(2) -def badge_remove_get(v): - badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() - - return render_template("admin/badge_remove.html", v=v, badge_types=badges) - - -@app.post("/admin/badge_remove") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def badge_remove_post(v): - badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() - - user = get_user(request.values.get("username").strip(), graceful=True) - if not user: - return render_template("admin/badge_remove.html", v=v, badge_types=badges, error="User not found.") - - try: badge_id = int(request.values.get("badge_id")) - except: abort(400) - - badge = user.has_badge(badge_id) - if not badge: - return render_template("admin/badge_remove.html", v=v, badge_types=badges, error="User doesn't have that badge.") - - ma = ModAction( - kind="badge_remove", - user_id=v.id, - target_user_id=user.id, - _note=badge.name - ) - g.db.add(ma) - - g.db.delete(badge) - - g.db.commit() - - return render_template("admin/badge_remove.html", v=v, badge_types=badges, msg="Badge removed!") - - -@app.get("/admin/users") -@admin_level_required(2) -def users_list(v): - - try: page = int(request.values.get("page", 1)) - except: page = 1 - - users = g.db.query(User).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(2) -def alt_votes_get(v): - - u1 = request.values.get("u1") - u2 = request.values.get("u2") - - if not u1 or not u2: - return render_template("admin/alt_votes.html", v=v) - - 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;30/minute;200/hour;1000/day") -@admin_level_required(2) -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) - - ma = ModAction( - kind="link_accounts", - user_id=v.id, - target_user_id=u1, - _note=f'with {u2}' - ) - g.db.add(ma) - - 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/posts") -@admin_level_required(2) -def admin_removed(v): - - try: page = int(request.values.get("page", 1)) - except: page = 1 - - if page < 1: abort(400) - - ids = g.db.query(Submission.id).join(User, User.id == Submission.author_id).filter(or_(Submission.is_banned==True, User.shadowbanned != None)).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.get("/admin/removed/comments") -@admin_level_required(2) -def admin_removed_comments(v): - - try: page = int(request.values.get("page", 1)) - except: page = 1 - - ids = g.db.query(Comment.id).join(User, User.id == Comment.author_id).filter(or_(Comment.is_banned==True, User.shadowbanned != None)).order_by(Comment.id.desc()).offset(25 * (page - 1)).limit(26).all() - - ids=[x[0] for x in ids] - - next_exists = len(ids) > 25 - - ids = ids[:25] - - comments = get_comments(ids, v=v) - - return render_template("admin/removed_comments.html", - v=v, - listing=comments, - page=page, - next_exists=next_exists - ) - - -@app.post("/agendaposter/") -@admin_level_required(2) -def agendaposter(user_id, v): - user = g.db.query(User).filter_by(id=user_id).one_or_none() - - days = request.values.get("days") or 30 - expiry = float(days) - expiry = int(time.time() + expiry*60*60*24) - - user.agendaposter = expiry - g.db.add(user) - - for alt in user.alts: - if alt.admin_level: return {"error": "User is an admin!"} - alt.agendaposter = expiry - g.db.add(alt) - - note = f"for {days} days" - - ma = ModAction( - kind="agendaposter", - user_id=v.id, - target_user_id=user.id, - note = note - ) - g.db.add(ma) - - if not user.has_badge(28): - badge = Badge(user_id=user.id, badge_id=28) - g.db.add(badge) - g.db.flush() - send_notification(user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({badge.path})\n\n{badge.name}") - - - send_repeatable_notification(user.id, f"@{v.username} has marked you as a chud ({note}).") - - g.db.commit() - - return redirect(user.url) - - - -@app.post("/unagendaposter/") -@admin_level_required(2) -def unagendaposter(user_id, v): - user = g.db.query(User).filter_by(id=user_id).one_or_none() - - user.agendaposter = 0 - g.db.add(user) - - for alt in user.alts: - alt.agendaposter = 0 - g.db.add(alt) - - ma = ModAction( - kind="unagendaposter", - user_id=v.id, - target_user_id=user.id - ) - - g.db.add(ma) - - badge = user.has_badge(28) - if badge: g.db.delete(badge) - - send_repeatable_notification(user.id, f"@{v.username} has unmarked you as a chud.") - - g.db.commit() - return {"message": "Chud theme disabled!"} - - -@app.post("/shadowban/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def shadowban(user_id, v): - user = g.db.query(User).filter_by(id=user_id).one_or_none() - if user.admin_level != 0: abort(403) - user.shadowbanned = v.username - g.db.add(user) - - for alt in user.alts: - if alt.admin_level: 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) - - body = f"@{v.username} has shadowbanned @{user.username}" - - body_html = sanitize(body) - - - new_comment = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=None, - level=1, - body_html=body_html, - distinguish_level=6 - ) - g.db.add(new_comment) - g.db.flush() - - new_comment.top_comment_id = new_comment.id - - for admin in g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all(): - notif = Notification(comment_id=new_comment.id, user_id=admin.id) - g.db.add(notif) - - - - g.db.commit() - return {"message": "User shadowbanned!"} - - -@app.post("/unshadowban/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def unshadowban(user_id, v): - user = g.db.query(User).filter_by(id=user_id).one_or_none() - user.shadowbanned = None - user.ban_evade = 0 - g.db.add(user) - for alt in user.alts: - alt.shadowbanned = None - alt.ban_evade = 0 - 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;30/minute;200/hour;1000/day") -@admin_level_required(3) -def verify(user_id, v): - user = g.db.query(User).filter_by(id=user_id).one_or_none() - user.verified = "Verified" - g.db.add(user) - - ma = ModAction( - kind="check", - user_id=v.id, - target_user_id=user.id, - ) - g.db.add(ma) - - g.db.commit() - return {"message": "User verfied!"} - -@app.post("/admin/unverify/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(3) -def unverify(user_id, v): - user = g.db.query(User).filter_by(id=user_id).one_or_none() - user.verified = None - g.db.add(user) - - ma = ModAction( - kind="uncheck", - user_id=v.id, - target_user_id=user.id, - ) - g.db.add(ma) - - g.db.commit() - return {"message": "User unverified!"} - - -@app.post("/admin/title_change/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def admin_title_change(user_id, v): - - user = g.db.query(User).filter_by(id=user_id).one_or_none() - - new_name=request.values.get("title").strip()[:256] - - user.customtitleplain=new_name - new_name = filter_emojis_only(new_name) - - user=g.db.query(User).filter_by(id=user.id).one_or_none() - user.customtitle=new_name - if request.values.get("locked"): user.flairchanged = int(time.time()) + 2629746 - else: - user.flairchanged = None - badge = user.has_badge(96) - if badge: g.db.delete(badge) - - 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'"{user.customtitleplain}"' - ) - g.db.add(ma) - g.db.commit() - - return redirect(user.url) - -@app.post("/ban_user/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def ban_user(user_id, v): - - user = g.db.query(User).filter_by(id=user_id).one_or_none() - - if not user: abort(404) - - if user.admin_level >= v.admin_level: abort(403) - - days = float(request.values.get("days")) if request.values.get('days') else 0 - - reason = request.values.get("reason", "").strip()[:256] - passed_reason = filter_emojis_only(reason) - - if len(passed_reason) > 256: passed_reason = reason - - user.ban(admin=v, reason=passed_reason, days=days) - - if request.values.get("alts"): - for x in user.alts: - if x.admin_level: break - x.ban(admin=v, reason=passed_reason, days=days) - - if days: - if reason: text = f"@{v.username} has banned you for **{days}** days for the following reason:\n\n> {reason}" - else: text = f"@{v.username} has banned you for **{days}** days." - else: - if reason: text = f"@{v.username} has banned you permanently for the following reason:\n\n> {reason}" - else: text = f"@{v.username} has banned you permanently." - - send_repeatable_notification(user.id, text) - - if days == 0: duration = "permanent" - elif days == 1: duration = "1 day" - else: duration = f"{days} days" - - note = f'reason: "{reason}", duration: {duration}' - ma=ModAction( - kind="ban_user", - user_id=v.id, - target_user_id=user.id, - _note=note - ) - g.db.add(ma) - - if 'reason' in request.values: - if reason.startswith("/post/"): - try: - post = int(reason.split("/post/")[1].split(None, 1)[0]) - post = get_post(post) - post.bannedfor = True - g.db.add(post) - except: pass - elif reason.startswith("/comment/"): - try: - comment = int(reason.split("/comment/")[1].split(None, 1)[0]) - comment = get_comment(comment) - comment.bannedfor = True - g.db.add(comment) - except: pass - - - body = f"@{v.username} has banned @{user.username} ({note})" - - body_html = sanitize(body) - - - new_comment = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=None, - level=1, - body_html=body_html, - distinguish_level=6 - ) - g.db.add(new_comment) - g.db.flush() - - new_comment.top_comment_id = new_comment.id - - for admin in g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all(): - notif = Notification(comment_id=new_comment.id, user_id=admin.id) - g.db.add(notif) - - - 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;30/minute;200/hour;1000/day") -@admin_level_required(2) -def unban_user(user_id, v): - - user = g.db.query(User).filter_by(id=user_id).one_or_none() - - if not user or not user.is_banned: abort(400) - - user.is_banned = 0 - user.unban_utc = 0 - user.ban_evade = 0 - user.ban_reason = None - send_repeatable_notification(user.id, f"@{v.username} has unbanned you!") - g.db.add(user) - - for x in user.alts: - if x.is_banned: send_repeatable_notification(x.id, f"@{v.username} has unbanned you!") - x.is_banned = 0 - x.unban_utc = 0 - x.ban_evade = 0 - x.ban_reason = None - g.db.add(x) - - 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;30/minute;200/hour;1000/day") -@admin_level_required(2) -def ban_post(post_id, v): - - post = g.db.query(Submission).filter_by(id=post_id).one_or_none() - - if not post: - abort(400) - - post.is_banned = True - post.is_approved = None - post.stickied = None - post.is_pinned = False - post.ban_reason = v.username - 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) - - requests.post(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache', headers=CF_HEADERS, json={'files': [f"{SITE_FULL}/logged_out/"]}, timeout=5) - - g.db.commit() - - return {"message": "Post removed!"} - - -@app.post("/unban_post/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def unban_post(post_id, v): - - post = g.db.query(Submission).filter_by(id=post_id).one_or_none() - - 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.ban_reason = None - 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) -def api_distinguish_post(post_id, v): - - post = g.db.query(Submission).filter_by(id=post_id).one_or_none() - - if not post: abort(404) - - if post.author_id != v.id and v.admin_level < 2 : abort(403) - - if post.distinguish_level: - post.distinguish_level = 0 - kind = 'undistinguish_post' - else: - post.distinguish_level = v.admin_level - kind = 'distinguish_post' - - g.db.add(post) - - ma = ModAction( - kind=kind, - user_id=v.id, - target_submission_id=post.id - ) - g.db.add(ma) - - g.db.commit() - - if post.distinguish_level: return {"message": "Post distinguished!"} - else: return {"message": "Post undistinguished!"} - - -@app.post("/sticky/") -@admin_level_required(2) -def sticky_post(post_id, v): - - post = g.db.query(Submission).filter_by(id=post_id).one_or_none() - if post and not post.stickied: - pins = g.db.query(Submission.id).filter(Submission.stickied != None, Submission.is_banned == False).count() - if pins > 2: - if v.admin_level > 2: - post.stickied = v.username - post.stickied_utc = int(time.time()) + 3600 - else: return {"error": "Can't exceed 3 pinned posts limit!"}, 403 - else: post.stickied = v.username - g.db.add(post) - - ma=ModAction( - kind="pin_post", - user_id=v.id, - target_submission_id=post.id - ) - g.db.add(ma) - - if v.id != post.author_id: - send_repeatable_notification(post.author_id, f"@{v.username} has pinned your [post](/post/{post_id})!") - - cache.delete_memoized(frontlist) - g.db.commit() - return {"message": "Post pinned!"} - -@app.post("/unsticky/") -@admin_level_required(2) -def unsticky_post(post_id, v): - - post = g.db.query(Submission).filter_by(id=post_id).one_or_none() - if post and post.stickied: - if post.stickied.endswith('(pin award)'): return {"error": "Can't unpin award pins!"}, 403 - - post.stickied = None - post.stickied_utc = None - g.db.add(post) - - ma=ModAction( - kind="unpin_post", - user_id=v.id, - target_submission_id=post.id - ) - g.db.add(ma) - - if v.id != post.author_id: - send_repeatable_notification(post.author_id, f"@{v.username} has unpinned your [post](/post/{post_id})!") - - cache.delete_memoized(frontlist) - g.db.commit() - return {"message": "Post unpinned!"} - -@app.post("/sticky_comment/") -@admin_level_required(2) -def sticky_comment(cid, v): - - comment = get_comment(cid, v=v) - - if not comment.is_pinned: - comment.is_pinned = v.username - g.db.add(comment) - - ma=ModAction( - kind="pin_comment", - user_id=v.id, - target_comment_id=comment.id - ) - g.db.add(ma) - - if v.id != comment.author_id: - message = f"@{v.username} has pinned your [comment]({comment.shortlink})!" - send_repeatable_notification(comment.author_id, message) - - g.db.commit() - return {"message": "Comment pinned!"} - - -@app.post("/unsticky_comment/") -@admin_level_required(2) -def unsticky_comment(cid, v): - - comment = get_comment(cid, v=v) - - if comment.is_pinned: - if comment.is_pinned.endswith("(pin award)"): return {"error": "Can't unpin award pins!"}, 403 - - comment.is_pinned = None - g.db.add(comment) - - ma=ModAction( - kind="unpin_comment", - user_id=v.id, - target_comment_id=comment.id - ) - g.db.add(ma) - - if v.id != comment.author_id: - message = f"@{v.username} has unpinned your [comment]({comment.shortlink})!" - send_repeatable_notification(comment.author_id, message) - - g.db.commit() - return {"message": "Comment unpinned!"} - - -@app.post("/ban_comment/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def api_ban_comment(c_id, v): - - comment = g.db.query(Comment).filter_by(id=c_id).one_or_none() - if not comment: - abort(404) - - comment.is_banned = True - comment.is_approved = None - comment.ban_reason = v.username - 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;30/minute;200/hour;1000/day") -@admin_level_required(2) -def api_unban_comment(c_id, v): - - comment = g.db.query(Comment).filter_by(id=c_id).one_or_none() - if not comment: abort(404) - - if comment.author.agendaposter and AGENDAPOSTER_PHRASE not in comment.body.lower(): - return {"error": "You can't bypass the chud award!"} - - 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.ban_reason = None - comment.is_approved = v.id - - g.db.add(comment) - - g.db.commit() - - return {"message": "Comment approved!"} - - -@app.post("/distinguish_comment/") -@admin_level_required(1) -def admin_distinguish_comment(c_id, v): - - - comment = get_comment(c_id, v=v) - - if comment.author_id != v.id: abort(403) - - if comment.distinguish_level: - comment.distinguish_level = 0 - kind = 'undistinguish_comment' - else: - comment.distinguish_level = v.admin_level - kind = 'distinguish_comment' - - g.db.add(comment) - - ma = ModAction( - kind=kind, - user_id=v.id, - target_comment_id=comment.id - ) - g.db.add(ma) - - g.db.commit() - - if comment.distinguish_level: return {"message": "Comment distinguished!"} - else: return {"message": "Comment undistinguished!"} - -@app.get("/admin/dump_cache") -@admin_level_required(2) -def admin_dump_cache(v): - cache.clear() - - ma = ModAction( - kind="dump_cache", - user_id=v.id - ) - g.db.add(ma) - - return {"message": "Internal cache cleared."} - - -@app.get("/admin/banned_domains/") -@admin_level_required(3) -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;30/minute;200/hour;1000/day") -@admin_level_required(3) -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).filter_by(domain=domain).one_or_none() - if d: - g.db.delete(d) - ma = ModAction( - kind="unban_domain", - user_id=v.id, - _note=domain - ) - g.db.add(ma) - - else: - d = BannedDomain(domain=domain, reason=reason) - g.db.add(d) - ma = ModAction( - kind="ban_domain", - user_id=v.id, - _note=f'{domain}, reason: {reason}' - ) - g.db.add(ma) - - g.db.commit() - - return redirect("/admin/banned_domains/") - - -@app.post("/admin/nuke_user") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def admin_nuke_user(v): - - user=get_user(request.values.get("user")) - - for post in g.db.query(Submission).filter_by(author_id=user.id).all(): - if post.is_banned: - continue - - post.is_banned = True - post.ban_reason = v.username - g.db.add(post) - - for comment in g.db.query(Comment).filter_by(author_id=user.id).all(): - if comment.is_banned: - continue - - comment.is_banned = True - comment.ban_reason = v.username - 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;30/minute;200/hour;1000/day") -@admin_level_required(2) -def admin_nunuke_user(v): - - user=get_user(request.values.get("user")) - - for post in g.db.query(Submission).filter_by(author_id=user.id).all(): - if not post.is_banned: - continue - - post.is_banned = False - post.ban_reason = None - g.db.add(post) - - for comment in g.db.query(Comment).filter_by(author_id=user.id).all(): - if not comment.is_banned: - continue - - comment.is_banned = False - comment.ban_reason = None - 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 +import re +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.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 +from datetime import datetime +import requests +from urllib.parse import quote, urlencode + +GUMROAD_ID = environ.get("GUMROAD_ID", "tfcvri").strip() +GUMROAD_TOKEN = environ.get("GUMROAD_TOKEN", "").strip() + +month = datetime.now().strftime('%B') + + +@app.get('/admin/merge//') +@admin_level_required(3) +def merge(v, id1, id2): + if v.id != AEVANN_ID: abort(403) + + if time.time() - session.get('verified', 0) > 3: + session.pop("session_id", None) + session.pop("lo_user", None) + path = request.path + qs = urlencode(dict(request.values)) + argval = quote(f"{path}?{qs}", safe='') + return redirect(f"/login?redirect={argval}") + + user1 = get_account(id1) + user2 = get_account(id2) + + awards = g.db.query(AwardRelationship).filter_by(user_id=user2.id) + comments = g.db.query(Comment).filter_by(author_id=user2.id) + submissions = g.db.query(Submission).filter_by(author_id=user2.id) + badges = g.db.query(Badge).filter_by(user_id=user2.id) + mods = g.db.query(Mod).filter_by(user_id=user2.id) + exiles = g.db.query(Exile).filter_by(user_id=user2.id) + + for award in awards: + award.user_id = user1.id + g.db.add(award) + for comment in comments: + comment.author_id = user1.id + g.db.add(comment) + for submission in submissions: + submission.author_id = user1.id + g.db.add(submission) + for badge in badges: + if not user1.has_badge(badge.badge_id): + badge.user_id = user1.id + g.db.add(badge) + g.db.flush() + for mod in mods: + if not user1.mods(mod.sub): + mod.user_id = user1.id + g.db.add(mod) + g.db.flush() + for exile in exiles: + if not user1.exiled_from(exile.sub): + exile.user_id = user1.id + g.db.add(exile) + g.db.flush() + + for kind in ('comment_count', 'post_count', 'winnings', 'received_award_count', 'coins_spent', 'lootboxes_bought', 'coins', 'truecoins', 'procoins', 'subs_created'): + amount = getattr(user1, kind) + getattr(user2, kind) + setattr(user1, kind, amount) + setattr(user2, kind, 0) + + g.db.add(user1) + g.db.add(user2) + g.db.commit() + cache.clear() + return redirect(user1.url) + + +@app.get('/admin/merge_all/') +@admin_level_required(3) +def merge_all(v, id): + if v.id != AEVANN_ID: abort(403) + + if time.time() - session.get('verified', 0) > 3: + session.pop("session_id", None) + session.pop("lo_user", None) + path = request.path + qs = urlencode(dict(request.values)) + argval = quote(f"{path}?{qs}", safe='') + return redirect(f"/login?redirect={argval}") + + user = get_account(id) + + alt_ids = [x.id for x in user.alts_unique] + + things = g.db.query(AwardRelationship).filter(AwardRelationship.user_id.in_(alt_ids)).all() + g.db.query(Mod).filter(Mod.user_id.in_(alt_ids)).all() + g.db.query(Exile).filter(Exile.user_id.in_(alt_ids)).all() + for thing in things: + thing.user_id = user.id + g.db.add(thing) + + things = g.db.query(Submission).filter(Submission.author_id.in_(alt_ids)).all() + g.db.query(Comment).filter(Comment.author_id.in_(alt_ids)).all() + for thing in things: + thing.author_id = user.id + g.db.add(thing) + + + badges = g.db.query(Badge).filter(Badge.user_id.in_(alt_ids)).all() + for badge in badges: + if not user.has_badge(badge.badge_id): + badge.user_id = user.id + g.db.add(badge) + g.db.flush() + + for alt in user.alts_unique: + for kind in ('comment_count', 'post_count', 'winnings', 'received_award_count', 'coins_spent', 'lootboxes_bought', 'coins', 'truecoins', 'procoins', 'subs_created'): + amount = getattr(user, kind) + getattr(alt, kind) + setattr(user, kind, amount) + setattr(alt, kind, 0) + g.db.add(alt) + + g.db.add(user) + g.db.commit() + cache.clear() + return redirect(user.url) + + + +if SITE_NAME == 'PCM': + @app.get('/admin/sidebar') + @admin_level_required(3) + def get_sidebar(v): + + try: + with open(f'files/templates/sidebar_{SITE_NAME}.html', 'r', encoding="utf-8") as f: sidebar = f.read() + except: + sidebar = None + + return render_template('admin/sidebar.html', v=v, sidebar=sidebar) + + + @app.post('/admin/sidebar') + @limiter.limit("1/second;30/minute;200/hour;1000/day") + @admin_level_required(3) + def post_sidebar(v): + + text = request.values.get('sidebar', '').strip() + + with open(f'files/templates/sidebar_{SITE_NAME}.html', 'w+', encoding="utf-8") as f: f.write(text) + + with open(f'files/templates/sidebar_{SITE_NAME}.html', 'r', encoding="utf-8") as f: sidebar = f.read() + + ma = ModAction( + kind="change_sidebar", + user_id=v.id, + ) + g.db.add(ma) + + g.db.commit() + + return render_template('admin/sidebar.html', v=v, sidebar=sidebar, msg='Sidebar edited successfully!') + + +@app.post("/@/make_admin") +@admin_level_required(3) +def make_admin(v, username): + user = get_user(username) + if not user: abort(404) + user.admin_level = 2 + g.db.add(user) + + ma = ModAction( + kind="make_admin", + user_id=v.id, + target_user_id=user.id + ) + g.db.add(ma) + + g.db.commit() + return {"message": "User has been made admin!"} + + +@app.post("/@/remove_admin") +@admin_level_required(3) +def remove_admin(v, username): + user = get_user(username) + if not user: abort(404) + user.admin_level = 0 + g.db.add(user) + + ma = ModAction( + kind="remove_admin", + user_id=v.id, + target_user_id=user.id + ) + g.db.add(ma) + + g.db.commit() + return {"message": "Admin removed!"} + +@app.post("/distribute/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(3) +def distribute(v, comment): + autobetter = g.db.query(User).filter_by(id=AUTOBETTER_ID).one_or_none() + if autobetter.coins == 0: return {"error": "@AutoBetter has 0 coins"} + + try: comment = int(comment) + except: abort(400) + post = g.db.query(Comment.parent_submission).filter_by(id=comment).one_or_none()[0] + post = g.db.query(Submission).filter_by(id=post).one_or_none() + + pool = 0 + for option in post.bet_options: pool += option.upvotes + pool *= 200 + + autobetter.coins -= pool + if autobetter.coins < 0: autobetter.coins = 0 + g.db.add(autobetter) + + votes = g.db.query(CommentVote).filter_by(comment_id=comment) + coinsperperson = int(pool / votes.count()) + + cid = notif_comment(f"You won {coinsperperson} coins betting on [{post.title}]({post.shortlink}) :marseyparty:") + for vote in votes: + u = vote.user + u.coins += coinsperperson + add_notif(cid, u.id) + + cid = notif_comment(f"You lost the 200 coins you bet on [{post.title}]({post.shortlink}) :marseylaugh:") + cids = [x.id for x in post.bet_options] + cids.remove(comment) + votes = g.db.query(CommentVote).filter(CommentVote.comment_id.in_(cids)).all() + for vote in votes: add_notif(cid, vote.user.id) + + post.body += '\n\nclosed' + g.db.add(post) + + ma = ModAction( + kind="distribute", + user_id=v.id, + target_comment_id=comment + ) + g.db.add(ma) + + g.db.commit() + return {"message": f"Each winner has received {coinsperperson} coins!"} + +@app.post("/@/revert_actions") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(3) +def revert_actions(v, username): + user = get_user(username) + if not user: abort(404) + + ma = ModAction( + kind="revert", + user_id=v.id, + target_user_id=user.id + ) + g.db.add(ma) + + cutoff = int(time.time()) - 86400 + + posts = [x[0] for x in g.db.query(ModAction.target_submission_id).filter(ModAction.user_id == user.id, ModAction.created_utc > cutoff, ModAction.kind == 'ban_post').all()] + posts = g.db.query(Submission).filter(Submission.id.in_(posts)).all() + + comments = [x[0] for x in g.db.query(ModAction.target_comment_id).filter(ModAction.user_id == user.id, ModAction.created_utc > cutoff, ModAction.kind == 'ban_comment').all()] + comments = g.db.query(Comment).filter(Comment.id.in_(comments)).all() + + for item in posts + comments: + item.is_banned = False + item.ban_reason = None + g.db.add(item) + + users = (x[0] for x in g.db.query(ModAction.target_user_id).filter(ModAction.user_id == user.id, ModAction.created_utc > cutoff, ModAction.kind.in_(('shadowban', 'ban_user'))).all()) + users = g.db.query(User).filter(User.id.in_(users)).all() + + for user in users: + user.shadowbanned = None + user.is_banned = 0 + user.unban_utc = 0 + user.ban_evade = 0 + send_repeatable_notification(user.id, f"@{v.username} has unbanned you!") + g.db.add(user) + for u in user.alts: + u.shadowbanned = None + u.is_banned = 0 + u.unban_utc = 0 + u.ban_evade = 0 + send_repeatable_notification(u.id, f"@{v.username} has unbanned you!") + g.db.add(u) + + g.db.commit() + return {"message": "Admin actions reverted!"} + +@app.post("/@/club_allow") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +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 + g.db.add(u) + + for x in u.alts_unique: + x.club_allowed = True + 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 {CC_TITLE}!"} + +@app.post("/@/club_ban") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +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_allowed = False + + for x in u.alts_unique: + 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 {CC_TITLE}. Deserved."} + + +@app.post("/@/make_meme_admin") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(3) +def make_meme_admin(v, username): + user = get_user(username) + if not user: abort(404) + user.admin_level = 1 + g.db.add(user) + + ma = ModAction( + kind="make_meme_admin", + user_id=v.id, + target_user_id=user.id + ) + g.db.add(ma) + + g.db.commit() + return {"message": "User has been made meme admin!"} + + +@app.post("/@/remove_meme_admin") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(3) +def remove_meme_admin(v, username): + user = get_user(username) + if not user: abort(404) + user.admin_level = 0 + g.db.add(user) + + ma = ModAction( + kind="remove_meme_admin", + user_id=v.id, + target_user_id=user.id + ) + g.db.add(ma) + + g.db.commit() + return {"message": "Meme admin removed!"} + + +@app.post("/admin/monthly") +@limiter.limit("1/day") +@admin_level_required(3) +def monthly(v): + if SITE_NAME == 'rDrama' and v.id != AEVANN_ID: abort (403) + + data = {'access_token': GUMROAD_TOKEN} + + emails = [x['email'] for x in requests.get(f'https://api.gumroad.com/v2/products/{GUMROAD_ID}/subscribers', data=data, timeout=5).json()["subscribers"]] + + for u in g.db.query(User).filter(User.patron > 0, User.patron_utc == 0).all(): + if u.email and u.email.lower() in emails: + procoins = procoins_li[u.patron] + u.procoins += procoins + g.db.add(u) + send_repeatable_notification(u.id, f"@{v.username} has given you {procoins} Marseybux for the month of {month}! You can use them to buy awards in the [shop](/shop).") + elif u.patron == 1 and u.admin_level > 0: + procoins = 2500 + u.procoins += procoins + g.db.add(u) + send_repeatable_notification(u.id, f"@{v.username} has given you {procoins} Marseybux for the month of {month}! You can use them to buy awards in the [shop](/shop).") + else: print(u.username) + + if request.host == 'pcmemes.net': + u = g.db.query(User).filter_by(id=KIPPY_ID).one() + u.procoins += 50000 + g.db.add(u) + + if request.host == 'rdrama.net': + u = g.db.query(User).filter_by(id=A_ID).one() + u.procoins += 25000 + g.db.add(u) + + ma = ModAction( + kind="monthly", + user_id=v.id + ) + g.db.add(ma) + + g.db.commit() + + return {"message": "Monthly coins granted"} + + +@app.get("/admin/shadowbanned") +@auth_required +def shadowbanned(v): + if not (v and v.admin_level > 1): abort(404) + users = [x for x in g.db.query(User).filter(User.shadowbanned != None).order_by(User.shadowbanned).all()] + return render_template("shadowbanned.html", v=v, users=users) + + +@app.get("/admin/image_posts") +@admin_level_required(2) +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(2) +def reported_posts(v): + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Submission).filter_by( + is_approved=None, + is_banned=False + ).join(Submission.reports).order_by(Submission.id.desc()).offset(25 * (page - 1)).limit(26) + + listing = [p.id for p in listing] + 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(2) +def reported_comments(v): + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Comment + ).filter_by( + is_approved=None, + is_banned=False + ).join(Comment.reports).order_by(Comment.id.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [c.id for c in listing] + 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(2) +def admin_home(v): + if CF_ZONE == 'blahblahblah': response = 'high' + else: response = requests.get(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/settings/security_level', headers=CF_HEADERS, timeout=5).json()['result']['value'] + under_attack = response == 'under_attack' + + gitref = admin_git_head() + + return render_template("admin/admin_home.html", v=v, + under_attack=under_attack, + site_settings=app.config['SETTINGS'], + gitref=gitref) + +def admin_git_head(): + short_len = 12 + # Note: doing zero sanitization. Git branch names are extremely permissive. + # However, they forbid '..', so I don't see an obvious dir traversal attack. + # Also, a malicious branch name would mean someone already owned the server + # or repo, so I think this isn't a weak link. + try: + with open('.git/HEAD') as head_f: + head_txt = head_f.read() + head_path = re.match('ref: (refs/.+)', head_txt).group(1) + with open('.git/' + head_path) as ref_f: + gitref = ref_f.read()[0:short_len] + except: + return '' + return gitref + +@app.post("/admin/site_settings/") +@admin_level_required(3) +def change_settings(v, setting): + site_settings = app.config['SETTINGS'] + site_settings[setting] = not site_settings[setting] + with open("site_settings.json", "w") as f: + json.dump(site_settings, f) + + if site_settings[setting]: word = 'enable' + else: word = 'disable' + + body = f"@{v.username} has {word}d `{setting}` in the [admin dashboard](/admin)!" + + body_html = sanitize(body) + + new_comment = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=None, + level=1, + body_html=body_html, + sentto=2, + distinguish_level=6 + ) + g.db.add(new_comment) + g.db.flush() + + new_comment.top_comment_id = new_comment.id + + for admin in g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all(): + notif = Notification(comment_id=new_comment.id, user_id=admin.id) + g.db.add(notif) + + ma = ModAction( + kind=f"{word}_{setting}", + user_id=v.id, + ) + g.db.add(ma) + + g.db.commit() + + return {'message': f"{setting} {word}d successfully!"} + + +@app.post("/admin/purge_cache") +@admin_level_required(3) +def purge_cache(v): + cache.clear() + response = str(requests.post(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache', headers=CF_HEADERS, data='{"purge_everything":true}', timeout=5)) + + ma = ModAction( + kind="purge_cache", + user_id=v.id + ) + g.db.add(ma) + + if response == "": return {"message": "Cache purged!"} + return {"error": "Failed to purge cache."} + + +@app.post("/admin/under_attack") +@admin_level_required(3) +def under_attack(v): + response = requests.get(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/settings/security_level', headers=CF_HEADERS, timeout=5).json()['result']['value'] + + if response == 'under_attack': + ma = ModAction( + kind="disable_under_attack", + user_id=v.id, + ) + g.db.add(ma) + g.db.commit() + + response = str(requests.patch(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/settings/security_level', headers=CF_HEADERS, data='{"value":"medium"}', timeout=5)) + if response == "": return {"message": "Under attack mode disabled!"} + return {"error": "Failed to disable under attack mode."} + else: + ma = ModAction( + kind="enable_under_attack", + user_id=v.id, + ) + g.db.add(ma) + g.db.commit() + + response = str(requests.patch(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/settings/security_level', headers=CF_HEADERS, data='{"value":"under_attack"}', timeout=5)) + if response == "": return {"message": "Under attack mode enabled!"} + return {"error": "Failed to enable under attack mode."} + +@app.get("/admin/badge_grant") +@admin_level_required(2) +def badge_grant_get(v): + badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() + return render_template("admin/badge_grant.html", v=v, badge_types=badges) + + +@app.post("/admin/badge_grant") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def badge_grant_post(v): + badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() + + user = get_user(request.values.get("username").strip(), graceful=True) + if not user: + return render_template("admin/badge_grant.html", v=v, badge_types=badges, error="User not found.") + + try: badge_id = int(request.values.get("badge_id")) + except: abort(400) + + if badge_id in {16,17,94,95,96,97,98,109} and v.id != AEVANN_ID: + abort(403) + + if user.has_badge(badge_id): + return render_template("admin/badge_grant.html", v=v, badge_types=badges, error="User already has that badge.") + + 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() + + if v.id != user.id: + text = f"@{v.username} has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}" + send_notification(user.id, text) + + ma = ModAction( + kind="badge_grant", + user_id=v.id, + target_user_id=user.id, + _note=new_badge.name + ) + g.db.add(ma) + + g.db.commit() + return render_template("admin/badge_grant.html", v=v, badge_types=badges, msg="Badge granted!") + + + +@app.get("/admin/badge_remove") +@admin_level_required(2) +def badge_remove_get(v): + badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() + + return render_template("admin/badge_remove.html", v=v, badge_types=badges) + + +@app.post("/admin/badge_remove") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def badge_remove_post(v): + badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() + + user = get_user(request.values.get("username").strip(), graceful=True) + if not user: + return render_template("admin/badge_remove.html", v=v, badge_types=badges, error="User not found.") + + try: badge_id = int(request.values.get("badge_id")) + except: abort(400) + + badge = user.has_badge(badge_id) + if not badge: + return render_template("admin/badge_remove.html", v=v, badge_types=badges, error="User doesn't have that badge.") + + ma = ModAction( + kind="badge_remove", + user_id=v.id, + target_user_id=user.id, + _note=badge.name + ) + g.db.add(ma) + + g.db.delete(badge) + + g.db.commit() + + return render_template("admin/badge_remove.html", v=v, badge_types=badges, msg="Badge removed!") + + +@app.get("/admin/users") +@admin_level_required(2) +def users_list(v): + + try: page = int(request.values.get("page", 1)) + except: page = 1 + + users = g.db.query(User).order_by(User.id.desc()).offset(25 * (page - 1)).limit(26).all() + + 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("/badge_owners/") +@auth_required +def bid_list(v, bid): + + try: bid = int(bid) + except: abort(400) + + try: page = int(request.values.get("page", 1)) + except: page = 1 + + users = g.db.query(User).join(Badge, Badge.user_id == User.id).filter(Badge.badge_id==bid).offset(25 * (page - 1)).limit(26).all() + + 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(2) +def alt_votes_get(v): + + u1 = request.values.get("u1") + u2 = request.values.get("u2") + + if not u1 or not u2: + return render_template("admin/alt_votes.html", v=v) + + 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;30/minute;200/hour;1000/day") +@admin_level_required(2) +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) + + ma = ModAction( + kind="link_accounts", + user_id=v.id, + target_user_id=u1, + _note=f'with {u2}' + ) + g.db.add(ma) + + 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/posts") +@admin_level_required(2) +def admin_removed(v): + + try: page = int(request.values.get("page", 1)) + except: page = 1 + + if page < 1: abort(400) + + ids = g.db.query(Submission.id).join(User, User.id == Submission.author_id).filter(or_(Submission.is_banned==True, User.shadowbanned != None)).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.get("/admin/removed/comments") +@admin_level_required(2) +def admin_removed_comments(v): + + try: page = int(request.values.get("page", 1)) + except: page = 1 + + ids = g.db.query(Comment.id).join(User, User.id == Comment.author_id).filter(or_(Comment.is_banned==True, User.shadowbanned != None)).order_by(Comment.id.desc()).offset(25 * (page - 1)).limit(26).all() + + ids=[x[0] for x in ids] + + next_exists = len(ids) > 25 + + ids = ids[:25] + + comments = get_comments(ids, v=v) + + return render_template("admin/removed_comments.html", + v=v, + listing=comments, + page=page, + next_exists=next_exists + ) + + +@app.post("/agendaposter/") +@admin_level_required(2) +def agendaposter(user_id, v): + user = g.db.query(User).filter_by(id=user_id).one_or_none() + + days = request.values.get("days") or 30 + expiry = float(days) + expiry = int(time.time() + expiry*60*60*24) + + user.agendaposter = expiry + g.db.add(user) + + for alt in user.alts: + if alt.admin_level: return {"error": "User is an admin!"} + alt.agendaposter = expiry + g.db.add(alt) + + note = f"for {days} days" + + ma = ModAction( + kind="agendaposter", + user_id=v.id, + target_user_id=user.id, + note = note + ) + g.db.add(ma) + + if not user.has_badge(28): + badge = Badge(user_id=user.id, badge_id=28) + g.db.add(badge) + g.db.flush() + send_notification(user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({badge.path})\n\n{badge.name}") + + + send_repeatable_notification(user.id, f"@{v.username} has marked you as a chud ({note}).") + + g.db.commit() + + return redirect(user.url) + + + +@app.post("/unagendaposter/") +@admin_level_required(2) +def unagendaposter(user_id, v): + user = g.db.query(User).filter_by(id=user_id).one_or_none() + + user.agendaposter = 0 + g.db.add(user) + + for alt in user.alts: + alt.agendaposter = 0 + g.db.add(alt) + + ma = ModAction( + kind="unagendaposter", + user_id=v.id, + target_user_id=user.id + ) + + g.db.add(ma) + + badge = user.has_badge(28) + if badge: g.db.delete(badge) + + send_repeatable_notification(user.id, f"@{v.username} has unmarked you as a chud.") + + g.db.commit() + return {"message": "Chud theme disabled!"} + + +@app.post("/shadowban/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def shadowban(user_id, v): + user = g.db.query(User).filter_by(id=user_id).one_or_none() + if user.admin_level != 0: abort(403) + user.shadowbanned = v.username + g.db.add(user) + + for alt in user.alts: + if alt.admin_level: 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) + + body = f"@{v.username} has shadowbanned @{user.username}" + + body_html = sanitize(body) + + + new_comment = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=None, + level=1, + body_html=body_html, + distinguish_level=6 + ) + g.db.add(new_comment) + g.db.flush() + + new_comment.top_comment_id = new_comment.id + + for admin in g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all(): + notif = Notification(comment_id=new_comment.id, user_id=admin.id) + g.db.add(notif) + + + + g.db.commit() + return {"message": "User shadowbanned!"} + + +@app.post("/unshadowban/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def unshadowban(user_id, v): + user = g.db.query(User).filter_by(id=user_id).one_or_none() + user.shadowbanned = None + user.ban_evade = 0 + g.db.add(user) + for alt in user.alts: + alt.shadowbanned = None + alt.ban_evade = 0 + 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/title_change/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def admin_title_change(user_id, v): + + user = g.db.query(User).filter_by(id=user_id).one_or_none() + + new_name=request.values.get("title").strip()[:256] + + user.customtitleplain=new_name + new_name = filter_emojis_only(new_name) + + user=g.db.query(User).filter_by(id=user.id).one_or_none() + user.customtitle=new_name + if request.values.get("locked"): user.flairchanged = int(time.time()) + 2629746 + else: + user.flairchanged = None + badge = user.has_badge(96) + if badge: g.db.delete(badge) + + 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'"{user.customtitleplain}"' + ) + g.db.add(ma) + g.db.commit() + + return redirect(user.url) + +@app.post("/ban_user/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def ban_user(user_id, v): + + user = g.db.query(User).filter_by(id=user_id).one_or_none() + + if not user: abort(404) + + if user.admin_level >= v.admin_level: abort(403) + + days = float(request.values.get("days")) if request.values.get('days') else 0 + + reason = request.values.get("reason", "").strip()[:256] + passed_reason = filter_emojis_only(reason) + + if len(passed_reason) > 256: passed_reason = reason + + user.ban(admin=v, reason=passed_reason, days=days) + + if request.values.get("alts"): + for x in user.alts: + if x.admin_level: break + x.ban(admin=v, reason=passed_reason, days=days) + + if days: + if reason: text = f"@{v.username} has banned you for **{days}** days for the following reason:\n\n> {reason}" + else: text = f"@{v.username} has banned you for **{days}** days." + else: + if reason: text = f"@{v.username} has banned you permanently for the following reason:\n\n> {reason}" + else: text = f"@{v.username} has banned you permanently." + + send_repeatable_notification(user.id, text) + + if days == 0: duration = "permanent" + elif days == 1: duration = "1 day" + else: duration = f"{days} days" + + note = f'reason: "{reason}", duration: {duration}' + ma=ModAction( + kind="ban_user", + user_id=v.id, + target_user_id=user.id, + _note=note + ) + g.db.add(ma) + + if 'reason' in request.values: + if reason.startswith("/post/"): + try: + post = int(reason.split("/post/")[1].split(None, 1)[0]) + post = get_post(post) + post.bannedfor = True + g.db.add(post) + except: pass + elif reason.startswith("/comment/"): + try: + comment = int(reason.split("/comment/")[1].split(None, 1)[0]) + comment = get_comment(comment) + comment.bannedfor = True + g.db.add(comment) + except: pass + + + body = f"@{v.username} has banned @{user.username} ({note})" + + body_html = sanitize(body) + + + new_comment = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=None, + level=1, + body_html=body_html, + distinguish_level=6 + ) + g.db.add(new_comment) + g.db.flush() + + new_comment.top_comment_id = new_comment.id + + for admin in g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all(): + notif = Notification(comment_id=new_comment.id, user_id=admin.id) + g.db.add(notif) + + + 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;30/minute;200/hour;1000/day") +@admin_level_required(2) +def unban_user(user_id, v): + + user = g.db.query(User).filter_by(id=user_id).one_or_none() + + if not user or not user.is_banned: abort(400) + + user.is_banned = 0 + user.unban_utc = 0 + user.ban_evade = 0 + user.ban_reason = None + send_repeatable_notification(user.id, f"@{v.username} has unbanned you!") + g.db.add(user) + + for x in user.alts: + if x.is_banned: send_repeatable_notification(x.id, f"@{v.username} has unbanned you!") + x.is_banned = 0 + x.unban_utc = 0 + x.ban_evade = 0 + x.ban_reason = None + g.db.add(x) + + 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;30/minute;200/hour;1000/day") +@admin_level_required(2) +def ban_post(post_id, v): + + post = g.db.query(Submission).filter_by(id=post_id).one_or_none() + + if not post: + abort(400) + + post.is_banned = True + post.is_approved = None + post.stickied = None + post.is_pinned = False + post.ban_reason = v.username + 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) + + requests.post(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache', headers=CF_HEADERS, json={'files': [f"{SITE_FULL}/logged_out/"]}, timeout=5) + + g.db.commit() + + return {"message": "Post removed!"} + + +@app.post("/unban_post/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def unban_post(post_id, v): + + post = g.db.query(Submission).filter_by(id=post_id).one_or_none() + + if post.author.agendaposter and AGENDAPOSTER_PHRASE not in post.body.lower(): + return {"error": "You can't bypass the chud award!"} + + 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.ban_reason = None + 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) +def api_distinguish_post(post_id, v): + + post = g.db.query(Submission).filter_by(id=post_id).one_or_none() + + if not post: abort(404) + + if post.author_id != v.id and v.admin_level < 2 : abort(403) + + if post.distinguish_level: + post.distinguish_level = 0 + kind = 'undistinguish_post' + else: + post.distinguish_level = v.admin_level + kind = 'distinguish_post' + + g.db.add(post) + + ma = ModAction( + kind=kind, + user_id=v.id, + target_submission_id=post.id + ) + g.db.add(ma) + + g.db.commit() + + if post.distinguish_level: return {"message": "Post distinguished!"} + else: return {"message": "Post undistinguished!"} + + +@app.post("/sticky/") +@admin_level_required(2) +def sticky_post(post_id, v): + + post = g.db.query(Submission).filter_by(id=post_id).one_or_none() + if post and not post.stickied: + pins = g.db.query(Submission).filter(Submission.stickied != None, Submission.is_banned == False).count() + if pins > 2: + if v.admin_level > 2: + post.stickied = v.username + post.stickied_utc = int(time.time()) + 3600 + else: return {"error": "Can't exceed 3 pinned posts limit!"}, 403 + else: post.stickied = v.username + g.db.add(post) + + ma=ModAction( + kind="pin_post", + user_id=v.id, + target_submission_id=post.id + ) + g.db.add(ma) + + if v.id != post.author_id: + send_repeatable_notification(post.author_id, f"@{v.username} has pinned your [post](/post/{post_id})!") + + cache.delete_memoized(frontlist) + g.db.commit() + return {"message": "Post pinned!"} + +@app.post("/unsticky/") +@admin_level_required(2) +def unsticky_post(post_id, v): + + post = g.db.query(Submission).filter_by(id=post_id).one_or_none() + if post and post.stickied: + if post.stickied.endswith('(pin award)'): return {"error": "Can't unpin award pins!"}, 403 + + post.stickied = None + post.stickied_utc = None + g.db.add(post) + + ma=ModAction( + kind="unpin_post", + user_id=v.id, + target_submission_id=post.id + ) + g.db.add(ma) + + if v.id != post.author_id: + send_repeatable_notification(post.author_id, f"@{v.username} has unpinned your [post](/post/{post_id})!") + + cache.delete_memoized(frontlist) + g.db.commit() + return {"message": "Post unpinned!"} + +@app.post("/sticky_comment/") +@admin_level_required(2) +def sticky_comment(cid, v): + + comment = get_comment(cid, v=v) + + if not comment.is_pinned: + comment.is_pinned = v.username + g.db.add(comment) + + ma=ModAction( + kind="pin_comment", + user_id=v.id, + target_comment_id=comment.id + ) + g.db.add(ma) + + if v.id != comment.author_id: + message = f"@{v.username} has pinned your [comment]({comment.shortlink})!" + send_repeatable_notification(comment.author_id, message) + + g.db.commit() + return {"message": "Comment pinned!"} + + +@app.post("/unsticky_comment/") +@admin_level_required(2) +def unsticky_comment(cid, v): + + comment = get_comment(cid, v=v) + + if comment.is_pinned: + if comment.is_pinned.endswith("(pin award)"): return {"error": "Can't unpin award pins!"}, 403 + + comment.is_pinned = None + g.db.add(comment) + + ma=ModAction( + kind="unpin_comment", + user_id=v.id, + target_comment_id=comment.id + ) + g.db.add(ma) + + if v.id != comment.author_id: + message = f"@{v.username} has unpinned your [comment]({comment.shortlink})!" + send_repeatable_notification(comment.author_id, message) + + g.db.commit() + return {"message": "Comment unpinned!"} + + +@app.post("/ban_comment/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def api_ban_comment(c_id, v): + + comment = g.db.query(Comment).filter_by(id=c_id).one_or_none() + if not comment: + abort(404) + + comment.is_banned = True + comment.is_approved = None + comment.ban_reason = v.username + 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;30/minute;200/hour;1000/day") +@admin_level_required(2) +def api_unban_comment(c_id, v): + + comment = g.db.query(Comment).filter_by(id=c_id).one_or_none() + if not comment: abort(404) + + if comment.author.agendaposter and AGENDAPOSTER_PHRASE not in comment.body.lower(): + return {"error": "You can't bypass the chud award!"} + + 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.ban_reason = None + comment.is_approved = v.id + + g.db.add(comment) + + g.db.commit() + + return {"message": "Comment approved!"} + + +@app.post("/distinguish_comment/") +@admin_level_required(1) +def admin_distinguish_comment(c_id, v): + + + comment = get_comment(c_id, v=v) + + if comment.author_id != v.id: abort(403) + + if comment.distinguish_level: + comment.distinguish_level = 0 + kind = 'undistinguish_comment' + else: + comment.distinguish_level = v.admin_level + kind = 'distinguish_comment' + + g.db.add(comment) + + ma = ModAction( + kind=kind, + user_id=v.id, + target_comment_id=comment.id + ) + g.db.add(ma) + + g.db.commit() + + if comment.distinguish_level: return {"message": "Comment distinguished!"} + else: return {"message": "Comment undistinguished!"} + +@app.get("/admin/dump_cache") +@admin_level_required(2) +def admin_dump_cache(v): + cache.clear() + + ma = ModAction( + kind="dump_cache", + user_id=v.id + ) + g.db.add(ma) + + return {"message": "Internal cache cleared."} + + +@app.get("/admin/banned_domains/") +@admin_level_required(3) +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;30/minute;200/hour;1000/day") +@admin_level_required(3) +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).filter_by(domain=domain).one_or_none() + if d: + g.db.delete(d) + ma = ModAction( + kind="unban_domain", + user_id=v.id, + _note=domain + ) + g.db.add(ma) + + else: + d = BannedDomain(domain=domain, reason=reason) + g.db.add(d) + ma = ModAction( + kind="ban_domain", + user_id=v.id, + _note=f'{domain}, reason: {reason}' + ) + g.db.add(ma) + + g.db.commit() + + return redirect("/admin/banned_domains/") + + +@app.post("/admin/nuke_user") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def admin_nuke_user(v): + + user=get_user(request.values.get("user")) + + for post in g.db.query(Submission).filter_by(author_id=user.id).all(): + if post.is_banned: + continue + + post.is_banned = True + post.ban_reason = v.username + g.db.add(post) + + for comment in g.db.query(Comment).filter_by(author_id=user.id).all(): + if comment.is_banned: + continue + + comment.is_banned = True + comment.ban_reason = v.username + 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;30/minute;200/hour;1000/day") +@admin_level_required(2) +def admin_nunuke_user(v): + + user=get_user(request.values.get("user")) + + for post in g.db.query(Submission).filter_by(author_id=user.id).all(): + if not post.is_banned: + continue + + post.is_banned = False + post.ban_reason = None + g.db.add(post) + + for comment in g.db.query(Comment).filter_by(author_id=user.id).all(): + if not comment.is_banned: + continue + + comment.is_banned = False + comment.ban_reason = None + 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 index d4efb060c..eb2ab12d5 100644 --- a/files/routes/awards.py +++ b/files/routes/awards.py @@ -81,6 +81,7 @@ def buy(v, award): send_notification(v.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") g.db.add(v) + if award == "lootbox": lootbox_items = [] for i in [1,2,3,4,5]: @@ -93,7 +94,7 @@ def buy(v, award): v.lootboxes_bought += 1 lootbox_msg = "You open your lootbox and receive: " + ', '.join(lootbox_items) send_repeatable_notification(v.id, lootbox_msg) - + if v.lootboxes_bought == 10 and not v.has_badge(76): new_badge = Badge(badge_id=76, user_id=v.id) g.db.add(new_badge) @@ -351,6 +352,8 @@ def award_post(pid, v): g.db.add(badge) g.db.flush() send_notification(author.id, f"@AutoJanny has given you the following profile badge:\n\n![]({badge.path})\n\n{badge.name}") + elif kind == "checkmark": + author.verified = "Verified" if author.received_award_count: author.received_award_count += 1 else: author.received_award_count = 1 @@ -590,6 +593,8 @@ def award_comment(cid, v): g.db.add(badge) g.db.flush() send_notification(author.id, f"@AutoJanny has given you the following profile badge:\n\n![]({badge.path})\n\n{badge.name}") + elif kind == "checkmark": + author.verified = "Verified" if author.received_award_count: author.received_award_count += 1 else: author.received_award_count = 1 diff --git a/files/routes/comments.py b/files/routes/comments.py index 8a726566b..2bfc229c4 100644 --- a/files/routes/comments.py +++ b/files/routes/comments.py @@ -1,1078 +1,1088 @@ -from files.helpers.wrappers import * -from files.helpers.alerts import * -from files.helpers.images import * -from files.helpers.const import * -from files.helpers.slots import * -from files.helpers.blackjack import * -from files.helpers.treasure import * -from files.classes import * -from files.routes.front import comment_idlist -from files.routes.static import marsey_list -from pusher_push_notifications import PushNotifications -from flask import * -from files.__main__ import app, limiter -from files.helpers.sanitize import filter_emojis_only -import requests -from shutil import copyfile -from json import loads -from collections import Counter -from enchant import Dict -import gevent -from sys import stdout - -d = Dict("en_US") - -if PUSHER_ID != 'blahblahblah': - beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY) - -WORDLE_COLOR_MAPPINGS = {-1: "🟥", 0: "🟨", 1: "🟩"} - -def pusher_thread(interests, c, username): - if len(c.body) > 500: notifbody = c.body[:500] + '...' - else: notifbody = c.body - - beams_client.publish_to_interests( - interests=[interests], - publish_body={ - 'web': { - 'notification': { - 'title': f'New reply by @{username}', - 'body': notifbody, - 'deep_link': f'{SITE_FULL}/comment/{c.id}?context=8&read=true#context', - 'icon': f'{SITE_FULL}/assets/images/{SITE_NAME}/icon.webp?v=1015', - } - }, - 'fcm': { - 'notification': { - 'title': f'New reply by @{username}', - 'body': notifbody, - }, - 'data': { - 'url': f'/comment/{c.id}?context=8&read=true#context', - } - } - }, - ) - stdout.flush() - -@app.get("/comment/") -@app.get("/post///") -@app.get("/h//comment/") -@app.get("/h//post///") -@auth_desired -def post_pid_comment_cid(cid, pid=None, anything=None, v=None, sub=None): - - try: cid = int(cid) - except: abort(404) - - comment = get_comment(cid, v=v) - - if v and request.values.get("read"): - notif = g.db.query(Notification).filter_by(comment_id=cid, user_id=v.id, read=False).one_or_none() - if notif: - notif.read = True - g.db.add(notif) - g.db.commit() - - if comment.post and comment.post.club and not (v and (v.paid_dues or v.id in [comment.author_id, comment.post.author_id])): abort(403) - - if comment.post and comment.post.private and not (v and (v.admin_level > 1 or v.id == comment.post.author.id)): 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 > 1) : abort(403) - - if not pid: - if comment.parent_submission: pid = comment.parent_submission - elif SITE_NAME == 'rDrama': pid = 6489 - elif request.host == 'pcmemes.net': pid = 2487 - 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': 'This content is not suitable for some users and situations.'} - else: return render_template("errors/nsfw.html", v=v) - - try: context = min(int(request.values.get("context", 0)), 8) - except: context = 0 - comment_info = comment - c = comment - while context 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) - - if v: - votes = g.db.query(CommentVote).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.target_id, - blocked.c.target_id, - ) - - if not (v and v.shadowbanned) and not (v and v.admin_level > 2): - comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) - - comments=comments.filter( - Comment.parent_submission == post.id, - Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID)) - ).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.replies=[top_comment] - - if request.headers.get("Authorization"): return top_comment.json - else: - if post.is_banned and not (v and (v.admin_level > 1 or post.author_id == v.id)): template = "submission_banned.html" - else: template = "submission.html" - return render_template(template, v=v, p=post, sort=sort, comment_info=comment_info, render_replies=True, sub=post.subr) - -@app.post("/comment") -@limiter.limit("1/second;20/minute;200/hour;1000/day") -@limiter.limit("1/second;20/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def api_comment(v): - if v.is_suspended: return {"error": "You can't perform this action while banned."}, 403 - - parent_submission = request.values.get("submission").strip() - parent_fullname = request.values.get("parent_fullname").strip() - - parent_post = get_post(parent_submission, v=v) - sub = parent_post.sub - if sub and v.exiled_from(sub): return {"error": f"You're exiled from /h/{sub}"}, 403 - - if parent_post.club and not (v and (v.paid_dues or v.id == parent_post.author_id)): abort(403) - - rts = False - 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 - if parent.author_id == v.id: rts = True - else: abort(400) - - body = request.values.get("body", "").strip()[:10000] - - if v.admin_level > 2 and parent_post.id == 37749 and level == 1: - with open(f"snappy_{SITE_NAME}.txt", "a", encoding="utf-8") as f: - f.write('\n{[para]}\n' + body) - - if parent_post.id not in ADMIGGERS: - if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): - return {"error":"You have to type more than 280 characters!"}, 403 - elif v.bird and len(body) > 140: - return {"error":"You have to type less than 140 characters!"}, 403 - - if not body and not request.files.get('file'): return {"error":"You need to actually write something!"}, 400 - - options = [] - for i in poll_regex.finditer(body): - options.append(i.group(1)) - body = body.replace(i.group(0), "") - - choices = [] - for i in choice_regex.finditer(body): - choices.append(i.group(1)) - body = body.replace(i.group(0), "") - - if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - files = request.files.getlist('file')[:4] - for file in files: - if file.content_type.startswith('image/'): - oldname = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(oldname) - image = process_image(oldname) - if image == "": return {"error":"Image upload failed"} - if v.admin_level > 2 and level == 1: - if parent_post.id == 37696: - filename = 'files/assets/images/rDrama/sidebar/' + str(len(listdir('files/assets/images/rDrama/sidebar'))+1) + '.webp' - copyfile(oldname, filename) - process_image(filename, 400) - elif parent_post.id == 37697: - filename = 'files/assets/images/rDrama/banners/' + str(len(listdir('files/assets/images/rDrama/banners'))+1) + '.webp' - copyfile(oldname, filename) - process_image(filename) - elif parent_post.id == 37833: - try: - badge_def = loads(body) - name = badge_def["name"] - - existing = g.db.query(BadgeDef).filter_by(name=name).one_or_none() - if existing: return {"error": "A badge with this name already exists!"}, 403 - - badge = BadgeDef(name=name, description=badge_def["description"]) - g.db.add(badge) - g.db.flush() - filename = f'files/assets/images/badges/{badge.id}.webp' - copyfile(oldname, filename) - process_image(filename, 200) - requests.post(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache', headers=CF_HEADERS, data={'files': [f"https://{request.host}/assets/images/badges/{badge.id}.webp"]}, timeout=5) - except Exception as e: - return {"error": str(e)}, 400 - elif v.admin_level > 2 and parent_post.id == 37838: - try: - marsey = loads(body.lower()) - - name = marsey["name"] - if not marsey_regex.fullmatch(name): return {"error": "Invalid name!"}, 400 - existing = g.db.query(Marsey.name).filter_by(name=name).one_or_none() - if existing: return {"error": "A marsey with this name already exists!"}, 403 - - tags = marsey["tags"] - if not tags_regex.fullmatch(tags): return {"error": "Invalid tags!"}, 400 - - if "author" in marsey: user = get_user(marsey["author"]) - elif "author_id" in marsey: user = get_account(marsey["author_id"]) - else: abort(400) - - filename = f'files/assets/images/emojis/{name}.webp' - copyfile(oldname, filename) - process_image(filename, 200) - - marsey = Marsey(name=name, author_id=user.id, tags=tags, count=0) - g.db.add(marsey) - g.db.flush() - - all_by_author = g.db.query(Marsey.author_id).filter_by(author_id=user.id).count() - - if all_by_author >= 10 and not user.has_badge(16): - new_badge = Badge(badge_id=16, user_id=user.id) - - g.db.add(new_badge) - g.db.flush() - - if v.id != user.id: - text = f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}" - send_notification(user.id, text) - - elif all_by_author < 10 and not user.has_badge(17): - new_badge = Badge(badge_id=17, user_id=user.id) - - g.db.add(new_badge) - g.db.flush() - - if v.id != user.id: - text = f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}" - send_notification(user.id, text) - - - - requests.post(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache', headers=CF_HEADERS, data={'files': [f"https://{request.host}/e/{name}.webp"]}, timeout=5) - cache.delete_memoized(marsey_list) - - except Exception as e: - return {"error": str(e)}, 400 - body += f"\n\n![]({image})" - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data'] - except requests.Timeout: return {"error": "Video upload timed out, please try again!"} - try: url = req['link'] - except: - error = req['error'] - if error == 'File exceeds max duration': error += ' (60 seconds)' - return {"error": error}, 400 - if url.endswith('.'): url += 'mp4' - body += f"\n\n{url}" - else: return {"error": "Image/Video files only"}, 400 - - if v.agendaposter and not v.marseyawarded and parent_post.id not in ADMIGGERS: - body = torture_ap(body, v.username) - - body_html = sanitize(body, comment=True) - - - if parent_post.id not in ADMIGGERS and '!slots' not in body.lower() and '!blackjack' not in body.lower() and '!wordle' not in body.lower() and AGENDAPOSTER_PHRASE not in body.lower(): - existing = g.db.query(Comment.id).filter(Comment.author_id == v.id, - Comment.deleted_utc == 0, - Comment.parent_comment_id == parent_comment_id, - Comment.parent_submission == parent_submission, - Comment.body_html == body_html - ).one_or_none() - if existing: return {"error": f"You already made that comment: /comment/{existing.id}"}, 409 - - if parent.author.any_block_exists(v) and v.admin_level < 2: - return {"error": "You can't reply to users who have blocked you, or users you have blocked."}, 403 - - is_bot = bool(request.headers.get("Authorization")) or (SITE == 'pcmemes.net' and v.id == SNAPPY_ID) - - if '!slots' not in body.lower() and '!blackjack' not in body.lower() and '!wordle' not in body.lower() and parent_post.id not in ADMIGGERS and not is_bot and not v.marseyawarded and AGENDAPOSTER_PHRASE not in body.lower() and len(body) > 10: - now = int(time.time()) - cutoff = now - 60 * 60 * 24 - - similar_comments = g.db.query(Comment).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 banned for **1 day** for the following reason:\n\n> Too much spam!" - send_repeatable_notification(v.id, text) - - v.ban(reason="Spamming.", - days=1) - - for comment in similar_comments: - comment.is_banned = True - comment.ban_reason = "AutoJanny" - g.db.add(comment) - ma=ModAction( - user_id=AUTOJANNY_ID, - target_comment_id=comment.id, - kind="ban_comment", - _note="spam" - ) - g.db.add(ma) - - return {"error": "Too much spam!"}, 403 - - 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], - ghost=parent_post.ghost - ) - - c.upvotes = 1 - g.db.add(c) - g.db.flush() - - if blackjack and any(i in c.body.lower() for i in blackjack.split()): - v.shadowbanned = 'AutoJanny' - notif = Notification(comment_id=c.id, user_id=CARP_ID) - g.db.add(notif) - - if c.level == 1: c.top_comment_id = c.id - else: c.top_comment_id = parent.top_comment_id - - for option in options: - c_option = Comment(author_id=AUTOPOLLER_ID, - parent_submission=parent_submission, - parent_comment_id=c.id, - level=level+1, - body_html=filter_emojis_only(option), - upvotes=0, - is_bot=True - ) - - g.db.add(c_option) - - for choice in choices: - c_choice = Comment(author_id=AUTOCHOICE_ID, - parent_submission=parent_submission, - parent_comment_id=c.id, - level=level+1, - body_html=filter_emojis_only(choice), - upvotes=0, - is_bot=True - ) - - g.db.add(c_choice) - - if request.host == 'pcmemes.net' and c.body.lower().startswith("based"): - pill = based_regex.match(body) - - if level == 1: basedguy = get_account(parent_post.author_id) - else: basedguy = get_account(c.parent_comment.author_id) - basedguy.basedcount += 1 - if pill: - if basedguy.pills: basedguy.pills += f", {pill.group(1)}" - else: basedguy.pills += f"{pill.group(1)}" - g.db.add(basedguy) - - body2 = f"@{basedguy.username}'s Based Count has increased by 1. Their Based Count is now {basedguy.basedcount}." - if basedguy.pills: body2 += f"\n\nPills: {basedguy.pills}" - - body_based_html = sanitize(body2) - - c_based = Comment(author_id=BASEDBOT_ID, - parent_submission=parent_submission, - distinguish_level=6, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_based_html, - top_comment_id=c.top_comment_id, - ghost=parent_post.ghost - ) - - g.db.add(c_based) - g.db.flush() - - n = Notification(comment_id=c_based.id, user_id=v.id) - g.db.add(n) - - if parent_post.id not in ADMIGGERS: - if v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower(): - - c.is_banned = True - c.ban_reason = "AutoJanny" - - g.db.add(c) - - - body = AGENDAPOSTER_MSG.format(username=v.username, type='comment', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) - - body_jannied_html = sanitize(body) - - - - c_jannied = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=parent_submission, - distinguish_level=6, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_jannied_html, - top_comment_id=c.top_comment_id, - ghost=parent_post.ghost - ) - - g.db.add(c_jannied) - g.db.flush() - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - - if SITE_NAME == 'rDrama' and len(c.body) >= 1000 and "<" not in body and "" not in body_html: - - body = random.choice(LONGPOST_REPLIES) - - - if body.startswith('▼'): - body = body[1:] - vote = CommentVote(user_id=LONGPOSTBOT_ID, - vote_type=-1, - comment_id=c.id, - real = True - ) - g.db.add(vote) - c.downvotes = 1 - - - - body_html2 = sanitize(body) - - c2 = Comment(author_id=LONGPOSTBOT_ID, - parent_submission=parent_submission, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_html2, - top_comment_id=c.top_comment_id, - ghost=parent_post.ghost - ) - - g.db.add(c2) - - longpostbot = g.db.query(User).filter_by(id = LONGPOSTBOT_ID).one_or_none() - longpostbot.comment_count += 1 - longpostbot.coins += 1 - g.db.add(longpostbot) - - g.db.flush() - - n = Notification(comment_id=c2.id, user_id=v.id) - g.db.add(n) - - - if SITE_NAME == 'rDrama' and random.random() < 0.001: - - body = "zoz" - body_html2 = sanitize(body) - - - - - c2 = Comment(author_id=ZOZBOT_ID, - parent_submission=parent_submission, - parent_comment_id=c.id, - level=level+1, - is_bot=True, - body_html=body_html2, - top_comment_id=c.top_comment_id, - ghost=parent_post.ghost, - distinguish_level=6 - ) - - g.db.add(c2) - g.db.flush() - n = Notification(comment_id=c2.id, user_id=v.id) - g.db.add(n) - - - - - - body = "zle" - body_html2 = sanitize(body) - - - - c3 = Comment(author_id=ZOZBOT_ID, - parent_submission=parent_submission, - parent_comment_id=c2.id, - level=level+2, - is_bot=True, - body_html=body_html2, - top_comment_id=c.top_comment_id, - ghost=parent_post.ghost, - distinguish_level=6 - ) - - g.db.add(c3) - g.db.flush() - - body = "zozzle" - body_html2 = sanitize(body) - - - c4 = Comment(author_id=ZOZBOT_ID, - parent_submission=parent_submission, - parent_comment_id=c3.id, - level=level+3, - is_bot=True, - body_html=body_html2, - top_comment_id=c.top_comment_id, - ghost=parent_post.ghost, - distinguish_level=6 - ) - - g.db.add(c4) - - zozbot = g.db.query(User).filter_by(id = ZOZBOT_ID).one_or_none() - zozbot.comment_count += 3 - zozbot.coins += 3 - g.db.add(zozbot) - - - if not v.shadowbanned: - notify_users = NOTIFY_USERS(body, v) - - for x in g.db.query(Subscription.user_id).filter_by(submission_id=c.parent_submission).all(): notify_users.add(x[0]) - - if parent.author.id not in (v.id, BASEDBOT_ID, AUTOJANNY_ID, SNAPPY_ID, LONGPOSTBOT_ID, ZOZBOT_ID, AUTOPOLLER_ID, AUTOCHOICE_ID): - notify_users.add(parent.author.id) - - for x in notify_users: - n = Notification(comment_id=c.id, user_id=x) - g.db.add(n) - - if parent.author.id != v.id and PUSHER_ID != 'blahblahblah' and not v.shadowbanned: - try: gevent.spawn(pusher_thread, f'{request.host}{parent.author.id}', c, c.author_name) - except: pass - - - - 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).filter(Comment.author_id == v.id, Comment.parent_submission != None).filter_by(is_banned=False, deleted_utc=0).count() - g.db.add(v) - - c.voted = 1 - - if v.id == PIZZASHILL_ID: - for uid in PIZZA_VOTERS: - autovote = CommentVote(user_id=uid, comment_id=c.id, vote_type=1) - g.db.add(autovote) - v.coins += 3 - v.truecoins += 3 - g.db.add(v) - c.upvotes += 3 - g.db.add(c) - - if not v.rehab: - check_for_slots_command(body, v, c) - - check_for_blackjack_commands(body, v, c) - - if not c.slots_result and not c.blackjack_result and v.marseyawarded and parent_post.id not in ADMIGGERS and marseyaward_body_regex.search(body_html): - return {"error":"You can only type marseys!"}, 403 - - check_for_treasure(body, c) - - if "!wordle" in body: - answer = random.choice(WORDLE_LIST) - c.wordle_result = f'_active_{answer}' - - if not c.slots_result and not c.blackjack_result and not c.wordle_result and not rts: - parent_post.comment_count += 1 - g.db.add(parent_post) - - g.db.commit() - - if request.headers.get("Authorization"): return c.json - return {"comment": render_template("comments.html", v=v, comments=[c], ajax=True)} - - - -@app.post("/edit_comment/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def edit_comment(cid, v): - - c = get_comment(cid, v=v) - - if c.author_id != v.id: abort(403) - - body = request.values.get("body", "").strip()[:10000] - - if len(body) < 1 and not (request.files.get("file") and request.headers.get("cf-ipcountry") != "T1"): - return {"error":"You have to actually type something!"}, 400 - - if body != c.body or request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): - return {"error":"You have to type more than 280 characters!"}, 403 - elif v.bird and len(body) > 140: - return {"error":"You have to type less than 140 characters!"}, 403 - - if v.agendaposter and not v.marseyawarded: - body = torture_ap(body, v.username) - - if not c.options: - for i in poll_regex.finditer(body): - body = body.replace(i.group(0), "") - c_option = Comment(author_id=AUTOPOLLER_ID, - parent_submission=c.parent_submission, - parent_comment_id=c.id, - level=c.level+1, - body_html=filter_emojis_only(i.group(1)), - upvotes=0, - is_bot=True - ) - g.db.add(c_option) - - if not c.choices: - for i in choice_regex.finditer(body): - body = body.replace(i.group(0), "") - c_choice = Comment(author_id=AUTOCHOICE_ID, - parent_submission=c.parent_submission, - parent_comment_id=c.id, - level=c.level+1, - body_html=filter_emojis_only(i.group(1)), - upvotes=0, - is_bot=True - ) - g.db.add(c_choice) - - body_html = sanitize(body, edit=True) - - if '!slots' not in body.lower() and '!blackjack' not in body.lower() and '!wordle' not in body.lower() and AGENDAPOSTER_PHRASE not in body.lower(): - now = int(time.time()) - cutoff = now - 60 * 60 * 24 - - similar_comments = g.db.query(Comment - ).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 banned for **1 day** for the following reason:\n\n> Too much spam!" - send_repeatable_notification(v.id, text) - - v.ban(reason="Spamming.", - days=1) - - for comment in similar_comments: - comment.is_banned = True - comment.ban_reason = "AutoJanny" - g.db.add(comment) - - return {"error": "Too much spam!"}, 403 - - if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - files = request.files.getlist('file')[:4] - for file in files: - if file.content_type.startswith('image/'): - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - url = process_image(name) - body += f"\n\n![]({url})" - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data'] - except requests.Timeout: return {"error": "Video upload timed out, please try again!"} - try: url = req['link'] - except: - error = req['error'] - if error == 'File exceeds max duration': error += ' (60 seconds)' - return {"error": error}, 400 - if url.endswith('.'): url += 'mp4' - body += f"\n\n{url}" - else: return {"error": "Image/Video files only"}, 400 - - body_html = sanitize(body, edit=True) - - if len(body_html) > 20000: abort(400) - - if v.marseyawarded and marseyaward_body_regex.search(body_html): - return {"error":"You can only type marseys!"}, 403 - - c.body = body[:10000] - c.body_html = body_html - - if blackjack and any(i in c.body.lower() for i in blackjack.split()): - v.shadowbanned = 'AutoJanny' - g.db.add(v) - notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=CARP_ID).one_or_none() - if not notif: - notif = Notification(comment_id=c.id, user_id=CARP_ID) - g.db.add(notif) - - if v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower() and not c.is_banned: - - c.is_banned = True - c.ban_reason = "AutoJanny" - - g.db.add(c) - - - body = AGENDAPOSTER_MSG.format(username=v.username, type='comment', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) - - body_jannied_html = sanitize(body) - - - - c_jannied = Comment(author_id=NOTIFICATIONS_ID, - 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, - top_comment_id=c.top_comment_id, - ghost=c.ghost - ) - - 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) - - notify_users = NOTIFY_USERS(body, v) - - for x in notify_users: - notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=x).one_or_none() - if not notif: - n = Notification(comment_id=c.id, user_id=x) - g.db.add(n) - - g.db.commit() - - return {"comment": c.realbody(v)} - - -@app.post("/delete/comment/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def delete_comment(cid, v): - - c = get_comment(cid, v=v) - - if not c.deleted_utc: - - if 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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def undelete_comment(cid, v): - - c = get_comment(cid, v=v) - - if c.deleted_utc: - if 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 -def pin_comment(cid, v): - - comment = get_comment(cid, v=v) - - if not comment.is_pinned: - if v.id != comment.post.author_id: abort(403) - - if comment.post.ghost: comment.is_pinned = "(OP)" - else: comment.is_pinned = v.username + " (OP)" - - g.db.add(comment) - - if v.id != comment.author_id: - if comment.post.ghost: message = f"OP has pinned your [comment]({comment.shortlink})!" - else: message = f"@{v.username} (OP) has pinned your [comment]({comment.shortlink})!" - send_repeatable_notification(comment.author_id, message) - - g.db.commit() - return {"message": "Comment pinned!"} - - -@app.post("/unpin_comment/") -@auth_required -def unpin_comment(cid, v): - - comment = get_comment(cid, v=v) - - if comment.is_pinned: - if v.id != comment.post.author_id: abort(403) - - if not comment.is_pinned.endswith(" (OP)"): - return {"error": "You can only unpin comments you have pinned!"} - - comment.is_pinned = None - g.db.add(comment) - - if v.id != comment.author_id: - message = f"@{v.username} (OP) has unpinned your [comment]({comment.shortlink})!" - send_repeatable_notification(comment.author_id, message) - g.db.commit() - return {"message": "Comment unpinned!"} - - -@app.post("/mod_pin/") -@auth_required -def mod_pin(cid, v): - - comment = get_comment(cid, v=v) - - if not comment.is_pinned: - if not (comment.post.sub and v.mods(comment.post.sub)): abort(403) - - comment.is_pinned = v.username + " (Mod)" - - g.db.add(comment) - - if v.id != comment.author_id: - message = f"@{v.username} (Mod) has pinned your [comment]({comment.shortlink})!" - send_repeatable_notification(comment.author_id, message) - - g.db.commit() - return {"message": "Comment pinned!"} - - -@app.post("/unmod_pin/") -@auth_required -def mod_unpin(cid, v): - - comment = get_comment(cid, v=v) - - if comment.is_pinned: - if not (comment.post.sub and v.mods(comment.post.sub)): abort(403) - - comment.is_pinned = None - g.db.add(comment) - - if v.id != comment.author_id: - message = f"@{v.username} (Mod) has unpinned your [comment]({comment.shortlink})!" - send_repeatable_notification(comment.author_id, message) - g.db.commit() - return {"message": "Comment unpinned!"} - - -@app.post("/save_comment/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def save_comment(cid, v): - - comment=get_comment(cid) - - save=g.db.query(CommentSaveRelationship).filter_by(user_id=v.id, comment_id=comment.id).one_or_none() - - if not save: - new_save=CommentSaveRelationship(user_id=v.id, comment_id=comment.id) - g.db.add(new_save) - - g.db.commit() - - return {"message": "Comment saved!"} - -@app.post("/unsave_comment/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def unsave_comment(cid, v): - - comment=get_comment(cid) - - save=g.db.query(CommentSaveRelationship).filter_by(user_id=v.id, comment_id=comment.id).one_or_none() - - if save: - g.db.delete(save) - g.db.commit() - - return {"message": "Comment unsaved!"} - -@app.post("/blackjack/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def handle_blackjack_action(cid, v): - comment = get_comment(cid) - if 'active' in comment.blackjack_result: - try: action = request.values.get("thing").strip().lower() - except: abort(400) - - if action == 'hit': player_hit(comment) - elif action == 'stay': player_stayed(comment) - elif action == 'doubledown': player_doubled_down(comment) - elif action == 'insurance': player_bought_insurance(comment) - - g.db.add(comment) - g.db.add(v) - g.db.commit() - return {"response" : comment.blackjack_html(v)} - - -def diff_words(answer, guess): - """ - Return a list of numbers corresponding to the char's relevance. - -1 means char is not in solution or the character appears too many times in the guess - 0 means char is in solution but in the wrong spot - 1 means char is in the correct spot - """ - diffs = [ - 1 if cs == cg else -1 for cs, cg in zip(answer, guess) - ] - char_freq = Counter( - c_guess for c_guess, diff, in zip(answer, diffs) if diff == -1 - ) - for i, cg in enumerate(guess): - if diffs[i] == -1 and cg in char_freq and char_freq[cg] > 0: - char_freq[cg] -= 1 - diffs[i] = 0 - return diffs - - -@app.post("/wordle/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def handle_wordle_action(cid, v): - - comment = get_comment(cid) - - guesses, status, answer = comment.wordle_result.split("_") - count = len(guesses.split(" -> ")) - - try: guess = request.values.get("thing").strip().lower() - except: abort(400) - - if len(guess) != 5 or not d.check(guess) and guess not in WORDLE_LIST: - return {"error": "Not a valid guess!"}, 400 - - if status == "active": - guesses += "".join(cg + WORDLE_COLOR_MAPPINGS[diff] for cg, diff in zip(guess, diff_words(answer, guess))) - - if (guess == answer): status = "won" - elif (count == 6): status = "lost" - else: guesses += ' -> ' - - comment.wordle_result = f'{guesses}_{status}_{answer}' - - g.db.add(comment) - g.db.commit() - +from files.helpers.wrappers import * +from files.helpers.alerts import * +from files.helpers.images import * +from files.helpers.const import * +from files.helpers.slots import * +from files.helpers.blackjack import * +from files.helpers.treasure import * +from files.classes import * +from files.routes.front import comment_idlist +from files.routes.static import marsey_list +from pusher_push_notifications import PushNotifications +from flask import * +from files.__main__ import app, limiter +from files.helpers.sanitize import filter_emojis_only +import requests +from shutil import copyfile +from json import loads +from collections import Counter +from enchant import Dict +import gevent +from sys import stdout +import os + +d = Dict("en_US") + +if PUSHER_ID != 'blahblahblah': + beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY) + +WORDLE_COLOR_MAPPINGS = {-1: "🟥", 0: "🟨", 1: "🟩"} + +def pusher_thread(interests, c, username): + if len(c.body) > 500: notifbody = c.body[:500] + '...' + else: notifbody = c.body + + beams_client.publish_to_interests( + interests=[interests], + publish_body={ + 'web': { + 'notification': { + 'title': f'New reply by @{username}', + 'body': notifbody, + 'deep_link': f'{SITE_FULL}/comment/{c.id}?context=8&read=true#context', + 'icon': f'{SITE_FULL}/assets/images/{SITE_NAME}/icon.webp?v=1015', + } + }, + 'fcm': { + 'notification': { + 'title': f'New reply by @{username}', + 'body': notifbody, + }, + 'data': { + 'url': f'/comment/{c.id}?context=8&read=true#context', + } + } + }, + ) + stdout.flush() + +@app.get("/comment/") +@app.get("/post///") +@app.get("/h//comment/") +@app.get("/h//post///") +@app.get("/logged_out/comment/") +@app.get("/logged_out/post///") +@app.get("/logged_out/h//comment/") +@app.get("/logged_out/h//post///") +@auth_desired +def post_pid_comment_cid(cid, pid=None, anything=None, v=None, sub=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'): return redirect(request.full_path.replace('/logged_out','')) + + try: cid = int(cid) + except: abort(404) + + comment = get_comment(cid, v=v) + + if v and request.values.get("read"): + notif = g.db.query(Notification).filter_by(comment_id=cid, user_id=v.id, read=False).one_or_none() + if notif: + notif.read = True + g.db.add(notif) + g.db.commit() + + if comment.post and comment.post.club and not (v and (v.paid_dues or v.id in [comment.author_id, comment.post.author_id])): 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 > 1) : abort(403) + + if not pid: + if comment.parent_submission: pid = comment.parent_submission + elif SITE_NAME == 'rDrama': pid = 6489 + elif request.host == 'pcmemes.net': pid = 2487 + 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': 'This content is not suitable for some users and situations.'} + else: return render_template("errors/nsfw.html", v=v) + + try: context = min(int(request.values.get("context", 0)), 8) + except: context = 0 + comment_info = comment + c = comment + while context 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) + + if v: + votes = g.db.query(CommentVote).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.target_id, + blocked.c.target_id, + ) + + if not (v and v.shadowbanned) and not (v and v.admin_level > 2): + comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) + + comments=comments.filter( + Comment.parent_submission == post.id, + Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID)) + ).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.replies=[top_comment] + + if request.headers.get("Authorization"): return top_comment.json + else: + if post.is_banned and not (v and (v.admin_level > 1 or post.author_id == v.id)): template = "submission_banned.html" + else: template = "submission.html" + return render_template(template, v=v, p=post, sort=sort, comment_info=comment_info, render_replies=True, sub=post.subr) + +@app.post("/comment") +@limiter.limit("1/second;20/minute;200/hour;1000/day") +@limiter.limit("1/second;20/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def api_comment(v): + if v.is_suspended: return {"error": "You can't perform this action while banned."}, 403 + + parent_submission = request.values.get("submission").strip() + parent_fullname = request.values.get("parent_fullname").strip() + + parent_post = get_post(parent_submission, v=v) + sub = parent_post.sub + if sub and v.exiled_from(sub): return {"error": f"You're exiled from /h/{sub}"}, 403 + + if parent_post.club and not (v and (v.paid_dues or v.id == parent_post.author_id)): abort(403) + + rts = False + 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 + if parent.author_id == v.id: rts = True + else: abort(400) + + body = request.values.get("body", "").strip()[:10000] + + if v.admin_level > 2 and parent_post.id == 37749 and level == 1: + with open(f"snappy_{SITE_NAME}.txt", "a", encoding="utf-8") as f: + f.write('\n{[para]}\n' + body) + + if parent_post.id not in ADMIGGERS: + if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): + return {"error":"You have to type more than 280 characters!"}, 403 + elif v.bird and len(body) > 140: + return {"error":"You have to type less than 140 characters!"}, 403 + + if not body and not request.files.get('file'): return {"error":"You need to actually write something!"}, 400 + + options = [] + for i in poll_regex.finditer(body): + options.append(i.group(1)) + body = body.replace(i.group(0), "") + + choices = [] + for i in choice_regex.finditer(body): + choices.append(i.group(1)) + body = body.replace(i.group(0), "") + + if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + files = request.files.getlist('file')[:4] + for file in files: + if file.content_type.startswith('image/'): + oldname = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(oldname) + image = process_image(v.patron, oldname) + if image == "": return {"error":"Image upload failed"} + if v.admin_level > 2 and level == 1: + if parent_post.id == 37696: + li = sorted(os.listdir('files/assets/images/rDrama/sidebar'), + key=lambda e: int(e.split('.webp')[0]))[-1] + num = int(li.split('.webp')[0]) + 1 + filename = f'files/assets/images/rDrama/sidebar/{num}.webp' + copyfile(oldname, filename) + process_image(v.patron, filename, 400) + elif parent_post.id == 37697: + li = sorted(os.listdir('files/assets/images/rDrama/banners'), + key=lambda e: int(e.split('.webp')[0]))[-1] + num = int(li.split('.webp')[0]) + 1 + filename = f'files/assets/images/rDrama/banners/{num}.webp' + copyfile(oldname, filename) + process_image(v.patron, filename) + elif parent_post.id == 37833: + try: + badge_def = loads(body) + name = badge_def["name"] + + existing = g.db.query(BadgeDef).filter_by(name=name).one_or_none() + if existing: return {"error": "A badge with this name already exists!"}, 403 + + badge = BadgeDef(name=name, description=badge_def["description"]) + g.db.add(badge) + g.db.flush() + filename = f'files/assets/images/badges/{badge.id}.webp' + copyfile(oldname, filename) + process_image(v.patron, filename, 200) + requests.post(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache', headers=CF_HEADERS, data={'files': [f"https://{request.host}/assets/images/badges/{badge.id}.webp"]}, timeout=5) + except Exception as e: + return {"error": str(e)}, 400 + elif v.admin_level > 2 and parent_post.id == 37838: + try: + marsey = loads(body.lower()) + + name = marsey["name"] + if not marsey_regex.fullmatch(name): return {"error": "Invalid name!"}, 400 + existing = g.db.query(Marsey.name).filter_by(name=name).one_or_none() + if existing: return {"error": "A marsey with this name already exists!"}, 403 + + tags = marsey["tags"] + if not tags_regex.fullmatch(tags): return {"error": "Invalid tags!"}, 400 + + if "author" in marsey: user = get_user(marsey["author"]) + elif "author_id" in marsey: user = get_account(marsey["author_id"]) + else: abort(400) + + filename = f'files/assets/images/emojis/{name}.webp' + copyfile(oldname, filename) + process_image(v.patron, filename, 200) + + marsey = Marsey(name=name, author_id=user.id, tags=tags, count=0) + g.db.add(marsey) + g.db.flush() + + all_by_author = g.db.query(Marsey).filter_by(author_id=user.id).count() + + if all_by_author >= 10 and not user.has_badge(16): + new_badge = Badge(badge_id=16, user_id=user.id) + + g.db.add(new_badge) + g.db.flush() + + if v.id != user.id: + text = f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}" + send_notification(user.id, text) + + elif all_by_author < 10 and not user.has_badge(17): + new_badge = Badge(badge_id=17, user_id=user.id) + + g.db.add(new_badge) + g.db.flush() + + if v.id != user.id: + text = f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}" + send_notification(user.id, text) + + + + requests.post(f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache', headers=CF_HEADERS, data={'files': [f"https://{request.host}/e/{name}.webp"]}, timeout=5) + cache.delete_memoized(marsey_list) + + except Exception as e: + return {"error": str(e)}, 400 + body += f"\n\n![]({image})" + elif file.content_type.startswith('video/'): + if file.content_type == 'video/webm': + file.save("video.mp4") + else: + file.save("unsanitized.mp4") + os.system(f'ffmpeg -y -loglevel warning -i unsanitized.mp4 -map_metadata -1 -c:v copy -c:a copy video.mp4') + with open("video.mp4", 'rb') as f: + try: req = requests.request("POST", "https://pomf2.lain.la/upload.php", files={'files[]': f}, timeout=5).json() + except requests.Timeout: return {"error": "Video upload timed out, please try again!"} + try: url = req['files'][0]['url'] + except: return {"error": req['description']}, 400 + body += f"\n\n{url}" + else: return {"error": "Image/Video files only"}, 400 + + if v.agendaposter and not v.marseyawarded and parent_post.id not in ADMIGGERS: + body = torture_ap(body, v.username) + + body_html = sanitize(body, comment=True) + + + if parent_post.id not in ADMIGGERS and '!slots' not in body.lower() and '!blackjack' not in body.lower() and '!wordle' not in body.lower() and AGENDAPOSTER_PHRASE not in body.lower(): + existing = g.db.query(Comment.id).filter(Comment.author_id == v.id, + Comment.deleted_utc == 0, + Comment.parent_comment_id == parent_comment_id, + Comment.parent_submission == parent_submission, + Comment.body_html == body_html + ).one_or_none() + if existing: return {"error": f"You already made that comment: /comment/{existing.id}"}, 409 + + if parent.author.any_block_exists(v) and v.admin_level < 2: + return {"error": "You can't reply to users who have blocked you, or users you have blocked."}, 403 + + is_bot = bool(request.headers.get("Authorization")) or (SITE == 'pcmemes.net' and v.id == SNAPPY_ID) + + if '!slots' not in body.lower() and '!blackjack' not in body.lower() and '!wordle' not in body.lower() and parent_post.id not in ADMIGGERS and not is_bot and not v.marseyawarded and AGENDAPOSTER_PHRASE not in body.lower() and len(body) > 10: + now = int(time.time()) + cutoff = now - 60 * 60 * 24 + + similar_comments = g.db.query(Comment).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 banned for **1 day** for the following reason:\n\n> Too much spam!" + send_repeatable_notification(v.id, text) + + v.ban(reason="Spamming.", + days=1) + + for comment in similar_comments: + comment.is_banned = True + comment.ban_reason = "AutoJanny" + g.db.add(comment) + ma=ModAction( + user_id=AUTOJANNY_ID, + target_comment_id=comment.id, + kind="ban_comment", + _note="spam" + ) + g.db.add(ma) + + return {"error": "Too much spam!"}, 403 + + 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], + ghost=parent_post.ghost + ) + + c.upvotes = 1 + g.db.add(c) + g.db.flush() + + if blackjack and any(i in c.body.lower() for i in blackjack.split()): + v.shadowbanned = 'AutoJanny' + notif = Notification(comment_id=c.id, user_id=CARP_ID) + g.db.add(notif) + + if c.level == 1: c.top_comment_id = c.id + else: c.top_comment_id = parent.top_comment_id + + for option in options: + c_option = Comment(author_id=AUTOPOLLER_ID, + parent_submission=parent_submission, + parent_comment_id=c.id, + level=level+1, + body_html=filter_emojis_only(option), + upvotes=0, + is_bot=True + ) + + g.db.add(c_option) + + for choice in choices: + c_choice = Comment(author_id=AUTOCHOICE_ID, + parent_submission=parent_submission, + parent_comment_id=c.id, + level=level+1, + body_html=filter_emojis_only(choice), + upvotes=0, + is_bot=True + ) + + g.db.add(c_choice) + + if request.host == 'pcmemes.net' and c.body.lower().startswith("based"): + pill = based_regex.match(body) + + if level == 1: basedguy = get_account(parent_post.author_id) + else: basedguy = get_account(c.parent_comment.author_id) + basedguy.basedcount += 1 + if pill: + if basedguy.pills: basedguy.pills += f", {pill.group(1)}" + else: basedguy.pills += f"{pill.group(1)}" + g.db.add(basedguy) + + body2 = f"@{basedguy.username}'s Based Count has increased by 1. Their Based Count is now {basedguy.basedcount}." + if basedguy.pills: body2 += f"\n\nPills: {basedguy.pills}" + + body_based_html = sanitize(body2) + + c_based = Comment(author_id=BASEDBOT_ID, + parent_submission=parent_submission, + distinguish_level=6, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_based_html, + top_comment_id=c.top_comment_id, + ghost=parent_post.ghost + ) + + g.db.add(c_based) + g.db.flush() + + n = Notification(comment_id=c_based.id, user_id=v.id) + g.db.add(n) + + if parent_post.id not in ADMIGGERS: + if v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower(): + + c.is_banned = True + c.ban_reason = "AutoJanny" + + g.db.add(c) + + + body = AGENDAPOSTER_MSG.format(username=v.username, type='comment', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) + + body_jannied_html = sanitize(body) + + + + c_jannied = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=parent_submission, + distinguish_level=6, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_jannied_html, + top_comment_id=c.top_comment_id, + ghost=parent_post.ghost + ) + + g.db.add(c_jannied) + g.db.flush() + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + + if SITE_NAME == 'rDrama' and len(c.body.split()) >= 200 and "<" not in body and "" not in body_html: + + body = random.choice(LONGPOST_REPLIES) + + + if body.startswith('▼'): + body = body[1:] + vote = CommentVote(user_id=LONGPOSTBOT_ID, + vote_type=-1, + comment_id=c.id, + real = True + ) + g.db.add(vote) + c.downvotes = 1 + + + + body_html2 = sanitize(body) + + c2 = Comment(author_id=LONGPOSTBOT_ID, + parent_submission=parent_submission, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_html2, + top_comment_id=c.top_comment_id, + ghost=parent_post.ghost + ) + + g.db.add(c2) + + longpostbot = g.db.query(User).filter_by(id = LONGPOSTBOT_ID).one_or_none() + longpostbot.comment_count += 1 + longpostbot.coins += 1 + g.db.add(longpostbot) + + g.db.flush() + + n = Notification(comment_id=c2.id, user_id=v.id) + g.db.add(n) + + + if SITE_NAME == 'rDrama' and random.random() < 0.001: + + body = "zoz" + body_html2 = sanitize(body) + + + + + c2 = Comment(author_id=ZOZBOT_ID, + parent_submission=parent_submission, + parent_comment_id=c.id, + level=level+1, + is_bot=True, + body_html=body_html2, + top_comment_id=c.top_comment_id, + ghost=parent_post.ghost, + distinguish_level=6 + ) + + g.db.add(c2) + g.db.flush() + n = Notification(comment_id=c2.id, user_id=v.id) + g.db.add(n) + + + + + + body = "zle" + body_html2 = sanitize(body) + + + + c3 = Comment(author_id=ZOZBOT_ID, + parent_submission=parent_submission, + parent_comment_id=c2.id, + level=level+2, + is_bot=True, + body_html=body_html2, + top_comment_id=c.top_comment_id, + ghost=parent_post.ghost, + distinguish_level=6 + ) + + g.db.add(c3) + g.db.flush() + + body = "zozzle" + body_html2 = sanitize(body) + + + c4 = Comment(author_id=ZOZBOT_ID, + parent_submission=parent_submission, + parent_comment_id=c3.id, + level=level+3, + is_bot=True, + body_html=body_html2, + top_comment_id=c.top_comment_id, + ghost=parent_post.ghost, + distinguish_level=6 + ) + + g.db.add(c4) + + zozbot = g.db.query(User).filter_by(id = ZOZBOT_ID).one_or_none() + zozbot.comment_count += 3 + zozbot.coins += 3 + g.db.add(zozbot) + + + if not v.shadowbanned: + notify_users = NOTIFY_USERS(body, v) + + for x in g.db.query(Subscription.user_id).filter_by(submission_id=c.parent_submission).all(): notify_users.add(x[0]) + + if parent.author.id not in (v.id, BASEDBOT_ID, AUTOJANNY_ID, SNAPPY_ID, LONGPOSTBOT_ID, ZOZBOT_ID, AUTOPOLLER_ID, AUTOCHOICE_ID): + notify_users.add(parent.author.id) + + for x in notify_users: + n = Notification(comment_id=c.id, user_id=x) + g.db.add(n) + + if parent.author.id != v.id and PUSHER_ID != 'blahblahblah' and not v.shadowbanned: + try: gevent.spawn(pusher_thread, f'{request.host}{parent.author.id}', c, c.author_name) + except: pass + + + + 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).filter(Comment.author_id == v.id, Comment.parent_submission != None).filter_by(is_banned=False, deleted_utc=0).count() + g.db.add(v) + + c.voted = 1 + + if v.id == PIZZASHILL_ID: + for uid in PIZZA_VOTERS: + autovote = CommentVote(user_id=uid, comment_id=c.id, vote_type=1) + g.db.add(autovote) + v.coins += 3 + v.truecoins += 3 + g.db.add(v) + c.upvotes += 3 + g.db.add(c) + + if not v.rehab: + check_for_slots_command(body, v, c) + + check_for_blackjack_commands(body, v, c) + + if not c.slots_result and not c.blackjack_result and v.marseyawarded and parent_post.id not in ADMIGGERS and marseyaward_body_regex.search(body_html): + return {"error":"You can only type marseys!"}, 403 + + check_for_treasure(body, c) + + if "!wordle" in body: + answer = random.choice(WORDLE_LIST) + c.wordle_result = f'_active_{answer}' + + if not c.slots_result and not c.blackjack_result and not c.wordle_result and not rts: + parent_post.comment_count += 1 + g.db.add(parent_post) + + g.db.commit() + + if request.headers.get("Authorization"): return c.json + return {"comment": render_template("comments.html", v=v, comments=[c], ajax=True)} + + + +@app.post("/edit_comment/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def edit_comment(cid, v): + + c = get_comment(cid, v=v) + + if c.author_id != v.id: abort(403) + + body = request.values.get("body", "").strip()[:10000] + + if len(body) < 1 and not (request.files.get("file") and request.headers.get("cf-ipcountry") != "T1"): + return {"error":"You have to actually type something!"}, 400 + + if body != c.body or request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): + return {"error":"You have to type more than 280 characters!"}, 403 + elif v.bird and len(body) > 140: + return {"error":"You have to type less than 140 characters!"}, 403 + + if v.agendaposter and not v.marseyawarded: + body = torture_ap(body, v.username) + + for i in poll_regex.finditer(body): + body = body.replace(i.group(0), "") + c_option = Comment(author_id=AUTOPOLLER_ID, + parent_submission=c.parent_submission, + parent_comment_id=c.id, + level=c.level+1, + body_html=filter_emojis_only(i.group(1)), + upvotes=0, + is_bot=True + ) + g.db.add(c_option) + + for i in choice_regex.finditer(body): + body = body.replace(i.group(0), "") + c_choice = Comment(author_id=AUTOCHOICE_ID, + parent_submission=c.parent_submission, + parent_comment_id=c.id, + level=c.level+1, + body_html=filter_emojis_only(i.group(1)), + upvotes=0, + is_bot=True + ) + g.db.add(c_choice) + + body_html = sanitize(body, edit=True) + + if '!slots' not in body.lower() and '!blackjack' not in body.lower() and '!wordle' not in body.lower() and AGENDAPOSTER_PHRASE not in body.lower(): + now = int(time.time()) + cutoff = now - 60 * 60 * 24 + + similar_comments = g.db.query(Comment + ).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 banned for **1 day** for the following reason:\n\n> Too much spam!" + send_repeatable_notification(v.id, text) + + v.ban(reason="Spamming.", + days=1) + + for comment in similar_comments: + comment.is_banned = True + comment.ban_reason = "AutoJanny" + g.db.add(comment) + + return {"error": "Too much spam!"}, 403 + + if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + files = request.files.getlist('file')[:4] + for file in files: + if file.content_type.startswith('image/'): + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + url = process_image(v.patron, name) + body += f"\n\n![]({url})" + elif file.content_type.startswith('video/'): + if file.content_type == 'video/webm': + file.save("video.mp4") + else: + file.save("unsanitized.mp4") + os.system(f'ffmpeg -y -loglevel warning -i unsanitized.mp4 -map_metadata -1 -c:v copy -c:a copy video.mp4') + with open("video.mp4", 'rb') as f: + try: req = requests.request("POST", "https://pomf2.lain.la/upload.php", files={'files[]': f}, timeout=5).json() + except requests.Timeout: return {"error": "Video upload timed out, please try again!"} + try: url = req['files'][0]['url'] + except: return {"error": req['description']}, 400 + body += f"\n\n{url}" + else: return {"error": "Image/Video files only"}, 400 + + body_html = sanitize(body, edit=True) + + if len(body_html) > 20000: abort(400) + + if v.marseyawarded and marseyaward_body_regex.search(body_html): + return {"error":"You can only type marseys!"}, 403 + + c.body = body[:10000] + c.body_html = body_html + + if blackjack and any(i in c.body.lower() for i in blackjack.split()): + v.shadowbanned = 'AutoJanny' + g.db.add(v) + notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=CARP_ID).one_or_none() + if not notif: + notif = Notification(comment_id=c.id, user_id=CARP_ID) + g.db.add(notif) + + if v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in c.body.lower() and not c.is_banned: + + c.is_banned = True + c.ban_reason = "AutoJanny" + + g.db.add(c) + + + body = AGENDAPOSTER_MSG.format(username=v.username, type='comment', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) + + body_jannied_html = sanitize(body) + + + + c_jannied = Comment(author_id=NOTIFICATIONS_ID, + 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, + top_comment_id=c.top_comment_id, + ghost=c.ghost + ) + + 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) + + notify_users = NOTIFY_USERS(body, v) + + for x in notify_users: + notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=x).one_or_none() + if not notif: + n = Notification(comment_id=c.id, user_id=x) + g.db.add(n) + + g.db.commit() + + return {"comment": c.realbody(v)} + + +@app.post("/delete/comment/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def delete_comment(cid, v): + + c = get_comment(cid, v=v) + + if not c.deleted_utc: + + if 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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def undelete_comment(cid, v): + + c = get_comment(cid, v=v) + + if c.deleted_utc: + if 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 +def pin_comment(cid, v): + + comment = get_comment(cid, v=v) + + if not comment.is_pinned: + if v.id != comment.post.author_id: abort(403) + + if comment.post.ghost: comment.is_pinned = "(OP)" + else: comment.is_pinned = v.username + " (OP)" + + g.db.add(comment) + + if v.id != comment.author_id: + if comment.post.ghost: message = f"OP has pinned your [comment]({comment.shortlink})!" + else: message = f"@{v.username} (OP) has pinned your [comment]({comment.shortlink})!" + send_repeatable_notification(comment.author_id, message) + + g.db.commit() + return {"message": "Comment pinned!"} + + +@app.post("/unpin_comment/") +@auth_required +def unpin_comment(cid, v): + + comment = get_comment(cid, v=v) + + if comment.is_pinned: + if v.id != comment.post.author_id: abort(403) + + if not comment.is_pinned.endswith(" (OP)"): + return {"error": "You can only unpin comments you have pinned!"} + + comment.is_pinned = None + g.db.add(comment) + + if v.id != comment.author_id: + message = f"@{v.username} (OP) has unpinned your [comment]({comment.shortlink})!" + send_repeatable_notification(comment.author_id, message) + g.db.commit() + return {"message": "Comment unpinned!"} + + +@app.post("/mod_pin/") +@auth_required +def mod_pin(cid, v): + + comment = get_comment(cid, v=v) + + if not comment.is_pinned: + if not (comment.post.sub and v.mods(comment.post.sub)): abort(403) + + comment.is_pinned = v.username + " (Mod)" + + g.db.add(comment) + + if v.id != comment.author_id: + message = f"@{v.username} (Mod) has pinned your [comment]({comment.shortlink})!" + send_repeatable_notification(comment.author_id, message) + + g.db.commit() + return {"message": "Comment pinned!"} + + +@app.post("/unmod_pin/") +@auth_required +def mod_unpin(cid, v): + + comment = get_comment(cid, v=v) + + if comment.is_pinned: + if not (comment.post.sub and v.mods(comment.post.sub)): abort(403) + + comment.is_pinned = None + g.db.add(comment) + + if v.id != comment.author_id: + message = f"@{v.username} (Mod) has unpinned your [comment]({comment.shortlink})!" + send_repeatable_notification(comment.author_id, message) + g.db.commit() + return {"message": "Comment unpinned!"} + + +@app.post("/save_comment/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def save_comment(cid, v): + + comment=get_comment(cid) + + save=g.db.query(CommentSaveRelationship).filter_by(user_id=v.id, comment_id=comment.id).one_or_none() + + if not save: + new_save=CommentSaveRelationship(user_id=v.id, comment_id=comment.id) + g.db.add(new_save) + + g.db.commit() + + return {"message": "Comment saved!"} + +@app.post("/unsave_comment/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def unsave_comment(cid, v): + + comment=get_comment(cid) + + save=g.db.query(CommentSaveRelationship).filter_by(user_id=v.id, comment_id=comment.id).one_or_none() + + if save: + g.db.delete(save) + g.db.commit() + + return {"message": "Comment unsaved!"} + +@app.post("/blackjack/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def handle_blackjack_action(cid, v): + comment = get_comment(cid) + if 'active' in comment.blackjack_result: + try: action = request.values.get("thing").strip().lower() + except: abort(400) + + if action == 'hit': player_hit(comment) + elif action == 'stay': player_stayed(comment) + elif action == 'doubledown': player_doubled_down(comment) + elif action == 'insurance': player_bought_insurance(comment) + + g.db.add(comment) + g.db.add(v) + g.db.commit() + return {"response" : comment.blackjack_html(v)} + + +def diff_words(answer, guess): + """ + Return a list of numbers corresponding to the char's relevance. + -1 means char is not in solution or the character appears too many times in the guess + 0 means char is in solution but in the wrong spot + 1 means char is in the correct spot + """ + diffs = [ + 1 if cs == cg else -1 for cs, cg in zip(answer, guess) + ] + char_freq = Counter( + c_guess for c_guess, diff, in zip(answer, diffs) if diff == -1 + ) + for i, cg in enumerate(guess): + if diffs[i] == -1 and cg in char_freq and char_freq[cg] > 0: + char_freq[cg] -= 1 + diffs[i] = 0 + return diffs + + +@app.post("/wordle/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def handle_wordle_action(cid, v): + + comment = get_comment(cid) + + guesses, status, answer = comment.wordle_result.split("_") + count = len(guesses.split(" -> ")) + + try: guess = request.values.get("thing").strip().lower() + except: abort(400) + + if len(guess) != 5 or not d.check(guess) and guess not in WORDLE_LIST: + return {"error": "Not a valid guess!"}, 400 + + if status == "active": + guesses += "".join(cg + WORDLE_COLOR_MAPPINGS[diff] for cg, diff in zip(guess, diff_words(answer, guess))) + + if (guess == answer): status = "won" + elif (count == 6): status = "lost" + else: guesses += ' -> ' + + comment.wordle_result = f'{guesses}_{status}_{answer}' + + g.db.add(comment) + g.db.commit() + return {"response" : comment.wordle_html(v)} \ No newline at end of file diff --git a/files/routes/discord.py b/files/routes/discord.py index 2160e6ede..25215499f 100644 --- a/files/routes/discord.py +++ b/files/routes/discord.py @@ -1,141 +1,141 @@ -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() -DISCORD_ENDPOINT = "https://discordapp.com/api/v6" -WELCOME_CHANNEL="846509313941700618" - -@app.get("/discord") -@is_not_permabanned -def join_discord(v): - - if v.shadowbanned: return {"error": "Internal server error"} - - if SITE_NAME == 'rDrama' and v.admin_level < 2 and v.patron == 0 and v.truecoins < 150: - return "You must receive 150 upvotes/downvotes from other users before being able to join the Discord server." - - 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, timeout=5) - - x=x.json() - - - token=x["access_token"] - - - url="https://discord.com/api/users/@me" - headers={ - 'Authorization': f"Bearer {token}" - } - x=requests.get(url, headers=headers, timeout=5) - - 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, timeout=5) - - if g.db.query(User).filter(User.id!=v.id, User.discord_id==x["id"]).one_or_none(): - 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, timeout=5) - - if x.status_code in {201, 204}: - - if v.admin_level > 2: - add_role(v, "owner") - time.sleep(0.1) - - if v.admin_level > 1: 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, timeout=5) - - 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() +DISCORD_ENDPOINT = "https://discordapp.com/api/v6" +WELCOME_CHANNEL="846509313941700618" + +@app.get("/discord") +@is_not_permabanned +def join_discord(v): + + if v.shadowbanned: return {"error": "Internal server error"} + + if SITE_NAME == 'rDrama' and v.admin_level < 2 and v.patron == 0 and v.truecoins < 150: + return "You must receive 150 upvotes/downvotes from other users before being able to join the Discord server." + + 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, timeout=5) + + x=x.json() + + + token=x["access_token"] + + + url="https://discord.com/api/users/@me" + headers={ + 'Authorization': f"Bearer {token}" + } + x=requests.get(url, headers=headers, timeout=5) + + 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, timeout=5) + + if g.db.query(User).filter(User.id!=v.id, User.discord_id==x["id"]).one_or_none(): + 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, timeout=5) + + if x.status_code in {201, 204}: + + if v.admin_level > 2: + add_role(v, "owner") + time.sleep(0.1) + + if v.admin_level > 1: 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, timeout=5) + + 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 index 6ef59640a..8a3024578 100644 --- a/files/routes/errors.py +++ b/files/routes/errors.py @@ -1,76 +1,76 @@ -from files.helpers.wrappers import * -from flask import * -from urllib.parse import quote, urlencode -import time -from files.__main__ import app, limiter - - - -@app.errorhandler(400) -def error_400(e): - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "400 Bad Request"}, 400 - else: return render_template('errors/400.html', err=True), 400 - -@app.errorhandler(401) -def error_401(e): - - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "401 Not Authorized"}, 401 - else: - path = request.path - qs = urlencode(dict(request.values)) - argval = quote(f"{path}?{qs}", safe='') - return redirect(f"/login?redirect={argval}") - -@app.errorhandler(403) -def error_403(e): - - description = e.description - if description == "You don't have the permission to access the requested resource. It is either read-protected or not readable by the server.": description = '' - - if request.headers.get("Authorization") or request.headers.get("xhr"): - if not description: description = "403 Forbidden" - return {"error": description}, 403 - else: - if not description: description = "YOU AREN'T WELCOME HERE GO AWAY" - return render_template('errors/403.html', description=description, err=True), 403 - - -@app.errorhandler(404) -def error_404(e): - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "404 Not Found"}, 404 - else: return render_template('errors/404.html', err=True), 404 - -@app.errorhandler(405) -def error_405(e): - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "405 Method Not Allowed"}, 405 - else: return render_template('errors/405.html', err=True), 405 - -@app.errorhandler(413) -def error_413(e): - return {"error": "Max file size is 8 MB (16 MB for paypigs)"}, 413 - if request.headers.get("Authorization") or request.headers.get("xhr"): - return {"error": "Max file size is 8 MB (16 MB for paypigs)"}, 413 - else: return render_template('errors/413.html', err=True), 413 - -@app.errorhandler(429) -def error_429(e): - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "429 Too Many Requests"}, 429 - else: return render_template('errors/429.html', err=True), 429 - - -@app.errorhandler(500) -def error_500(e): - g.db.rollback() - - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "500 Internal Server Error"}, 500 - else: return render_template('errors/500.html', err=True), 500 - - -@app.post("/allow_nsfw") -def allow_nsfw(): - session["over_18"] = int(time.time()) + 3600 - redir = request.values.get("redir") - if redir: - if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) - if redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') +from files.helpers.wrappers import * +from flask import * +from urllib.parse import quote, urlencode +import time +from files.__main__ import app, limiter + + + +@app.errorhandler(400) +def error_400(e): + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "400 Bad Request"}, 400 + else: return render_template('errors/400.html', err=True), 400 + +@app.errorhandler(401) +def error_401(e): + + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "401 Not Authorized"}, 401 + else: + path = request.path + qs = urlencode(dict(request.values)) + argval = quote(f"{path}?{qs}", safe='') + return redirect(f"/login?redirect={argval}") + +@app.errorhandler(403) +def error_403(e): + + description = e.description + if description == "You don't have the permission to access the requested resource. It is either read-protected or not readable by the server.": description = '' + + if request.headers.get("Authorization") or request.headers.get("xhr"): + if not description: description = "403 Forbidden" + return {"error": description}, 403 + else: + if not description: description = "YOU AREN'T WELCOME HERE GO AWAY" + return render_template('errors/403.html', description=description, err=True), 403 + + +@app.errorhandler(404) +def error_404(e): + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "404 Not Found"}, 404 + else: return render_template('errors/404.html', err=True), 404 + +@app.errorhandler(405) +def error_405(e): + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "405 Method Not Allowed"}, 405 + else: return render_template('errors/405.html', err=True), 405 + +@app.errorhandler(413) +def error_413(e): + return {"error": "Max image size is 8 MB (16 MB for paypigs)"}, 413 + if request.headers.get("Authorization") or request.headers.get("xhr"): + return {"error": "Max image size is 8 MB (16 MB for paypigs)"}, 413 + else: return render_template('errors/413.html', err=True), 413 + +@app.errorhandler(429) +def error_429(e): + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "429 Too Many Requests"}, 429 + else: return render_template('errors/429.html', err=True), 429 + + +@app.errorhandler(500) +def error_500(e): + g.db.rollback() + + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "500 Internal Server Error"}, 500 + else: return render_template('errors/500.html', err=True), 500 + + +@app.post("/allow_nsfw") +def allow_nsfw(): + session["over_18"] = int(time.time()) + 3600 + redir = request.values.get("redir") + if redir: + if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) + if redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') return redirect('/') \ No newline at end of file diff --git a/files/routes/feeds.py b/files/routes/feeds.py index 18cff9030..2d97ef0d2 100644 --- a/files/routes/feeds.py +++ b/files/routes/feeds.py @@ -1,69 +1,70 @@ -import html -from .front import frontlist -from datetime import datetime -from files.helpers.get import * -from yattag import Doc -from files.helpers.wrappers import * -from files.helpers.jinja2 import * - -from files.__main__ import app - -@app.get('/rss//') -@auth_required -def feeds_user(v=None, sort='hot', t='all'): - - try: page = max(int(request.values.get("page", 1)), 1) - except: 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=SITE_FULL + request.full_path) - doc.stag("link", href=SITE_FULL) - - for post in posts: - with tag("entry", ("xml:base", SITE_FULL + request.full_path)): - with tag("title", type="text"): - text(post.realtitle(None)) - - with tag("id"): - text(post.fullname) - - if (post.edited_utc): - 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_name) - with tag("uri"): - text(f'/@{post.author_name}') - - doc.stag("link", href=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): - with tag("content", type="html"): - doc.cdata(f'''{post.realtitle(None)}
{post.realbody(None)}''') - +import html +from .front import frontlist +from datetime import datetime +from files.helpers.get import * +from yattag import Doc +from files.helpers.wrappers import * +from files.helpers.jinja2 import * + +from files.__main__ import app + +@app.get('/rss') +@app.get('/feed') +@app.get('/rss//') +def feeds_user(sort='hot', t='all'): + + try: page = max(int(request.values.get("page", 1)), 1) + except: 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=SITE_FULL + request.full_path) + doc.stag("link", href=SITE_FULL) + + for post in posts: + with tag("entry", ("xml:base", SITE_FULL + request.full_path)): + with tag("title", type="text"): + text(post.realtitle(None)) + + with tag("id"): + text(post.fullname) + + if (post.edited_utc): + 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_name) + with tag("uri"): + text(f'/@{post.author_name}') + + doc.stag("link", href=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): + with tag("content", type="html"): + doc.cdata(f'''{post.realtitle(None)}
{post.realbody(None)}''') + 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 index 91c81b64a..93111f6ca 100644 --- a/files/routes/front.py +++ b/files/routes/front.py @@ -1,599 +1,607 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from files.helpers.discord import * -from files.__main__ import app, cache, limiter -from files.classes.submission import Submission - -defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip() - -@app.post("/clear") -@auth_required -def clear(v): - notifs = g.db.query(Notification).join(Comment, Notification.comment_id == Comment.id).filter(Notification.read == False, Notification.user_id == v.id).all() - for n in notifs: - n.read = True - g.db.add(n) - g.db.commit() - return {"message": "Notifications cleared!"} - -@app.get("/unread") -@auth_required -def unread(v): - listing = g.db.query(Notification, Comment).join(Comment, Notification.comment_id == Comment.id).filter( - Notification.read == False, - Notification.user_id == v.id, - Comment.is_banned == False, - Comment.deleted_utc == 0, - Comment.author_id != AUTOJANNY_ID, - ).order_by(Notification.created_utc.desc()).all() - - for n, c in listing: - n.read = True - g.db.add(n) - g.db.commit() - - return {"data":[x[1].json for x in listing]} - - -@app.get("/notifications") -@auth_required -def notifications(v): - try: page = max(int(request.values.get("page", 1)), 1) - except: page = 1 - - messages = request.values.get('messages') - modmail = request.values.get('modmail') - posts = request.values.get('posts') - reddit = request.values.get('reddit') - if modmail and v.admin_level > 1: - comments = g.db.query(Comment).filter(Comment.sentto==2).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all() - next_exists = (len(comments) > 25) - listing = comments[:25] - elif messages: - if v and (v.shadowbanned or v.admin_level > 2): - comments = g.db.query(Comment).filter(Comment.sentto != None, or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None, Comment.level == 1).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all() - else: - comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.sentto != None, or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None, Comment.level == 1).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all() - - next_exists = (len(comments) > 25) - listing = comments[:25] - elif posts: - notifications = g.db.query(Notification, Comment).join(Comment, Notification.comment_id == Comment.id).filter(Notification.user_id == v.id, Comment.author_id == AUTOJANNY_ID).order_by(Notification.created_utc.desc()).offset(25 * (page - 1)).limit(101).all() - - listing = [] - - for index, x in enumerate(notifications[:100]): - n, c = x - if n.read and index > 24: break - elif not n.read: - n.read = True - c.unread = True - g.db.add(n) - if n.created_utc > 1620391248: c.notif_utc = n.created_utc - listing.append(c) - - g.db.commit() - - next_exists = (len(notifications) > len(listing)) - elif reddit: - notifications = g.db.query(Notification, Comment).join(Comment, Notification.comment_id == Comment.id).filter(Notification.user_id == v.id, Comment.body_html.like('%

New site mention: 2)): - comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) - - comments = comments.offset(25 * (page - 1)).limit(26).all() - - next_exists = (len(comments) > 25) - comments = comments[:25] - - cids = [x[0].id for x in comments] - - comms = get_comments(cids, v=v) - - listing = [] - for c, n in comments: - if n.created_utc > 1620391248: c.notif_utc = n.created_utc - if not n.read: - n.read = True - c.unread = True - g.db.add(n) - - if c.parent_submission: - if c.replies2 == None: - c.replies2 = c.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all() - for x in c.replies2: - if x.replies2 == None: x.replies2 = [] - count = 0 - while count < 50 and c.parent_comment and (c.parent_comment.author_id == v.id or c.parent_comment.id in cids): - count += 1 - c = c.parent_comment - if c.replies2 == None: - c.replies2 = c.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all() - for x in c.replies2: - if x.replies2 == None: - x.replies2 = x.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all() - else: - while c.parent_comment: - c = c.parent_comment - c.replies2 = g.db.query(Comment).filter_by(parent_comment_id=c.id).order_by(Comment.id).all() - - if c not in listing: listing.append(c) - - g.db.commit() - - if request.headers.get("Authorization"): return {"data":[x.json for x in listing]} - - return render_template("notifications.html", - v=v, - notifications=listing, - next_exists=next_exists, - page=page, - standalone=True, - render_replies=True - ) - - -@app.get("/") -@app.get("/catalog") -@app.get("/h/") -@app.get("/s/") -@limiter.limit("3/second;30/minute;1000/hour;5000/day") -@auth_desired -def front_all(v, sub=None, subdomain=None): - if sub: sub = g.db.query(Sub).filter_by(name=sub.strip().lower()).one_or_none() - - if (request.path.startswith('/h/') or request.path.startswith('/s/')) and not sub: abort(404) - - if g.webview and not session.get("session_id"): - session["session_id"] = secrets.token_hex(49) - - try: page = max(int(request.values.get("page", 1)), 1) - except: abort(400) - - if v: - defaultsorting = v.defaultsorting - if sub or SITE_NAME != 'rDrama': defaulttime = 'all' - else: defaulttime = v.defaulttime - else: - defaultsorting = "hot" - if sub or SITE_NAME != 'rDrama': defaulttime = 'all' - else: defaulttime = defaulttimefilter - - sort=request.values.get("sort", defaultsorting) - t=request.values.get('t', defaulttime) - ccmode=request.values.get('ccmode', "false").lower() - - if sort == 'bump': t='all' - - try: gt=int(request.values.get("after", 0)) - except: gt=0 - - try: lt=int(request.values.get("before", 0)) - except: lt=0 - - ids, next_exists = frontlist(sort=sort, - page=page, - t=t, - v=v, - ccmode=ccmode, - filter_words=v.filter_words if v else [], - gt=gt, - lt=lt, - sub=sub, - site=SITE - ) - - posts = get_posts(ids, v=v) - - if v: - if v.hidevotedon: posts = [x for x in posts if not hasattr(x, 'voted') or not x.voted] - - - if v.patron_utc and v.patron_utc < time.time(): - v.patron = 0 - v.patron_utc = 0 - send_repeatable_notification(v.id, "Your paypig status has expired!") - if v.discord_id: remove_role(v, "1") - g.db.add(v) - g.db.commit() - - if v.unban_utc and v.unban_utc < time.time(): - v.is_banned = 0 - v.unban_utc = 0 - v.ban_evade = 0 - send_repeatable_notification(v.id, "You have been unbanned!") - g.db.add(v) - g.db.commit() - - if v.agendaposter and v.agendaposter < time.time(): - v.agendaposter = 0 - send_repeatable_notification(v.id, "Your chud theme has expired!") - g.db.add(v) - badge = v.has_badge(28) - if badge: g.db.delete(badge) - g.db.commit() - - if v.flairchanged and v.flairchanged < time.time(): - v.flairchanged = None - send_repeatable_notification(v.id, "Your flair lock has expired. You can now change your flair!") - g.db.add(v) - badge = v.has_badge(96) - if badge: g.db.delete(badge) - g.db.commit() - - if v.marseyawarded and v.marseyawarded < time.time(): - v.marseyawarded = None - send_repeatable_notification(v.id, "Your marsey award has expired!") - g.db.add(v) - badge = v.has_badge(98) - if badge: g.db.delete(badge) - g.db.commit() - - if v.longpost and v.longpost < time.time(): - v.longpost = None - send_repeatable_notification(v.id, "Your pizzashill award has expired!") - g.db.add(v) - badge = v.has_badge(97) - if badge: g.db.delete(badge) - g.db.commit() - - if v.bird and v.bird < time.time(): - v.bird = None - send_repeatable_notification(v.id, "Your bird site award has expired!") - g.db.add(v) - badge = v.has_badge(95) - if badge: g.db.delete(badge) - g.db.commit() - - if v.progressivestack and v.progressivestack < time.time(): - v.progressivestack = None - send_repeatable_notification(v.id, "Your progressive stack has expired!") - g.db.add(v) - badge = v.has_badge(94) - if badge: g.db.delete(badge) - g.db.commit() - - if v.rehab and v.rehab < time.time(): - v.rehab = None - send_repeatable_notification(v.id, "Your rehab has finished!") - g.db.add(v) - badge = v.has_badge(109) - if badge: g.db.delete(badge) - g.db.commit() - - if v.deflector and v.deflector < time.time(): - v.deflector = None - send_repeatable_notification(v.id, "Your deflector has expired!") - g.db.add(v) - g.db.commit() - - if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists} - return render_template("home.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page, ccmode=ccmode, sub=sub, home=True) - - - -@cache.memoize(timeout=86400) -def frontlist(v=None, sort="hot", page=1, t="all", ids_only=True, ccmode="false", filter_words='', gt=0, lt=0, sub=None, site=None): - - posts = g.db.query(Submission) - - if v and v.hidevotedon: - voted = [x[0] for x in g.db.query(Vote.submission_id).filter_by(user_id=v.id).all()] - posts = posts.filter(Submission.id.notin_(voted)) - - if sub: posts = posts.filter_by(sub=sub.name) - elif v: posts = posts.filter(or_(Submission.sub == None, Submission.sub.notin_(v.all_blocks))) - - if gt: posts = posts.filter(Submission.created_utc > gt) - if lt: posts = posts.filter(Submission.created_utc < lt) - - if not gt and not lt: - if t == 'all': cutoff = 0 - else: - 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) - - if (ccmode == "true"): - posts = posts.filter(Submission.club == True) - - posts = posts.filter_by(is_banned=False, private=False, deleted_utc = 0) - - if (sort == "hot" or (v and v.id == Q_ID)) and ccmode == "false" and not gt and not lt: - posts = posts.filter_by(stickied=None) - - if v and v.admin_level < 2: - posts = posts.filter(Submission.author_id.notin_(v.userblocks)) - - if not (v and v.changelogsub): - posts=posts.filter(not_(Submission.title.ilike('[changelog]%'))) - - if v and filter_words: - for word in filter_words: - word = word.replace('\\', '').replace('_', '\_').replace('%', '\%').strip() - posts=posts.filter(not_(Submission.title.ilike(f'%{word}%'))) - - if not (v and v.shadowbanned): - posts = posts.join(User, User.id == Submission.author_id).filter(User.shadowbanned == None) - - if request.host == 'rdrama.net': num = 5 - else: num = 1 - - if sort == "hot": - ti = int(time.time()) + 3600 - posts = posts.order_by(-1000000*(Submission.realupvotes + 1 + Submission.comment_count/num + (func.length(Submission.body_html)-func.length(func.replace(Submission.body_html,'','')))/4)/(func.power(((ti - Submission.created_utc)/1000), 1.23)), Submission.created_utc.desc()) - elif sort == "bump": - posts = posts.filter(Submission.comment_count > 1).order_by(Submission.bump_utc.desc(), Submission.created_utc.desc()) - elif sort == "new": - posts = posts.order_by(Submission.created_utc.desc()) - elif sort == "old": - posts = posts.order_by(Submission.created_utc) - elif sort == "controversial": - posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc(), Submission.created_utc.desc()) - elif sort == "top": - posts = posts.order_by(Submission.downvotes - Submission.upvotes, Submission.created_utc.desc()) - elif sort == "bottom": - posts = posts.order_by(Submission.upvotes - Submission.downvotes, Submission.created_utc.desc()) - elif sort == "comments": - posts = posts.order_by(Submission.comment_count.desc(), Submission.created_utc.desc()) - - if v: size = v.frontsize or 0 - else: size = 25 - - posts = posts.offset(size * (page - 1)).limit(size+1).all() - - next_exists = (len(posts) > size) - - posts = posts[:size] - - if (sort == "hot" or (v and v.id == Q_ID)) and page == 1 and ccmode == "false" and not gt and not lt: - pins = g.db.query(Submission).filter(Submission.stickied != None, Submission.is_banned == False) - if sub: pins = pins.filter_by(sub=sub.name) - elif v: - pins = pins.filter(or_(Submission.sub == None, Submission.sub.notin_(v.all_blocks))) - if v.admin_level < 2: - pins = pins.filter(Submission.author_id.notin_(v.userblocks)) - - pins = pins.all() - - for pin in pins: - if pin.stickied_utc and int(time.time()) > pin.stickied_utc: - pin.stickied = None - pin.stickied_utc = None - g.db.add(pin) - pins.remove(pin) - - posts = pins + posts - - if ids_only: posts = [x.id for x in posts] - - g.db.commit() - - return posts, next_exists - - -@app.get("/changelog") -@auth_required -def changelog(v): - - - try: page = max(int(request.values.get("page", 1)), 1) - except: page = 1 - - sort=request.values.get("sort", "new") - t=request.values.get('t', "all") - - ids = changeloglist(sort=sort, - page=page, - t=t, - v=v, - site=SITE - ) - - 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} - 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", site=None): - - posts = g.db.query(Submission.id).filter_by(is_banned=False, private=False,).filter(Submission.deleted_utc == 0) - - if v.admin_level < 2: - posts = posts.filter(Submission.author_id.notin_(v.userblocks)) - - admins = [x[0] for x in g.db.query(User.id).filter(User.admin_level > 0).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) - elif sort == "controversial": - posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc()) - 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_post") -@auth_required -def random_post(v): - - p = g.db.query(Submission.id).filter(Submission.deleted_utc == 0, Submission.is_banned == False, Submission.private == False).order_by(func.random()).first() - - if p: p = p[0] - else: abort(404) - - return redirect(f"/post/{p}") - - -@app.get("/random_user") -@auth_required -def random_user(v): - - u = g.db.query(User.username).filter(User.song != None).order_by(func.random()).first() - - if u: u = u[0] - else: abort(404) - - return redirect(f"/@{u}") - - -@app.get("/comments") -@auth_required -def all_comments(v): - - - try: page = max(int(request.values.get("page", 1)), 1) - except: page = 1 - - sort=request.values.get("sort", "new") - t=request.values.get("t", defaulttimefilter) - - try: gt=int(request.values.get("after", 0)) - except: gt=0 - - try: lt=int(request.values.get("before", 0)) - except: lt=0 - - idlist = comment_idlist(v=v, - page=page, - sort=sort, - t=t, - gt=gt, - lt=lt, - site=SITE - ) - - 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]} - return render_template("home_comments.html", v=v, sort=sort, t=t, page=page, comments=comments, standalone=True, next_exists=next_exists) - - - -@cache.memoize(timeout=86400) -def comment_idlist(page=1, v=None, nsfw=False, sort="new", t="all", gt=0, lt=0, site=None): - - comments = g.db.query(Comment.id).filter(Comment.parent_submission != None) - - if v.admin_level < 2: - private = [x[0] for x in g.db.query(Submission.id).filter(Submission.private == True).all()] - - comments = comments.filter(Comment.author_id.notin_(v.userblocks), Comment.is_banned==False, Comment.deleted_utc == 0, Comment.parent_submission.notin_(private)) - - - if not v.paid_dues: - club = [x[0] for x in g.db.query(Submission.id).filter(Submission.club == True).all()] - comments = comments.filter(Comment.parent_submission.notin_(club)) - - - if gt: comments = comments.filter(Comment.created_utc > gt) - if lt: comments = comments.filter(Comment.created_utc < lt) - - if not gt and not lt: - 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) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - 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("/transfers") -@auth_required -def transfers(v): - - comments = g.db.query(Comment).filter(Comment.author_id == NOTIFICATIONS_ID, Comment.parent_submission == None, Comment.body_html.like("% has transferred %")).order_by(Comment.id.desc()) - - if request.headers.get("Authorization"): return {"data": [x.json for x in comments.all()]} - - try: page = max(int(request.values.get("page", 1)), 1) - except: page = 1 - - comments = comments.offset(25 * (page - 1)).limit(26).all() - next_exists = len(comments) > 25 - comments = comments[:25] +from files.helpers.wrappers import * +from files.helpers.get import * +from files.helpers.discord import * +from files.__main__ import app, cache, limiter +from files.classes.submission import Submission + +defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip() + +@app.post("/clear") +@auth_required +def clear(v): + notifs = g.db.query(Notification).join(Comment, Notification.comment_id == Comment.id).filter(Notification.read == False, Notification.user_id == v.id).all() + for n in notifs: + n.read = True + g.db.add(n) + g.db.commit() + return {"message": "Notifications cleared!"} + +@app.get("/unread") +@auth_required +def unread(v): + listing = g.db.query(Notification, Comment).join(Comment, Notification.comment_id == Comment.id).filter( + Notification.read == False, + Notification.user_id == v.id, + Comment.is_banned == False, + Comment.deleted_utc == 0, + Comment.author_id != AUTOJANNY_ID, + ).order_by(Notification.created_utc.desc()).all() + + for n, c in listing: + n.read = True + g.db.add(n) + g.db.commit() + + return {"data":[x[1].json for x in listing]} + + +@app.get("/notifications") +@auth_required +def notifications(v): + try: page = max(int(request.values.get("page", 1)), 1) + except: page = 1 + + messages = request.values.get('messages') + modmail = request.values.get('modmail') + posts = request.values.get('posts') + reddit = request.values.get('reddit') + if modmail and v.admin_level > 1: + comments = g.db.query(Comment).filter(Comment.sentto==2).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all() + next_exists = (len(comments) > 25) + listing = comments[:25] + elif messages: + if v and (v.shadowbanned or v.admin_level > 2): + comments = g.db.query(Comment).filter(Comment.sentto != None, or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None, Comment.level == 1).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all() + else: + comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.sentto != None, or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None, Comment.level == 1).order_by(Comment.id.desc()).offset(25*(page-1)).limit(26).all() + + next_exists = (len(comments) > 25) + listing = comments[:25] + elif posts: + notifications = g.db.query(Notification, Comment).join(Comment, Notification.comment_id == Comment.id).filter(Notification.user_id == v.id, Comment.author_id == AUTOJANNY_ID).order_by(Notification.created_utc.desc()).offset(25 * (page - 1)).limit(101).all() + + listing = [] + + for index, x in enumerate(notifications[:100]): + n, c = x + if n.read and index > 24: break + elif not n.read: + n.read = True + c.unread = True + g.db.add(n) + if n.created_utc > 1620391248: c.notif_utc = n.created_utc + listing.append(c) + + g.db.commit() + + next_exists = (len(notifications) > len(listing)) + elif reddit: + notifications = g.db.query(Notification, Comment).join(Comment, Notification.comment_id == Comment.id).filter(Notification.user_id == v.id, Comment.body_html.like('%

New site mention: 2)): + comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) + + comments = comments.offset(25 * (page - 1)).limit(26).all() + + next_exists = (len(comments) > 25) + comments = comments[:25] + + cids = [x[0].id for x in comments] + + comms = get_comments(cids, v=v) + + listing = [] + for c, n in comments: + if n.created_utc > 1620391248: c.notif_utc = n.created_utc + if not n.read: + n.read = True + c.unread = True + g.db.add(n) + + if c.parent_submission: + if c.replies2 == None: + c.replies2 = c.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all() + for x in c.replies2: + if x.replies2 == None: x.replies2 = [] + count = 0 + while count < 10 and c.parent_comment and (c.parent_comment.author_id == v.id or c.parent_comment.id in cids): + count += 1 + c = c.parent_comment + if c.replies2 == None: + c.replies2 = c.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all() + for x in c.replies2: + if x.replies2 == None: + x.replies2 = x.child_comments.filter(or_(Comment.author_id == v.id, Comment.id.in_(cids))).all() + else: + while c.parent_comment: + c = c.parent_comment + c.replies2 = g.db.query(Comment).filter_by(parent_comment_id=c.id).order_by(Comment.id).all() + + if c not in listing: listing.append(c) + + g.db.commit() + + if request.headers.get("Authorization"): return {"data":[x.json for x in listing]} + + return render_template("notifications.html", + v=v, + notifications=listing, + next_exists=next_exists, + page=page, + standalone=True, + render_replies=True + ) + + +@app.get("/") +@app.get("/catalog") +@app.get("/h/") +@app.get("/s/") +@app.get("/logged_out") +@app.get("/logged_out/catalog") +@app.get("/logged_out/h/") +@app.get("/logged_out/s/") +@limiter.limit("3/second;30/minute;5000/hour;10000/day") +@auth_desired +def front_all(v, sub=None, subdomain=None): + + if g.webview and not session.get("session_id"): + session["session_id"] = secrets.token_hex(49) + + 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'): return redirect(request.full_path.replace('/logged_out','')) + + if sub: sub = g.db.query(Sub).filter_by(name=sub.strip().lower()).one_or_none() + + if (request.path.startswith('/h/') or request.path.startswith('/s/')) and not sub: abort(404) + + try: page = max(int(request.values.get("page", 1)), 1) + except: abort(400) + + if v: + defaultsorting = v.defaultsorting + if sub or SITE_NAME != 'rDrama': defaulttime = 'all' + else: defaulttime = v.defaulttime + else: + defaultsorting = "hot" + if sub or SITE_NAME != 'rDrama': defaulttime = 'all' + else: defaulttime = defaulttimefilter + + sort=request.values.get("sort", defaultsorting) + t=request.values.get('t', defaulttime) + ccmode=request.values.get('ccmode', "false").lower() + + if sort == 'bump': t='all' + + try: gt=int(request.values.get("after", 0)) + except: gt=0 + + try: lt=int(request.values.get("before", 0)) + except: lt=0 + + ids, next_exists = frontlist(sort=sort, + page=page, + t=t, + v=v, + ccmode=ccmode, + filter_words=v.filter_words if v else [], + gt=gt, + lt=lt, + sub=sub, + site=SITE + ) + + posts = get_posts(ids, v=v) + + if v: + if v.hidevotedon: posts = [x for x in posts if not hasattr(x, 'voted') or not x.voted] + + + if v.patron_utc and v.patron_utc < time.time(): + v.patron = 0 + v.patron_utc = 0 + send_repeatable_notification(v.id, "Your paypig status has expired!") + if v.discord_id: remove_role(v, "1") + g.db.add(v) + g.db.commit() + + if v.unban_utc and v.unban_utc < time.time(): + v.is_banned = 0 + v.unban_utc = 0 + v.ban_evade = 0 + send_repeatable_notification(v.id, "You have been unbanned!") + g.db.add(v) + g.db.commit() + + if v.agendaposter and v.agendaposter < time.time(): + v.agendaposter = 0 + send_repeatable_notification(v.id, "Your chud theme has expired!") + g.db.add(v) + badge = v.has_badge(28) + if badge: g.db.delete(badge) + g.db.commit() + + if v.flairchanged and v.flairchanged < time.time(): + v.flairchanged = None + send_repeatable_notification(v.id, "Your flair lock has expired. You can now change your flair!") + g.db.add(v) + badge = v.has_badge(96) + if badge: g.db.delete(badge) + g.db.commit() + + if v.marseyawarded and v.marseyawarded < time.time(): + v.marseyawarded = None + send_repeatable_notification(v.id, "Your marsey award has expired!") + g.db.add(v) + badge = v.has_badge(98) + if badge: g.db.delete(badge) + g.db.commit() + + if v.longpost and v.longpost < time.time(): + v.longpost = None + send_repeatable_notification(v.id, "Your pizzashill award has expired!") + g.db.add(v) + badge = v.has_badge(97) + if badge: g.db.delete(badge) + g.db.commit() + + if v.bird and v.bird < time.time(): + v.bird = None + send_repeatable_notification(v.id, "Your bird site award has expired!") + g.db.add(v) + badge = v.has_badge(95) + if badge: g.db.delete(badge) + g.db.commit() + + if v.progressivestack and v.progressivestack < time.time(): + v.progressivestack = None + send_repeatable_notification(v.id, "Your progressive stack has expired!") + g.db.add(v) + badge = v.has_badge(94) + if badge: g.db.delete(badge) + g.db.commit() + + if v.rehab and v.rehab < time.time(): + v.rehab = None + send_repeatable_notification(v.id, "Your rehab has finished!") + g.db.add(v) + badge = v.has_badge(109) + if badge: g.db.delete(badge) + g.db.commit() + + if v.deflector and v.deflector < time.time(): + v.deflector = None + send_repeatable_notification(v.id, "Your deflector has expired!") + g.db.add(v) + g.db.commit() + + if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists} + return render_template("home.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page, ccmode=ccmode, sub=sub, home=True) + + + +@cache.memoize(timeout=86400) +def frontlist(v=None, sort="hot", page=1, t="all", ids_only=True, ccmode="false", filter_words='', gt=0, lt=0, sub=None, site=None): + + posts = g.db.query(Submission) + + if v and v.hidevotedon: + voted = [x[0] for x in g.db.query(Vote.submission_id).filter_by(user_id=v.id).all()] + posts = posts.filter(Submission.id.notin_(voted)) + + if sub: posts = posts.filter_by(sub=sub.name) + elif v: posts = posts.filter(or_(Submission.sub == None, Submission.sub.notin_(v.all_blocks))) + + if gt: posts = posts.filter(Submission.created_utc > gt) + if lt: posts = posts.filter(Submission.created_utc < lt) + + if not gt and not lt: + if t == 'all': cutoff = 0 + else: + 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) + + if (ccmode == "true"): + posts = posts.filter(Submission.club == True) + + posts = posts.filter_by(is_banned=False, private=False, deleted_utc = 0) + + if (sort == "hot" or (v and v.id == Q_ID)) and ccmode == "false" and not gt and not lt: + posts = posts.filter_by(stickied=None) + + if v and v.admin_level < 2: + posts = posts.filter(Submission.author_id.notin_(v.userblocks)) + + if not (v and v.changelogsub): + posts=posts.filter(not_(Submission.title.ilike('[changelog]%'))) + + if v and filter_words: + for word in filter_words: + word = word.replace('\\', '').replace('_', '\_').replace('%', '\%').strip() + posts=posts.filter(not_(Submission.title.ilike(f'%{word}%'))) + + if not (v and v.shadowbanned): + posts = posts.join(User, User.id == Submission.author_id).filter(User.shadowbanned == None) + + if request.host == 'rdrama.net': num = 5 + else: num = 0.5 + + if sort == "hot": + ti = int(time.time()) + 3600 + posts = posts.order_by(-1000000*(Submission.realupvotes + 1 + Submission.comment_count/num + (func.length(Submission.body_html)-func.length(func.replace(Submission.body_html,'','')))/4)/(func.power(((ti - Submission.created_utc)/1000), 1.23)), Submission.created_utc.desc()) + elif sort == "bump": + posts = posts.filter(Submission.comment_count > 1).order_by(Submission.bump_utc.desc(), Submission.created_utc.desc()) + elif sort == "new": + posts = posts.order_by(Submission.created_utc.desc()) + elif sort == "old": + posts = posts.order_by(Submission.created_utc) + elif sort == "controversial": + posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc(), Submission.created_utc.desc()) + elif sort == "top": + posts = posts.order_by(Submission.downvotes - Submission.upvotes, Submission.created_utc.desc()) + elif sort == "bottom": + posts = posts.order_by(Submission.upvotes - Submission.downvotes, Submission.created_utc.desc()) + elif sort == "comments": + posts = posts.order_by(Submission.comment_count.desc(), Submission.created_utc.desc()) + + if v: size = v.frontsize or 0 + else: size = 25 + + posts = posts.offset(size * (page - 1)).limit(size+1).all() + + next_exists = (len(posts) > size) + + posts = posts[:size] + + if (sort == "hot" or (v and v.id == Q_ID)) and page == 1 and ccmode == "false" and not gt and not lt: + pins = g.db.query(Submission).filter(Submission.stickied != None, Submission.is_banned == False) + if sub: pins = pins.filter_by(sub=sub.name) + elif v: + pins = pins.filter(or_(Submission.sub == None, Submission.sub.notin_(v.all_blocks))) + if v.admin_level < 2: + pins = pins.filter(Submission.author_id.notin_(v.userblocks)) + + pins = pins.all() + + for pin in pins: + if pin.stickied_utc and int(time.time()) > pin.stickied_utc: + pin.stickied = None + pin.stickied_utc = None + g.db.add(pin) + pins.remove(pin) + + posts = pins + posts + + if ids_only: posts = [x.id for x in posts] + + g.db.commit() + + return posts, next_exists + + +@app.get("/changelog") +@auth_required +def changelog(v): + + + try: page = max(int(request.values.get("page", 1)), 1) + except: page = 1 + + sort=request.values.get("sort", "new") + t=request.values.get('t', "all") + + ids = changeloglist(sort=sort, + page=page, + t=t, + v=v, + site=SITE + ) + + 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} + 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", site=None): + + posts = g.db.query(Submission.id).filter_by(is_banned=False, private=False,).filter(Submission.deleted_utc == 0) + + if v.admin_level < 2: + posts = posts.filter(Submission.author_id.notin_(v.userblocks)) + + admins = [x[0] for x in g.db.query(User.id).filter(User.admin_level > 0).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) + elif sort == "controversial": + posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc()) + 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_post") +@auth_required +def random_post(v): + + p = g.db.query(Submission.id).filter(Submission.deleted_utc == 0, Submission.is_banned == False, Submission.private == False).order_by(func.random()).first() + + if p: p = p[0] + else: abort(404) + + return redirect(f"/post/{p}") + + +@app.get("/random_user") +@auth_required +def random_user(v): + + u = g.db.query(User.username).filter(User.song != None).order_by(func.random()).first() + + if u: u = u[0] + else: abort(404) + + return redirect(f"/@{u}") + + +@app.get("/comments") +@auth_required +def all_comments(v): + + + try: page = max(int(request.values.get("page", 1)), 1) + except: page = 1 + + sort=request.values.get("sort", "new") + t=request.values.get("t", defaulttimefilter) + + try: gt=int(request.values.get("after", 0)) + except: gt=0 + + try: lt=int(request.values.get("before", 0)) + except: lt=0 + + idlist = comment_idlist(v=v, + page=page, + sort=sort, + t=t, + gt=gt, + lt=lt, + site=SITE + ) + + 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]} + return render_template("home_comments.html", v=v, sort=sort, t=t, page=page, comments=comments, standalone=True, next_exists=next_exists) + + + +@cache.memoize(timeout=86400) +def comment_idlist(page=1, v=None, nsfw=False, sort="new", t="all", gt=0, lt=0, site=None): + + comments = g.db.query(Comment.id).filter(Comment.parent_submission != None) + + if v.admin_level < 2: + private = [x[0] for x in g.db.query(Submission.id).filter(Submission.private == True).all()] + + comments = comments.filter(Comment.author_id.notin_(v.userblocks), Comment.is_banned==False, Comment.deleted_utc == 0, Comment.parent_submission.notin_(private)) + + + if not v.paid_dues: + club = [x[0] for x in g.db.query(Submission.id).filter(Submission.club == True).all()] + comments = comments.filter(Comment.parent_submission.notin_(club)) + + + if gt: comments = comments.filter(Comment.created_utc > gt) + if lt: comments = comments.filter(Comment.created_utc < lt) + + if not gt and not lt: + 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) + elif sort == "controversial": + comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) + 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("/transfers") +@auth_required +def transfers(v): + + comments = g.db.query(Comment).filter(Comment.author_id == NOTIFICATIONS_ID, Comment.parent_submission == None, Comment.body_html.like("% has transferred %")).order_by(Comment.id.desc()) + + if request.headers.get("Authorization"): return {"data": [x.json for x in comments.all()]} + + try: page = max(int(request.values.get("page", 1)), 1) + except: page = 1 + + comments = comments.offset(25 * (page - 1)).limit(26).all() + next_exists = len(comments) > 25 + comments = comments[:25] return render_template("transfers.html", v=v, page=page, comments=comments, standalone=True, next_exists=next_exists) \ No newline at end of file diff --git a/files/routes/giphy.py b/files/routes/giphy.py index 031dfc39c..e89951689 100644 --- a/files/routes/giphy.py +++ b/files/routes/giphy.py @@ -1,24 +1,24 @@ -from flask import * -from os import environ -import requests -from files.helpers.wrappers import * - -from files.__main__ import app - -GIPHY_KEY = environ.get('GIPHY_KEY').rstrip() - - -@app.get("/giphy") -@app.get("/giphy") -@auth_required -def giphy(v=None, 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, timeout=5).json()) +from flask import * +from os import environ +import requests +from files.helpers.wrappers import * + +from files.__main__ import app + +GIPHY_KEY = environ.get('GIPHY_KEY').rstrip() + + +@app.get("/giphy") +@app.get("/giphy") +@auth_required +def giphy(v=None, 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, timeout=5).json()) diff --git a/files/routes/login.py b/files/routes/login.py index a89da564e..949b9834a 100644 --- a/files/routes/login.py +++ b/files/routes/login.py @@ -1,572 +1,582 @@ -from urllib.parse import urlencode -from files.mail import * -from files.__main__ import app, limiter -from files.helpers.const import * -import requests - -@app.get("/login") -@auth_desired -def login_get(v): - - redir = request.values.get("redirect") - if redir: - redir = redir.replace("/logged_out", "").strip() - if not redir.startswith(f'{SITE_FULL}/') and not redir.startswith('/'): redir = None - - if v and redir: - if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) - elif redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') - - return render_template("login.html", failed=False, redirect=redir) - - -def check_for_alts(current_id): - ids = [x[0] for x in g.db.query(User.id).all()] - past_accs = set(session.get("history", [])) - - for past_id in list(past_accs): - - if past_id not in ids: - past_accs.remove(past_id) - continue - - if past_id == MOM_ID or current_id == MOM_ID: break - if past_id == current_id: continue - - li = [past_id, current_id] - existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() - - if not existing: - new_alt = Alt(user1=past_id, user2=current_id) - g.db.add(new_alt) - g.db.flush() - - otheralts = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).all() - for a in otheralts: - if a.user1 != past_id: - li = [a.user1, past_id] - existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() - if not existing: - new_alt = Alt(user1=a.user1, user2=past_id) - g.db.add(new_alt) - g.db.flush() - - if a.user1 != current_id: - li = [a.user1, current_id] - existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() - if not existing: - new_alt = Alt(user1=a.user1, user2=current_id) - g.db.add(new_alt) - g.db.flush() - - if a.user2 != past_id: - li = [a.user2, past_id] - existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() - if not existing: - new_alt = Alt(user1=a.user2, user2=past_id) - g.db.add(new_alt) - g.db.flush() - - if a.user2 != current_id: - li = [a.user2, current_id] - existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() - if not existing: - new_alt = Alt(user1=a.user2, user2=current_id) - g.db.add(new_alt) - g.db.flush() - - past_accs.add(current_id) - session["history"] = list(past_accs) - - -@app.post("/login") -@limiter.limit("1/second;6/minute;200/hour;1000/day") -def login_post(): - template = '' - - username = request.values.get("username") - - if not username: abort(400) - username = username.lstrip('@').replace('\\', '').replace('_', '\_').replace('%', '').strip() - - if not username: abort(400) - if username.startswith('@'): username = username[1:] - - if "@" in username: - try: account = g.db.query(User).filter(User.email.ilike(username)).one_or_none() - except: return "Multiple users use this email!" - 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) - - session.permanent = True - session["session_id"] = token_hex(49) - session["lo_user"] = account.id - session["login_nonce"] = account.login_nonce - if account.id == AEVANN_ID: session["verified"] = time.time() - - check_for_alts(account.id) - - g.db.commit() - - redir = request.values.get("redirect") - if redir: - redir = redir.replace("/logged_out", "").strip() - if not redir.startswith(f'{SITE_FULL}/') and not redir.startswith('/'): redir = '/' - - if redir: - if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) - if redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') - return redirect('/') - -@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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def logout(v): - - session.pop("session_id", None) - session.pop("lo_user", None) - - return {"message": "Logout successful!"} - - -@app.get("/signup") -@auth_desired -def sign_up_get(v): - if not app.config['SETTINGS']['Signups']: - return {"error": "New account registration is currently closed. Please come back later."}, 403 - - if v: return redirect(SITE_FULL) - - agent = request.headers.get("User-Agent") - if not agent: abort(403) - - ref = request.values.get("ref") - - if ref: - ref = ref.replace('\\', '').replace('_', '\_').replace('%', '').strip() - ref_user = g.db.query(User).filter(User.username.ilike(ref)).one_or_none() - - 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() - - error = request.values.get("error") - - return render_template("sign_up.html", - formkey=formkey, - now=now, - ref_user=ref_user, - hcaptcha=app.config["HCAPTCHA_SITEKEY"], - error=error - ) - - -@app.post("/signup") -@limiter.limit("10/day") -@auth_desired -def sign_up_post(v): - if not app.config['SETTINGS']['Signups']: - return {"error": "New account registration is currently closed. Please come back later."}, 403 - - if v: abort(403) - - agent = request.headers.get("User-Agent") - 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") - - if not username: abort(400) - - username = username.strip() - - def signup_error(error): - - args = {"error": error} - if request.values.get("referred_by"): - user = g.db.query(User).filter_by(id=request.values.get("referred_by")).one_or_none() - if user: args["ref"] = user.username - - return redirect(f"/signup?{urlencode(args)}") - - if now - int(form_timestamp) < 5: - return signup_error("There was a problem. Please try again.") - - if not hmac.compare_digest(correct_formkey, form_formkey): - return signup_error("There was a problem. Please try again.") - - if not request.values.get( - "password") == request.values.get("password_confirm"): - return signup_error("Passwords did not match. Please try again.") - - if not valid_username_regex.fullmatch(username): - return signup_error("Invalid username") - - if not valid_password_regex.fullmatch(request.values.get("password")): - return signup_error("Password must be between 8 and 100 characters.") - - email = request.values.get("email").strip().lower() - - if email: - if not email_regex.fullmatch(email): - return signup_error("Invalid email.") - else: email = None - - existing_account = get_user(username, graceful=True) - if existing_account and existing_account.reserved: - return redirect(existing_account.url) - - if existing_account: - return signup_error("An account with that username already exists.") - - if app.config.get("HCAPTCHA_SITEKEY"): - token = request.values.get("h-captcha-response") - if not token: - return signup_error("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, timeout=5) - - if not x.json()["success"]: - return signup_error("Unable to verify captcha [2].") - - session.pop("signup_token") - - ref_id = int(request.values.get("referred_by", 0)) - - id_1 = g.db.query(User.id).filter_by(id=9).count() - users_count = g.db.query(User.id).count() - if id_1 == 0 and users_count == 8: - admin_level=3 - session["history"] = [] - else: admin_level=0 - - profileurl = '/e/' + random.choice(marseys_const) + '.webp' - - new_user = User( - username=username, - original_username = username, - admin_level = admin_level, - password=request.values.get("password"), - email=email, - referred_by=ref_id or None, - ban_evade = int(any((x.is_banned or x.shadowbanned) and not x.unban_utc for x in g.db.query(User).filter(User.id.in_(session.get("history", []))).all() if x)), - profileurl=profileurl - ) - - g.db.add(new_user) - g.db.flush() - - if ref_id: - ref_user = g.db.query(User).filter_by(id=ref_id).one_or_none() - - if ref_user: - if ref_user.referral_count and not ref_user.has_badge(10): - new_badge = Badge(user_id=ref_user.id, badge_id=10) - g.db.add(new_badge) - g.db.flush() - send_notification(ref_user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") - if ref_user.referral_count >= 10 and not ref_user.has_badge(11): - new_badge = Badge(user_id=ref_user.id, badge_id=11) - g.db.add(new_badge) - g.db.flush() - send_notification(ref_user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") - if ref_user.referral_count >= 100 and not ref_user.has_badge(12): - new_badge = Badge(user_id=ref_user.id, badge_id=12) - g.db.add(new_badge) - g.db.flush() - send_notification(ref_user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") - - check_for_alts(new_user.id) - - if email: send_verification_email(new_user) - - send_notification(new_user.id, WELCOME_MSG) - - session.permanent = True - session["session_id"] = token_hex(49) - session["lo_user"] = new_user.id - - g.db.commit() - - return redirect(SITE_FULL) - - -@app.get("/forgot") -def get_forgot(): - return render_template("forgot_password.html") - - -@app.post("/forgot") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -def post_forgot(): - - username = request.values.get("username") - if not username: abort(400) - - email = request.values.get("email",'').strip().lower() - - if not email_regex.fullmatch(email): - return render_template("forgot_password.html", error="Invalid email.") - - - username = username.lstrip('@').replace('\\', '').replace('_', '\_').replace('%', '').strip() - email = email.replace('\\', '').replace('_', '\_').replace('%', '').strip() - - user = g.db.query(User).filter( - User.username.ilike(username), - User.email.ilike(email)).one_or_none() - - if user: - now = int(time.time()) - token = generate_hash(f"{user.id}+{now}+forgot+{user.login_nonce}") - url = f"{SITE_FULL}/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).filter_by(id=user_id).one_or_none() - - if not user: abort(400) - - 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;30/minute;200/hour;1000/day") -@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).filter_by(id=user_id).one_or_none() - - if not validate_hash(f"{user_id}+{timestamp}+reset+{user.login_nonce}", token): - abort(400) - if not user: - abort(404) - - if 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;6/minute;200/hour;1000/day") -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").strip().lower() - - if not email_regex.fullmatch(email): - return render_template("message.html", title="Invalid email.", error="Invalid 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"{SITE_FULL}/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 = request.values.get("t") - if not t: abort(400) - t = int(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 + +@app.get("/login") +@auth_desired +def login_get(v): + + redir = request.values.get("redirect") + if redir: + redir = redir.replace("/logged_out", "").strip() + if not redir.startswith(f'{SITE_FULL}/') and not redir.startswith('/'): redir = None + + if v and redir: + if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) + elif redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') + + return render_template("login.html", failed=False, redirect=redir) + + +def check_for_alts(current_id): + ids = [x[0] for x in g.db.query(User.id).all()] + past_accs = set(session.get("history", [])) + + for past_id in list(past_accs): + + if past_id not in ids: + past_accs.remove(past_id) + continue + + if past_id == MOM_ID or current_id == MOM_ID: break + if past_id == current_id: continue + + li = [past_id, current_id] + existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() + + if not existing: + new_alt = Alt(user1=past_id, user2=current_id) + g.db.add(new_alt) + g.db.flush() + + otheralts = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).all() + for a in otheralts: + if a.user1 != past_id: + li = [a.user1, past_id] + existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() + if not existing: + new_alt = Alt(user1=a.user1, user2=past_id) + g.db.add(new_alt) + g.db.flush() + + if a.user1 != current_id: + li = [a.user1, current_id] + existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() + if not existing: + new_alt = Alt(user1=a.user1, user2=current_id) + g.db.add(new_alt) + g.db.flush() + + if a.user2 != past_id: + li = [a.user2, past_id] + existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() + if not existing: + new_alt = Alt(user1=a.user2, user2=past_id) + g.db.add(new_alt) + g.db.flush() + + if a.user2 != current_id: + li = [a.user2, current_id] + existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() + if not existing: + new_alt = Alt(user1=a.user2, user2=current_id) + g.db.add(new_alt) + g.db.flush() + + past_accs.add(current_id) + session["history"] = list(past_accs) + + +@app.post("/login") +@limiter.limit("1/second;6/minute;200/hour;1000/day") +def login_post(): + template = '' + + username = request.values.get("username") + + if not username: abort(400) + username = username.lstrip('@').replace('\\', '').replace('_', '\_').replace('%', '').strip() + + if not username: abort(400) + if username.startswith('@'): username = username[1:] + + if "@" in username: + try: account = g.db.query(User).filter(User.email.ilike(username)).one_or_none() + except: return "Multiple users use this email!" + 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) + + session.permanent = True + session["session_id"] = token_hex(49) + session["lo_user"] = account.id + session["login_nonce"] = account.login_nonce + if account.id == AEVANN_ID: session["verified"] = time.time() + + check_for_alts(account.id) + + g.db.commit() + + redir = request.values.get("redirect") + if redir: + redir = redir.replace("/logged_out", "").strip() + if not redir.startswith(f'{SITE_FULL}/') and not redir.startswith('/'): redir = '/' + + if redir: + if redir.startswith(f'{SITE_FULL}/'): return redirect(redir) + if redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}') + return redirect('/') + +@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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def logout(v): + + session.pop("session_id", None) + session.pop("lo_user", None) + + return {"message": "Logout successful!"} + + +@app.get("/signup") +@auth_desired +def sign_up_get(v): + if not app.config['SETTINGS']['Signups']: + return {"error": "New account registration is currently closed. Please come back later."}, 403 + + if v: return redirect(SITE_FULL) + + agent = request.headers.get("User-Agent") + if not agent: abort(403) + + ref = request.values.get("ref") + + if ref: + ref = ref.replace('\\', '').replace('_', '\_').replace('%', '').strip() + ref_user = g.db.query(User).filter(User.username.ilike(ref)).one_or_none() + + 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() + + error = request.values.get("error") + + return render_template("sign_up.html", + formkey=formkey, + now=now, + ref_user=ref_user, + hcaptcha=app.config["HCAPTCHA_SITEKEY"], + error=error + ) + + +@app.post("/signup") +@limiter.limit("10/day") +@auth_desired +def sign_up_post(v): + if not app.config['SETTINGS']['Signups']: + return {"error": "New account registration is currently closed. Please come back later."}, 403 + + if v: abort(403) + + agent = request.headers.get("User-Agent") + 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") + + if not username: abort(400) + + username = username.strip() + + def signup_error(error): + + args = {"error": error} + if request.values.get("referred_by"): + user = g.db.query(User).filter_by(id=request.values.get("referred_by")).one_or_none() + if user: args["ref"] = user.username + + return redirect(f"/signup?{urlencode(args)}") + + if now - int(form_timestamp) < 5: + return signup_error("There was a problem. Please try again.") + + if not hmac.compare_digest(correct_formkey, form_formkey): + return signup_error("There was a problem. Please try again.") + + if not request.values.get( + "password") == request.values.get("password_confirm"): + return signup_error("Passwords did not match. Please try again.") + + if not valid_username_regex.fullmatch(username): + return signup_error("Invalid username") + + if not valid_password_regex.fullmatch(request.values.get("password")): + return signup_error("Password must be between 8 and 100 characters.") + + email = request.values.get("email").strip().lower() + + if email: + if not email_regex.fullmatch(email): + return signup_error("Invalid email.") + else: email = None + + existing_account = get_user(username, graceful=True) + if existing_account and existing_account.reserved: + return redirect(existing_account.url) + + if existing_account: + return signup_error("An account with that username already exists.") + + if app.config.get("HCAPTCHA_SITEKEY"): + token = request.values.get("h-captcha-response") + if not token: + return signup_error("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, timeout=5) + + if not x.json()["success"]: + return signup_error("Unable to verify captcha [2].") + + session.pop("signup_token") + + ref_id = int(request.values.get("referred_by", 0)) + + id_1 = g.db.query(User).filter_by(id=9).count() + users_count = g.db.query(User).count() + if id_1 == 0 and users_count == 8: + admin_level=3 + session["history"] = [] + else: admin_level=0 + + profileurl = '/e/' + random.choice(marseys_const) + '.webp' + + if SITE == "watchpeopledie.co": print(f'1: {username}') + + new_user = User( + username=username, + original_username = username, + admin_level = admin_level, + password=request.values.get("password"), + email=email, + referred_by=ref_id or None, + ban_evade = int(any((x.is_banned or x.shadowbanned) and not x.unban_utc for x in g.db.query(User).filter(User.id.in_(session.get("history", []))).all() if x)), + profileurl=profileurl + ) + + if SITE == "watchpeopledie.co": print(f'2: {username}') + + g.db.add(new_user) + g.db.flush() + + if SITE == "watchpeopledie.co": print(f'3: {username}') + + if ref_id: + ref_user = g.db.query(User).filter_by(id=ref_id).one_or_none() + + if ref_user: + if ref_user.referral_count and not ref_user.has_badge(10): + new_badge = Badge(user_id=ref_user.id, badge_id=10) + g.db.add(new_badge) + g.db.flush() + send_notification(ref_user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") + if ref_user.referral_count >= 10 and not ref_user.has_badge(11): + new_badge = Badge(user_id=ref_user.id, badge_id=11) + g.db.add(new_badge) + g.db.flush() + send_notification(ref_user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") + if ref_user.referral_count >= 100 and not ref_user.has_badge(12): + new_badge = Badge(user_id=ref_user.id, badge_id=12) + g.db.add(new_badge) + g.db.flush() + send_notification(ref_user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") + + check_for_alts(new_user.id) + + if email: send_verification_email(new_user) + + send_notification(new_user.id, WELCOME_MSG) + + session.permanent = True + session["session_id"] = token_hex(49) + session["lo_user"] = new_user.id + + if SITE == "watchpeopledie.co": print(f'4: {username}') + + g.db.commit() + + if SITE == "watchpeopledie.co": print(f'5: {username}') + + return redirect(SITE_FULL) + + +@app.get("/forgot") +def get_forgot(): + return render_template("forgot_password.html") + + +@app.post("/forgot") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +def post_forgot(): + + username = request.values.get("username") + if not username: abort(400) + + email = request.values.get("email",'').strip().lower() + + if not email_regex.fullmatch(email): + return render_template("forgot_password.html", error="Invalid email.") + + + username = username.lstrip('@').replace('\\', '').replace('_', '\_').replace('%', '').strip() + email = email.replace('\\', '').replace('_', '\_').replace('%', '').strip() + + user = g.db.query(User).filter( + User.username.ilike(username), + User.email.ilike(email)).one_or_none() + + if user: + now = int(time.time()) + token = generate_hash(f"{user.id}+{now}+forgot+{user.login_nonce}") + url = f"{SITE_FULL}/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).filter_by(id=user_id).one_or_none() + + if not user: abort(400) + + 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;30/minute;200/hour;1000/day") +@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).filter_by(id=user_id).one_or_none() + + if not validate_hash(f"{user_id}+{timestamp}+reset+{user.login_nonce}", token): + abort(400) + if not user: + abort(404) + + if 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;6/minute;200/hour;1000/day") +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").strip().lower() + + if not email_regex.fullmatch(email): + return render_template("message.html", title="Invalid email.", error="Invalid 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"{SITE_FULL}/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 = request.values.get("t") + if not t: abort(400) + t = int(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 index 0a5d811e0..a54ba5b19 100644 --- a/files/routes/oauth.py +++ b/files/routes/oauth.py @@ -1,286 +1,286 @@ -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 -import sqlalchemy.exc - -@app.get("/authorize") -@auth_required -def authorize_prompt(v): - client_id = request.values.get("client_id") - application = g.db.query(OauthApp).filter_by(client_id=client_id).one_or_none() - 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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def authorize(v): - - client_id = request.values.get("client_id") - application = g.db.query(OauthApp).filter_by(client_id=client_id).one_or_none() - if not application: return {"oauth_error": "Invalid `client_id`"}, 401 - access_token = secrets.token_urlsafe(128)[:128] - - try: - new_auth = ClientAuth(oauth_client = application.id, user_id = v.id, access_token=access_token) - g.db.add(new_auth) - g.db.commit() - except sqlalchemy.exc.IntegrityError: - g.db.rollback() - old_auth = g.db.query(ClientAuth).filter_by(oauth_client = application.id, user_id = v.id).one() - access_token = old_auth.access_token - - return redirect(f"{application.redirect_uri}?token={access_token}") - - -@app.post("/api_keys") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@is_not_permabanned -def request_api_keys(v): - - new_app = OauthApp( - app_name=request.values.get('name').replace('<','').replace('>',''), - redirect_uri=request.values.get('redirect_uri'), - author_id=v.id, - description=request.values.get("description")[:256] - ) - - g.db.add(new_app) - - body = f"@{v.username} has requested API keys for `{request.values.get('name')}`. You can approve or deny the request [here](/admin/apps)." - - body_html = sanitize(body) - - - new_comment = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=None, - level=1, - body_html=body_html, - sentto=2, - distinguish_level=6 - ) - g.db.add(new_comment) - g.db.flush() - - new_comment.top_comment_id = new_comment.id - - for admin in g.db.query(User).filter(User.admin_level > 2).all(): - notif = Notification(comment_id=new_comment.id, user_id=admin.id) - g.db.add(notif) - - - g.db.commit() - - return redirect('/settings/apps') - - -@app.post("/delete_app/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def delete_oauth_app(v, aid): - - aid = int(aid) - app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() - - if app.author_id != v.id: abort(403) - - for auth in g.db.query(ClientAuth).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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@is_not_permabanned -def edit_oauth_app(v, aid): - - aid = int(aid) - app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() - - if app.author_id != v.id: abort(403) - - 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;30/minute;200/hour;1000/day") -@admin_level_required(3) -def admin_app_approve(v, aid): - - app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() - 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_repeatable_notification(user.id, f"@{v.username} has approved your application `{app.app_name}`. Here's your access token: `{access_token}`\nPlease check the guide [here](/api) if you don't know what to do next.") - - ma = ModAction( - kind="approve_app", - user_id=v.id, - target_user_id=user.id, - ) - g.db.add(ma) - - g.db.commit() - - return {"message": "Application approved"} - - -@app.post("/admin/app/revoke/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def admin_app_revoke(v, aid): - - app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() - if app: - for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all(): g.db.delete(auth) - - send_repeatable_notification(app.author.id, f"@{v.username} has revoked your application `{app.app_name}`.") - - g.db.delete(app) - - ma = ModAction( - kind="revoke_app", - user_id=v.id, - target_user_id=app.author.id, - ) - g.db.add(ma) - - g.db.commit() - - return {"message": "App revoked"} - - -@app.post("/admin/app/reject/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def admin_app_reject(v, aid): - - app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() - - if app: - for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all(): g.db.delete(auth) - - send_repeatable_notification(app.author.id, f"@{v.username} has rejected your application `{app.app_name}`.") - - g.db.delete(app) - - ma = ModAction( - kind="reject_app", - user_id=v.id, - target_user_id=app.author.id, - ) - g.db.add(ma) - - g.db.commit() - - return {"message": "App rejected"} - - -@app.get("/admin/app/") -@admin_level_required(2) -def admin_app_id(v, aid): - - aid=aid - - oauth = g.db.query(OauthApp).filter_by(id=aid).one_or_none() - - 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(2) -def admin_app_id_comments(v, aid): - - aid=aid - - oauth = g.db.query(OauthApp).filter_by(id=aid).one_or_none() - - 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(2) -def admin_apps_list(v): - - apps = g.db.query(OauthApp).order_by(OauthApp.id.desc()).all() - - return render_template("admin/apps.html", v=v, apps=apps) - - -@app.post("/oauth/reroll/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def reroll_oauth_tokens(aid, v): - - aid = aid - - a = g.db.query(OauthApp).filter_by(id=aid).one_or_none() - - 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 +import sqlalchemy.exc + +@app.get("/authorize") +@auth_required +def authorize_prompt(v): + client_id = request.values.get("client_id") + application = g.db.query(OauthApp).filter_by(client_id=client_id).one_or_none() + 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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def authorize(v): + + client_id = request.values.get("client_id") + application = g.db.query(OauthApp).filter_by(client_id=client_id).one_or_none() + if not application: return {"oauth_error": "Invalid `client_id`"}, 401 + access_token = secrets.token_urlsafe(128)[:128] + + try: + new_auth = ClientAuth(oauth_client = application.id, user_id = v.id, access_token=access_token) + g.db.add(new_auth) + g.db.commit() + except sqlalchemy.exc.IntegrityError: + g.db.rollback() + old_auth = g.db.query(ClientAuth).filter_by(oauth_client = application.id, user_id = v.id).one() + access_token = old_auth.access_token + + return redirect(f"{application.redirect_uri}?token={access_token}") + + +@app.post("/api_keys") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@is_not_permabanned +def request_api_keys(v): + + new_app = OauthApp( + app_name=request.values.get('name').replace('<','').replace('>',''), + redirect_uri=request.values.get('redirect_uri'), + author_id=v.id, + description=request.values.get("description")[:256] + ) + + g.db.add(new_app) + + body = f"@{v.username} has requested API keys for `{request.values.get('name')}`. You can approve or deny the request [here](/admin/apps)." + + body_html = sanitize(body) + + + new_comment = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=None, + level=1, + body_html=body_html, + sentto=2, + distinguish_level=6 + ) + g.db.add(new_comment) + g.db.flush() + + new_comment.top_comment_id = new_comment.id + + for admin in g.db.query(User).filter(User.admin_level > 2).all(): + notif = Notification(comment_id=new_comment.id, user_id=admin.id) + g.db.add(notif) + + + g.db.commit() + + return redirect('/settings/apps') + + +@app.post("/delete_app/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def delete_oauth_app(v, aid): + + aid = int(aid) + app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() + + if app.author_id != v.id: abort(403) + + for auth in g.db.query(ClientAuth).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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@is_not_permabanned +def edit_oauth_app(v, aid): + + aid = int(aid) + app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() + + if app.author_id != v.id: abort(403) + + 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;30/minute;200/hour;1000/day") +@admin_level_required(3) +def admin_app_approve(v, aid): + + app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() + 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_repeatable_notification(user.id, f"@{v.username} has approved your application `{app.app_name}`. Here's your access token: `{access_token}`\nPlease check the guide [here](/api) if you don't know what to do next.") + + ma = ModAction( + kind="approve_app", + user_id=v.id, + target_user_id=user.id, + ) + g.db.add(ma) + + g.db.commit() + + return {"message": "Application approved"} + + +@app.post("/admin/app/revoke/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def admin_app_revoke(v, aid): + + app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() + if app: + for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all(): g.db.delete(auth) + + send_repeatable_notification(app.author.id, f"@{v.username} has revoked your application `{app.app_name}`.") + + g.db.delete(app) + + ma = ModAction( + kind="revoke_app", + user_id=v.id, + target_user_id=app.author.id, + ) + g.db.add(ma) + + g.db.commit() + + return {"message": "App revoked"} + + +@app.post("/admin/app/reject/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def admin_app_reject(v, aid): + + app = g.db.query(OauthApp).filter_by(id=aid).one_or_none() + + if app: + for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all(): g.db.delete(auth) + + send_repeatable_notification(app.author.id, f"@{v.username} has rejected your application `{app.app_name}`.") + + g.db.delete(app) + + ma = ModAction( + kind="reject_app", + user_id=v.id, + target_user_id=app.author.id, + ) + g.db.add(ma) + + g.db.commit() + + return {"message": "App rejected"} + + +@app.get("/admin/app/") +@admin_level_required(2) +def admin_app_id(v, aid): + + aid=aid + + oauth = g.db.query(OauthApp).filter_by(id=aid).one_or_none() + + 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(2) +def admin_app_id_comments(v, aid): + + aid=aid + + oauth = g.db.query(OauthApp).filter_by(id=aid).one_or_none() + + 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(2) +def admin_apps_list(v): + + apps = g.db.query(OauthApp).order_by(OauthApp.id.desc()).all() + + return render_template("admin/apps.html", v=v, apps=apps) + + +@app.post("/oauth/reroll/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def reroll_oauth_tokens(aid, v): + + aid = aid + + a = g.db.query(OauthApp).filter_by(id=aid).one_or_none() + + 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 index f0bb44947..c8fd8d0a0 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -1,1545 +1,1557 @@ -import time -import gevent -import requests -from files.helpers.wrappers import * -from files.helpers.sanitize import * -from files.helpers.alerts import * -from files.helpers.discord import send_discord_message, send_cringetopia_message -from files.helpers.const import * -from files.helpers.slots 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, unquote -from os import path -import requests -from shutil import copyfile -from sys import stdout - - -if SITE_NAME == 'PCM': snappyquotes = [] -else: snappyquotes = [f':#{x}:' for x in marseys_const2] - -if path.exists(f'snappy_{SITE_NAME}.txt'): - with open(f'snappy_{SITE_NAME}.txt', "r", encoding="utf-8") as f: - snappyquotes += f.read().split("\n{[para]}\n") - -discounts = { - 69: 0.02, - 70: 0.04, - 71: 0.06, - 72: 0.08, - 73: 0.10, -} - -titleheaders = {"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"} - - -@app.post("/toggle_club/") -@auth_required -def toggle_club(pid, v): - - post = get_post(pid) - if post.author_id != v.id and v.admin_level < 2: abort(403) - - post.club = not post.club - g.db.add(post) - - 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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def publish(pid, v): - post = get_post(pid) - if not post.private: return {"message": "Post published!"} - - if post.author_id != v.id: abort(403) - post.private = False - post.created_utc = int(time.time()) - g.db.add(post) - - if not post.ghost: - notify_users = NOTIFY_USERS(f'{post.title} {post.body}', v) - - if notify_users: - cid = notif_comment2(post) - for x in notify_users: - add_notif(cid, x) - - if v.followers: - text = f"@{v.username} has made a new post: [{post.title}]({post.shortlink})" - if post.sub: text += f" in /h/{post.sub}" - - cid = notif_comment(text, autojanny=True) - for follow in v.followers: - user = get_account(follow.user_id) - if post.club and not user.paid_dues: continue - add_notif(cid, user.id) - - g.db.commit() - - cache.delete_memoized(frontlist) - cache.delete_memoized(User.userpagelisting) - - if SITE == 'cringetopia.org': - send_cringetopia_message(post.permalink) - elif v.admin_level > 0 and ("[changelog]" in post.title.lower() or "(changelog)" in post.title.lower()): - send_discord_message(post.permalink) - cache.delete_memoized(changeloglist) - - return redirect(post.permalink) - -@app.get("/submit") -@app.get("/h//submit") -@auth_required -def submit_get(v, sub=None): - if sub: sub = g.db.query(Sub.name).filter_by(name=sub.strip().lower()).one_or_none() - - if request.path.startswith('/h/') and not sub: abort(404) - - SUBS = [x[0] for x in g.db.query(Sub.name).order_by(Sub.name).all()] - - return render_template("submit.html", SUBS=SUBS, v=v, sub=sub) - -@app.get("/post/") -@app.get("/post//") -@app.get("/h//post/") -@app.get("/h//post//") -@auth_desired -def post_id(pid, anything=None, v=None, sub=None): - - try: pid = int(pid) - except Exception as e: pass - - - 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 session.get('over_18', 0) < int(time.time()): - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error":"Must be 18+ to view"}, 451 - return render_template("errors/nsfw.html", v=v) - - if post.new or 'megathread' in post.title.lower(): defaultsortingcomments = 'new' - elif v: defaultsortingcomments = v.defaultsortingcomments - else: defaultsortingcomments = "top" - sort = request.values.get("sort", defaultsortingcomments) - - if post.club and not (v and (v.paid_dues or v.id == post.author_id)): abort(403) - - if v: - votes = g.db.query(CommentVote).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.target_id, - blocked.c.target_id, - ) - - if not (v and v.shadowbanned) and not (v and v.admin_level > 2): - comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) - - comments=comments.filter(Comment.parent_submission == post.id, Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID))).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.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) - - pinned = [c[0] for c in comments.filter(Comment.is_pinned != None).all()] - - comments = comments.filter(Comment.level == 1, Comment.is_pinned == None) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.realupvotes.desc()) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - first = [c[0] for c in comments.filter(or_(and_(Comment.slots_result == None, Comment.blackjack_result == None, Comment.wordle_result == None), func.length(Comment.body_html) > 100)).all()] - second = [c[0] for c in comments.filter(or_(Comment.slots_result != None, Comment.blackjack_result != None, Comment.wordle_result != None), func.length(Comment.body_html) <= 100).all()] - comments = first + second - else: - pinned = g.db.query(Comment).filter(Comment.parent_submission == post.id, Comment.is_pinned != None).all() - - comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.parent_submission == post.id, Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID)), Comment.level == 1, Comment.is_pinned == None) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.realupvotes.desc()) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - first = comments.filter(or_(and_(Comment.slots_result == None, Comment.blackjack_result == None, Comment.wordle_result == None), func.length(Comment.body_html) > 100)).all() - second = comments.filter(or_(Comment.slots_result != None, Comment.blackjack_result != None, Comment.wordle_result != None), func.length(Comment.body_html) <= 100).all() - comments = first + second - - offset = 0 - ids = set() - - if post.comment_count > 60 and not request.headers.get("Authorization") and not request.values.get("all"): - comments2 = [] - count = 0 - if post.created_utc > 1638672040: - for comment in comments: - comments2.append(comment) - ids.add(comment.id) - count += g.db.query(Comment.id).filter_by(parent_submission=post.id, top_comment_id=comment.id).count() + 1 - if count > 50: break - else: - for comment in comments: - comments2.append(comment) - ids.add(comment.id) - count += g.db.query(Comment.id).filter_by(parent_submission=post.id, parent_comment_id=comment.id).count() + 1 - if count > 10: break - - if len(comments) == len(comments2): offset = 0 - else: offset = 1 - comments = comments2 - - for pin in pinned: - if pin.is_pinned_utc and int(time.time()) > pin.is_pinned_utc: - pin.is_pinned = None - pin.is_pinned_utc = None - g.db.add(pin) - pinned.remove(pin) - - post.replies = pinned + comments - - post.views += 1 - g.db.add(post) - g.db.commit() - if request.headers.get("Authorization"): return post.json - else: - if post.is_banned and not (v and (v.admin_level > 1 or post.author_id == v.id)): template = "submission_banned.html" - else: template = "submission.html" - return render_template(template, v=v, p=post, ids=list(ids), sort=sort, render_replies=True, offset=offset, sub=post.subr, fart=app.config['SETTINGS']['Fart mode']) - -@app.get("/viewmore///") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@auth_desired -def viewmore(v, pid, sort, offset): - try: pid = int(pid) - except: abort(400) - post = get_post(pid, v=v) - if post.club and not (v and (v.paid_dues or v.id == post.author_id)): abort(403) - - offset = int(offset) - try: ids = set(int(x) for x in request.values.get("ids").split(',')) - except: abort(400) - - if v: - votes = g.db.query(CommentVote).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.target_id, - blocked.c.target_id, - ).filter(Comment.parent_submission == pid, Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID)), Comment.is_pinned == None, Comment.id.notin_(ids)) - - if not (v and v.shadowbanned) and not (v and v.admin_level > 2): - comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) - - 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 - ) - - 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) - - comments = comments.filter(Comment.level == 1) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.realupvotes.desc()) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - first = [c[0] for c in comments.filter(or_(and_(Comment.slots_result == None, Comment.blackjack_result == None, Comment.wordle_result == None), func.length(Comment.body_html) > 100)).all()] - second = [c[0] for c in comments.filter(or_(Comment.slots_result != None, Comment.blackjack_result != None, Comment.wordle_result != None), func.length(Comment.body_html) <= 100).all()] - comments = first + second - else: - comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.parent_submission == pid, Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID)), Comment.level == 1, Comment.is_pinned == None, Comment.id.notin_(ids)) - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - elif sort == "top": - comments = comments.order_by(Comment.realupvotes.desc()) - elif sort == "bottom": - comments = comments.order_by(Comment.upvotes - Comment.downvotes) - - first = comments.filter(or_(and_(Comment.slots_result == None, Comment.blackjack_result == None, Comment.wordle_result == None), func.length(Comment.body_html) > 100)).all() - second = comments.filter(or_(Comment.slots_result != None, Comment.blackjack_result != None, Comment.wordle_result != None), func.length(Comment.body_html) <= 100).all() - comments = first + second - comments = comments[offset:] - - comments2 = [] - count = 0 - if post.created_utc > 1638672040: - for comment in comments: - comments2.append(comment) - ids.add(comment.id) - count += g.db.query(Comment.id).filter_by(parent_submission=post.id, top_comment_id=comment.id).count() + 1 - if count > 50: break - else: - for comment in comments: - comments2.append(comment) - ids.add(comment.id) - count += g.db.query(Comment.id).filter_by(parent_submission=post.id, parent_comment_id=comment.id).count() + 1 - if count > 10: break - - if len(comments) == len(comments2): offset = 0 - else: offset += 1 - comments = comments2 - - return render_template("comments.html", v=v, comments=comments, p=post, ids=list(ids), render_replies=True, pid=pid, sort=sort, offset=offset, ajax=True) - - -@app.get("/morecomments/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@auth_desired -def morecomments(v, cid): - try: cid = int(cid) - except: abort(400) - - tcid = g.db.query(Comment.top_comment_id).filter_by(id=cid).one_or_none()[0] - - if v: - votes = g.db.query(CommentVote).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.target_id, - blocked.c.target_id, - ).filter(Comment.top_comment_id == tcid, Comment.level > 9).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 = [] - dump = [] - 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 - if c[0].parent_comment_id == int(cid): output.append(comment) - else: dump.append(comment) - comments = output - else: - c = g.db.query(Comment).filter_by(id=cid).one_or_none() - comments = c.replies - - if comments: p = comments[0].post - else: p = None - - return render_template("comments.html", v=v, comments=comments, p=p, render_replies=True, ajax=True) - -@app.post("/edit_post/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def edit_post(pid, v): - p = get_post(pid) - - if p.author_id != v.id and not (v.admin_level > 1 and v.admin_level > 2): abort(403) - - title = request.values.get("title", "").strip().replace('‎','') - - body = request.values.get("body", "").strip().replace('‎','') - - if len(body) > 20000: return {"error":"Character limit is 20000!"}, 403 - - if v.id == p.author_id: - if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): - return {"error":"You have to type more than 280 characters!"}, 403 - elif v.bird and len(body) > 140: - return {"error":"You have to type less than 140 characters!"}, 403 - - if title != p.title: - if v.id == p.author_id and v.agendaposter and not v.marseyawarded: title = torture_ap(title, v.username) - - title_html = filter_emojis_only(title, edit=True) - - if v.id == p.author_id and v.marseyawarded and not marseyaward_title_regex.fullmatch(title_html): - return {"error":"You can only type marseys!"}, 403 - - p.title = title[:500] - p.title_html = title_html - - if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - files = request.files.getlist('file')[:4] - for file in files: - if file.content_type.startswith('image/'): - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - url = process_image(name) - body += f"\n\n![]({url})" - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data'] - except requests.Timeout: return {"error": "Video upload timed out, please try again!"} - try: url = req['link'] - except: - error = req['error'] - if error == 'File exceeds max duration': error += ' (60 seconds)' - return {"error": error}, 400 - if url.endswith('.'): url += 'mp4' - body += f"\n\n{url}" - else: return {"error": "Image/Video files only"}, 400 - - if body != p.body: - if v.id == p.author_id and v.agendaposter and not v.marseyawarded: body = torture_ap(body, v.username) - - if not p.options: - for i in poll_regex.finditer(body): - body = body.replace(i.group(0), "") - c = Comment(author_id=AUTOPOLLER_ID, - parent_submission=p.id, - level=1, - body_html=filter_emojis_only(i.group(1)), - upvotes=0, - is_bot=True - ) - g.db.add(c) - - if not p.choices: - for i in choice_regex.finditer(body): - body = body.replace(i.group(0), "") - c = Comment(author_id=AUTOCHOICE_ID, - parent_submission=p.id, - level=1, - body_html=filter_emojis_only(i.group(1)), - upvotes=0, - is_bot=True - ) - g.db.add(c) - - body_html = sanitize(body, edit=True) - - if v.id == p.author_id and v.marseyawarded and marseyaward_body_regex.search(body_html): - return {"error":"You can only type marseys!"}, 403 - - - p.body = body - - if blackjack and any(i in f'{p.body} {p.title} {p.url}'.lower() for i in blackjack.split()): - v.shadowbanned = 'AutoJanny' - g.db.add(v) - send_repeatable_notification(CARP_ID, p.permalink) - - if len(body_html) > 40000: return {"error":"Submission body_html too long! (max 40k characters)"}, 400 - - p.body_html = body_html - - if v.id == p.author_id and v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in f'{p.body}{p.title}'.lower() and not p.is_banned: - - p.is_banned = True - p.ban_reason = "AutoJanny" - - g.db.add(p) - - body = AGENDAPOSTER_MSG.format(username=v.username, type='post', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) - - body_jannied_html = sanitize(body) - - c_jannied = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=p.id, - level=1, - over_18=False, - is_bot=True, - app_id=None, - is_pinned='AutoJanny', - distinguish_level=6, - body_html=body_jannied_html, - ghost=p.ghost - ) - - g.db.add(c_jannied) - g.db.flush() - - c_jannied.top_comment_id = c_jannied.id - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - if not p.private and not p.ghost: - notify_users = NOTIFY_USERS(f'{p.title} {p.body}', v) - if notify_users: - cid = notif_comment2(p) - for x in notify_users: - add_notif(cid, x) - - if v.id == p.author_id: - if int(time.time()) - p.created_utc > 60 * 3: p.edited_utc = int(time.time()) - g.db.add(p) - else: - ma=ModAction( - kind="edit_post", - user_id=v.id, - target_submission_id=p.id - ) - g.db.add(ma) - - g.db.commit() - - return redirect(p.permalink) - -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: pass - - -def thumbnail_thread(pid): - - db = db_session() - - def expand_url(post_url, fragment_url): - - if fragment_url.startswith("https://"): - return fragment_url - elif fragment_url.startswith("https://"): - return f"https://{fragment_url.split('https://')[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}" - - post = db.query(Submission).filter_by(id=pid).one_or_none() - - if not post or not post.url: - time.sleep(5) - post = db.query(Submission).filter_by(id=pid).one_or_none() - - if not post or not post.url: return - - fetch_url = post.url - - if fetch_url.startswith('/'): fetch_url = f"{SITE_FULL}{fetch_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, timeout=5, proxies=proxies) - 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, 'lxml') - - thumb_candidate_urls=[] - - meta_tags = [ - "drama: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, timeout=5, proxies=proxies) - 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 - - size = len(image.fp.read()) - if size > 8 * 1024 * 1024: - db.close() - return - - name = f'/images/{time.time()}'.replace('.','') + '.webp' - - with open(name, "wb") as file: - for chunk in image_req.iter_content(1024): - file.write(chunk) - - post.thumburl = process_image(name, resize=100) - db.add(post) - db.commit() - - if SITE_NAME == 'rDrama': - for t in ("submission","comment"): - word = random.choice(('rdrama','marsey')) - - try: - data = requests.get(f'https://api.pushshift.io/reddit/{t}/search?html_decode=true&q={word}&size=1', timeout=5).json()["data"] - except: break - - for i in data: - - if i["subreddit"] == 'PokemonGoRaids': continue - - body_html = f'''

New site mention: https://old.reddit.com{i["permalink"]}?context=89

''' - - existing_comment = db.query(Comment.id).filter_by(author_id=NOTIFICATIONS_ID, parent_submission=None, body_html=body_html).one_or_none() - if existing_comment: break - - new_comment = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=None, - body_html=body_html, - distinguish_level=6 - ) - db.add(new_comment) - db.flush() - - new_comment.top_comment_id = new_comment.id - - - admins = db.query(User).filter(User.admin_level > 0).all() - for admin in admins: - notif = Notification(comment_id=new_comment.id, user_id=admin.id) - db.add(notif) - - k,val = random.choice(tuple(REDDIT_NOTIFS.items())) - - try: - data = requests.get(f'https://api.pushshift.io/reddit/{t}/search?html_decode=true&q={k}&size=1', timeout=5).json()["data"] - except: break - - for i in data: - - body_html = f'''

New mention of you: https://old.reddit.com{i["permalink"]}?context=89

''' - - existing_comment = db.query(Comment.id).filter_by(author_id=NOTIFICATIONS_ID, parent_submission=None,body_html=body_html).one_or_none() - if existing_comment: break - - new_comment = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=None, - body_html=body_html, - distinguish_level=6 - ) - - db.add(new_comment) - db.flush() - - new_comment.top_comment_id = new_comment.id - - - notif = Notification(comment_id=new_comment.id, user_id=val) - db.add(notif) - - - if SITE == 'pcmemes.net': - for t in ("submission","comment"): - - try: - data = requests.get(f'https://api.pushshift.io/reddit/{t}/search?html_decode=true&q=pcmemes.net&size=1', timeout=5).json()["data"] - except: break - - for i in data: - body_html = f'''

New site mention: https://old.reddit.com{i["permalink"]}?context=89

''' - - existing_comment = db.query(Comment.id).filter_by(author_id=NOTIFICATIONS_ID, parent_submission=None, body_html=body_html).one_or_none() - - if existing_comment: break - - new_comment = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=None, - body_html=body_html, - distinguish_level=6 - ) - db.add(new_comment) - db.flush() - - new_comment.top_comment_id = new_comment.id - - - admins = db.query(User).filter(User.admin_level > 2).all() - for admin in admins: - notif = Notification(comment_id=new_comment.id, user_id=admin.id) - db.add(notif) - - db.commit() - db.close() - stdout.flush() - return - - -@app.post("/is_repost") -def api_is_repost(): - - url = request.values.get('url') - if not url: abort(400) - - for rd in ("://reddit.com", "://new.reddit.com", "://www.reddit.com", "://redd.it", "://libredd.it", "://teddit.net"): - url = url.replace(rd, "://old.reddit.com") - - url = url.replace("nitter.net", "twitter.com").replace("old.reddit.com/gallery", "reddit.com/gallery").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").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("m.wikipedia.org", "wikipedia.org").replace("https://m.youtube", "https://youtube").replace("https://www.youtube", "https://youtube").replace("https://www.twitter", "https://twitter").replace("https://www.instagram", "https://instagram").replace("https://www.tiktok", "https://tiktok") - - 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") - - 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 - if domain in ('old.reddit.com','twitter.com','instagram.com','tiktok.com'): - new_url = ParseResult(scheme="https", - netloc=parsed_url.netloc, - path=parsed_url.path, - params=parsed_url.params, - query=None, - fragment=parsed_url.fragment) - else: - qd = parse_qs(parsed_url.query) - filtered = {k: val for k, val in qd.items() if not k.startswith('utm_') and not k.startswith('ref_')} - - 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) - - if url.endswith('/'): url = url[:-1] - - search_url = url.replace('%', '').replace('\\', '').replace('_', '\_').strip() - repost = g.db.query(Submission).filter( - Submission.url.ilike(search_url), - Submission.deleted_utc == 0, - Submission.is_banned == False - ).first() - if repost: return {'permalink': repost.permalink} - else: return {'permalink': ''} - -@app.post("/submit") -@app.post("/h//submit") -@limiter.limit("1/second;2/minute;10/hour;50/day") -@limiter.limit("1/second;2/minute;10/hour;50/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def submit_post(v, sub=None): - - title = request.values.get("title", "").strip()[:500].replace('‎','') - - url = request.values.get("url", "").strip() - - body = request.values.get("body", "").strip().replace('‎','') - - def error(error): - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": error}, 403 - - SUBS = [x[0] for x in g.db.query(Sub.name).order_by(Sub.name).all()] - return render_template("submit.html", SUBS=SUBS, v=v, error=error, title=title, url=url, body=body), 400 - - - sub = request.values.get("sub") - if sub: sub = sub.replace('/h/','').replace('s/','') - - if sub and sub != 'none': - sname = sub.strip().lower() - sub = g.db.query(Sub.name).filter_by(name=sname).one_or_none() - if not sub: return error(f"/h/{sname} not found!") - sub = sub[0] - if v.exiled_from(sub): return error(f"You're exiled from /h/{sub}") - else: sub = None - - if v.is_suspended: return error("You can't perform this action while banned.") - - if v.agendaposter and not v.marseyawarded: title = torture_ap(title, v.username) - - title_html = filter_emojis_only(title, graceful=True) - - if v.marseyawarded and not marseyaward_title_regex.fullmatch(title_html): - return error("You can only type marseys!") - - if len(title_html) > 1500: return error("Rendered title is too big!") - - if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): - return error("You have to type more than 280 characters!") - elif v.bird and len(body) > 140: - return error("You have to type less than 140 characters!") - - - embed = None - - if url: - for rd in ("://reddit.com", "://new.reddit.com", "://www.reddit.com", "://redd.it", "://libredd.it", "://teddit.net"): - url = url.replace(rd, "://old.reddit.com") - - url = url.replace("nitter.net", "twitter.com").replace("old.reddit.com/gallery", "reddit.com/gallery").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").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("m.wikipedia.org", "wikipedia.org").replace("https://m.youtube", "https://youtube").replace("https://www.youtube", "https://youtube").replace("https://www.twitter", "https://twitter").replace("https://www.instagram", "https://instagram").replace("https://www.tiktok", "https://tiktok") - - 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") - - 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 - if domain in ('old.reddit.com','twitter.com','instagram.com','tiktok.com'): - new_url = ParseResult(scheme="https", - netloc=parsed_url.netloc, - path=parsed_url.path, - params=parsed_url.params, - query=None, - fragment=parsed_url.fragment) - else: - qd = parse_qs(parsed_url.query) - filtered = {k: val for k, val in qd.items() if not k.startswith('utm_') and not k.startswith('ref_')} - - 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) - - if url.endswith('/'): url = url[:-1] - - search_url = url.replace('%', '').replace('\\', '').replace('_', '\_').strip() - repost = g.db.query(Submission).filter( - Submission.url.ilike(search_url), - Submission.deleted_utc == 0, - Submission.is_banned == False - ).first() - if repost and SITE != 'localhost': return redirect(repost.permalink) - - domain_obj = get_domain(domain) - if not domain_obj: domain_obj = get_domain(domain+parsed_url.path) - - if domain_obj: - reason = f"Remove the {domain_obj.domain} link from your post and try again. {domain_obj.reason}" - return error(reason) - elif "twitter.com" == domain: - try: embed = requests.get("https://publish.twitter.com/oembed", params={"url":url, "omit_script":"t"}, timeout=5).json()["html"] - except: pass - elif url.startswith('https://youtube.com/watch?v='): - url = unquote(url).replace('?t', '&t') - yt_id = url.split('https://youtube.com/watch?v=')[1].split('&')[0].split('%')[0] - - if yt_id_regex.fullmatch(yt_id): - req = requests.get(f"https://www.googleapis.com/youtube/v3/videos?id={yt_id}&key={YOUTUBE_KEY}&part=contentDetails", timeout=5).json() - if req.get('items'): - params = parse_qs(urlparse(url).query) - t = params.get('t', params.get('start', [0]))[0] - if isinstance(t, str): t = t.replace('s','') - - embed = f'' - - 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 = str(int(id)) - - - if not url and not request.values.get("body") and not request.files.get("file") and not request.files.get("file2"): - return error("Please enter a url or some text.") - - if not title: - return error("Please enter a better title.") - - - elif len(title) > 500: - return error("There's a 500 character limit for titles.") - - dup = g.db.query(Submission).filter( - Submission.author_id == v.id, - Submission.deleted_utc == 0, - Submission.title == title, - Submission.url == url, - Submission.body == body - ).one_or_none() - - if dup and SITE != 'localhost': return redirect(dup.permalink) - - now = int(time.time()) - cutoff = now - 60 * 60 * 24 - - - similar_posts = g.db.query(Submission).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).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 banned for **1 day** for the following reason:\n\n> Too much spam!" - send_repeatable_notification(v.id, text) - - v.ban(reason="Spamming.", - days=1) - - for post in similar_posts + similar_urls: - post.is_banned = True - post.is_pinned = False - post.ban_reason = "AutoJanny" - g.db.add(post) - ma=ModAction( - user_id=AUTOJANNY_ID, - target_submission_id=post.id, - kind="ban_post", - _note="spam" - ) - g.db.add(ma) - return redirect("/notifications") - - if len(str(body)) > 20000: - return error("There's a 20000 character limit for text body.") - - if len(url) > 2048: - return error("There's a 2048 character limit for URLs.") - - if v and v.admin_level > 2: - bet_options = [] - for i in bet_regex.finditer(body): - bet_options.append(i.group(1)) - body = body.replace(i.group(0), "") - - options = [] - for i in poll_regex.finditer(body): - options.append(i.group(1)) - body = body.replace(i.group(0), "") - - choices = [] - for i in choice_regex.finditer(body): - choices.append(i.group(1)) - body = body.replace(i.group(0), "") - - if v.agendaposter and not v.marseyawarded: body = torture_ap(body, v.username) - - if request.files.get("file2") and request.headers.get("cf-ipcountry") != "T1": - files = request.files.getlist('file2')[:4] - for file in files: - if file.content_type.startswith('image/'): - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - body += f"\n\n![]({process_image(name)})" - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data'] - except requests.Timeout: return error("Video upload timed out, please try again!") - try: url = req['link'] - except: - err = req['error'] - if err == 'File exceeds max duration': err += ' (60 seconds)' - return error(err) - if url.endswith('.'): url += 'mp4' - body += f"\n\n{url}" - else: - return error("Image/Video files only.") - - body_html = sanitize(body) - - if v.marseyawarded and marseyaward_body_regex.search(body_html): - return error("You can only type marseys!") - - if len(body_html) > 40000: return error("Submission body_html too long! (max 40k characters)") - - club = bool(request.values.get("club","")) - - if embed and len(embed) > 1500: embed = None - - is_bot = bool(request.headers.get("Authorization")) or (SITE == 'pcmemes.net' and v.id == SNAPPY_ID) - - if request.values.get("ghost") and v.coins >= 100: - v.coins -= 100 - ghost = True - else: ghost = False - - post = Submission( - private=bool(request.values.get("private","")), - club=club, - author_id=v.id, - over_18=bool(request.values.get("over_18","")), - new=bool(request.values.get("new","")), - app_id=v.client.application.id if v.client else None, - is_bot = is_bot, - url=url, - body=body[:20000], - body_html=body_html, - embed_url=embed, - title=title[:500], - title_html=title_html, - sub=sub, - ghost=ghost - ) - - g.db.add(post) - g.db.flush() - - if blackjack and any(i in f'{post.body} {post.title} {post.url}'.lower() for i in blackjack.split()): - v.shadowbanned = 'AutoJanny' - g.db.add(v) - send_repeatable_notification(CARP_ID, post.permalink) - - if v and v.admin_level > 2: - for option in bet_options: - bet_option = Comment(author_id=AUTOBETTER_ID, - parent_submission=post.id, - level=1, - body_html=filter_emojis_only(option), - upvotes=0, - is_bot=True - ) - - g.db.add(bet_option) - - for option in options: - c = Comment(author_id=AUTOPOLLER_ID, - parent_submission=post.id, - level=1, - body_html=filter_emojis_only(option), - upvotes=0, - is_bot=True - ) - g.db.add(c) - - for choice in choices: - c = Comment(author_id=AUTOCHOICE_ID, - parent_submission=post.id, - level=1, - body_html=filter_emojis_only(choice), - upvotes=0, - is_bot=True - ) - g.db.add(c) - - vote = Vote(user_id=v.id, - vote_type=1, - submission_id=post.id - ) - g.db.add(vote) - - if request.files.get('file') and request.headers.get("cf-ipcountry") != "T1": - - file = request.files['file'] - - if file.content_type.startswith('image/'): - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - post.url = process_image(name) - - name2 = name.replace('.webp', 'r.webp') - copyfile(name, name2) - post.thumburl = process_image(name2, resize=100) - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data'] - except requests.Timeout: return error("Video upload timed out, please try again!") - try: url = req['link'] - except: - err = req['error'] - if err == 'File exceeds max duration': err += ' (60 seconds)' - return error(err) - if url.endswith('.'): url += 'mp4' - post.url = url - else: - return error("Image/Video files only.") - - if not post.thumburl and post.url: - gevent.spawn(thumbnail_thread, post.id) - - - - - if not post.private and not post.ghost: - - notify_users = NOTIFY_USERS(f'{title} {body}', v) - - if notify_users: - cid = notif_comment2(post) - for x in notify_users: - add_notif(cid, x) - - if (request.values.get('followers') or is_bot) and v.followers: - text = f"@{v.username} has made a new post: [{post.title}]({post.shortlink})" - if post.sub: text += f" in /h/{post.sub}" - - cid = notif_comment(text, autojanny=True) - for follow in v.followers: - user = get_account(follow.user_id) - if post.club and not user.paid_dues: continue - add_notif(cid, user.id) - - - - - - if v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in f'{post.body}{post.title}'.lower(): - post.is_banned = True - post.ban_reason = "AutoJanny" - - body = AGENDAPOSTER_MSG.format(username=v.username, type='post', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) - - body_jannied_html = sanitize(body) - - - - c_jannied = Comment(author_id=NOTIFICATIONS_ID, - parent_submission=post.id, - level=1, - over_18=False, - is_bot=True, - app_id=None, - is_pinned='AutoJanny', - distinguish_level=6, - body_html=body_jannied_html, - ) - - g.db.add(c_jannied) - g.db.flush() - - c_jannied.top_comment_id = c_jannied.id - - n = Notification(comment_id=c_jannied.id, user_id=v.id) - g.db.add(n) - - - - if not (post.sub and g.db.query(Exile.user_id).filter_by(user_id=SNAPPY_ID, sub=post.sub).one_or_none()): - if post.sub == 'dankchristianmemes': - body = random.choice(christian_emojis) - elif v.id == CARP_ID: - if random.random() < 0.02: body = "i love you carp" - else: body = ":#marseyfuckoffcarp:" - elif v.id == LAWLZ_ID: - if random.random() < 0.5: body = "wow, this lawlzpost sucks!" - else: body = "wow, a good lawlzpost for once!" - else: - body = random.choice(snappyquotes) - if body.startswith('▼'): - body = body[1:] - vote = Vote(user_id=SNAPPY_ID, - vote_type=-1, - submission_id=post.id, - real = True - ) - g.db.add(vote) - post.downvotes += 1 - if body.startswith('OP is a Trump supporter'): - flag = Flag(post_id=post.id, user_id=SNAPPY_ID, reason='Trump supporter') - g.db.add(flag) - elif body.startswith('▲'): - body = body[1:] - vote = Vote(user_id=SNAPPY_ID, - vote_type=1, - submission_id=post.id, - real = True - ) - g.db.add(vote) - post.upvotes += 1 - - - body += "\n\n" - - if post.url: - if post.url.startswith('https://old.reddit.com/r/'): - rev = post.url.replace('https://old.reddit.com/', '') - rev = f"* [unddit.com](https://unddit.com/{rev})\n" - elif post.url.startswith("https://old.reddit.com/u/"): - rev = post.url.replace('https://old.reddit.com/u/', '') - rev = f"* [camas.github.io](https://camas.github.io/reddit-search/#\u007b\"author\":\"{rev}\",\"resultSize\":100\u007d)\n" - else: rev = '' - - newposturl = post.url - if newposturl.startswith('/'): newposturl = f"{SITE_FULL}{newposturl}" - body += f"Snapshots:\n\n{rev}* [archive.org](https://web.archive.org/{newposturl})\n* [archive.ph](https://archive.ph/?url={quote(newposturl)}&run=1) (click to archive)\n* [ghostarchive.org](https://ghostarchive.org/search?term={quote(newposturl)}) (click to archive)\n\n" - gevent.spawn(archiveorg, newposturl) - - captured = [] - for i in list(snappy_url_regex.finditer(post.body_html))[:20]: - if i.group(0) in captured: continue - captured.append(i.group(0)) - - href = i.group(1) - if not href: continue - - title = i.group(2) - if "Snapshots:\n\n" not in body: body += "Snapshots:\n\n" - - if f'**[{title}]({href})**:\n\n' not in body: - body += f'**[{title}]({href})**:\n\n' - if href.startswith('https://old.reddit.com/r/'): - body += f'* [unddit.com](https://unddit.com/{href.replace("https://old.reddit.com/", "")})\n' - if href.startswith('https://old.reddit.com/u/'): - rev = post.url.replace('https://old.reddit.com/u/', '') - body += f"* [camas.github.io](https://camas.github.io/reddit-search/#\u007b\"author\":\"{rev}\",\"resultSize\":100\u007d)\n" - body += f'* [archive.org](https://web.archive.org/{href})\n' - body += f'* [archive.ph](https://archive.ph/?url={quote(href)}&run=1) (click to archive)\n' - body += f'* [ghostarchive.org](https://ghostarchive.org/search?term={quote(href)}) (click to archive)\n\n' - gevent.spawn(archiveorg, href) - - body_html = sanitize(body) - - if len(body_html) < 40000: - c = Comment(author_id=SNAPPY_ID, - distinguish_level=6, - parent_submission=post.id, - level=1, - over_18=False, - is_bot=True, - app_id=None, - body_html=body_html - ) - - g.db.add(c) - - snappy = g.db.query(User).filter_by(id = SNAPPY_ID).one_or_none() - snappy.comment_count += 1 - snappy.coins += 1 - g.db.add(snappy) - - if body.startswith('!slots1000'): - check_for_slots_command(body, snappy, c) - - g.db.flush() - - c.top_comment_id = c.id - - post.comment_count += 1 - post.replies = [c] - - v.post_count = g.db.query(Submission.id).filter_by(author_id=v.id, is_banned=False, deleted_utc=0).count() - g.db.add(v) - - if v.id == PIZZASHILL_ID: - for uid in PIZZA_VOTERS: - autovote = Vote(user_id=uid, submission_id=post.id, vote_type=1) - g.db.add(autovote) - v.coins += 3 - v.truecoins += 3 - g.db.add(v) - post.upvotes += 3 - g.db.add(post) - - g.db.commit() - - cache.delete_memoized(frontlist) - cache.delete_memoized(User.userpagelisting) - - if SITE == 'cringetopia.org': - send_cringetopia_message(post.permalink) - elif v.admin_level > 0 and ("[changelog]" in post.title.lower() or "(changelog)" in post.title.lower()) and not post.private: - send_discord_message(post.permalink) - cache.delete_memoized(changeloglist) - - if request.headers.get("Authorization"): return post.json - else: - post.voted = 1 - if post.new or 'megathread' in post.title.lower(): sort = 'new' - else: sort = v.defaultsortingcomments - return render_template('submission.html', v=v, p=post, sort=sort, render_replies=True, offset=0, success=True, sub=post.subr) - - -@app.post("/delete_post/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def delete_post_pid(pid, v): - - post = get_post(pid) - if 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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def undelete_post_pid(pid, v): - post = get_post(pid) - if 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 -def toggle_comment_nsfw(cid, v): - - comment = g.db.query(Comment).filter_by(id=cid).one_or_none() - if comment.author_id != v.id and not v.admin_level > 1: abort(403) - comment.over_18 = not comment.over_18 - g.db.add(comment) - - 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 -def toggle_post_nsfw(pid, v): - - post = get_post(pid) - - if post.author_id != v.id and not v.admin_level > 1: - 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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def save_post(pid, v): - - post=get_post(pid) - - save = g.db.query(SaveRelationship).filter_by(user_id=v.id, submission_id=post.id).one_or_none() - - if not save: - new_save=SaveRelationship(user_id=v.id, submission_id=post.id) - g.db.add(new_save) - g.db.commit() - - return {"message": "Post saved!"} - -@app.post("/unsave_post/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def unsave_post(pid, v): - - post=get_post(pid) - - save = g.db.query(SaveRelationship).filter_by(user_id=v.id, submission_id=post.id).one_or_none() - - if save: - g.db.delete(save) - g.db.commit() - - return {"message": "Post unsaved!"} - -@app.post("/pin/") -@auth_required -def api_pin_post(post_id, v): - - post = g.db.query(Submission).filter_by(id=post_id).one_or_none() - if post: - if v.id != post.author_id: return {"error": "Only the post author's can do that!"} - post.is_pinned = not post.is_pinned - g.db.add(post) - - cache.delete_memoized(User.userpagelisting) - - g.db.commit() - if post.is_pinned: return {"message": "Post pinned!"} - else: return {"message": "Post unpinned!"} - return {"error": "Post not found!"} - - -@app.get("/submit/title") -@limiter.limit("6/minute") -@limiter.limit("6/minute", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def get_post_title(v): - - url = request.values.get("url") - if not url: abort(400) - - try: x = requests.get(url, headers=titleheaders, timeout=5, proxies=proxies) - except: abort(400) - - soup = BeautifulSoup(x.content, 'lxml') - - title = soup.find('title') - if not title: abort(400) - - return {"url": url, "title": title.string} +import time +import gevent +import requests +from files.helpers.wrappers import * +from files.helpers.sanitize import * +from files.helpers.alerts import * +from files.helpers.discord import send_discord_message +from files.helpers.const import * +from files.helpers.slots 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, unquote +from os import path +import requests +from shutil import copyfile +from sys import stdout +import os + +if SITE_NAME == 'PCM': snappyquotes = [] +else: snappyquotes = [f':#{x}:' for x in marseys_const2] + +if path.exists(f'snappy_{SITE_NAME}.txt'): + with open(f'snappy_{SITE_NAME}.txt', "r", encoding="utf-8") as f: + snappyquotes += f.read().split("\n{[para]}\n") + +discounts = { + 69: 0.02, + 70: 0.04, + 71: 0.06, + 72: 0.08, + 73: 0.10, +} + +titleheaders = {"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"} + + +@app.post("/toggle_club/") +@auth_required +def toggle_club(pid, v): + + post = get_post(pid) + if post.author_id != v.id and v.admin_level < 2: abort(403) + + post.club = not post.club + g.db.add(post) + + 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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def publish(pid, v): + post = get_post(pid) + if not post.private: return {"message": "Post published!"} + + if post.author_id != v.id: abort(403) + post.private = False + post.created_utc = int(time.time()) + g.db.add(post) + + if not post.ghost: + notify_users = NOTIFY_USERS(f'{post.title} {post.body}', v) + + if notify_users: + cid = notif_comment2(post) + for x in notify_users: + add_notif(cid, x) + + if v.followers: + text = f"@{v.username} has made a new post: [{post.title}]({post.shortlink})" + if post.sub: text += f" in /h/{post.sub}" + + cid = notif_comment(text, autojanny=True) + for follow in v.followers: + user = get_account(follow.user_id) + if post.club and not user.paid_dues: continue + add_notif(cid, user.id) + + g.db.commit() + + cache.delete_memoized(frontlist) + cache.delete_memoized(User.userpagelisting) + + if v.admin_level > 0 and ("[changelog]" in post.title.lower() or "(changelog)" in post.title.lower()): + send_discord_message(post.permalink) + cache.delete_memoized(changeloglist) + + return redirect(post.permalink) + +@app.get("/submit") +@app.get("/h//submit") +@auth_required +def submit_get(v, sub=None): + if sub: sub = g.db.query(Sub.name).filter_by(name=sub.strip().lower()).one_or_none() + + if request.path.startswith('/h/') and not sub: abort(404) + + SUBS = [x[0] for x in g.db.query(Sub.name).order_by(Sub.name).all()] + + return render_template("submit.html", SUBS=SUBS, v=v, sub=sub) + +@app.get("/post/") +@app.get("/post//") +@app.get("/h//post/") +@app.get("/h//post//") +@app.get("/logged_out/post/") +@app.get("/logged_out/post//") +@app.get("/logged_out/h//post/") +@app.get("/logged_out/h//post//") +@auth_desired +def post_id(pid, anything=None, v=None, sub=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'): return redirect(request.full_path.replace('/logged_out','')) + + try: pid = int(pid) + except Exception as e: pass + + + 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 session.get('over_18', 0) < int(time.time()): + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error":"Must be 18+ to view"}, 451 + return render_template("errors/nsfw.html", v=v) + + if post.new or 'megathread' in post.title.lower(): defaultsortingcomments = 'new' + elif v: defaultsortingcomments = v.defaultsortingcomments + else: defaultsortingcomments = "top" + sort = request.values.get("sort", defaultsortingcomments) + + if post.club and not (v and (v.paid_dues or v.id == post.author_id)): abort(403) + + if v: + votes = g.db.query(CommentVote).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.target_id, + blocked.c.target_id, + ) + + if not (v and v.shadowbanned) and not (v and v.admin_level > 2): + comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) + + comments=comments.filter(Comment.parent_submission == post.id, Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID))).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.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) + + pinned = [c[0] for c in comments.filter(Comment.is_pinned != None).all()] + + comments = comments.filter(Comment.level == 1, Comment.is_pinned == None) + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc) + elif sort == "controversial": + comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) + elif sort == "top": + comments = comments.order_by(Comment.realupvotes.desc()) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + first = [c[0] for c in comments.filter(or_(and_(Comment.slots_result == None, Comment.blackjack_result == None, Comment.wordle_result == None), func.length(Comment.body_html) > 100)).all()] + second = [c[0] for c in comments.filter(or_(Comment.slots_result != None, Comment.blackjack_result != None, Comment.wordle_result != None), func.length(Comment.body_html) <= 100).all()] + comments = first + second + else: + pinned = g.db.query(Comment).filter(Comment.parent_submission == post.id, Comment.is_pinned != None).all() + + comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.parent_submission == post.id, Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID)), Comment.level == 1, Comment.is_pinned == None) + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc) + elif sort == "controversial": + comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) + elif sort == "top": + comments = comments.order_by(Comment.realupvotes.desc()) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + first = comments.filter(or_(and_(Comment.slots_result == None, Comment.blackjack_result == None, Comment.wordle_result == None), func.length(Comment.body_html) > 100)).all() + second = comments.filter(or_(Comment.slots_result != None, Comment.blackjack_result != None, Comment.wordle_result != None), func.length(Comment.body_html) <= 100).all() + comments = first + second + + offset = 0 + ids = set() + + if post.comment_count > 60 and not request.headers.get("Authorization") and not request.values.get("all"): + comments2 = [] + count = 0 + if post.created_utc > 1638672040: + for comment in comments: + comments2.append(comment) + ids.add(comment.id) + count += g.db.query(Comment).filter_by(parent_submission=post.id, top_comment_id=comment.id).count() + 1 + if count > 50: break + else: + for comment in comments: + comments2.append(comment) + ids.add(comment.id) + count += g.db.query(Comment).filter_by(parent_submission=post.id, parent_comment_id=comment.id).count() + 1 + if count > 10: break + + if len(comments) == len(comments2): offset = 0 + else: offset = 1 + comments = comments2 + + for pin in pinned: + if pin.is_pinned_utc and int(time.time()) > pin.is_pinned_utc: + pin.is_pinned = None + pin.is_pinned_utc = None + g.db.add(pin) + pinned.remove(pin) + + post.replies = pinned + comments + + post.views += 1 + g.db.add(post) + g.db.commit() + if request.headers.get("Authorization"): return post.json + else: + if post.is_banned and not (v and (v.admin_level > 1 or post.author_id == v.id)): template = "submission_banned.html" + else: template = "submission.html" + return render_template(template, v=v, p=post, ids=list(ids), sort=sort, render_replies=True, offset=offset, sub=post.subr, fart=app.config['SETTINGS']['Fart mode']) + +@app.get("/viewmore///") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@auth_desired +def viewmore(v, pid, sort, offset): + try: pid = int(pid) + except: abort(400) + post = get_post(pid, v=v) + if post.club and not (v and (v.paid_dues or v.id == post.author_id)): abort(403) + + offset = int(offset) + try: ids = set(int(x) for x in request.values.get("ids").split(',')) + except: abort(400) + + if v: + votes = g.db.query(CommentVote).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.target_id, + blocked.c.target_id, + ).filter(Comment.parent_submission == pid, Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID)), Comment.is_pinned == None, Comment.id.notin_(ids)) + + if not (v and v.shadowbanned) and not (v and v.admin_level > 2): + comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None) + + 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 + ) + + 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) + + comments = comments.filter(Comment.level == 1) + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc) + elif sort == "controversial": + comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) + elif sort == "top": + comments = comments.order_by(Comment.realupvotes.desc()) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + first = [c[0] for c in comments.filter(or_(and_(Comment.slots_result == None, Comment.blackjack_result == None, Comment.wordle_result == None), func.length(Comment.body_html) > 100)).all()] + second = [c[0] for c in comments.filter(or_(Comment.slots_result != None, Comment.blackjack_result != None, Comment.wordle_result != None), func.length(Comment.body_html) <= 100).all()] + comments = first + second + else: + comments = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.parent_submission == pid, Comment.author_id.notin_((AUTOPOLLER_ID, AUTOBETTER_ID, AUTOCHOICE_ID)), Comment.level == 1, Comment.is_pinned == None, Comment.id.notin_(ids)) + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc) + elif sort == "controversial": + comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) + elif sort == "top": + comments = comments.order_by(Comment.realupvotes.desc()) + elif sort == "bottom": + comments = comments.order_by(Comment.upvotes - Comment.downvotes) + + first = comments.filter(or_(and_(Comment.slots_result == None, Comment.blackjack_result == None, Comment.wordle_result == None), func.length(Comment.body_html) > 100)).all() + second = comments.filter(or_(Comment.slots_result != None, Comment.blackjack_result != None, Comment.wordle_result != None), func.length(Comment.body_html) <= 100).all() + comments = first + second + comments = comments[offset:] + + comments2 = [] + count = 0 + if post.created_utc > 1638672040: + for comment in comments: + comments2.append(comment) + ids.add(comment.id) + count += g.db.query(Comment).filter_by(parent_submission=post.id, top_comment_id=comment.id).count() + 1 + if count > 50: break + else: + for comment in comments: + comments2.append(comment) + ids.add(comment.id) + count += g.db.query(Comment).filter_by(parent_submission=post.id, parent_comment_id=comment.id).count() + 1 + if count > 10: break + + if len(comments) == len(comments2): offset = 0 + else: offset += 1 + comments = comments2 + + return render_template("comments.html", v=v, comments=comments, p=post, ids=list(ids), render_replies=True, pid=pid, sort=sort, offset=offset, ajax=True) + + +@app.get("/morecomments/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@auth_desired +def morecomments(v, cid): + try: cid = int(cid) + except: abort(400) + + tcid = g.db.query(Comment.top_comment_id).filter_by(id=cid).one_or_none()[0] + + if v: + votes = g.db.query(CommentVote).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.target_id, + blocked.c.target_id, + ).filter(Comment.top_comment_id == tcid, Comment.level > 9).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 = [] + dump = [] + 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 + if c[0].parent_comment_id == int(cid): output.append(comment) + else: dump.append(comment) + comments = output + else: + c = g.db.query(Comment).filter_by(id=cid).one_or_none() + comments = c.replies + + if comments: p = comments[0].post + else: p = None + + return render_template("comments.html", v=v, comments=comments, p=p, render_replies=True, ajax=True) + +@app.post("/edit_post/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def edit_post(pid, v): + p = get_post(pid) + + if p.author_id != v.id and not (v.admin_level > 1 and v.admin_level > 2): abort(403) + + title = request.values.get("title", "").strip().replace('‎','') + + body = request.values.get("body", "").strip().replace('‎','') + + if len(body) > 20000: return {"error":"Character limit is 20000!"}, 403 + + if v.id == p.author_id: + if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): + return {"error":"You have to type more than 280 characters!"}, 403 + elif v.bird and len(body) > 140: + return {"error":"You have to type less than 140 characters!"}, 403 + + if title != p.title: + if v.id == p.author_id and v.agendaposter and not v.marseyawarded: title = torture_ap(title, v.username) + + title_html = filter_emojis_only(title, edit=True) + + if v.id == p.author_id and v.marseyawarded and not marseyaward_title_regex.fullmatch(title_html): + return {"error":"You can only type marseys!"}, 403 + + p.title = title[:500] + p.title_html = title_html + + if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + files = request.files.getlist('file')[:4] + for file in files: + if file.content_type.startswith('image/'): + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + url = process_image(v.patron, name) + body += f"\n\n![]({url})" + elif file.content_type.startswith('video/'): + if file.content_type == 'video/webm': + file.save("video.mp4") + else: + file.save("unsanitized.mp4") + os.system(f'ffmpeg -y -loglevel warning -i unsanitized.mp4 -map_metadata -1 -c:v copy -c:a copy video.mp4') + with open("video.mp4", 'rb') as f: + try: req = requests.request("POST", "https://pomf2.lain.la/upload.php", files={'files[]': f}, timeout=5).json() + except requests.Timeout: return {"error": "Video upload timed out, please try again!"} + try: url = req['files'][0]['url'] + except: return {"error": req['description']}, 400 + body += f"\n\n{url}" + else: return {"error": "Image/Video files only"}, 400 + + if body != p.body: + if v.id == p.author_id and v.agendaposter and not v.marseyawarded: body = torture_ap(body, v.username) + + for i in poll_regex.finditer(body): + body = body.replace(i.group(0), "") + c = Comment(author_id=AUTOPOLLER_ID, + parent_submission=p.id, + level=1, + body_html=filter_emojis_only(i.group(1)), + upvotes=0, + is_bot=True + ) + g.db.add(c) + + for i in choice_regex.finditer(body): + body = body.replace(i.group(0), "") + c = Comment(author_id=AUTOCHOICE_ID, + parent_submission=p.id, + level=1, + body_html=filter_emojis_only(i.group(1)), + upvotes=0, + is_bot=True + ) + g.db.add(c) + + body_html = sanitize(body, edit=True) + + if v.id == p.author_id and v.marseyawarded and marseyaward_body_regex.search(body_html): + return {"error":"You can only type marseys!"}, 403 + + + p.body = body + + if blackjack and any(i in f'{p.body} {p.title} {p.url}'.lower() for i in blackjack.split()): + v.shadowbanned = 'AutoJanny' + g.db.add(v) + send_repeatable_notification(CARP_ID, p.permalink) + + if len(body_html) > 40000: return {"error":"Submission body_html too long! (max 40k characters)"}, 400 + + p.body_html = body_html + + if v.id == p.author_id and v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in f'{p.body}{p.title}'.lower() and not p.is_banned: + + p.is_banned = True + p.ban_reason = "AutoJanny" + + g.db.add(p) + + body = AGENDAPOSTER_MSG.format(username=v.username, type='post', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) + + body_jannied_html = sanitize(body) + + c_jannied = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=p.id, + level=1, + over_18=False, + is_bot=True, + app_id=None, + is_pinned='AutoJanny', + distinguish_level=6, + body_html=body_jannied_html, + ghost=p.ghost + ) + + g.db.add(c_jannied) + g.db.flush() + + c_jannied.top_comment_id = c_jannied.id + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + if not p.private and not p.ghost: + notify_users = NOTIFY_USERS(f'{p.title} {p.body}', v) + if notify_users: + cid = notif_comment2(p) + for x in notify_users: + add_notif(cid, x) + + if v.id == p.author_id: + if int(time.time()) - p.created_utc > 60 * 3: p.edited_utc = int(time.time()) + g.db.add(p) + else: + ma=ModAction( + kind="edit_post", + user_id=v.id, + target_submission_id=p.id + ) + g.db.add(ma) + + g.db.commit() + + return redirect(p.permalink) + +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: pass + + +def thumbnail_thread(pid): + + db = db_session() + + def expand_url(post_url, fragment_url): + + if fragment_url.startswith("https://"): + return fragment_url + elif fragment_url.startswith("https://"): + return f"https://{fragment_url.split('https://')[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}" + + post = db.query(Submission).filter_by(id=pid).one_or_none() + + if not post or not post.url: + time.sleep(5) + post = db.query(Submission).filter_by(id=pid).one_or_none() + + if not post or not post.url: return + + fetch_url = post.url + + if fetch_url.startswith('/'): fetch_url = f"{SITE_FULL}{fetch_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, timeout=5, proxies=proxies) + 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, 'lxml') + + thumb_candidate_urls=[] + + meta_tags = [ + "drama: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, timeout=5, proxies=proxies) + 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 + + size = len(image.fp.read()) + if size > 8 * 1024 * 1024: + db.close() + return + + name = f'/images/{time.time()}'.replace('.','') + '.webp' + + with open(name, "wb") as file: + for chunk in image_req.iter_content(1024): + file.write(chunk) + + post.thumburl = process_image(0, name, resize=100) + db.add(post) + db.commit() + + if SITE_NAME == 'rDrama': + for t in ("submission","comment"): + word = random.choice(('rdrama','marsey')) + + try: + data = requests.get(f'https://api.pushshift.io/reddit/{t}/search?html_decode=true&q={word}&size=1', timeout=5).json()["data"] + except: break + + for i in data: + + if i["subreddit"] == 'PokemonGoRaids': continue + + body_html = f'''

New site mention: https://old.reddit.com{i["permalink"]}?context=89

''' + + existing_comment = db.query(Comment.id).filter_by(author_id=NOTIFICATIONS_ID, parent_submission=None, body_html=body_html).one_or_none() + if existing_comment: break + + new_comment = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=None, + body_html=body_html, + distinguish_level=6 + ) + db.add(new_comment) + db.flush() + + new_comment.top_comment_id = new_comment.id + + + admins = db.query(User).filter(User.admin_level > 0).all() + for admin in admins: + notif = Notification(comment_id=new_comment.id, user_id=admin.id) + db.add(notif) + + k,val = random.choice(tuple(REDDIT_NOTIFS.items())) + + try: + data = requests.get(f'https://api.pushshift.io/reddit/{t}/search?html_decode=true&q={k}&size=1', timeout=5).json()["data"] + except: break + + for i in data: + + body_html = f'''

New mention of you: https://old.reddit.com{i["permalink"]}?context=89

''' + + existing_comment = db.query(Comment.id).filter_by(author_id=NOTIFICATIONS_ID, parent_submission=None,body_html=body_html).one_or_none() + if existing_comment: break + + new_comment = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=None, + body_html=body_html, + distinguish_level=6 + ) + + db.add(new_comment) + db.flush() + + new_comment.top_comment_id = new_comment.id + + + notif = Notification(comment_id=new_comment.id, user_id=val) + db.add(notif) + + + if SITE == 'pcmemes.net': + for t in ("submission","comment"): + + try: + data = requests.get(f'https://api.pushshift.io/reddit/{t}/search?html_decode=true&q=pcmemes.net&size=1', timeout=5).json()["data"] + except: break + + for i in data: + body_html = f'''

New site mention: https://old.reddit.com{i["permalink"]}?context=89

''' + + existing_comment = db.query(Comment.id).filter_by(author_id=NOTIFICATIONS_ID, parent_submission=None, body_html=body_html).one_or_none() + + if existing_comment: break + + new_comment = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=None, + body_html=body_html, + distinguish_level=6 + ) + db.add(new_comment) + db.flush() + + new_comment.top_comment_id = new_comment.id + + + admins = db.query(User).filter(User.admin_level > 2).all() + for admin in admins: + notif = Notification(comment_id=new_comment.id, user_id=admin.id) + db.add(notif) + + db.commit() + db.close() + stdout.flush() + return + + +@app.post("/is_repost") +def api_is_repost(): + + url = request.values.get('url') + if not url: abort(400) + + for rd in ("://reddit.com", "://new.reddit.com", "://www.reddit.com", "://redd.it", "://libredd.it", "://teddit.net"): + url = url.replace(rd, "://old.reddit.com") + + url = url.replace("nitter.net", "twitter.com").replace("old.reddit.com/gallery", "reddit.com/gallery").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").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("m.wikipedia.org", "wikipedia.org").replace("https://m.youtube", "https://youtube").replace("https://www.youtube", "https://youtube").replace("https://www.twitter", "https://twitter").replace("https://www.instagram", "https://instagram").replace("https://www.tiktok", "https://tiktok") + + 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.co/" in url: url = url.replace(".png", ".webp").replace(".jpg", ".webp").replace(".jpeg", ".webp").replace(".gif", ".webp") + + 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 + if domain in ('old.reddit.com','twitter.com','instagram.com','tiktok.com'): + new_url = ParseResult(scheme="https", + netloc=parsed_url.netloc, + path=parsed_url.path, + params=parsed_url.params, + query=None, + fragment=parsed_url.fragment) + else: + qd = parse_qs(parsed_url.query) + filtered = {k: val for k, val in qd.items() if not k.startswith('utm_') and not k.startswith('ref_')} + + 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) + + if url.endswith('/'): url = url[:-1] + + search_url = url.replace('%', '').replace('\\', '').replace('_', '\_').strip() + repost = g.db.query(Submission).filter( + Submission.url.ilike(search_url), + Submission.deleted_utc == 0, + Submission.is_banned == False + ).first() + if repost: return {'permalink': repost.permalink} + else: return {'permalink': ''} + +@app.post("/submit") +@app.post("/h//submit") +@limiter.limit("1/second;2/minute;10/hour;50/day") +@limiter.limit("1/second;2/minute;10/hour;50/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def submit_post(v, sub=None): + + title = request.values.get("title", "").strip()[:500].replace('‎','') + + url = request.values.get("url", "").strip() + + body = request.values.get("body", "").strip().replace('‎','') + + def error(error): + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": error}, 403 + + SUBS = [x[0] for x in g.db.query(Sub.name).order_by(Sub.name).all()] + return render_template("submit.html", SUBS=SUBS, v=v, error=error, title=title, url=url, body=body), 400 + + + sub = request.values.get("sub") + if sub: sub = sub.replace('/h/','').replace('s/','') + + if sub and sub != 'none': + sname = sub.strip().lower() + sub = g.db.query(Sub.name).filter_by(name=sname).one_or_none() + if not sub: return error(f"/h/{sname} not found!") + sub = sub[0] + if v.exiled_from(sub): return error(f"You're exiled from /h/{sub}") + else: sub = None + + if v.is_suspended: return error("You can't perform this action while banned.") + + if v.agendaposter and not v.marseyawarded: title = torture_ap(title, v.username) + + title_html = filter_emojis_only(title, graceful=True) + + if v.marseyawarded and not marseyaward_title_regex.fullmatch(title_html): + return error("You can only type marseys!") + + if len(title_html) > 1500: return error("Rendered title is too big!") + + if v.longpost and (len(body) < 280 or ' [](' in body or body.startswith('[](')): + return error("You have to type more than 280 characters!") + elif v.bird and len(body) > 140: + return error("You have to type less than 140 characters!") + + + embed = None + + if url: + for rd in ("://reddit.com", "://new.reddit.com", "://www.reddit.com", "://redd.it", "://libredd.it", "://teddit.net"): + url = url.replace(rd, "://old.reddit.com") + + url = url.replace("nitter.net", "twitter.com").replace("old.reddit.com/gallery", "reddit.com/gallery").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").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("m.wikipedia.org", "wikipedia.org").replace("https://m.youtube", "https://youtube").replace("https://www.youtube", "https://youtube").replace("https://www.twitter", "https://twitter").replace("https://www.instagram", "https://instagram").replace("https://www.tiktok", "https://tiktok") + + 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.co/" in url: url = url.replace(".png", ".webp").replace(".jpg", ".webp").replace(".jpeg", ".webp").replace(".gif", ".webp") + + 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 + if domain in ('old.reddit.com','twitter.com','instagram.com','tiktok.com'): + new_url = ParseResult(scheme="https", + netloc=parsed_url.netloc, + path=parsed_url.path, + params=parsed_url.params, + query=None, + fragment=parsed_url.fragment) + else: + qd = parse_qs(parsed_url.query) + filtered = {k: val for k, val in qd.items() if not k.startswith('utm_') and not k.startswith('ref_')} + + 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) + + if url.endswith('/'): url = url[:-1] + + search_url = url.replace('%', '').replace('\\', '').replace('_', '\_').strip() + repost = g.db.query(Submission).filter( + Submission.url.ilike(search_url), + Submission.deleted_utc == 0, + Submission.is_banned == False + ).first() + if repost and SITE != 'localhost': return redirect(repost.permalink) + + domain_obj = get_domain(domain) + if not domain_obj: domain_obj = get_domain(domain+parsed_url.path) + + if domain_obj: + reason = f"Remove the {domain_obj.domain} link from your post and try again. {domain_obj.reason}" + return error(reason) + elif "twitter.com" == domain: + try: embed = requests.get("https://publish.twitter.com/oembed", params={"url":url, "omit_script":"t"}, timeout=5).json()["html"] + except: pass + elif url.startswith('https://youtube.com/watch?v='): + url = unquote(url).replace('?t', '&t') + yt_id = url.split('https://youtube.com/watch?v=')[1].split('&')[0].split('%')[0] + + if yt_id_regex.fullmatch(yt_id): + req = requests.get(f"https://www.googleapis.com/youtube/v3/videos?id={yt_id}&key={YOUTUBE_KEY}&part=contentDetails", timeout=5).json() + if req.get('items'): + params = parse_qs(urlparse(url).query) + t = params.get('t', params.get('start', [0]))[0] + if isinstance(t, str): t = t.replace('s','') + + embed = f'' + + 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 = str(int(id)) + + + if not url and not request.values.get("body") and not request.files.get("file") and not request.files.get("file2"): + return error("Please enter a url or some text.") + + if not title: + return error("Please enter a better title.") + + + elif len(title) > 500: + return error("There's a 500 character limit for titles.") + + dup = g.db.query(Submission).filter( + Submission.author_id == v.id, + Submission.deleted_utc == 0, + Submission.title == title, + Submission.url == url, + Submission.body == body + ).one_or_none() + + if dup and SITE != 'localhost': return redirect(dup.permalink) + + now = int(time.time()) + cutoff = now - 60 * 60 * 24 + + + similar_posts = g.db.query(Submission).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).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 banned for **1 day** for the following reason:\n\n> Too much spam!" + send_repeatable_notification(v.id, text) + + v.ban(reason="Spamming.", + days=1) + + for post in similar_posts + similar_urls: + post.is_banned = True + post.is_pinned = False + post.ban_reason = "AutoJanny" + g.db.add(post) + ma=ModAction( + user_id=AUTOJANNY_ID, + target_submission_id=post.id, + kind="ban_post", + _note="spam" + ) + g.db.add(ma) + return redirect("/notifications") + + if len(str(body)) > 20000: + return error("There's a 20000 character limit for text body.") + + if len(url) > 2048: + return error("There's a 2048 character limit for URLs.") + + if v and v.admin_level > 2: + bet_options = [] + for i in bet_regex.finditer(body): + bet_options.append(i.group(1)) + body = body.replace(i.group(0), "") + + options = [] + for i in poll_regex.finditer(body): + options.append(i.group(1)) + body = body.replace(i.group(0), "") + + choices = [] + for i in choice_regex.finditer(body): + choices.append(i.group(1)) + body = body.replace(i.group(0), "") + + if v.agendaposter and not v.marseyawarded: body = torture_ap(body, v.username) + + if request.files.get("file2") and request.headers.get("cf-ipcountry") != "T1": + files = request.files.getlist('file2')[:4] + for file in files: + if file.content_type.startswith('image/'): + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + body += f"\n\n![]({process_image(v.patron, name)})" + elif file.content_type.startswith('video/'): + if file.content_type == 'video/webm': + file.save("video.mp4") + else: + file.save("unsanitized.mp4") + os.system(f'ffmpeg -y -loglevel warning -i unsanitized.mp4 -map_metadata -1 -c:v copy -c:a copy video.mp4') + with open("video.mp4", 'rb') as f: + try: req = requests.request("POST", "https://pomf2.lain.la/upload.php", files={'files[]': f}, timeout=5).json() + except requests.Timeout: return {"error": "Video upload timed out, please try again!"} + try: url = req['files'][0]['url'] + except: return {"error": req['description']}, 400 + body += f"\n\n{url}" + else: + return error("Image/Video files only.") + + body_html = sanitize(body) + + if v.marseyawarded and marseyaward_body_regex.search(body_html): + return error("You can only type marseys!") + + if len(body_html) > 40000: return error("Submission body_html too long! (max 40k characters)") + + club = bool(request.values.get("club","")) + + if embed and len(embed) > 1500: embed = None + + is_bot = bool(request.headers.get("Authorization")) or (SITE == 'pcmemes.net' and v.id == SNAPPY_ID) + + if request.values.get("ghost") and v.coins >= 100: + v.coins -= 100 + ghost = True + else: ghost = False + + post = Submission( + private=bool(request.values.get("private","")), + club=club, + author_id=v.id, + over_18=bool(request.values.get("over_18","")), + new=bool(request.values.get("new","")), + app_id=v.client.application.id if v.client else None, + is_bot = is_bot, + url=url, + body=body[:20000], + body_html=body_html, + embed_url=embed, + title=title[:500], + title_html=title_html, + sub=sub, + ghost=ghost + ) + + g.db.add(post) + g.db.flush() + + if blackjack and any(i in f'{post.body} {post.title} {post.url}'.lower() for i in blackjack.split()): + v.shadowbanned = 'AutoJanny' + g.db.add(v) + send_repeatable_notification(CARP_ID, post.permalink) + + if v and v.admin_level > 2: + for option in bet_options: + bet_option = Comment(author_id=AUTOBETTER_ID, + parent_submission=post.id, + level=1, + body_html=filter_emojis_only(option), + upvotes=0, + is_bot=True + ) + + g.db.add(bet_option) + + for option in options: + c = Comment(author_id=AUTOPOLLER_ID, + parent_submission=post.id, + level=1, + body_html=filter_emojis_only(option), + upvotes=0, + is_bot=True + ) + g.db.add(c) + + for choice in choices: + c = Comment(author_id=AUTOCHOICE_ID, + parent_submission=post.id, + level=1, + body_html=filter_emojis_only(choice), + upvotes=0, + is_bot=True + ) + g.db.add(c) + + vote = Vote(user_id=v.id, + vote_type=1, + submission_id=post.id + ) + g.db.add(vote) + + if request.files.get('file') and request.headers.get("cf-ipcountry") != "T1": + + file = request.files['file'] + + if file.content_type.startswith('image/'): + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + post.url = process_image(v.patron, name) + + name2 = name.replace('.webp', 'r.webp') + copyfile(name, name2) + post.thumburl = process_image(v.patron, name2, resize=100) + elif file.content_type.startswith('video/'): + if file.content_type == 'video/webm': + file.save("video.mp4") + else: + file.save("unsanitized.mp4") + os.system(f'ffmpeg -y -loglevel warning -i unsanitized.mp4 -map_metadata -1 -c:v copy -c:a copy video.mp4') + with open("video.mp4", 'rb') as f: + try: req = requests.request("POST", "https://pomf2.lain.la/upload.php", files={'files[]': f}, timeout=5).json() + except requests.Timeout: return {"error": "Video upload timed out, please try again!"} + try: url = req['files'][0]['url'] + except: return {"error": req['description']}, 400 + post.url = url + else: + return error("Image/Video files only.") + + if not post.thumburl and post.url: + gevent.spawn(thumbnail_thread, post.id) + + + + + if not post.private and not post.ghost: + + notify_users = NOTIFY_USERS(f'{title} {body}', v) + + if notify_users: + cid = notif_comment2(post) + for x in notify_users: + add_notif(cid, x) + + if (request.values.get('followers') or is_bot) and v.followers: + text = f"@{v.username} has made a new post: [{post.title}]({post.shortlink})" + if post.sub: text += f" in /h/{post.sub}" + + cid = notif_comment(text, autojanny=True) + for follow in v.followers: + user = get_account(follow.user_id) + if post.club and not user.paid_dues: continue + add_notif(cid, user.id) + + + + + + if v.agendaposter and not v.marseyawarded and AGENDAPOSTER_PHRASE not in f'{post.body}{post.title}'.lower(): + post.is_banned = True + post.ban_reason = "AutoJanny" + + body = AGENDAPOSTER_MSG.format(username=v.username, type='post', AGENDAPOSTER_PHRASE=AGENDAPOSTER_PHRASE) + + body_jannied_html = sanitize(body) + + + + c_jannied = Comment(author_id=NOTIFICATIONS_ID, + parent_submission=post.id, + level=1, + over_18=False, + is_bot=True, + app_id=None, + is_pinned='AutoJanny', + distinguish_level=6, + body_html=body_jannied_html, + ) + + g.db.add(c_jannied) + g.db.flush() + + c_jannied.top_comment_id = c_jannied.id + + n = Notification(comment_id=c_jannied.id, user_id=v.id) + g.db.add(n) + + + + if not (post.sub and g.db.query(Exile.user_id).filter_by(user_id=SNAPPY_ID, sub=post.sub).one_or_none()): + if post.sub == 'dankchristianmemes': + body = random.choice(christian_emojis) + elif v.id == CARP_ID: + if random.random() < 0.02: body = "i love you carp" + else: body = ":#marseyfuckoffcarp:" + elif v.id == LAWLZ_ID: + if random.random() < 0.5: body = "wow, this lawlzpost sucks!" + else: body = "wow, a good lawlzpost for once!" + else: + body = random.choice(snappyquotes) + if body.startswith('▼'): + body = body[1:] + vote = Vote(user_id=SNAPPY_ID, + vote_type=-1, + submission_id=post.id, + real = True + ) + g.db.add(vote) + post.downvotes += 1 + if body.startswith('OP is a Trump supporter'): + flag = Flag(post_id=post.id, user_id=SNAPPY_ID, reason='Trump supporter') + g.db.add(flag) + elif body.startswith('You had your chance. Downvoted and reported'): + flag = Flag(post_id=post.id, user_id=SNAPPY_ID, reason='Retard') + g.db.add(flag) + elif body.startswith('▲'): + body = body[1:] + vote = Vote(user_id=SNAPPY_ID, + vote_type=1, + submission_id=post.id, + real = True + ) + g.db.add(vote) + post.upvotes += 1 + + + body += "\n\n" + + if post.url: + if post.url.startswith('https://old.reddit.com/r/'): + rev = post.url.replace('https://old.reddit.com/', '') + rev = f"* [unddit.com](https://unddit.com/{rev})\n" + elif post.url.startswith("https://old.reddit.com/u/"): + rev = post.url.replace('https://old.reddit.com/u/', '') + rev = f"* [search.marsey.cat](https://search.marsey.cat/reddit-search/#\u007b\"author\":\"{rev}\",\"resultSize\":100\u007d)\n" + else: rev = '' + + newposturl = post.url + if newposturl.startswith('/'): newposturl = f"{SITE_FULL}{newposturl}" + body += f"Snapshots:\n\n{rev}* [archive.org](https://web.archive.org/{newposturl})\n* [archive.ph](https://archive.ph/?url={quote(newposturl)}&run=1) (click to archive)\n* [ghostarchive.org](https://ghostarchive.org/search?term={quote(newposturl)}) (click to archive)\n\n" + gevent.spawn(archiveorg, newposturl) + + captured = [] + for i in list(snappy_url_regex.finditer(post.body_html))[:20]: + if i.group(0) in captured: continue + captured.append(i.group(0)) + + href = i.group(1) + if not href: continue + + title = i.group(2) + if "Snapshots:\n\n" not in body: body += "Snapshots:\n\n" + + if f'**[{title}]({href})**:\n\n' not in body: + body += f'**[{title}]({href})**:\n\n' + if href.startswith('https://old.reddit.com/r/'): + rev = href.replace('https://old.reddit.com/', '') + body += f'* [unddit.com](https://unddit.com/{rev})\n' + if href.startswith('https://old.reddit.com/u/'): + rev = href.replace('https://old.reddit.com/u/', '') + body += f"* [search.marsey.cat](https://search.marsey.cat/reddit-search/#\u007b\"author\":\"{rev}\",\"resultSize\":100\u007d)\n" + body += f'* [archive.org](https://web.archive.org/{href})\n' + body += f'* [archive.ph](https://archive.ph/?url={quote(href)}&run=1) (click to archive)\n' + body += f'* [ghostarchive.org](https://ghostarchive.org/search?term={quote(href)}) (click to archive)\n\n' + gevent.spawn(archiveorg, href) + + if body == '!slots': + body = f'!slots{snappy.coins}' + + body_html = sanitize(body) + + if len(body_html) < 40000: + c = Comment(author_id=SNAPPY_ID, + distinguish_level=6, + parent_submission=post.id, + level=1, + over_18=False, + is_bot=True, + app_id=None, + body_html=body_html + ) + + g.db.add(c) + + snappy = g.db.query(User).filter_by(id = SNAPPY_ID).one_or_none() + snappy.comment_count += 1 + snappy.coins += 1 + g.db.add(snappy) + + if body.startswith('!slots'): + check_for_slots_command(body, snappy, c) + + if body.startswith(':#marseypin'): + post.stickied = "Snappy" + post.stickied_utc = int(time.time()) + 3600 + + g.db.flush() + + c.top_comment_id = c.id + + post.comment_count += 1 + post.replies = [c] + + v.post_count = g.db.query(Submission).filter_by(author_id=v.id, is_banned=False, deleted_utc=0).count() + g.db.add(v) + + if v.id == PIZZASHILL_ID: + for uid in PIZZA_VOTERS: + autovote = Vote(user_id=uid, submission_id=post.id, vote_type=1) + g.db.add(autovote) + v.coins += 3 + v.truecoins += 3 + g.db.add(v) + post.upvotes += 3 + g.db.add(post) + + g.db.commit() + + cache.delete_memoized(frontlist) + cache.delete_memoized(User.userpagelisting) + + if v.admin_level > 0 and ("[changelog]" in post.title.lower() or "(changelog)" in post.title.lower()) and not post.private: + send_discord_message(post.permalink) + cache.delete_memoized(changeloglist) + + if request.headers.get("Authorization"): return post.json + else: + post.voted = 1 + if post.new or 'megathread' in post.title.lower(): sort = 'new' + else: sort = v.defaultsortingcomments + return render_template('submission.html', v=v, p=post, sort=sort, render_replies=True, offset=0, success=True, sub=post.subr) + + +@app.post("/delete_post/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def delete_post_pid(pid, v): + + post = get_post(pid) + if 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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def undelete_post_pid(pid, v): + post = get_post(pid) + if 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 +def toggle_comment_nsfw(cid, v): + + comment = g.db.query(Comment).filter_by(id=cid).one_or_none() + if comment.author_id != v.id and not v.admin_level > 1: abort(403) + comment.over_18 = not comment.over_18 + g.db.add(comment) + + 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 +def toggle_post_nsfw(pid, v): + + post = get_post(pid) + + if post.author_id != v.id and not v.admin_level > 1: + 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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def save_post(pid, v): + + post=get_post(pid) + + save = g.db.query(SaveRelationship).filter_by(user_id=v.id, submission_id=post.id).one_or_none() + + if not save: + new_save=SaveRelationship(user_id=v.id, submission_id=post.id) + g.db.add(new_save) + g.db.commit() + + return {"message": "Post saved!"} + +@app.post("/unsave_post/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def unsave_post(pid, v): + + post=get_post(pid) + + save = g.db.query(SaveRelationship).filter_by(user_id=v.id, submission_id=post.id).one_or_none() + + if save: + g.db.delete(save) + g.db.commit() + + return {"message": "Post unsaved!"} + +@app.post("/pin/") +@auth_required +def api_pin_post(post_id, v): + + post = g.db.query(Submission).filter_by(id=post_id).one_or_none() + if post: + if v.id != post.author_id: return {"error": "Only the post author's can do that!"} + post.is_pinned = not post.is_pinned + g.db.add(post) + + cache.delete_memoized(User.userpagelisting) + + g.db.commit() + if post.is_pinned: return {"message": "Post pinned!"} + else: return {"message": "Post unpinned!"} + return {"error": "Post not found!"} + + +@app.get("/submit/title") +@limiter.limit("6/minute") +@limiter.limit("6/minute", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def get_post_title(v): + + url = request.values.get("url") + if not url: abort(400) + + try: x = requests.get(url, headers=titleheaders, timeout=5, proxies=proxies) + except: abort(400) + + soup = BeautifulSoup(x.content, 'lxml') + + title = soup.find('title') + if not title: abort(400) + + return {"url": url, "title": title.string} diff --git a/files/routes/reporting.py b/files/routes/reporting.py index 889ebeef6..eb2ae792d 100644 --- a/files/routes/reporting.py +++ b/files/routes/reporting.py @@ -1,140 +1,140 @@ -from files.helpers.wrappers import * -from files.helpers.get import * -from flask import g -from files.__main__ import app, limiter -from os import path -from files.helpers.sanitize import filter_emojis_only - -@app.post("/report/post/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def api_flag_post(pid, v): - - post = get_post(pid) - - reason = request.values.get("reason", "").strip() - - if blackjack and any(i in reason.lower() for i in blackjack.split()): - v.shadowbanned = 'AutoJanny' - send_repeatable_notification(CARP_ID, f"reports on {post.permalink}") - - reason = reason[:100] - - if not reason.startswith('!'): - existing = g.db.query(Flag.post_id).filter_by(user_id=v.id, post_id=post.id).one_or_none() - if existing: return "", 409 - - reason = filter_emojis_only(reason) - - if len(reason) > 350: return {"error": "Too long."} - - if reason.startswith('!') and v.admin_level > 1: - post.flair = reason[1:] - g.db.add(post) - ma=ModAction( - kind="flair_post", - user_id=v.id, - target_submission_id=post.id, - _note=f'"{post.flair}"' - ) - g.db.add(ma) - elif reason.startswith('/h/') and v.admin_level > 2: - post.sub = reason[3:] - g.db.add(post) - ma=ModAction( - kind="move_hole", - user_id=v.id, - target_submission_id=post.id, - ) - g.db.add(ma) - else: - 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("/report/comment/") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def api_flag_comment(cid, v): - - comment = get_comment(cid) - - existing = g.db.query(CommentFlag.comment_id).filter_by( user_id=v.id, comment_id=comment.id).one_or_none() - if existing: return "", 409 - - reason = request.values.get("reason", "").strip() - - if blackjack and any(i in reason.lower() for i in blackjack.split()): - v.shadowbanned = 'AutoJanny' - send_repeatable_notification(CARP_ID, f"reports on {comment.permalink}") - - reason = reason[:100] - - reason = filter_emojis_only(reason) - - if len(reason) > 350: return {"error": "Too long."} - - 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/post//') -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def remove_report_post(v, pid, uid): - - try: - pid = int(pid) - uid = int(uid) - except: abort(400) - - report = g.db.query(Flag).filter_by(post_id=pid, user_id=uid).one() - - g.db.delete(report) - - ma=ModAction( - kind="delete_report", - user_id=v.id, - target_submission_id=pid - ) - - g.db.add(ma) - - g.db.commit() - - return {"message": "Report removed successfully!"} - - -@app.post('/del_report/comment//') -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@admin_level_required(2) -def remove_report_comment(v, cid, uid): - - cid = int(cid) - uid = int(uid) - - report = g.db.query(CommentFlag).filter_by(comment_id=cid, user_id=uid).one() - - g.db.delete(report) - - ma=ModAction( - kind="delete_report", - user_id=v.id, - target_comment_id=cid - ) - - g.db.add(ma) - - 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 +from files.helpers.sanitize import filter_emojis_only + +@app.post("/report/post/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def api_flag_post(pid, v): + + post = get_post(pid) + + reason = request.values.get("reason", "").strip() + + if blackjack and any(i in reason.lower() for i in blackjack.split()): + v.shadowbanned = 'AutoJanny' + send_repeatable_notification(CARP_ID, f"reports on {post.permalink}") + + reason = reason[:100] + + if not reason.startswith('!'): + existing = g.db.query(Flag.post_id).filter_by(user_id=v.id, post_id=post.id).one_or_none() + if existing: return "", 409 + + reason = filter_emojis_only(reason) + + if len(reason) > 350: return {"error": "Too long."} + + if reason.startswith('!') and v.admin_level > 1: + post.flair = reason[1:] + g.db.add(post) + ma=ModAction( + kind="flair_post", + user_id=v.id, + target_submission_id=post.id, + _note=f'"{post.flair}"' + ) + g.db.add(ma) + elif reason.startswith('/h/') and v.admin_level > 1: + post.sub = reason[3:] + g.db.add(post) + ma=ModAction( + kind="move_hole", + user_id=v.id, + target_submission_id=post.id, + ) + g.db.add(ma) + else: + 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("/report/comment/") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def api_flag_comment(cid, v): + + comment = get_comment(cid) + + existing = g.db.query(CommentFlag.comment_id).filter_by( user_id=v.id, comment_id=comment.id).one_or_none() + if existing: return "", 409 + + reason = request.values.get("reason", "").strip() + + if blackjack and any(i in reason.lower() for i in blackjack.split()): + v.shadowbanned = 'AutoJanny' + send_repeatable_notification(CARP_ID, f"reports on {comment.permalink}") + + reason = reason[:100] + + reason = filter_emojis_only(reason) + + if len(reason) > 350: return {"error": "Too long."} + + 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/post//') +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def remove_report_post(v, pid, uid): + + try: + pid = int(pid) + uid = int(uid) + except: abort(400) + + report = g.db.query(Flag).filter_by(post_id=pid, user_id=uid).one() + + g.db.delete(report) + + ma=ModAction( + kind="delete_report", + user_id=v.id, + target_submission_id=pid + ) + + g.db.add(ma) + + g.db.commit() + + return {"message": "Report removed successfully!"} + + +@app.post('/del_report/comment//') +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@admin_level_required(2) +def remove_report_comment(v, cid, uid): + + cid = int(cid) + uid = int(uid) + + report = g.db.query(CommentFlag).filter_by(comment_id=cid, user_id=uid).one() + + g.db.delete(report) + + ma=ModAction( + kind="delete_report", + user_id=v.id, + target_comment_id=cid + ) + + g.db.add(ma) + + g.db.commit() + return {"message": "Report removed successfully!"} \ No newline at end of file diff --git a/files/routes/search.py b/files/routes/search.py index 8f6c1d6e4..30e1a6237 100644 --- a/files/routes/search.py +++ b/files/routes/search.py @@ -1,290 +1,290 @@ -from files.helpers.wrappers import * -import re -from sqlalchemy import * -from flask import * -from files.__main__ import app - - -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_required -def searchposts(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() - - criteria=searchparse(query) - - - - - - - - - - - - - posts = g.db.query(Submission.id) - - if not v.paid_dues: posts = posts.filter_by(club=False) - - if v.admin_level < 2: - posts = posts.filter(Submission.deleted_utc == 0, Submission.is_banned == False, Submission.private == False, Submission.author_id.notin_(v.userblocks)) - - - - if 'author' in criteria: - posts = posts.filter(Submission.ghost == False) - author = get_user(criteria['author']) - if not author: return {"error": "User not found"} - if author.is_private and author.id != v.id and v.admin_level < 2 and not v.eye: - if request.headers.get("Authorization"): - return {"error": f"@{author.username}'s profile is private; You can't use the 'author' syntax on them"} - return render_template("search.html", - v=v, - query=query, - total=0, - page=page, - listing=[], - sort=sort, - t=t, - next_exists=False, - domain=None, - domain_obj=None, - error=f"@{author.username}'s profile is private; You can't use the 'author' syntax on them." - ) - else: posts = posts.filter(Submission.author_id == author.id) - - if 'q' in criteria: - words=criteria['q'].split() - words = criteria['q'].replace('\\', '').replace('_', '\_').replace('%', '\%').strip().split() - words=[Submission.title.ilike('%'+x+'%') for x in words] - posts=posts.filter(*words) - - if 'over18' in criteria: posts = posts.filter(Submission.over_18==True) - - if 'domain' in criteria: - domain=criteria['domain'] - - domain = domain.replace('\\', '').replace('_', '\_').replace('%', '').strip() - - 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 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) - elif sort == "controversial": - posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc()) - 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 request.headers.get("Authorization"): return {"total":total, "data":[x.json for x in posts]} - - return render_template("search.html", - v=v, - query=query, - total=total, - page=page, - listing=posts, - sort=sort, - t=t, - next_exists=next_exists - ) - -@app.get("/search/comments") -@auth_required -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).filter(Comment.parent_submission != None) - - if 'author' in criteria: - comments = comments.filter(Comment.ghost == False) - author = get_user(criteria['author']) - if not author: return {"error": "User not found"} - if author.is_private and author.id != v.id and v.admin_level < 2 and not v.eye: - if request.headers.get("Authorization"): - return {"error": f"@{author.username}'s profile is private; You can't use the 'author' syntax on them"} - - return render_template("search_comments.html", v=v, query=query, total=0, page=page, comments=[], sort=sort, t=t, next_exists=False, error=f"@{author.username}'s profile is private; You can't use the 'author' syntax on them.") - - else: comments = comments.filter(Comment.author_id == author.id) - - if 'q' in criteria: - words = criteria['q'].replace('\\', '').replace('_', '\_').replace('%', '\%').strip().split() - - words = [Comment.body.ilike('%'+x+'%') for x in words] - comments = comments.filter(*words) - - if 'over18' in criteria: comments = comments.filter(Comment.over_18 == True) - - 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 v.admin_level < 2: - private = [x[0] for x in g.db.query(Submission.id).filter(Submission.private == True).all()] - - comments = comments.filter(Comment.author_id.notin_(v.userblocks), Comment.is_banned==False, Comment.deleted_utc == 0, Comment.parent_submission.notin_(private)) - - - if not v.paid_dues: - club = [x[0] for x in g.db.query(Submission.id).filter(Submission.club == True).all()] - comments = comments.filter(Comment.parent_submission.notin_(club)) - - - if sort == "new": - comments = comments.order_by(Comment.created_utc.desc()) - elif sort == "old": - comments = comments.order_by(Comment.created_utc) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - 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 {"total":total, "data":[x.json for x in comments]} - return render_template("search_comments.html", v=v, query=query, total=total, page=page, comments=comments, sort=sort, t=t, next_exists=next_exists, standalone=True) - - -@app.get("/search/users") -@auth_required -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('\\','').replace('_','\_').replace('%','') - - users=g.db.query(User).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 {"data": [x.json for x in users]} +from files.helpers.wrappers import * +import re +from sqlalchemy import * +from flask import * +from files.__main__ import app + + +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_required +def searchposts(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() + + criteria=searchparse(query) + + + + + + + + + + + + + posts = g.db.query(Submission.id) + + if not v.paid_dues: posts = posts.filter_by(club=False) + + if v.admin_level < 2: + posts = posts.filter(Submission.deleted_utc == 0, Submission.is_banned == False, Submission.private == False, Submission.author_id.notin_(v.userblocks)) + + + + if 'author' in criteria: + posts = posts.filter(Submission.ghost == False) + author = get_user(criteria['author']) + if not author: return {"error": "User not found"} + if author.is_private and author.id != v.id and v.admin_level < 2 and not v.eye: + if request.headers.get("Authorization"): + return {"error": f"@{author.username}'s profile is private; You can't use the 'author' syntax on them"} + return render_template("search.html", + v=v, + query=query, + total=0, + page=page, + listing=[], + sort=sort, + t=t, + next_exists=False, + domain=None, + domain_obj=None, + error=f"@{author.username}'s profile is private; You can't use the 'author' syntax on them." + ) + else: posts = posts.filter(Submission.author_id == author.id) + + if 'q' in criteria: + words=criteria['q'].split() + words = criteria['q'].replace('\\', '').replace('_', '\_').replace('%', '\%').strip().split() + words=[Submission.title.ilike('%'+x+'%') for x in words] + posts=posts.filter(*words) + + if 'over18' in criteria: posts = posts.filter(Submission.over_18==True) + + if 'domain' in criteria: + domain=criteria['domain'] + + domain = domain.replace('\\', '').replace('_', '\_').replace('%', '').strip() + + 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 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) + elif sort == "controversial": + posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc()) + 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 request.headers.get("Authorization"): return {"total":total, "data":[x.json for x in posts]} + + return render_template("search.html", + v=v, + query=query, + total=total, + page=page, + listing=posts, + sort=sort, + t=t, + next_exists=next_exists + ) + +@app.get("/search/comments") +@auth_required +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).filter(Comment.parent_submission != None) + + if 'author' in criteria: + comments = comments.filter(Comment.ghost == False) + author = get_user(criteria['author']) + if not author: return {"error": "User not found"} + if author.is_private and author.id != v.id and v.admin_level < 2 and not v.eye: + if request.headers.get("Authorization"): + return {"error": f"@{author.username}'s profile is private; You can't use the 'author' syntax on them"} + + return render_template("search_comments.html", v=v, query=query, total=0, page=page, comments=[], sort=sort, t=t, next_exists=False, error=f"@{author.username}'s profile is private; You can't use the 'author' syntax on them.") + + else: comments = comments.filter(Comment.author_id == author.id) + + if 'q' in criteria: + words = criteria['q'].replace('\\', '').replace('_', '\_').replace('%', '\%').strip().split() + + words = [Comment.body.ilike('%'+x+'%') for x in words] + comments = comments.filter(*words) + + if 'over18' in criteria: comments = comments.filter(Comment.over_18 == True) + + 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 v.admin_level < 2: + private = [x[0] for x in g.db.query(Submission.id).filter(Submission.private == True).all()] + + comments = comments.filter(Comment.author_id.notin_(v.userblocks), Comment.is_banned==False, Comment.deleted_utc == 0, Comment.parent_submission.notin_(private)) + + + if not v.paid_dues: + club = [x[0] for x in g.db.query(Submission.id).filter(Submission.club == True).all()] + comments = comments.filter(Comment.parent_submission.notin_(club)) + + + if sort == "new": + comments = comments.order_by(Comment.created_utc.desc()) + elif sort == "old": + comments = comments.order_by(Comment.created_utc) + elif sort == "controversial": + comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) + 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 {"total":total, "data":[x.json for x in comments]} + return render_template("search_comments.html", v=v, query=query, total=total, page=page, comments=comments, sort=sort, t=t, next_exists=next_exists, standalone=True) + + +@app.get("/search/users") +@auth_required +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('\\','').replace('_','\_').replace('%','') + + users=g.db.query(User).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 {"data": [x.json for x in users]} 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 index ecc7d76da..8a9157635 100644 --- a/files/routes/settings.py +++ b/files/routes/settings.py @@ -1,897 +1,927 @@ -from __future__ import unicode_literals -from files.helpers.alerts import * -from files.helpers.sanitize 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 files.helpers.sanitize import filter_emojis_only -from files.helpers.discord import add_role -from shutil import copyfile -import requests - -GUMROAD_TOKEN = environ.get("GUMROAD_TOKEN", "").strip() -GUMROAD_ID = environ.get("GUMROAD_ID", "tfcvri").strip() - -tiers={ - "(Paypig)": 1, - "(Renthog)": 2, - "(Landchad)": 3, - "(Terminally online turboautist)": 4, - "(Marsey's Sugar Daddy)": 5, - "(JIDF Bankroller)": 6, - "(Rich Bich)": 7, - "(LlamaBean)": 1, - } - -@app.post("/settings/removebackground") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def settings_profile_post(v): - updated = False - - if request.values.get("background", v.background) != v.background: - updated = True - v.background = request.values.get("background") - - elif request.values.get("reddit", v.reddit) != v.reddit: - reddit = request.values.get("reddit") - if reddit in {'old.reddit.com', 'reddit.com', 'i.reddit.com', 'teddit.net', 'libredd.it', 'unddit.com'}: - updated = True - v.reddit = reddit - - elif request.values.get("slurreplacer", v.slurreplacer) != v.slurreplacer: - updated = True - v.slurreplacer = request.values.get("slurreplacer") == 'true' - - elif request.values.get("hidevotedon", v.hidevotedon) != v.hidevotedon: - updated = True - v.hidevotedon = request.values.get("hidevotedon") == 'true' - - elif request.values.get("cardview", v.cardview) != v.cardview: - updated = True - v.cardview = request.values.get("cardview") == 'true' - - elif request.values.get("highlightcomments", v.highlightcomments) != v.highlightcomments: - updated = True - v.highlightcomments = request.values.get("highlightcomments") == 'true' - - elif request.values.get("newtab", v.newtab) != v.newtab: - updated = True - v.newtab = request.values.get("newtab") == 'true' - - elif request.values.get("newtabexternal", v.newtabexternal) != v.newtabexternal: - updated = True - v.newtabexternal = request.values.get("newtabexternal") == 'true' - - elif request.values.get("nitter", v.nitter) != v.nitter: - updated = True - v.nitter = request.values.get("nitter") == 'true' - - elif request.values.get("controversial", v.controversial) != v.controversial: - updated = True - v.controversial = request.values.get("controversial") == 'true' - - elif request.values.get("sigs_disabled", v.sigs_disabled) != v.sigs_disabled: - updated = True - v.sigs_disabled = request.values.get("sigs_disabled") == 'true' - - elif request.values.get("over18", v.over_18) != v.over_18: - updated = True - v.over_18 = request.values.get("over18") == 'true' - - elif request.values.get("private", v.is_private) != v.is_private: - updated = True - v.is_private = request.values.get("private") == 'true' - - elif request.values.get("nofollow", v.is_nofollow) != v.is_nofollow: - updated = True - v.is_nofollow = request.values.get("nofollow") == 'true' - - elif request.values.get("bio") == "": - v.bio = None - v.bio_html = None - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", v=v, msg="Your bio has been updated.") - - elif request.values.get("sig") == "": - v.sig = None - v.sig_html = None - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", v=v, msg="Your sig has been updated.") - - elif request.values.get("friends") == "": - v.friends = None - v.friends_html = None - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", v=v, msg="Your friends list has been updated.") - - elif request.values.get("enemies") == "": - v.enemies = None - v.enemies_html = None - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", v=v, msg="Your enemies list has been updated.") - - elif (v.patron or v.id == MOOSE_ID) and request.values.get("sig"): - sig = request.values.get("sig")[:200] - - sig_html = sanitize(sig) - - if len(sig_html) > 1000: - return render_template("settings_profile.html", - v=v, - error="Your sig is too long") - - v.sig = sig[:200] - v.sig_html=sig_html - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", - v=v, - msg="Your sig has been updated.") - - - - - elif request.values.get("friends"): - friends = request.values.get("friends")[:500] - - friends_html = sanitize(friends) - - if len(friends_html) > 2000: - return render_template("settings_profile.html", - v=v, - error="Your friends list is too long") - - - notify_users = NOTIFY_USERS(friends, v) - - if notify_users: - cid = notif_comment(f"@{v.username} has added you to their friends list!") - for x in notify_users: - add_notif(cid, x) - - v.friends = friends[:500] - v.friends_html=friends_html - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", - v=v, - msg="Your friends list has been updated.") - - - elif request.values.get("enemies"): - enemies = request.values.get("enemies")[:500] - - enemies_html = sanitize(enemies) - - if len(enemies_html) > 2000: - return render_template("settings_profile.html", - v=v, - error="Your enemies list is too long") - - - notify_users = NOTIFY_USERS(enemies, v) - - if notify_users: - cid = notif_comment(f"@{v.username} has added you to their enemies list!") - for x in notify_users: - add_notif(cid, x) - - v.enemies = enemies[:500] - v.enemies_html=enemies_html - g.db.add(v) - g.db.commit() - return render_template("settings_profile.html", - v=v, - msg="Your enemies list has been updated.") - - - elif request.values.get("bio") or request.files.get('file') and request.headers.get("cf-ipcountry") != "T1": - bio = request.values.get("bio")[:1500] - - if request.files.get('file'): - file = request.files['file'] - if file.content_type.startswith('image/'): - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - url = process_image(name) - bio += f"\n\n![]({url})" - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data'] - except requests.Timeout: return {"error": "Video upload timed out, please try again!"} - try: url = req['link'] - except: - error = req['error'] - if error == 'File exceeds max duration': error += ' (60 seconds)' - return {"error": error}, 400 - if url.endswith('.'): url += 'mp4' - bio += f"\n\n{url}" - else: - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "Image/Video files only"}, 400 - return render_template("settings_profile.html", v=v, error="Image/Video files only."), 400 - - bio_html = sanitize(bio) - - if len(bio_html) > 10000: - return render_template("settings_profile.html", - v=v, - error="Your bio is too long") - - 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.") - - - frontsize = request.values.get("frontsize") - if frontsize: - if frontsize in {"15", "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", "bump", "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: - if theme in {"dramblr", "reddit", "classic", "classic_dark", "transparent", "win98", "dark", "light", "coffee", "tron", "4chan", "midnight"}: - if theme == "transparent" and not v.background: - return {"error": "You need to set a background to use the transparent theme!"} - v.theme = theme - if theme == "win98": v.themecolor = "30409f" - updated = True - else: abort(400) - - house = request.values.get("house") - if house and house in ("None","Furry","Femboy","Vampire","Racist"): - if v.house: cost = 2000 - else: cost = 500 - - if v.coins >= cost: v.coins -= cost - elif v.procoins >= cost: v.procoins -= cost - else: abort(403) - - if house == "None": house = None - v.house = house - - if v.house == "Vampire": - send_repeatable_notification(DAD_ID, f"@{v.username} has joined House Vampire!") - - 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("/settings/filters") -@auth_required -def filters(v): - filters=request.values.get("filters")[:1000].strip() - - if filters == v.custom_filter_list: - return render_template("settings_filters.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_filters.html", v=v, msg="Your custom filters have been updated.") - -@app.post("/changelogsub") -@auth_required -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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def gumroad(v): - 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, timeout=5).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 - - procoins = procoins_li[tier] - procoins_li[v.patron] - if procoins < 0: return {"error": f"{patron} rewards already claimed"}, 400 - - existing = g.db.query(User.id).filter(User.email == v.email, User.is_activated == True, User.patron >= tier).one_or_none() - if existing: return {"error": f"{patron} rewards already claimed on another account"}, 400 - - v.patron = tier - if v.discord_id: add_role(v, f"{tier}") - - v.procoins += procoins - send_repeatable_notification(v.id, f"You have received {procoins} Marseybux! You can use them to buy awards in the [shop](/shop).") - - if v.patron > 1 and v.verified == None: v.verified = "Verified" - - g.db.add(v) - - 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.flush() - send_notification(v.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") - - g.db.commit() - - return {"message": f"{patron} rewards claimed!"} - -@app.post("/settings/titlecolor") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def titlecolor(v): - - titlecolor = str(request.values.get("titlecolor", "")).strip() - if titlecolor.startswith('#'): titlecolor = titlecolor[1:] - if len(titlecolor) != 6: return render_template("settings_profile.html", v=v, error="Invalid color code") - v.titlecolor = titlecolor - g.db.add(v) - g.db.commit() - return redirect("/settings/profile") - -@app.post("/settings/verifiedcolor") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def verifiedcolor(v): - verifiedcolor = str(request.values.get("verifiedcolor", "")).strip() - if verifiedcolor.startswith('#'): verifiedcolor = verifiedcolor[1:] - if len(verifiedcolor) != 6: return render_template("settings_profile.html", v=v, error="Invalid color code") - v.verifiedcolor = verifiedcolor - g.db.add(v) - g.db.commit() - return redirect("/settings/profile") - -@app.post("/settings/security") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def settings_security_post(v): - if request.values.get("new_password"): - if request.values.get("new_password") != request.values.get("cnf_password"): - return render_template("settings_security.html", v=v, error="Passwords do not match.") - - if not valid_password_regex.fullmatch(request.values.get("new_password")): - return render_template("settings_security.html", v=v, error="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 render_template("settings_security.html", v=v, msg="Your password has been changed.") - - if request.values.get("new_email"): - - if not v.verifyPass(request.values.get('password')): - return render_template("settings_security.html", v=v, error="Invalid password.") - - new_email = request.values.get("new_email","").strip().lower() - - if new_email == v.email: - return render_template("settings_security.html", v=v, error="That email is already yours!") - - url = f"{SITE_FULL}/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 render_template("settings_security.html", v=v, msg="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 render_template("settings_security.html", v=v, error="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 render_template("settings_security.html", v=v, error="Invalid password or token.") - - v.mfa_secret = secret - g.db.add(v) - - g.db.commit() - - return render_template("settings_security.html", v=v, msg="Two-factor authentication enabled.") - - if request.values.get("2fa_remove"): - - if not v.verifyPass(request.values.get('password')): - return render_template("settings_security.html", v=v, error="Invalid password or token.") - - token = request.values.get("2fa_remove") - - if not v.validate_2fa(token): - return render_template("settings_security.html", v=v, error="Invalid password or token.") - - v.mfa_secret = None - g.db.add(v) - - g.db.commit() - - return render_template("settings_security.html", v=v, msg="Two-factor authentication disabled.") - -@app.post("/settings/log_out_all_others") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def settings_images_profile(v): - if request.headers.get("cf-ipcountry") == "T1": return {"error":"Image uploads are not allowed through TOR."}, 403 - - file = request.files["profile"] - - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - highres = process_image(name) - - if not highres: abort(400) - - name2 = name.replace('.webp', 'r.webp') - copyfile(name, name2) - imageurl = process_image(name2, resize=100) - - if not imageurl: abort(400) - - if v.highres and '/images/' in v.highres: - fpath = '/images/' + v.highres.split('/images/')[1] - if path.isfile(fpath): os.remove(fpath) - if v.profileurl and '/images/' in v.profileurl: - fpath = '/images/' + v.profileurl.split('/images/')[1] - if path.isfile(fpath): os.remove(fpath) - 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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def settings_images_banner(v): - if request.headers.get("cf-ipcountry") == "T1": return {"error":"Image uploads are not allowed through TOR."}, 403 - - file = request.files["banner"] - - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - bannerurl = process_image(name) - - if bannerurl: - if v.bannerurl and '/images/' in v.bannerurl: - fpath = '/images/' + v.bannerurl.split('/images/')[1] - if path.isfile(fpath): os.remove(fpath) - v.bannerurl = bannerurl - g.db.add(v) - g.db.commit() - - return render_template("settings_profile.html", v=v, msg="Banner successfully updated.") - - -@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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def settings_css(v): - if v.agendaposter: return {"error": "Agendapostered users can't edit css!"} - - css = request.values.get("css").strip().replace('\\', '').strip()[:4000] - v.css = css - 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): - return render_template("settings_profilecss.html", v=v) - -@app.post("/settings/profilecss") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def settings_profilecss(v): - 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;10/day") -@limiter.limit("1/second;10/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -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.unblockable: - send_notification(user.id, f"@{v.username} has tried to block you and failed because of your unblockable status!") - g.db.commit() - return {"error": "This user is unblockable."}, 403 - - if user.id == v.id: - return {"error": "You can't block yourself."}, 409 - - if v.is_blocking(user): - return {"error": f"You have already blocked @{user.username}."}, 409 - - if user.id == NOTIFICATIONS_ID: - 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) - - send_notification(user.id, f"@{v.username} has blocked you!") - - cache.delete_memoized(frontlist) - - g.db.commit() - - return {"message": f"@{user.username} blocked."} - - -@app.post("/settings/unblock") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def settings_unblock_user(v): - - user = get_user(request.values.get("username")) - - x = v.is_blocking(user) - - if not x: abort(409) - - g.db.delete(x) - - send_notification(user.id, f"@{v.username} has unblocked you!") - - cache.delete_memoized(frontlist) - - g.db.commit() - - 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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@is_not_permabanned -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 valid_username_regex.fullmatch(new_name): - return render_template("settings_profile.html", - v=v, - error="This isn't a valid username.") - - search_name = new_name.replace('\\', '').replace('_','\_').replace('%','') - - x= g.db.query(User).filter( - or_( - User.username.ilike(search_name), - User.original_username.ilike(search_name) - ) - ).one_or_none() - - 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).filter_by(id=v.id).one_or_none() - - 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("2/second;10/day") -@limiter.limit("2/second;10/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def settings_song_change(v): - song=request.values.get("song").strip() - - if song == "" and v.song: - if path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User.id).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="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", timeout=5).json() - duration = req['items'][0]['contentDetails']['duration'] - if duration == 'P0D': - return render_template("settings_profile.html", v=v, error="Can't use a live youtube video!") - - if "H" in duration: - return render_template("settings_profile.html", v=v, error="Duration of the video must not exceed 15 minutes.") - - if "M" in duration: - duration = int(duration.split("PT")[1].split("M")[0]) - if duration > 15: - return render_template("settings_profile.html", v=v, error="Duration of the video must not exceed 15 minutes.") - - - if v.song and path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User.id).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="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;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -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_emojis_only(new_name) - - if len(v.customtitle) < 1000: - g.db.add(v) - g.db.commit() - - return redirect("/settings/profile") - - -@app.get("/settings") -@auth_required -def settings(v): - return redirect("/settings/profile") - - -@app.get("/settings/profile") -@auth_required -def settings_profile(v): - if v.flairchanged: ti = datetime.utcfromtimestamp(v.flairchanged).strftime('%Y-%m-%d %H:%M:%S') - else: ti = '' - return render_template("settings_profile.html", v=v, ti=ti) \ No newline at end of file +from __future__ import unicode_literals +from files.helpers.alerts import * +from files.helpers.sanitize 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 files.helpers.sanitize import filter_emojis_only +from files.helpers.discord import add_role +from shutil import copyfile +import requests +import tldextract + +GUMROAD_TOKEN = environ.get("GUMROAD_TOKEN", "").strip() +GUMROAD_ID = environ.get("GUMROAD_ID", "tfcvri").strip() + +tiers={ + "(Paypig)": 1, + "(Renthog)": 2, + "(Landchad)": 3, + "(Terminally online turboautist)": 4, + "(Marsey's Sugar Daddy)": 5, + "(JIDF Bankroller)": 6, + "(Rich Bich)": 7, + "(LlamaBean)": 1, + } + +@app.post("/settings/removebackground") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_profile_post(v): + updated = False + + if request.values.get("background", v.background) != v.background: + updated = True + v.background = request.values.get("background") + + elif request.values.get("reddit", v.reddit) != v.reddit: + reddit = request.values.get("reddit") + if reddit in {'old.reddit.com', 'reddit.com', 'i.reddit.com', 'teddit.net', 'libredd.it', 'unddit.com'}: + updated = True + v.reddit = reddit + + elif request.values.get("slurreplacer", v.slurreplacer) != v.slurreplacer: + updated = True + v.slurreplacer = request.values.get("slurreplacer") == 'true' + + elif request.values.get("hidevotedon", v.hidevotedon) != v.hidevotedon: + updated = True + v.hidevotedon = request.values.get("hidevotedon") == 'true' + + elif request.values.get("cardview", v.cardview) != v.cardview: + updated = True + v.cardview = request.values.get("cardview") == 'true' + + elif request.values.get("highlightcomments", v.highlightcomments) != v.highlightcomments: + updated = True + v.highlightcomments = request.values.get("highlightcomments") == 'true' + + elif request.values.get("newtab", v.newtab) != v.newtab: + updated = True + v.newtab = request.values.get("newtab") == 'true' + + elif request.values.get("newtabexternal", v.newtabexternal) != v.newtabexternal: + updated = True + v.newtabexternal = request.values.get("newtabexternal") == 'true' + + elif request.values.get("nitter", v.nitter) != v.nitter: + updated = True + v.nitter = request.values.get("nitter") == 'true' + + elif request.values.get("controversial", v.controversial) != v.controversial: + updated = True + v.controversial = request.values.get("controversial") == 'true' + + elif request.values.get("sigs_disabled", v.sigs_disabled) != v.sigs_disabled: + updated = True + v.sigs_disabled = request.values.get("sigs_disabled") == 'true' + + elif request.values.get("over18", v.over_18) != v.over_18: + updated = True + v.over_18 = request.values.get("over18") == 'true' + + elif request.values.get("private", v.is_private) != v.is_private: + updated = True + v.is_private = request.values.get("private") == 'true' + + elif request.values.get("nofollow", v.is_nofollow) != v.is_nofollow: + updated = True + v.is_nofollow = request.values.get("nofollow") == 'true' + + elif request.values.get("bio") == "": + v.bio = None + v.bio_html = None + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", v=v, msg="Your bio has been updated.") + + elif request.values.get("sig") == "": + v.sig = None + v.sig_html = None + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", v=v, msg="Your sig has been updated.") + + elif request.values.get("friends") == "": + v.friends = None + v.friends_html = None + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", v=v, msg="Your friends list has been updated.") + + elif request.values.get("enemies") == "": + v.enemies = None + v.enemies_html = None + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", v=v, msg="Your enemies list has been updated.") + + elif (v.patron or v.id == MOOSE_ID) and request.values.get("sig"): + sig = request.values.get("sig")[:200] + + sig_html = sanitize(sig) + + if len(sig_html) > 1000: + return render_template("settings_profile.html", + v=v, + error="Your sig is too long") + + v.sig = sig[:200] + v.sig_html=sig_html + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", + v=v, + msg="Your sig has been updated.") + + + + + elif request.values.get("friends"): + friends = request.values.get("friends")[:500] + + friends_html = sanitize(friends) + + if len(friends_html) > 2000: + return render_template("settings_profile.html", + v=v, + error="Your friends list is too long") + + + notify_users = NOTIFY_USERS(friends, v) + + if notify_users: + cid = notif_comment(f"@{v.username} has added you to their friends list!") + for x in notify_users: + add_notif(cid, x) + + v.friends = friends[:500] + v.friends_html=friends_html + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", + v=v, + msg="Your friends list has been updated.") + + + elif request.values.get("enemies"): + enemies = request.values.get("enemies")[:500] + + enemies_html = sanitize(enemies) + + if len(enemies_html) > 2000: + return render_template("settings_profile.html", + v=v, + error="Your enemies list is too long") + + + notify_users = NOTIFY_USERS(enemies, v) + + if notify_users: + cid = notif_comment(f"@{v.username} has added you to their enemies list!") + for x in notify_users: + add_notif(cid, x) + + v.enemies = enemies[:500] + v.enemies_html=enemies_html + g.db.add(v) + g.db.commit() + return render_template("settings_profile.html", + v=v, + msg="Your enemies list has been updated.") + + + elif request.values.get("bio") or request.files.get('file') and request.headers.get("cf-ipcountry") != "T1": + bio = request.values.get("bio")[:1500] + + if request.files.get('file'): + file = request.files['file'] + if file.content_type.startswith('image/'): + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + url = process_image(v.patron, name) + bio += f"\n\n![]({url})" + elif file.content_type.startswith('video/'): + if file.content_type == 'video/webm': + file.save("video.mp4") + else: + file.save("unsanitized.mp4") + os.system(f'ffmpeg -y -loglevel warning -i unsanitized.mp4 -map_metadata -1 -c:v copy -c:a copy video.mp4') + with open("video.mp4", 'rb') as f: + try: req = requests.request("POST", "https://pomf2.lain.la/upload.php", files={'files[]': f}, timeout=5).json() + except requests.Timeout: return {"error": "Video upload timed out, please try again!"} + try: url = req['files'][0]['url'] + except: return {"error": req['description']}, 400 + bio += f"\n\n{url}" + else: + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "Image/Video files only"}, 400 + return render_template("settings_profile.html", v=v, error="Image/Video files only."), 400 + + bio_html = sanitize(bio) + + if len(bio_html) > 10000: + return render_template("settings_profile.html", + v=v, + error="Your bio is too long") + + 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.") + + + frontsize = request.values.get("frontsize") + if frontsize: + if frontsize in {"15", "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", "bump", "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: + if theme in {"dramblr", "reddit", "classic", "classic_dark", "transparent", "win98", "dark", "light", "coffee", "tron", "4chan", "midnight"}: + if theme == "transparent" and not v.background: + return {"error": "You need to set a background to use the transparent theme!"} + v.theme = theme + if theme == "win98": v.themecolor = "30409f" + updated = True + else: abort(400) + + house = request.values.get("house") + if house and house in ("None","Furry","Femboy","Vampire","Racist"): + if v.house: cost = 2000 + else: cost = 500 + + if v.coins >= cost: v.coins -= cost + elif v.procoins >= cost: v.procoins -= cost + else: abort(403) + + if house == "None": house = None + v.house = house + + if v.house == "Vampire": + send_repeatable_notification(DAD_ID, f"@{v.username} has joined House Vampire!") + + 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("/settings/filters") +@auth_required +def filters(v): + filters=request.values.get("filters")[:1000].strip() + + if filters == v.custom_filter_list: + return render_template("settings_filters.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_filters.html", v=v, msg="Your custom filters have been updated.") + +@app.post("/changelogsub") +@auth_required +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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def gumroad(v): + 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, timeout=5).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 + + procoins = procoins_li[tier] - procoins_li[v.patron] + if procoins < 0: return {"error": f"{patron} rewards already claimed"}, 400 + + existing = g.db.query(User.id).filter(User.email == v.email, User.is_activated == True, User.patron >= tier).one_or_none() + if existing: return {"error": f"{patron} rewards already claimed on another account"}, 400 + + v.patron = tier + if v.discord_id: add_role(v, f"{tier}") + + v.procoins += procoins + send_repeatable_notification(v.id, f"You have received {procoins} Marseybux! You can use them to buy awards in the [shop](/shop).") + + g.db.add(v) + + 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.flush() + send_notification(v.id, f"@AutoJanny has given you the following profile badge:\n\n![]({new_badge.path})\n\n{new_badge.name}") + + g.db.commit() + + return {"message": f"{patron} rewards claimed!"} + +@app.post("/settings/titlecolor") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def titlecolor(v): + + titlecolor = str(request.values.get("titlecolor", "")).strip() + if titlecolor.startswith('#'): titlecolor = titlecolor[1:] + if len(titlecolor) != 6: return render_template("settings_profile.html", v=v, error="Invalid color code") + v.titlecolor = titlecolor + g.db.add(v) + g.db.commit() + return redirect("/settings/profile") + +@app.post("/settings/verifiedcolor") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def verifiedcolor(v): + verifiedcolor = str(request.values.get("verifiedcolor", "")).strip() + if verifiedcolor.startswith('#'): verifiedcolor = verifiedcolor[1:] + if len(verifiedcolor) != 6: return render_template("settings_profile.html", v=v, error="Invalid color code") + v.verifiedcolor = verifiedcolor + g.db.add(v) + g.db.commit() + return redirect("/settings/profile") + +@app.post("/settings/security") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_security_post(v): + if request.values.get("new_password"): + if request.values.get("new_password") != request.values.get("cnf_password"): + return render_template("settings_security.html", v=v, error="Passwords do not match.") + + if not valid_password_regex.fullmatch(request.values.get("new_password")): + return render_template("settings_security.html", v=v, error="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 render_template("settings_security.html", v=v, msg="Your password has been changed.") + + if request.values.get("new_email"): + + if not v.verifyPass(request.values.get('password')): + return render_template("settings_security.html", v=v, error="Invalid password.") + + new_email = request.values.get("new_email","").strip().lower() + + if new_email == v.email: + return render_template("settings_security.html", v=v, error="That email is already yours!") + + url = f"{SITE_FULL}/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 render_template("settings_security.html", v=v, msg="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 render_template("settings_security.html", v=v, error="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 render_template("settings_security.html", v=v, error="Invalid password or token.") + + v.mfa_secret = secret + g.db.add(v) + + g.db.commit() + + return render_template("settings_security.html", v=v, msg="Two-factor authentication enabled.") + + if request.values.get("2fa_remove"): + + if not v.verifyPass(request.values.get('password')): + return render_template("settings_security.html", v=v, error="Invalid password or token.") + + token = request.values.get("2fa_remove") + + if not v.validate_2fa(token): + return render_template("settings_security.html", v=v, error="Invalid password or token.") + + v.mfa_secret = None + g.db.add(v) + + g.db.commit() + + return render_template("settings_security.html", v=v, msg="Two-factor authentication disabled.") + +@app.post("/settings/log_out_all_others") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_images_profile(v): + if request.headers.get("cf-ipcountry") == "T1": return {"error":"Image uploads are not allowed through TOR."}, 403 + + file = request.files["profile"] + + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + highres = process_image(v.patron, name) + + if not highres: abort(400) + + name2 = name.replace('.webp', 'r.webp') + copyfile(name, name2) + imageurl = process_image(v.patron, name2, resize=100) + + if not imageurl: abort(400) + + if v.highres and '/images/' in v.highres: + fpath = '/images/' + v.highres.split('/images/')[1] + if path.isfile(fpath): os.remove(fpath) + if v.profileurl and '/images/' in v.profileurl: + fpath = '/images/' + v.profileurl.split('/images/')[1] + if path.isfile(fpath): os.remove(fpath) + 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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_images_banner(v): + if request.headers.get("cf-ipcountry") == "T1": return {"error":"Image uploads are not allowed through TOR."}, 403 + + file = request.files["banner"] + + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + bannerurl = process_image(v.patron, name) + + if bannerurl: + if v.bannerurl and '/images/' in v.bannerurl: + fpath = '/images/' + v.bannerurl.split('/images/')[1] + if path.isfile(fpath): os.remove(fpath) + v.bannerurl = bannerurl + g.db.add(v) + g.db.commit() + + return render_template("settings_profile.html", v=v, msg="Banner successfully updated.") + + +@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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_css(v): + if v.agendaposter: return {"error": "Agendapostered users can't edit css!"} + + css = request.values.get("css").strip().replace('\\', '').strip()[:4000] + v.css = css + 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): + return render_template("settings_profilecss.html", v=v) + +@app.post("/settings/profilecss") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_profilecss(v): + profilecss = request.values.get("profilecss").strip().replace('\\', '').strip()[:4000] + + + urls = list(css_regex.finditer(profilecss)) + list(css_regex2.finditer(profilecss)) + for i in urls: + url = i.group(1) + if url.startswith('/'): continue + domain = tldextract.extract(url).registered_domain + if domain not in approved_embed_hosts: + error = f"The domain '{domain}' is not allowed, please use one of these domains\n\n{approved_embed_hosts}." + return render_template("settings_profilecss.html", error=error, v=v) + + + 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;10/day") +@limiter.limit("1/second;10/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +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.unblockable: + send_notification(user.id, f"@{v.username} has tried to block you and failed because of your unblockable status!") + g.db.commit() + return {"error": "This user is unblockable."}, 403 + + if user.id == v.id: + return {"error": "You can't block yourself."}, 409 + + if v.is_blocking(user): + return {"error": f"You have already blocked @{user.username}."}, 409 + + if user.id == NOTIFICATIONS_ID: + 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) + + send_notification(user.id, f"@{v.username} has blocked you!") + + cache.delete_memoized(frontlist) + + g.db.commit() + + return {"message": f"@{user.username} blocked."} + + +@app.post("/settings/unblock") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_unblock_user(v): + + user = get_user(request.values.get("username")) + + x = v.is_blocking(user) + + if not x: abort(409) + + g.db.delete(x) + + send_notification(user.id, f"@{v.username} has unblocked you!") + + cache.delete_memoized(frontlist) + + g.db.commit() + + 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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@is_not_permabanned +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 valid_username_regex.fullmatch(new_name): + return render_template("settings_profile.html", + v=v, + error="This isn't a valid username.") + + search_name = new_name.replace('\\', '').replace('_','\_').replace('%','') + + x= g.db.query(User).filter( + or_( + User.username.ilike(search_name), + User.original_username.ilike(search_name) + ) + ).one_or_none() + + 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).filter_by(id=v.id).one_or_none() + + 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("3/second;10/day") +@limiter.limit("3/second;10/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_song_change(v): + song=request.values.get("song").strip() + + if song == "" and v.song: + if path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User).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="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", timeout=5).json() + duration = req['items'][0]['contentDetails']['duration'] + if duration == 'P0D': + return render_template("settings_profile.html", v=v, error="Can't use a live youtube video!") + + if "H" in duration: + return render_template("settings_profile.html", v=v, error="Duration of the video must not exceed 15 minutes.") + + if "M" in duration: + duration = int(duration.split("PT")[1].split("M")[0]) + if duration > 15: + return render_template("settings_profile.html", v=v, error="Duration of the video must not exceed 15 minutes.") + + + if v.song and path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User).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="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;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +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_emojis_only(new_name) + + if len(v.customtitle) < 1000: + g.db.add(v) + g.db.commit() + + return redirect("/settings/profile") + + +@app.post("/settings/checkmark_text") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_checkmark_text(v): + + if not v.verified: abort(403) + + new_name=request.values.get("title").strip()[:100].replace("𒐪","") + + if not new_name: abort(400) + + if new_name == v.verified: return render_template("settings_profile.html", v=v, error="You didn't change anything") + + v.verified = new_name + g.db.add(v) + g.db.commit() + + return redirect("/settings/profile") + + +@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) \ No newline at end of file diff --git a/files/routes/static.py b/files/routes/static.py index cd4e3b9c4..4f522d6ad 100644 --- a/files/routes/static.py +++ b/files/routes/static.py @@ -1,560 +1,568 @@ -from files.mail import * -from files.__main__ import app, limiter, mail -from files.helpers.alerts import * -from files.helpers.const import * -from files.classes.award import AWARDS -from sqlalchemy import func -from os import path -import calendar -import matplotlib.pyplot as plt -from files.classes.mod_logs import ACTIONTYPES, ACTIONTYPES2 -from files.classes.badges import BadgeDef - -@app.get("/r/drama/comments//") -@app.get("/r/Drama/comments/<id>/<title>") -def rdrama(id, title): - id = ''.join(f'{x}/' for x in id) - return redirect(f'/archives/drama/comments/{id}{title}.html') - -@app.get('/logged_out/') -@app.get('/logged_out/<path:old>') -def logged_out(old = ""): - # Remove trailing question mark from request.full_path which flask adds if there are no query parameters - redirect_url = request.full_path.replace("/logged_out", "", 1) - if redirect_url.endswith("?"): - redirect_url = redirect_url[:-1] - - # Handle cases like /logged_out?asdf by adding a slash to the beginning - if not redirect_url.startswith('/'): - redirect_url = f"/{redirect_url}" - - # Prevent redirect loop caused by visiting /logged_out/logged_out/logged_out/etc... - if redirect_url.startswith('/logged_out'): - abort(400) - - return redirect(redirect_url) - - -@app.get("/marseys") -@auth_required -def marseys(v): - if SITE_NAME == 'rDrama': - marseys = g.db.query(Marsey, User).join(User, User.id==Marsey.author_id) - sort = request.values.get("sort", "usage") - if sort == "usage": marseys = marseys.order_by(Marsey.count.desc(), User.username) - else: marseys = marseys.order_by(User.username, Marsey.count.desc()) - else: - marseys = g.db.query(Marsey).order_by(Marsey.count.desc()) - return render_template("marseys.html", v=v, marseys=marseys) - -@app.get("/marsey_list") -@cache.memoize(timeout=600, make_name=make_name) -def marsey_list(): - if SITE_NAME == 'rDrama': - marseys = [f"{x.name} : {y} {x.tags}" for x, y in g.db.query(Marsey, User.username).join(User, User.id==Marsey.author_id).order_by(Marsey.count.desc())] - else: - marseys = [f"{x.name} : {x.tags}" for x in g.db.query(Marsey).order_by(Marsey.count.desc())] - - return str(marseys).replace("'",'"') - -@app.get('/rules') -@app.get('/sidebar') -@auth_desired -def sidebar(v): - return render_template('sidebar.html', v=v) - - -@app.get("/stats") -@auth_required -@cache.memoize(timeout=86400, make_name=make_name) -def participation_stats(v): - - - day = int(time.time()) - 86400 - - week = int(time.time()) - 604800 - posters = g.db.query(Submission.author_id).distinct(Submission.author_id).filter(Submission.created_utc > week).all() - commenters = g.db.query(Comment.author_id).distinct(Comment.author_id).filter(Comment.created_utc > week).all() - voters = g.db.query(Vote.user_id).distinct(Vote.user_id).filter(Vote.created_utc > week).all() - commentvoters = g.db.query(CommentVote.user_id).distinct(CommentVote.user_id).filter(CommentVote.created_utc > week).all() - - active_users = set(posters) | set(commenters) | set(voters) | set(commentvoters) - - stats = {"marseys": g.db.query(Marsey.name).count(), - "users": g.db.query(User.id).count(), - "private users": g.db.query(User.id).filter_by(is_private=True).count(), - "banned users": g.db.query(User.id).filter(User.is_banned > 0).count(), - "verified email users": g.db.query(User.id).filter_by(is_activated=True).count(), - "coins in circulation": g.db.query(func.sum(User.coins)).scalar(), - "total shop sales": g.db.query(func.sum(User.coins_spent)).scalar(), - "signups last 24h": g.db.query(User.id).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).filter_by(is_banned=False).filter(Submission.deleted_utc == 0).count(), - "removed posts (by admins)": g.db.query(Submission.id).filter_by(is_banned=True).count(), - "deleted posts (by author)": g.db.query(Submission.id).filter(Submission.deleted_utc > 0).count(), - "posts last 24h": g.db.query(Submission.id).filter(Submission.created_utc > day).count(), - "total comments": g.db.query(Comment.id).filter(Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(), - "commenting users": g.db.query(Comment.author_id).distinct().count(), - "removed comments (by admins)": g.db.query(Comment.id).filter_by(is_banned=True).count(), - "deleted comments (by author)": g.db.query(Comment.id).filter(Comment.deleted_utc > 0).count(), - "comments last_24h": g.db.query(Comment.id).filter(Comment.created_utc > day, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(), - "post votes": g.db.query(Vote.submission_id).count(), - "post voting users": g.db.query(Vote.user_id).distinct().count(), - "comment votes": g.db.query(CommentVote.comment_id).count(), - "comment voting users": g.db.query(CommentVote.user_id).distinct().count(), - "total upvotes": g.db.query(Vote.submission_id).filter_by(vote_type=1).count() + g.db.query(CommentVote.comment_id).filter_by(vote_type=1).count(), - "total downvotes": g.db.query(Vote.submission_id).filter_by(vote_type=-1).count() + g.db.query(CommentVote.comment_id).filter_by(vote_type=-1).count(), - "total awards": g.db.query(AwardRelationship.id).count(), - "awards given": g.db.query(AwardRelationship.id).filter(or_(AwardRelationship.submission_id != None, AwardRelationship.comment_id != None)).count(), - "users who posted, commented, or voted in the past 7 days": len(active_users), - } - - - if SITE_NAME == 'rDrama': - furries1 = g.db.query(User.id).filter(User.house.like('Furry%')).count() - femboys1 = g.db.query(User.id).filter(User.house.like('Femboy%')).count() - vampires1 = g.db.query(User.id).filter(User.house.like('Vampire%')).count() - racists1 = g.db.query(User.id).filter(User.house.like('Racist%')).count() - - furries2 = g.db.query(func.sum(User.truecoins)).filter(User.house.like('Furry%')).scalar() - femboys2 = g.db.query(func.sum(User.truecoins)).filter(User.house.like('Femboy%')).scalar() - vampires2 = g.db.query(func.sum(User.truecoins)).filter(User.house.like('Vampire%')).scalar() - racists2 = g.db.query(func.sum(User.truecoins)).filter(User.house.like('Racist%')).scalar() - - stats2 = {"House furry members": furries1, - "House femboy members": femboys1, - "House vampire members": vampires1, - "House racist members": racists1, - "House furry total truescore": furries2, - "House femboy total truescore": femboys2, - "House vampire total truescore": vampires2, - "House racist total truescore": racists2, - } - - stats.update(stats2) - - ids = (NOTIFICATIONS_ID, AUTOJANNY_ID, SNAPPY_ID, LONGPOSTBOT_ID, ZOZBOT_ID) - bots = g.db.query(User).filter(User.id.in_(ids)) - - for u in bots: - g.db.add(u) - - if u.patron_utc and u.patron_utc < time.time(): - u.patron = 0 - u.patron_utc = 0 - send_repeatable_notification(u.id, "Your paypig status has expired!") - if u.discord_id: remove_role(v, "1") - - if u.unban_utc and u.unban_utc < time.time(): - u.is_banned = 0 - u.unban_utc = 0 - u.ban_evade = 0 - send_repeatable_notification(u.id, "You have been unbanned!") - - if u.agendaposter and u.agendaposter < time.time(): - u.agendaposter = 0 - send_repeatable_notification(u.id, "Your chud theme has expired!") - badge = u.has_badge(28) - if badge: g.db.delete(badge) - - if u.flairchanged and u.flairchanged < time.time(): - u.flairchanged = None - send_repeatable_notification(u.id, "Your flair lock has expired. You can now change your flair!") - badge = u.has_badge(96) - if badge: g.db.delete(badge) - - if u.marseyawarded and u.marseyawarded < time.time(): - u.marseyawarded = None - send_repeatable_notification(u.id, "Your marsey award has expired!") - badge = u.has_badge(98) - if badge: g.db.delete(badge) - - if u.longpost and u.longpost < time.time(): - u.longpost = None - send_repeatable_notification(u.id, "Your pizzashill award has expired!") - badge = u.has_badge(97) - if badge: g.db.delete(badge) - - if u.bird and u.bird < time.time(): - u.bird = None - send_repeatable_notification(u.id, "Your bird site award has expired!") - badge = u.has_badge(95) - if badge: g.db.delete(badge) - - if u.progressivestack and u.progressivestack < time.time(): - u.progressivestack = None - send_repeatable_notification(u.id, "Your progressive stack has expired!") - badge = u.has_badge(94) - if badge: g.db.delete(badge) - - if u.rehab and u.rehab < time.time(): - u.rehab = None - send_repeatable_notification(u.id, "Your rehab has finished!") - badge = u.has_badge(109) - if badge: g.db.delete(badge) - - if u.deflector and u.deflector < time.time(): - u.deflector = None - send_repeatable_notification(u.id, "Your deflector has expired!") - - g.db.commit() - - return render_template("admin/content_stats.html", v=v, title="Content Statistics", data=stats) - - -@app.get("/chart") -def chart(): - return redirect('/weekly_chart') - - -@app.get("/weekly_chart") -@auth_required -def weekly_chart(v): - file = cached_chart(kind="weekly", site=SITE) - f = send_file(file) - return f - -@app.get("/daily_chart") -@auth_required -def daily_chart(v): - file = cached_chart(kind="daily", site=SITE) - f = send_file(file) - return f - - -@cache.memoize(timeout=86400) -def cached_chart(kind, site): - 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) - - if kind == "daily": day_cutoffs = [today_cutoff - 86400 * i for i in range(47)][1:] - else: day_cutoffs = [today_cutoff - 86400 * 7 * i for i in range(47)][1:] - - day_cutoffs.insert(0, calendar.timegm(now)) - - daily_times = [time.strftime("%d/%m", time.gmtime(day_cutoffs[i + 1])) for i in range(len(day_cutoffs) - 1)][::-1] - - daily_signups = [g.db.query(User.id).filter(User.created_utc < day_cutoffs[i], User.created_utc > day_cutoffs[i + 1]).count() for i in range(len(day_cutoffs) - 1)][::-1] - - post_stats = [g.db.query(Submission.id).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)][::-1] - - comment_stats = [g.db.query(Comment.id).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count() for i in range(len(day_cutoffs) - 1)][::-1] - - plt.rcParams["figure.figsize"] = (30, 20) - - signup_chart = plt.subplot2grid((30, 20), (0, 0), rowspan=6, colspan=30) - posts_chart = plt.subplot2grid((30, 20), (10, 0), rowspan=6, colspan=30) - comments_chart = plt.subplot2grid((30, 20), (20, 0), rowspan=6, colspan=30) - - 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='blue') - comments_chart.plot( - daily_times, - comment_stats, - color='purple') - - signup_chart.set_ylim(ymin=0) - posts_chart.set_ylim(ymin=0) - comments_chart.set_ylim(ymin=0) - - 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 = f"/{SITE}_{kind}.png" - - plt.savefig(file) - plt.clf() - return file - - -@app.get("/patrons") -@app.get("/paypigs") -@admin_level_required(3) -def patrons(v): - users = g.db.query(User).filter(User.patron > 0).order_by(User.patron.desc(), User.id).all() - - return render_template("patrons.html", v=v, users=users) - -@app.get("/admins") -@app.get("/badmins") -@auth_required -def admins(v): - if v and v.admin_level > 2: - admins = g.db.query(User).filter(User.admin_level>1).order_by(User.truecoins.desc()).all() - admins += g.db.query(User).filter(User.admin_level==1).order_by(User.truecoins.desc()).all() - else: admins = g.db.query(User).filter(User.admin_level>0).order_by(User.truecoins.desc()).all() - return render_template("admins.html", v=v, admins=admins) - - -@app.get("/log") -@app.get("/modlog") -@auth_required -def log(v): - - try: page = max(int(request.values.get("page", 1)), 1) - except: page = 1 - - admin = request.values.get("admin") - if admin: admin_id = get_id(admin) - else: admin_id = 0 - - kind = request.values.get("kind") - - if v and v.admin_level > 1: types = ACTIONTYPES - else: types = ACTIONTYPES2 - - if kind not in types: kind = None - - actions = g.db.query(ModAction) - if not (v and v.admin_level > 1): - actions = actions.filter(ModAction.kind.notin_(["shadowban","unshadowban","flair_post","edit_post"])) - - if admin_id: - actions = actions.filter_by(user_id=admin_id) - kinds = set([x.kind for x in actions]) - types2 = {} - for k,val in types.items(): - if k in kinds: types2[k] = val - types = types2 - if kind: actions = actions.filter_by(kind=kind) - - actions = actions.order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all() - next_exists=len(actions)>25 - actions=actions[:25] - - admins = [x[0] for x in g.db.query(User.username).filter(User.admin_level > 1).order_by(User.username).all()] - - return render_template("log.html", v=v, admins=admins, types=types, admin=admin, type=kind, actions=actions, next_exists=next_exists, page=page) - -@app.get("/log/<id>") -@auth_required -def log_item(id, v): - - try: id = int(id) - except: abort(404) - - action=g.db.query(ModAction).filter_by(id=id).one_or_none() - - if not action: abort(404) - - admins = [x[0] for x in g.db.query(User.username).filter(User.admin_level > 1).all()] - - if v and v.admin_level > 1: types = ACTIONTYPES - else: types = ACTIONTYPES2 - - return render_template("log.html", v=v, actions=[action], next_exists=False, page=1, action=action, admins=admins, types=types) - - -@app.get("/api") -@auth_required -def api(v): - return render_template("api.html", v=v) - -@app.get("/contact") -@app.get("/press") -@app.get("/media") -@auth_required -def contact(v): - - return render_template("contact.html", v=v) - -@app.post("/send_admin") -@limiter.limit("1/second;2/minute;6/hour;10/day") -@limiter.limit("1/second;2/minute;6/hour;10/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def submit_contact(v): - body = request.values.get("message") - if not body: abort(400) - - body = f'This message has been sent automatically to all admins via [/contact](/contact)\n\nMessage:\n\n' + body - body_html = sanitize(body) - - if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - file=request.files["file"] - if file.content_type.startswith('image/'): - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - url = process_image(name) - body_html += f'<img data-bs-target="#expandImageModal" data-bs-toggle="modal" onclick="expandDesktopImage(this.src)" class="img" src="{url}" loading="lazy">' - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data'] - except requests.Timeout: return {"error": "Video upload timed out, please try again!"} - try: url = req['link'] - except: - error = req['error'] - if error == 'File exceeds max duration': error += ' (60 seconds)' - return {"error": error}, 400 - if url.endswith('.'): url += 'mp4' - body_html += f"<p>{url}</p>" - else: return {"error": "Image/Video files only"}, 400 - - - - new_comment = Comment(author_id=v.id, - parent_submission=None, - level=1, - body_html=body_html, - sentto=2 - ) - g.db.add(new_comment) - g.db.flush() - new_comment.top_comment_id = new_comment.id - - for admin in g.db.query(User).filter(User.admin_level > 2).all(): - notif = Notification(comment_id=new_comment.id, user_id=admin.id) - g.db.add(notif) - - - - 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/<path:path>') -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('/e/<emoji>') -@limiter.exempt -def emoji(emoji): - if not emoji.endswith('.webp'): abort(404) - resp = make_response(send_from_directory('assets/images/emojis', emoji)) - resp.headers.remove("Cache-Control") - resp.headers.add("Cache-Control", "public, max-age=3153600") - resp.headers.remove("Content-Type") - resp.headers.add("Content-Type", "image/webp") - return resp - -@app.get('/assets/<path:path>') -@app.get('/static/assets/<path:path>') -@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('.woff2'): - resp.headers.remove("Cache-Control") - resp.headers.add("Cache-Control", "public, max-age=3153600") - - if request.path.endswith('.webp'): - resp.headers.remove("Content-Type") - resp.headers.add("Content-Type", "image/webp") - - return resp - -@app.get('/images/<path>') -@app.get('/hostedimages/<path>') -@app.get("/static/images/<path>") -@limiter.exempt -def images(path): - resp = make_response(send_from_directory('/images', path.replace('.WEBP','.webp'))) - resp.headers.remove("Cache-Control") - resp.headers.add("Cache-Control", "public, max-age=3153600") - if request.path.endswith('.webp'): - resp.headers.remove("Content-Type") - resp.headers.add("Content-Type", "image/webp") - return resp - -@app.get("/robots.txt") -def robots_txt(): - try: f = send_file("assets/robots.txt") - except: - print('/robots.txt', flush=True) - abort(404) - return f - -@app.get("/badges") -@auth_required -@cache.memoize(timeout=3600, make_name=make_name) -def badges(v): - badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all() - counts_raw = g.db.query(Badge.badge_id, func.count()).group_by(Badge.badge_id).all() - users = g.db.query(User.id).count() - - counts = {} - for c in counts_raw: - counts[c[0]] = (c[1], float(c[1]) * 100 / max(users, 1)) - - return render_template("badges.html", v=v, badges=badges, counts=counts) - -@app.get("/blocks") -@auth_required -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_required -def banned(v): - - users = [x for x in g.db.query(User).filter(User.is_banned > 0, User.unban_utc == 0).all()] - return render_template("banned.html", v=v, users=users) - -@app.get("/formatting") -@auth_required -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", encoding="utf-8") 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 - ) - -@app.get("/.well-known/assetlinks.json") -def googleplayapp(): - with open("files/assets/assetlinks.json", "r") as f: - return Response(f.read(), mimetype='application/json') - - - -@app.post("/dismiss_mobile_tip") -def dismiss_mobile_tip(): - session["tooltip_last_dismissed"] = int(time.time()) - return "", 204 +from files.mail import * +from files.__main__ import app, limiter, mail +from files.helpers.alerts import * +from files.helpers.const import * +from files.classes.award import AWARDS +from sqlalchemy import func +import os +import calendar +import matplotlib.pyplot as plt +from files.classes.mod_logs import ACTIONTYPES, ACTIONTYPES2 +from files.classes.badges import BadgeDef + +@app.get("/r/drama/comments/<id>/<title>") +@app.get("/r/Drama/comments/<id>/<title>") +def rdrama(id, title): + id = ''.join(f'{x}/' for x in id) + return redirect(f'/archives/drama/comments/{id}{title}.html') + + +@app.get("/marseys") +@auth_required +def marseys(v): + if SITE_NAME == 'rDrama': + marseys = g.db.query(Marsey, User).join(User, User.id==Marsey.author_id) + sort = request.values.get("sort", "usage") + if sort == "usage": marseys = marseys.order_by(Marsey.count.desc(), User.username) + else: marseys = marseys.order_by(User.username, Marsey.count.desc()) + else: + marseys = g.db.query(Marsey).order_by(Marsey.count.desc()) + return render_template("marseys.html", v=v, marseys=marseys) + +@app.get("/marsey_list.json") +@cache.memoize(timeout=600) +def marsey_list(): + # From database + emojis = [{ + "name": emoji.name, + "author": author if SITE_NAME == 'rDrama' or author == "anton-d" else None, + # yikes, I don't really like this DB schema. Next time be better + "tags": emoji.tags.split(" ") + [emoji.name[len("marsey"):] if emoji.name.startswith("marsey") else emoji.name], + "count": emoji.count, + "class": "Marsey" + } for emoji, author in g.db.query(Marsey, User.username).join(User, User.id==Marsey.author_id).order_by(Marsey.count.desc())] + + # Stastic shit + shit = open("files/assets/shit emojis.json", "r", encoding="utf-8") + emojis = emojis + json.load(shit) + shit.close() + + if SITE_NAME == 'Cringetopia': + shit = open("files/assets/shit emojis.cringetopia.json", "r", encoding="utf-8") + emojis = emojis + json.load(shit) + shit.close() + + # return str(marseys).replace("'",'"') + return jsonify(emojis) + +@app.get('/rules') +@app.get('/sidebar') +@app.get('/logged_out/rules') +@app.get('/logged_out/sidebar') +@auth_desired +def sidebar(v): + 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'): return redirect(request.full_path.replace('/logged_out','')) + + return render_template('sidebar.html', v=v) + + +@app.get("/stats") +@auth_required +def participation_stats(v): + + return render_template("admin/content_stats.html", v=v, title="Content Statistics", data=stats(site=SITE)) + + +@cache.memoize(timeout=86400) +def stats(site=None): + day = int(time.time()) - 86400 + + week = int(time.time()) - 604800 + posters = g.db.query(Submission.author_id).distinct(Submission.author_id).filter(Submission.created_utc > week).all() + commenters = g.db.query(Comment.author_id).distinct(Comment.author_id).filter(Comment.created_utc > week).all() + voters = g.db.query(Vote.user_id).distinct(Vote.user_id).filter(Vote.created_utc > week).all() + commentvoters = g.db.query(CommentVote.user_id).distinct(CommentVote.user_id).filter(CommentVote.created_utc > week).all() + + active_users = set(posters) | set(commenters) | set(voters) | set(commentvoters) + + stats = {"marseys": g.db.query(Marsey).count(), + "users": g.db.query(User).count(), + "private users": g.db.query(User).filter_by(is_private=True).count(), + "banned users": g.db.query(User).filter(User.is_banned > 0).count(), + "verified email users": g.db.query(User).filter_by(is_activated=True).count(), + "coins in circulation": g.db.query(func.sum(User.coins)).scalar(), + "total shop sales": g.db.query(func.sum(User.coins_spent)).scalar(), + "signups last 24h": g.db.query(User).filter(User.created_utc > day).count(), + "total posts": g.db.query(Submission).count(), + "posting users": g.db.query(Submission.author_id).distinct().count(), + "listed posts": g.db.query(Submission).filter_by(is_banned=False).filter(Submission.deleted_utc == 0).count(), + "removed posts (by admins)": g.db.query(Submission).filter_by(is_banned=True).count(), + "deleted posts (by author)": g.db.query(Submission).filter(Submission.deleted_utc > 0).count(), + "posts last 24h": g.db.query(Submission).filter(Submission.created_utc > day).count(), + "total comments": g.db.query(Comment).filter(Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(), + "commenting users": g.db.query(Comment.author_id).distinct().count(), + "removed comments (by admins)": g.db.query(Comment).filter_by(is_banned=True).count(), + "deleted comments (by author)": g.db.query(Comment).filter(Comment.deleted_utc > 0).count(), + "comments last_24h": g.db.query(Comment).filter(Comment.created_utc > day, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(), + "post votes": g.db.query(Vote).count(), + "post voting users": g.db.query(Vote.user_id).distinct().count(), + "comment votes": g.db.query(CommentVote).count(), + "comment voting users": g.db.query(CommentVote.user_id).distinct().count(), + "total upvotes": g.db.query(Vote).filter_by(vote_type=1).count() + g.db.query(CommentVote.comment_id).filter_by(vote_type=1).count(), + "total downvotes": g.db.query(Vote).filter_by(vote_type=-1).count() + g.db.query(CommentVote.comment_id).filter_by(vote_type=-1).count(), + "total awards": g.db.query(AwardRelationship).count(), + "awards given": g.db.query(AwardRelationship).filter(or_(AwardRelationship.submission_id != None, AwardRelationship.comment_id != None)).count(), + "users who posted, commented, or voted in the past 7 days": len(active_users), + } + + + if SITE_NAME == 'rDrama': + furries1 = g.db.query(User).filter(User.house.like('Furry%')).count() + femboys1 = g.db.query(User).filter(User.house.like('Femboy%')).count() + vampires1 = g.db.query(User).filter(User.house.like('Vampire%')).count() + racists1 = g.db.query(User).filter(User.house.like('Racist%')).count() + + furries2 = g.db.query(func.sum(User.truecoins)).filter(User.house.like('Furry%')).scalar() + femboys2 = g.db.query(func.sum(User.truecoins)).filter(User.house.like('Femboy%')).scalar() + vampires2 = g.db.query(func.sum(User.truecoins)).filter(User.house.like('Vampire%')).scalar() + racists2 = g.db.query(func.sum(User.truecoins)).filter(User.house.like('Racist%')).scalar() + + stats2 = {"House furry members": furries1, + "House femboy members": femboys1, + "House vampire members": vampires1, + "House racist members": racists1, + "House furry total truescore": furries2, + "House femboy total truescore": femboys2, + "House vampire total truescore": vampires2, + "House racist total truescore": racists2, + } + + stats.update(stats2) + + ids = (NOTIFICATIONS_ID, AUTOJANNY_ID, SNAPPY_ID, LONGPOSTBOT_ID, ZOZBOT_ID) + bots = g.db.query(User).filter(User.id.in_(ids)) + + for u in bots: + g.db.add(u) + + if u.patron_utc and u.patron_utc < time.time(): + u.patron = 0 + u.patron_utc = 0 + send_repeatable_notification(u.id, "Your paypig status has expired!") + if u.discord_id: remove_role(v, "1") + + if u.unban_utc and u.unban_utc < time.time(): + u.is_banned = 0 + u.unban_utc = 0 + u.ban_evade = 0 + send_repeatable_notification(u.id, "You have been unbanned!") + + if u.agendaposter and u.agendaposter < time.time(): + u.agendaposter = 0 + send_repeatable_notification(u.id, "Your chud theme has expired!") + badge = u.has_badge(28) + if badge: g.db.delete(badge) + + if u.flairchanged and u.flairchanged < time.time(): + u.flairchanged = None + send_repeatable_notification(u.id, "Your flair lock has expired. You can now change your flair!") + badge = u.has_badge(96) + if badge: g.db.delete(badge) + + if u.marseyawarded and u.marseyawarded < time.time(): + u.marseyawarded = None + send_repeatable_notification(u.id, "Your marsey award has expired!") + badge = u.has_badge(98) + if badge: g.db.delete(badge) + + if u.longpost and u.longpost < time.time(): + u.longpost = None + send_repeatable_notification(u.id, "Your pizzashill award has expired!") + badge = u.has_badge(97) + if badge: g.db.delete(badge) + + if u.bird and u.bird < time.time(): + u.bird = None + send_repeatable_notification(u.id, "Your bird site award has expired!") + badge = u.has_badge(95) + if badge: g.db.delete(badge) + + if u.progressivestack and u.progressivestack < time.time(): + u.progressivestack = None + send_repeatable_notification(u.id, "Your progressive stack has expired!") + badge = u.has_badge(94) + if badge: g.db.delete(badge) + + if u.rehab and u.rehab < time.time(): + u.rehab = None + send_repeatable_notification(u.id, "Your rehab has finished!") + badge = u.has_badge(109) + if badge: g.db.delete(badge) + + if u.deflector and u.deflector < time.time(): + u.deflector = None + send_repeatable_notification(u.id, "Your deflector has expired!") + + g.db.commit() + + return stats + + +@app.get("/chart") +def chart(): + return redirect('/weekly_chart') + + +@app.get("/weekly_chart") +@auth_required +def weekly_chart(v): + file = cached_chart(kind="weekly", site=SITE) + f = send_file(file) + return f + +@app.get("/daily_chart") +@auth_required +def daily_chart(v): + file = cached_chart(kind="daily", site=SITE) + f = send_file(file) + return f + + +@cache.memoize(timeout=86400) +def cached_chart(kind, site): + 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) + + if kind == "daily": day_cutoffs = [today_cutoff - 86400 * i for i in range(55)][1:] + else: day_cutoffs = [today_cutoff - 86400 * 7 * i for i in range(55)][1:] + + day_cutoffs.insert(0, calendar.timegm(now)) + + daily_times = [time.strftime("%d/%m", time.gmtime(day_cutoffs[i + 1])) for i in range(len(day_cutoffs) - 1)][::-1] + + daily_signups = [g.db.query(User).filter(User.created_utc < day_cutoffs[i], User.created_utc > day_cutoffs[i + 1]).count() for i in range(len(day_cutoffs) - 1)][::-1] + + post_stats = [g.db.query(Submission).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)][::-1] + + comment_stats = [g.db.query(Comment).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count() for i in range(len(day_cutoffs) - 1)][::-1] + + plt.rcParams["figure.figsize"] = (30, 20) + + signup_chart = plt.subplot2grid((30, 20), (0, 0), rowspan=6, colspan=30) + posts_chart = plt.subplot2grid((30, 20), (10, 0), rowspan=6, colspan=30) + comments_chart = plt.subplot2grid((30, 20), (20, 0), rowspan=6, colspan=30) + + 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='blue') + comments_chart.plot( + daily_times, + comment_stats, + color='purple') + + signup_chart.set_ylim(ymin=0) + posts_chart.set_ylim(ymin=0) + comments_chart.set_ylim(ymin=0) + + signup_chart.set_ylabel("Signups") + posts_chart.set_ylabel("Posts") + comments_chart.set_ylabel("Comments") + comments_chart.set_xlabel("Time (UTC)") + + file = f"/{SITE}_{kind}.png" + + plt.savefig(file) + plt.clf() + return file + + +@app.get("/patrons") +@app.get("/paypigs") +@admin_level_required(3) +def patrons(v): + users = g.db.query(User).filter(User.patron > 0).order_by(User.patron.desc(), User.id).all() + + return render_template("patrons.html", v=v, users=users) + +@app.get("/admins") +@app.get("/badmins") +@auth_required +def admins(v): + if v and v.admin_level > 2: + admins = g.db.query(User).filter(User.admin_level>1).order_by(User.truecoins.desc()).all() + admins += g.db.query(User).filter(User.admin_level==1).order_by(User.truecoins.desc()).all() + else: admins = g.db.query(User).filter(User.admin_level>0).order_by(User.truecoins.desc()).all() + return render_template("admins.html", v=v, admins=admins) + + +@app.get("/log") +@app.get("/modlog") +@auth_required +def log(v): + + try: page = max(int(request.values.get("page", 1)), 1) + except: page = 1 + + admin = request.values.get("admin") + if admin: admin_id = get_id(admin) + else: admin_id = 0 + + kind = request.values.get("kind") + + if v and v.admin_level > 1: types = ACTIONTYPES + else: types = ACTIONTYPES2 + + if kind not in types: kind = None + + actions = g.db.query(ModAction) + if not (v and v.admin_level > 1): + actions = actions.filter(ModAction.kind.notin_(["shadowban","unshadowban","flair_post","edit_post"])) + + if admin_id: + actions = actions.filter_by(user_id=admin_id) + kinds = set([x.kind for x in actions]) + types2 = {} + for k,val in types.items(): + if k in kinds: types2[k] = val + types = types2 + if kind: actions = actions.filter_by(kind=kind) + + actions = actions.order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all() + next_exists=len(actions)>25 + actions=actions[:25] + + admins = [x[0] for x in g.db.query(User.username).filter(User.admin_level > 1).order_by(User.username).all()] + + return render_template("log.html", v=v, admins=admins, types=types, admin=admin, type=kind, actions=actions, next_exists=next_exists, page=page) + +@app.get("/log/<id>") +@auth_required +def log_item(id, v): + + try: id = int(id) + except: abort(404) + + action=g.db.query(ModAction).filter_by(id=id).one_or_none() + + if not action: abort(404) + + admins = [x[0] for x in g.db.query(User.username).filter(User.admin_level > 1).all()] + + if v and v.admin_level > 1: types = ACTIONTYPES + else: types = ACTIONTYPES2 + + return render_template("log.html", v=v, actions=[action], next_exists=False, page=1, action=action, admins=admins, types=types) + + +@app.get("/api") +@auth_required +def api(v): + return render_template("api.html", v=v) + +@app.get("/contact") +@app.get("/press") +@app.get("/media") +@auth_required +def contact(v): + + return render_template("contact.html", v=v) + +@app.post("/send_admin") +@limiter.limit("1/second;2/minute;6/hour;10/day") +@limiter.limit("1/second;2/minute;6/hour;10/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def submit_contact(v): + body = request.values.get("message") + if not body: abort(400) + + body = f'This message has been sent automatically to all admins via [/contact](/contact)\n\nMessage:\n\n' + body + body_html = sanitize(body) + + if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + file=request.files["file"] + if file.content_type.startswith('image/'): + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + url = process_image(v.patron, name) + body_html += f'<img data-bs-target="#expandImageModal" data-bs-toggle="modal" onclick="expandDesktopImage(this.src)" class="img" src="{url}" loading="lazy">' + elif file.content_type.startswith('video/'): + if file.content_type == 'video/webm': + file.save("video.mp4") + else: + file.save("unsanitized.mp4") + os.system(f'ffmpeg -y -loglevel warning -i unsanitized.mp4 -map_metadata -1 -c:v copy -c:a copy video.mp4') + with open("video.mp4", 'rb') as f: + try: req = requests.request("POST", "https://pomf2.lain.la/upload.php", files={'files[]': f}, timeout=5).json() + except requests.Timeout: return {"error": "Video upload timed out, please try again!"} + try: url = req['files'][0]['url'] + except: return {"error": req['description']}, 400 + body_html += f"<p>{url}</p>" + else: return {"error": "Image/Video files only"}, 400 + + + + new_comment = Comment(author_id=v.id, + parent_submission=None, + level=1, + body_html=body_html, + sentto=2 + ) + g.db.add(new_comment) + g.db.flush() + new_comment.top_comment_id = new_comment.id + + for admin in g.db.query(User).filter(User.admin_level > 2).all(): + notif = Notification(comment_id=new_comment.id, user_id=admin.id) + g.db.add(notif) + + + + 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/<path:path>') +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('/e/<emoji>') +@limiter.exempt +def emoji(emoji): + if not emoji.endswith('.webp'): abort(404) + resp = make_response(send_from_directory('assets/images/emojis', emoji)) + resp.headers.remove("Cache-Control") + resp.headers.add("Cache-Control", "public, max-age=3153600") + resp.headers.remove("Content-Type") + resp.headers.add("Content-Type", "image/webp") + return resp + +@app.get('/assets/<path:path>') +@app.get('/static/assets/<path:path>') +@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('.woff2'): + resp.headers.remove("Cache-Control") + resp.headers.add("Cache-Control", "public, max-age=3153600") + + if request.path.endswith('.webp'): + resp.headers.remove("Content-Type") + resp.headers.add("Content-Type", "image/webp") + + return resp + +@app.get('/images/<path>') +@app.get('/hostedimages/<path>') +@app.get("/static/images/<path>") +@limiter.exempt +def images(path): + resp = make_response(send_from_directory('/images', path.replace('.WEBP','.webp'))) + resp.headers.remove("Cache-Control") + resp.headers.add("Cache-Control", "public, max-age=3153600") + if request.path.endswith('.webp'): + resp.headers.remove("Content-Type") + resp.headers.add("Content-Type", "image/webp") + return resp + +@app.get("/robots.txt") +def robots_txt(): + try: f = send_file("assets/robots.txt") + except: + print('/robots.txt', flush=True) + abort(404) + return f + +no = (21,22,23,24,25,26,27) + +@cache.memoize(timeout=3600) +def badge_list(site): + badges = g.db.query(BadgeDef).filter(BadgeDef.id.notin_(no)).order_by(BadgeDef.id).all() + counts_raw = g.db.query(Badge.badge_id, func.count()).group_by(Badge.badge_id).all() + users = g.db.query(User).count() + + counts = {} + for c in counts_raw: + counts[c[0]] = (c[1], float(c[1]) * 100 / max(users, 1)) + + return badges, counts + +@app.get("/badges") +@auth_required +def badges(v): + badges, counts = badge_list(SITE) + return render_template("badges.html", v=v, badges=badges, counts=counts) + +@app.get("/blocks") +@auth_required +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_required +def banned(v): + + users = [x for x in g.db.query(User).filter(User.is_banned > 0, User.unban_utc == 0).all()] + return render_template("banned.html", v=v, users=users) + +@app.get("/formatting") +@auth_required +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", encoding="utf-8") 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 + ) + +@app.get("/.well-known/assetlinks.json") +def googleplayapp(): + with open("files/assets/assetlinks.json", "r") as f: + return Response(f.read(), mimetype='application/json') + + + +@app.post("/dismiss_mobile_tip") +def dismiss_mobile_tip(): + session["tooltip_last_dismissed"] = int(time.time()) + return "", 204 diff --git a/files/routes/subs.py b/files/routes/subs.py index c657aa17b..09def2747 100644 --- a/files/routes/subs.py +++ b/files/routes/subs.py @@ -3,8 +3,7 @@ from files.helpers.alerts import * from files.helpers.wrappers import * from files.classes import * from .front import frontlist - - +import tldextract @app.post("/exile/post/<pid>") @is_not_permabanned @@ -224,7 +223,7 @@ def remove_mod(v, sub): @app.get("/create_sub") @is_not_permabanned def create_sub(v): - if SITE_NAME == 'rDrama' and v.admin_level < 3: abort(403) + if SITE_NAME != 'PCM' and v.admin_level < 3: abort(403) if request.host == 'rdrama.net': cost = 0 else: @@ -336,9 +335,22 @@ def post_sub_css(v, sub): if not v.mods(sub.name): abort(403) - sub.css = request.values.get('css', '').strip() - g.db.add(sub) + css = request.values.get('css', '').strip() + + urls = list(css_regex.finditer(css)) + list(css_regex2.finditer(css)) + for i in urls: + url = i.group(1) + if url.startswith('/'): continue + domain = tldextract.extract(url).registered_domain + if domain not in approved_embed_hosts: + error = f"The domain '{domain}' is not allowed, please use one of these domains\n\n{approved_embed_hosts}." + return render_template('sub/settings.html', v=v, sidebar=sub.sidebar, sub=sub, error=error) + + + + sub.css = css + g.db.add(sub) g.db.commit() return redirect(f'/h/{sub.name}/settings') @@ -369,7 +381,7 @@ def sub_banner(v, sub): name = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(name) - bannerurl = process_image(name) + bannerurl = process_image(v.patron, name) if bannerurl: if sub.bannerurl and '/images/' in sub.bannerurl: @@ -396,7 +408,7 @@ def sub_sidebar(v, sub): file = request.files["sidebar"] name = f'/images/{time.time()}'.replace('.','') + '.webp' file.save(name) - sidebarurl = process_image(name) + sidebarurl = process_image(v.patron, name) if sidebarurl: if sub.sidebarurl and '/images/' in sub.sidebarurl: diff --git a/files/routes/users.py b/files/routes/users.py index aff504b71..c76b76925 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -1,1216 +1,1243 @@ -import qrcode -import io -import time -import math -from files.classes.views import ViewerRelationship -from files.helpers.alerts import * -from files.helpers.sanitize import * -from files.helpers.const import * -from files.mail import * -from flask import * -from files.__main__ import app, limiter, db_session -from pusher_push_notifications import PushNotifications -from collections import Counter -import gevent -from sys import stdout - -if PUSHER_ID != 'blahblahblah': - beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY) - -def pusher_thread2(interests, notifbody, username): - beams_client.publish_to_interests( - interests=[interests], - publish_body={ - 'web': { - 'notification': { - 'title': f'New message from @{username}', - 'body': notifbody, - 'deep_link': f'{SITE_FULL}/notifications?messages=true', - 'icon': f'{SITE_FULL}/assets/images/{SITE_NAME}/icon.webp?v=1015', - } - }, - 'fcm': { - 'notification': { - 'title': f'New message from @{username}', - 'body': notifbody, - }, - 'data': { - 'url': '/notifications?messages=true', - } - } - }, - ) - stdout.flush() - -def leaderboard_thread(): - global users9, users9_25, users13, users13_25 - - db = db_session() - - votes1 = db.query(Submission.author_id, func.count(Submission.author_id)).join(Vote, Vote.submission_id==Submission.id).filter(Vote.vote_type==-1).group_by(Submission.author_id).order_by(func.count(Submission.author_id).desc()).all() - votes2 = db.query(Comment.author_id, func.count(Comment.author_id)).join(CommentVote, CommentVote.comment_id==Comment.id).filter(CommentVote.vote_type==-1).group_by(Comment.author_id).order_by(func.count(Comment.author_id).desc()).all() - votes3 = Counter(dict(votes1)) + Counter(dict(votes2)) - users8 = db.query(User).filter(User.id.in_(votes3.keys())).all() - users9 = [] - for user in users8: users9.append((user, votes3[user.id])) - users9 = sorted(users9, key=lambda x: x[1], reverse=True) - users9_25 = users9[:25] - - votes1 = db.query(Vote.user_id, func.count(Vote.user_id)).filter(Vote.vote_type==1).group_by(Vote.user_id).order_by(func.count(Vote.user_id).desc()).all() - votes2 = db.query(CommentVote.user_id, func.count(CommentVote.user_id)).filter(CommentVote.vote_type==1).group_by(CommentVote.user_id).order_by(func.count(CommentVote.user_id).desc()).all() - votes3 = Counter(dict(votes1)) + Counter(dict(votes2)) - users14 = db.query(User).filter(User.id.in_(votes3.keys())).all() - users13 = [] - for user in users14: - users13.append((user, votes3[user.id]-user.post_count-user.comment_count)) - users13 = sorted(users13, key=lambda x: x[1], reverse=True) - users13_25 = users13[:25] - - db.close() - stdout.flush() - -gevent.spawn(leaderboard_thread()) - - - - - - - - - - - -@app.get("/@<username>/upvoters/<uid>/posts") -@auth_required -def upvoters_posts(v, username, uid): - u = get_user(username) - if u.is_private and v.id != u.id: abort(403) - id = u.id - uid = int(uid) - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Submission).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==1, Submission.author_id==id, Vote.user_id==uid).order_by(Submission.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [p.id for p in listing] - next_exists = len(listing) > 25 - listing = listing[:25] - - listing = get_posts(listing, v=v) - - return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) - - -@app.get("/@<username>/upvoters/<uid>/comments") -@auth_required -def upvoters_comments(v, username, uid): - u = get_user(username) - if u.is_private and v.id != u.id: abort(403) - id = u.id - uid = int(uid) - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Comment).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==1, Comment.author_id==id, CommentVote.user_id==uid).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [c.id for c in listing] - next_exists = len(listing) > 25 - listing = listing[:25] - - listing = get_comments(listing, v=v) - - return render_template("voted_comments.html", next_exists=next_exists, listing=listing, page=page, v=v, standalone=True) - - -@app.get("/@<username>/downvoters/<uid>/posts") -@auth_required -def downvoters_posts(v, username, uid): - u = get_user(username) - if u.is_private and v.id != u.id: abort(403) - id = u.id - uid = int(uid) - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Submission).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==-1, Submission.author_id==id, Vote.user_id==uid).order_by(Submission.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [p.id for p in listing] - next_exists = len(listing) > 25 - listing = listing[:25] - - listing = get_posts(listing, v=v) - - return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) - - -@app.get("/@<username>/downvoters/<uid>/comments") -@auth_required -def downvoters_comments(v, username, uid): - u = get_user(username) - if u.is_private and v.id != u.id: abort(403) - id = u.id - uid = int(uid) - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Comment).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==-1, Comment.author_id==id, CommentVote.user_id==uid).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [c.id for c in listing] - next_exists = len(listing) > 25 - listing = listing[:25] - - listing = get_comments(listing, v=v) - - return render_template("voted_comments.html", next_exists=next_exists, listing=listing, page=page, v=v, standalone=True) - - - - - -@app.get("/@<username>/upvoting/<uid>/posts") -@auth_required -def upvoting_posts(v, username, uid): - u = get_user(username) - if u.is_private and v.id != u.id: abort(403) - id = u.id - uid = int(uid) - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Submission).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==1, Vote.user_id==id, Submission.author_id==uid).order_by(Submission.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [p.id for p in listing] - next_exists = len(listing) > 25 - listing = listing[:25] - - listing = get_posts(listing, v=v) - - return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) - - -@app.get("/@<username>/upvoting/<uid>/comments") -@auth_required -def upvoting_comments(v, username, uid): - u = get_user(username) - if u.is_private and v.id != u.id: abort(403) - id = u.id - uid = int(uid) - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Comment).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==1, CommentVote.user_id==id, Comment.author_id==uid).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [c.id for c in listing] - next_exists = len(listing) > 25 - listing = listing[:25] - - listing = get_comments(listing, v=v) - - return render_template("voted_comments.html", next_exists=next_exists, listing=listing, page=page, v=v, standalone=True) - - -@app.get("/@<username>/downvoting/<uid>/posts") -@auth_required -def downvoting_posts(v, username, uid): - u = get_user(username) - if u.is_private and v.id != u.id: abort(403) - id = u.id - uid = int(uid) - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Submission).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==-1, Vote.user_id==id, Submission.author_id==uid).order_by(Submission.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [p.id for p in listing] - next_exists = len(listing) > 25 - listing = listing[:25] - - listing = get_posts(listing, v=v) - - return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) - - -@app.get("/@<username>/downvoting/<uid>/comments") -@auth_required -def downvoting_comments(v, username, uid): - u = get_user(username) - if u.is_private and v.id != u.id: abort(403) - id = u.id - uid = int(uid) - - page = max(1, int(request.values.get("page", 1))) - - listing = g.db.query(Comment).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==-1, CommentVote.user_id==id, Comment.author_id==uid).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() - - listing = [c.id for c in listing] - next_exists = len(listing) > 25 - listing = listing[:25] - - listing = get_comments(listing, v=v) - - return render_template("voted_comments.html", next_exists=next_exists, listing=listing, page=page, v=v, standalone=True) - - - - - -@app.get("/grassed") -@auth_required -def grassed(v): - users = g.db.query(User).filter(User.ban_reason.like('grass award used by @%')).all() - - return render_template("grassed.html", v=v, users=users) - -@app.get("/agendaposters") -@auth_required -def agendaposters(v): - users = [x for x in g.db.query(User).filter(User.agendaposter > 0).order_by(User.username).all()] - return render_template("agendaposters.html", v=v, users=users) - - -@app.get("/@<username>/upvoters") -@auth_required -def upvoters(v, username): - id = get_user(username).id - - votes = g.db.query(Vote.user_id, func.count(Vote.user_id)).join(Submission, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==1, Submission.author_id==id).group_by(Vote.user_id).order_by(func.count(Vote.user_id).desc()).all() - - votes2 = g.db.query(CommentVote.user_id, func.count(CommentVote.user_id)).join(Comment, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==1, Comment.author_id==id).group_by(CommentVote.user_id).order_by(func.count(CommentVote.user_id).desc()).all() - - votes = Counter(dict(votes)) + Counter(dict(votes2)) - - users = g.db.query(User).filter(User.id.in_(votes.keys())).all() - users2 = [] - for user in users: users2.append((user, votes[user.id])) - - users = sorted(users2, key=lambda x: x[1], reverse=True) - - try: - pos = [x[0].id for x in users].index(v.id) - pos = (pos+1, users[pos][1]) - except: pos = (len(users)+1, 0) - - return render_template("voters.html", v=v, users=users[:25], pos=pos, name='Up', name2=f'@{username} biggest simps') - - - -@app.get("/@<username>/downvoters") -@auth_required -def downvoters(v, username): - id = get_user(username).id - - votes = g.db.query(Vote.user_id, func.count(Vote.user_id)).join(Submission, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==-1, Submission.author_id==id).group_by(Vote.user_id).order_by(func.count(Vote.user_id).desc()).all() - - votes2 = g.db.query(CommentVote.user_id, func.count(CommentVote.user_id)).join(Comment, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==-1, Comment.author_id==id).group_by(CommentVote.user_id).order_by(func.count(CommentVote.user_id).desc()).all() - - votes = Counter(dict(votes)) + Counter(dict(votes2)) - - users = g.db.query(User).filter(User.id.in_(votes.keys())).all() - users2 = [] - for user in users: users2.append((user, votes[user.id])) - - users = sorted(users2, key=lambda x: x[1], reverse=True) - - try: - pos = [x[0].id for x in users].index(v.id) - pos = (pos+1, users[pos][1]) - except: pos = (len(users)+1, 0) - - return render_template("voters.html", v=v, users=users[:25], pos=pos, name='Down', name2=f'@{username} biggest haters') - -@app.get("/@<username>/upvoting") -@auth_required -def upvoting(v, username): - id = get_user(username).id - - votes = g.db.query(Submission.author_id, func.count(Submission.author_id)).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==1, Vote.user_id==id).group_by(Submission.author_id).order_by(func.count(Submission.author_id).desc()).all() - - votes2 = g.db.query(Comment.author_id, func.count(Comment.author_id)).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==1, CommentVote.user_id==id).group_by(Comment.author_id).order_by(func.count(Comment.author_id).desc()).all() - - votes = Counter(dict(votes)) + Counter(dict(votes2)) - - users = g.db.query(User).filter(User.id.in_(votes.keys())).all() - users2 = [] - for user in users: users2.append((user, votes[user.id])) - - users = sorted(users2, key=lambda x: x[1], reverse=True) - - try: - pos = [x[0].id for x in users].index(v.id) - pos = (pos+1, users[pos][1]) - except: pos = (len(users)+1, 0) - - return render_template("voters.html", v=v, users=users[:25], pos=pos, name='Up', name2=f'Who @{username} simps for') - -@app.get("/@<username>/downvoting") -@auth_required -def downvoting(v, username): - id = get_user(username).id - - votes = g.db.query(Submission.author_id, func.count(Submission.author_id)).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==-1, Vote.user_id==id).group_by(Submission.author_id).order_by(func.count(Submission.author_id).desc()).all() - - votes2 = g.db.query(Comment.author_id, func.count(Comment.author_id)).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==-1, CommentVote.user_id==id).group_by(Comment.author_id).order_by(func.count(Comment.author_id).desc()).all() - - votes = Counter(dict(votes)) + Counter(dict(votes2)) - - users = g.db.query(User).filter(User.id.in_(votes.keys())).all() - users2 = [] - for user in users: users2.append((user, votes[user.id])) - - users = sorted(users2, key=lambda x: x[1], reverse=True) - - try: - pos = [x[0].id for x in users].index(v.id) - pos = (pos+1, users[pos][1]) - except: pos = (len(users)+1, 0) - - return render_template("voters.html", v=v, users=users[:25], pos=pos, name='Down', name2=f'Who @{username} hates') - - - -@app.post("/@<username>/suicide") -@limiter.limit("1/second;5/day") -@limiter.limit("1/second;5/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def suicide(v, username): - user = get_user(username) - suicide = f"Hi there,\n\nA [concerned user](/id/{v.id}) 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 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(user.id, suicide) - g.db.commit() - return {"message": "Help message sent!"} - - -@app.get("/@<username>/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("/@<username>/transfer_coins") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@is_not_permabanned -def transfer_coins(v, username): - receiver = g.db.query(User).filter_by(username=username).one_or_none() - - 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": "Invalid amount of coins."}, 400 - if v.coins < amount: return {"error": "You don't have enough coins."}, 400 - if amount < 100: return {"error": "You have to gift at least 100 coins."}, 400 - - if not v.patron and not receiver.patron and not v.alts_patron and not receiver.alts_patron: tax = math.ceil(amount*0.03) - else: tax = 0 - - log_message = f"@{v.username} has transferred {amount} coins to @{receiver.username}" - send_repeatable_notification(GIFT_NOTIF_ID, log_message) - - receiver.coins += amount-tax - v.coins -= amount - send_repeatable_notification(receiver.id, f":marseycapitalistmanlet: @{v.username} has gifted you {amount-tax} coins!") - g.db.add(receiver) - g.db.add(v) - - g.db.commit() - return {"message": f"{amount-tax} coins transferred!"}, 200 - - return {"message": "You can't transfer coins to yourself!"}, 400 - - -@app.post("/@<username>/transfer_bux") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@is_not_permabanned -def transfer_bux(v, username): - receiver = g.db.query(User).filter_by(username=username).one_or_none() - - if not receiver: 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 not amount or amount < 0: return {"error": "Invalid amount of marseybux."}, 400 - if v.procoins < amount: return {"error": "You don't have enough marseybux"}, 400 - if amount < 100: return {"error": "You have to gift at least 100 marseybux."}, 400 - - log_message = f"@{v.username} has transferred {amount} Marseybux to @{receiver.username}" - send_repeatable_notification(GIFT_NOTIF_ID, log_message) - - receiver.procoins += amount - v.procoins -= amount - send_repeatable_notification(receiver.id, f":marseycapitalistmanlet: @{v.username} has gifted you {amount} marseybux!") - g.db.add(receiver) - g.db.add(v) - - g.db.commit() - return {"message": f"{amount} marseybux transferred!"}, 200 - - return {"message": "You can't transfer marseybux to yourself!"}, 400 - - -@app.get("/leaderboard") -@auth_required -def leaderboard(v): - - users = g.db.query(User) - - users1 = users.order_by(User.coins.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.coins.desc()).label("rank")).subquery() - pos1 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - users2 = users.order_by(User.stored_subscriber_count.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.stored_subscriber_count.desc()).label("rank")).subquery() - pos2 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - users3 = users.order_by(User.post_count.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.post_count.desc()).label("rank")).subquery() - pos3 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - users4 = users.order_by(User.comment_count.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.comment_count.desc()).label("rank")).subquery() - pos4 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - users5 = users.order_by(User.received_award_count.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.received_award_count.desc()).label("rank")).subquery() - pos5 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - if request.host == 'pcmemes.net': - users6 = users.order_by(User.basedcount.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.basedcount.desc()).label("rank")).subquery() - pos6 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - else: - users6 = None - pos6 = None - - users7 = users.order_by(User.coins_spent.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.coins_spent.desc()).label("rank")).subquery() - pos7 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - try: - pos9 = [x[0].id for x in users9].index(v.id) - pos9 = (pos9+1, users9[pos9][1]) - except: pos9 = (len(users9)+1, 0) - - users10 = users.order_by(User.truecoins.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.truecoins.desc()).label("rank")).subquery() - pos10 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - sq = g.db.query(Badge.user_id, func.count(Badge.user_id).label("count"), func.rank().over(order_by=func.count(Badge.user_id).desc()).label("rank")).group_by(Badge.user_id).subquery() - users11 = g.db.query(User, sq.c.count).join(sq, User.id==sq.c.user_id).order_by(sq.c.count.desc()) - pos11 = g.db.query(User.id, sq.c.rank, sq.c.count).join(sq, User.id==sq.c.user_id).filter(User.id == v.id).one_or_none() - if pos11: pos11 = (pos11[1],pos11[2]) - else: pos11 = (users11.count()+1, 0) - users11 = users11.limit(25).all() - - if pos11[1] < 25 and v not in (x[0] for x in users11): - pos11 = (26, pos11[1]) - - if SITE_NAME == 'rDrama': - sq = g.db.query(Marsey.author_id, func.count(Marsey.author_id).label("count"), func.rank().over(order_by=func.count(Marsey.author_id).desc()).label("rank")).group_by(Marsey.author_id).subquery() - users12 = g.db.query(User, sq.c.count).join(sq, User.id==sq.c.author_id).order_by(sq.c.count.desc()) - pos12 = g.db.query(User.id, sq.c.rank, sq.c.count).join(sq, User.id==sq.c.author_id).filter(User.id == v.id).one_or_none() - if pos12: pos12 = (pos12[1],pos12[2]) - else: pos12 = (users12.count()+1, 0) - users12 = users12.limit(25).all() - else: - users12 = None - pos12 = None - - try: - pos13 = [x[0].id for x in users13].index(v.id) - pos13 = (pos13+1, users13[pos13][1]) - except: pos13 = (len(users13)+1, 0) - - users14 = users.order_by(User.winnings.desc()).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.winnings.desc()).label("rank")).subquery() - pos14 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - users15 = users.order_by(User.winnings).limit(25).all() - sq = g.db.query(User.id, func.rank().over(order_by=User.winnings).label("rank")).subquery() - pos15 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - - return render_template("leaderboard.html", v=v, users1=users1, pos1=pos1, users2=users2, pos2=pos2, users3=users3, pos3=pos3, users4=users4, pos4=pos4, users5=users5, pos5=pos5, users6=users6, pos6=pos6, users7=users7, pos7=pos7, users9=users9_25, pos9=pos9, users10=users10, pos10=pos10, users11=users11, pos11=pos11, users12=users12, pos12=pos12, users13=users13_25, pos13=pos13, users14=users14, pos14=pos14, users15=users15, pos15=pos15) - -@app.get("/@<username>/css") -def get_css(username): - user = get_user(username) - resp=make_response(user.css or "") - resp.headers.add("Content-Type", "text/css") - return resp - -@app.get("/@<username>/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("/@<username>/song") -def usersong(username): - user = get_user(username) - if user.song: return redirect(f"/song/{user.song}.mp3") - else: abort(404) - -@app.get("/song/<song>") -@app.get("/static/song/<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=3153600") - return resp - -@app.post("/subscribe/<post_id>") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@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/<post_id>") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def unsubscribe(v, post_id): - sub=g.db.query(Subscription).filter_by(user_id=v.id, submission_id=post_id).one_or_none() - if sub: - g.db.delete(sub) - g.db.commit() - return {"message": "Post unsubscribed!"} - -@app.get("/report_bugs") -@auth_required -def reportbugs(v): - return redirect(f'/post/{BUG_THREAD}') - -@app.post("/@<username>/message") -@limiter.limit("1/second;10/minute;20/hour;50/day") -@limiter.limit("1/second;10/minute;20/hour;50/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@is_not_permabanned -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 and hasattr(user, 'is_blocked') and user.is_blocked: - return {"error": "This user is blocking you."}, 403 - - message = request.values.get("message", "").strip()[:10000].strip() - - if not message: return {"error": "Message is empty!"} - - if 'linkedin.com' in message: return {"error": "This domain 'linkedin.com' is banned."}, 403 - - body_html = sanitize(message) - - existing = g.db.query(Comment.id).filter(Comment.author_id == v.id, - Comment.sentto == user.id, - Comment.body_html == body_html, - ).one_or_none() - - if existing: return {"error": "Message already exists."}, 403 - - c = Comment(author_id=v.id, - parent_submission=None, - level=1, - sentto=user.id, - body_html=body_html - ) - g.db.add(c) - - g.db.flush() - - if blackjack and any(i in c.body_html.lower() for i in blackjack.split()): - v.shadowbanned = 'AutoJanny' - g.db.add(v) - notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=CARP_ID).one_or_none() - if not notif: - notif = Notification(comment_id=c.id, user_id=CARP_ID) - g.db.add(notif) - g.db.flush() - - c.top_comment_id = c.id - - notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=user.id).one_or_none() - if not notif: - notif = Notification(comment_id=c.id, user_id=user.id) - g.db.add(notif) - - g.db.commit() - - if PUSHER_ID != 'blahblahblah' and not v.shadowbanned: - if len(message) > 500: notifbody = message[:500] + '...' - else: notifbody = message - - try: gevent.spawn(pusher_thread2, f'{request.host}{user.id}', notifbody, v.username) - except: pass - - return {"message": "Message sent!"} - - -@app.post("/reply") -@limiter.limit("1/second;6/minute;50/hour;200/day") -@limiter.limit("1/second;6/minute;50/hour;200/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def messagereply(v): - - message = request.values.get("body", "").strip()[:10000].strip() - - if not message and not request.files.get("file"): return {"error": "Message is empty!"} - - if 'linkedin.com' in message: return {"error": "this domain 'linkedin.com' is banned"} - - id = int(request.values.get("parent_id")) - parent = get_comment(id, v=v) - user_id = parent.author.id - - if parent.sentto == 2: user_id = None - elif v.id == user_id: user_id = parent.sentto - - body_html = sanitize(message) - - if parent.sentto == 2 and request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": - file=request.files["file"] - if file.content_type.startswith('image/'): - name = f'/images/{time.time()}'.replace('.','') + '.webp' - file.save(name) - url = process_image(name) - body_html += f'<img data-bs-target="#expandImageModal" data-bs-toggle="modal" onclick="expandDesktopImage(this.src)" class="img" src="{url}" loading="lazy">' - elif file.content_type.startswith('video/'): - file.save("video.mp4") - with open("video.mp4", 'rb') as f: - try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data'] - except requests.Timeout: return {"error": "Video upload timed out, please try again!"} - try: url = req['link'] - except: - error = req['error'] - if error == 'File exceeds max duration': error += ' (60 seconds)' - return {"error": error}, 400 - if url.endswith('.'): url += 'mp4' - body_html += f"<p>{url}</p>" - else: return {"error": "Image/Video files only"}, 400 - - - c = Comment(author_id=v.id, - parent_submission=None, - parent_comment_id=id, - top_comment_id=parent.top_comment_id, - level=parent.level + 1, - sentto=user_id, - body_html=body_html, - ) - g.db.add(c) - g.db.flush() - - if blackjack and any(i in c.body_html.lower() for i in blackjack.split()): - v.shadowbanned = 'AutoJanny' - g.db.add(v) - notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=CARP_ID).one_or_none() - if not notif: - notif = Notification(comment_id=c.id, user_id=CARP_ID) - g.db.add(notif) - g.db.flush() - - if user_id and user_id != v.id and user_id != 2: - notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=user_id).one_or_none() - if not notif: - notif = Notification(comment_id=c.id, user_id=user_id) - g.db.add(notif) - ids = [c.top_comment.id] + [x.id for x in c.top_comment.replies] - notifications = g.db.query(Notification).filter(Notification.comment_id.in_(ids), Notification.user_id == user_id) - for n in notifications: - g.db.delete(n) - - if PUSHER_ID != 'blahblahblah' and not v.shadowbanned: - if len(message) > 500: notifbody = message[:500] + '...' - else: notifbody = message - - beams_client.publish_to_interests( - interests=[f'{request.host}{user_id}'], - publish_body={ - 'web': { - 'notification': { - 'title': f'New message from @{v.username}', - 'body': notifbody, - 'deep_link': f'{SITE_FULL}/notifications?messages=true', - 'icon': f'{SITE_FULL}/assets/images/{SITE_NAME}/icon.webp"a=1010', - } - }, - 'fcm': { - 'notification': { - 'title': f'New message from @{v.username}', - 'body': notifbody, - }, - 'data': { - 'url': '/notifications?messages=true', - } - } - }, - ) - - - if c.top_comment.sentto == 2: - admins = g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all() - for admin in admins: - notif = Notification(comment_id=c.id, user_id=admin.id) - g.db.add(notif) - - ids = [c.top_comment.id] + [x.id for x in c.top_comment.replies] - notifications = g.db.query(Notification).filter(Notification.comment_id.in_(ids)) - for n in notifications: - g.db.delete(n) - - g.db.commit() - - return {"comment": render_template("comments.html", v=v, comments=[c], ajax=True)} - -@app.get("/2faqr/<secret>") -@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) - - try: f = send_file(mem, mimetype="image/png", as_attachment=False) - except: - print('/2faqr/<secret>', flush=True) - abort(404) - return f - - -@app.get("/is_available/<name>") -def api_is_available(name): - - name=name.strip() - - if len(name)<3 or len(name)>25: - return {name:False} - - name2 = name.replace('\\', '').replace('_','\_').replace('%','') - - x= g.db.query(User).filter( - or_( - User.username.ilike(name2), - User.original_username.ilike(name2) - ) - ).one_or_none() - - if x: - return {name: False} - else: - return {name: True} - -@app.get("/id/<id>") -@auth_required -def user_id(id, v): - user = get_account(id) - return redirect(user.url) - -@app.get("/u/<username>") -@auth_required -def redditor_moment_redirect(username, v): - return redirect(f"/@{username}") - -@app.get("/@<username>/followers") -@auth_required -def followers(username, v): - u = get_user(username, v=v) - users = g.db.query(User).join(Follow, Follow.target_id == u.id).filter(Follow.user_id == User.id).order_by(Follow.created_utc).all() - return render_template("followers.html", v=v, u=u, users=users) - -@app.get("/@<username>/following") -@auth_required -def following(username, v): - u = get_user(username, v=v) - users = g.db.query(User).join(Follow, Follow.user_id == u.id).filter(Follow.target_id == User.id).order_by(Follow.created_utc).all() - return render_template("following.html", v=v, u=u, users=users) - -@app.get("/views") -@auth_required -def visitors(v): - if v.admin_level < 2 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("/@<username>") -@auth_desired -def u_username(username, v=None): - - - u = get_user(username, v=v) - - - if username != u.username: - return redirect(SITE_FULL + request.full_path.replace(username, u.username)[:-1]) - - if u.reserved: - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": f"That username is reserved for: {u.reserved}"} - return render_template("userpage_reserved.html", u=u, v=v) - - if v and v.id not in (u.id,DAD_ID) and (u.patron or u.admin_level > 1): - view = g.db.query(ViewerRelationship).filter_by(viewer_id=v.id, user_id=u.id).one_or_none() - - if view: view.last_view_utc = int(time.time()) - else: view = ViewerRelationship(viewer_id=v.id, user_id=u.id) - - g.db.add(view) - g.db.commit() - - - if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "That userpage is private"} - return render_template("userpage_private.html", u=u, v=v) - - - if v and hasattr(u, 'is_blocking') and u.is_blocking: - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": f"You are blocking @{u.username}."} - return render_template("userpage_blocking.html", u=u, v=v) - - - if v and v.admin_level < 2 and hasattr(u, 'is_blocked') and u.is_blocked: - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "This person is blocking you."} - return render_template("userpage_blocked.html", u=u, v=v) - - - sort = request.values.get("sort", "new") - t = request.values.get("t", "all") - try: page = max(int(request.values.get("page", 1)), 1) - except: page = 1 - - ids = u.userpagelisting(site=SITE, v=v, page=page, sort=sort, t=t) - - next_exists = (len(ids) > 25) - ids = ids[:25] - - if page == 1: - sticky = [] - sticky = g.db.query(Submission).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]} - 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]} - 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("/@<username>/comments") -@auth_desired -def u_username_comments(username, v=None): - - user = get_user(username, v=v) - - if username != user.username: return redirect(f'/@{user.username}/comments') - - u = user - - if u.reserved: - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": f"That username is reserved for: {u.reserved}"} - 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 < 2 and not v.eye)): - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "That userpage is private"} - return render_template("userpage_private.html", u=u, v=v) - - if v and hasattr(u, 'is_blocking') and u.is_blocking: - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": f"You are blocking @{u.username}."} - return render_template("userpage_blocking.html", u=u, v=v) - - if v and v.admin_level < 2 and hasattr(u, 'is_blocked') and u.is_blocked: - if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "This person is blocking you."} - return render_template("userpage_blocked.html", u=u, v=v) - - - try: page = max(int(request.values.get("page", "1")), 1) - except: page = 1 - - sort=request.values.get("sort","new") - t=request.values.get("t","all") - - - comments = g.db.query(Comment.id).filter(Comment.author_id == u.id, Comment.parent_submission != None) - - if not v or (v.id != u.id and v.admin_level < 2): - comments = comments.filter(Comment.deleted_utc == 0, Comment.is_banned == False, Comment.ghost == 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) - elif sort == "controversial": - comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) - 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]} - 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("/@<username>/info") -@auth_required -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.get("/<id>/info") -@auth_required -def u_user_id_info(id, v=None): - - user=get_account(id, 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/<username>") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@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).filter_by(user_id=v.id, target_id=target.id).one_or_none(): 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.target_id).filter_by(target_id=target.id).count() - g.db.add(target) - - send_notification(target.id, f"@{v.username} has followed you!") - - g.db.commit() - - return {"message": "User followed!"} - -@app.post("/unfollow/<username>") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def unfollow_user(username, v): - - target = get_user(username) - - if target.fish: - send_notification(target.id, f"@{v.username} has tried to unfollow you and failed because of your fish award!") - g.db.commit() - return {"error": "You can't unfollow this user!"} - - follow = g.db.query(Follow).filter_by(user_id=v.id, target_id=target.id).one_or_none() - - if follow: - g.db.delete(follow) - - g.db.flush() - target.stored_subscriber_count = g.db.query(Follow.target_id).filter_by(target_id=target.id).count() - g.db.add(target) - - send_notification(target.id, f"@{v.username} has unfollowed you!") - - g.db.commit() - - return {"message": "User unfollowed!"} - -@app.post("/remove_follow/<username>") -@limiter.limit("1/second;30/minute;200/hour;1000/day") -@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') -@auth_required -def remove_follow(username, v): - target = get_user(username) - - follow = g.db.query(Follow).filter_by(user_id=target.id, target_id=v.id).one_or_none() - - if not follow: return {"message": "Follower removed!"} - - g.db.delete(follow) - - g.db.flush() - v.stored_subscriber_count = g.db.query(Follow.target_id).filter_by(target_id=v.id).count() - g.db.add(v) - - send_repeatable_notification(target.id, f"@{v.username} has removed your follow!") - - g.db.commit() - - return {"message": "Follower removed!"} - -@app.get("/pp/<id>") -@app.get("/uid/<id>/pic") -@app.get("/uid/<id>/pic/profile") -@limiter.exempt -@auth_desired -def user_profile_uid(v, 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("/@<username>/pic") -@limiter.exempt -@auth_required -def user_profile_name(v, username): - x = get_user(username) - return redirect(x.profile_url) - -@app.get("/@<username>/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]} - return render_template("userpage.html", - u=v, - v=v, - listing=listing, - page=page, - next_exists=next_exists, - ) - - -@app.get("/@<username>/saved/comments") -@auth_required -def saved_comments(v, username): - - page=int(request.values.get("page",1)) - - ids=v.saved_comment_idlist(page=page) - - 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]} - return render_template("userpage_comments.html", - u=v, - v=v, - listing=listing, - page=page, - next_exists=next_exists, - standalone=True) - - -@app.post("/fp/<fp>") -@auth_required -def fp(v, fp): - v.fp = fp - users = g.db.query(User).filter(User.fp == fp, User.id != v.id).all() - if users: print(f'{v.username}: fp {v.fp}') - if v.email and v.is_activated: - alts = g.db.query(User).filter(User.email == v.email, User.is_activated, User.id != v.id).all() - if alts: - print(f'{v.username}: email {v.email}') - users += alts - for u in users: - li = [v.id, u.id] - existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() - if existing: continue - new_alt = Alt(user1=v.id, user2=u.id) - g.db.add(new_alt) - g.db.flush() - print(v.username + ' + ' + u.username) - g.db.add(v) - g.db.commit() +import qrcode +import io +import time +import math +from files.classes.views import ViewerRelationship +from files.helpers.alerts import * +from files.helpers.sanitize import * +from files.helpers.const import * +from files.mail import * +from flask import * +from files.__main__ import app, limiter, db_session +from pusher_push_notifications import PushNotifications +from collections import Counter +import gevent +from sys import stdout +import os + +if PUSHER_ID != 'blahblahblah': + beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY) + +def pusher_thread2(interests, notifbody, username): + beams_client.publish_to_interests( + interests=[interests], + publish_body={ + 'web': { + 'notification': { + 'title': f'New message from @{username}', + 'body': notifbody, + 'deep_link': f'{SITE_FULL}/notifications?messages=true', + 'icon': f'{SITE_FULL}/assets/images/{SITE_NAME}/icon.webp?v=1015', + } + }, + 'fcm': { + 'notification': { + 'title': f'New message from @{username}', + 'body': notifbody, + }, + 'data': { + 'url': '/notifications?messages=true', + } + } + }, + ) + stdout.flush() + +def leaderboard_thread(): + global users9, users9_25, users13, users13_25 + + db = db_session() + + votes1 = db.query(Submission.author_id, func.count(Submission.author_id)).join(Vote, Vote.submission_id==Submission.id).filter(Vote.vote_type==-1).group_by(Submission.author_id).order_by(func.count(Submission.author_id).desc()).all() + votes2 = db.query(Comment.author_id, func.count(Comment.author_id)).join(CommentVote, CommentVote.comment_id==Comment.id).filter(CommentVote.vote_type==-1).group_by(Comment.author_id).order_by(func.count(Comment.author_id).desc()).all() + votes3 = Counter(dict(votes1)) + Counter(dict(votes2)) + users8 = db.query(User).filter(User.id.in_(votes3.keys())).all() + users9 = [] + for user in users8: users9.append((user, votes3[user.id])) + users9 = sorted(users9, key=lambda x: x[1], reverse=True) + users9_25 = users9[:25] + + votes1 = db.query(Vote.user_id, func.count(Vote.user_id)).filter(Vote.vote_type==1).group_by(Vote.user_id).order_by(func.count(Vote.user_id).desc()).all() + votes2 = db.query(CommentVote.user_id, func.count(CommentVote.user_id)).filter(CommentVote.vote_type==1).group_by(CommentVote.user_id).order_by(func.count(CommentVote.user_id).desc()).all() + votes3 = Counter(dict(votes1)) + Counter(dict(votes2)) + users14 = db.query(User).filter(User.id.in_(votes3.keys())).all() + users13 = [] + for user in users14: + users13.append((user, votes3[user.id]-user.post_count-user.comment_count)) + users13 = sorted(users13, key=lambda x: x[1], reverse=True) + users13_25 = users13[:25] + + db.close() + stdout.flush() + +gevent.spawn(leaderboard_thread()) + + + + + + + + + + + +@app.get("/@<username>/upvoters/<uid>/posts") +@auth_required +def upvoters_posts(v, username, uid): + u = get_user(username) + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): abort(403) + id = u.id + uid = int(uid) + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Submission).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==1, Submission.author_id==id, Vote.user_id==uid).order_by(Submission.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [p.id for p in listing] + next_exists = len(listing) > 25 + listing = listing[:25] + + listing = get_posts(listing, v=v) + + return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) + + +@app.get("/@<username>/upvoters/<uid>/comments") +@auth_required +def upvoters_comments(v, username, uid): + u = get_user(username) + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): abort(403) + id = u.id + uid = int(uid) + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Comment).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==1, Comment.author_id==id, CommentVote.user_id==uid).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [c.id for c in listing] + next_exists = len(listing) > 25 + listing = listing[:25] + + listing = get_comments(listing, v=v) + + return render_template("voted_comments.html", next_exists=next_exists, listing=listing, page=page, v=v, standalone=True) + + +@app.get("/@<username>/downvoters/<uid>/posts") +@auth_required +def downvoters_posts(v, username, uid): + u = get_user(username) + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): abort(403) + id = u.id + uid = int(uid) + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Submission).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==-1, Submission.author_id==id, Vote.user_id==uid).order_by(Submission.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [p.id for p in listing] + next_exists = len(listing) > 25 + listing = listing[:25] + + listing = get_posts(listing, v=v) + + return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) + + +@app.get("/@<username>/downvoters/<uid>/comments") +@auth_required +def downvoters_comments(v, username, uid): + u = get_user(username) + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): abort(403) + id = u.id + uid = int(uid) + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Comment).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==-1, Comment.author_id==id, CommentVote.user_id==uid).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [c.id for c in listing] + next_exists = len(listing) > 25 + listing = listing[:25] + + listing = get_comments(listing, v=v) + + return render_template("voted_comments.html", next_exists=next_exists, listing=listing, page=page, v=v, standalone=True) + + + + + +@app.get("/@<username>/upvoting/<uid>/posts") +@auth_required +def upvoting_posts(v, username, uid): + u = get_user(username) + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): abort(403) + id = u.id + uid = int(uid) + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Submission).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==1, Vote.user_id==id, Submission.author_id==uid).order_by(Submission.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [p.id for p in listing] + next_exists = len(listing) > 25 + listing = listing[:25] + + listing = get_posts(listing, v=v) + + return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) + + +@app.get("/@<username>/upvoting/<uid>/comments") +@auth_required +def upvoting_comments(v, username, uid): + u = get_user(username) + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): abort(403) + id = u.id + uid = int(uid) + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Comment).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==1, CommentVote.user_id==id, Comment.author_id==uid).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [c.id for c in listing] + next_exists = len(listing) > 25 + listing = listing[:25] + + listing = get_comments(listing, v=v) + + return render_template("voted_comments.html", next_exists=next_exists, listing=listing, page=page, v=v, standalone=True) + + +@app.get("/@<username>/downvoting/<uid>/posts") +@auth_required +def downvoting_posts(v, username, uid): + u = get_user(username) + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): abort(403) + id = u.id + uid = int(uid) + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Submission).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==-1, Vote.user_id==id, Submission.author_id==uid).order_by(Submission.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [p.id for p in listing] + next_exists = len(listing) > 25 + listing = listing[:25] + + listing = get_posts(listing, v=v) + + return render_template("voted_posts.html", next_exists=next_exists, listing=listing, page=page, v=v) + + +@app.get("/@<username>/downvoting/<uid>/comments") +@auth_required +def downvoting_comments(v, username, uid): + u = get_user(username) + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): abort(403) + id = u.id + uid = int(uid) + + page = max(1, int(request.values.get("page", 1))) + + listing = g.db.query(Comment).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==-1, CommentVote.user_id==id, Comment.author_id==uid).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all() + + listing = [c.id for c in listing] + next_exists = len(listing) > 25 + listing = listing[:25] + + listing = get_comments(listing, v=v) + + return render_template("voted_comments.html", next_exists=next_exists, listing=listing, page=page, v=v, standalone=True) + + + + + +@app.get("/grassed") +@auth_required +def grassed(v): + users = g.db.query(User).filter(User.ban_reason.like('grass award used by @%')).all() + + return render_template("grassed.html", v=v, users=users) + +@app.get("/agendaposters") +@auth_required +def agendaposters(v): + users = [x for x in g.db.query(User).filter(User.agendaposter > 0).order_by(User.username).all()] + return render_template("agendaposters.html", v=v, users=users) + + +@app.get("/@<username>/upvoters") +@auth_required +def upvoters(v, username): + id = get_user(username).id + + votes = g.db.query(Vote.user_id, func.count(Vote.user_id)).join(Submission, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==1, Submission.author_id==id).group_by(Vote.user_id).order_by(func.count(Vote.user_id).desc()).all() + + votes2 = g.db.query(CommentVote.user_id, func.count(CommentVote.user_id)).join(Comment, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==1, Comment.author_id==id).group_by(CommentVote.user_id).order_by(func.count(CommentVote.user_id).desc()).all() + + votes = Counter(dict(votes)) + Counter(dict(votes2)) + + users = g.db.query(User).filter(User.id.in_(votes.keys())).all() + users2 = [] + for user in users: users2.append((user, votes[user.id])) + + users = sorted(users2, key=lambda x: x[1], reverse=True) + + try: + pos = [x[0].id for x in users].index(v.id) + pos = (pos+1, users[pos][1]) + except: pos = (len(users)+1, 0) + + return render_template("voters.html", v=v, users=users[:25], pos=pos, name='Up', name2=f'@{username} biggest simps') + + + +@app.get("/@<username>/downvoters") +@auth_required +def downvoters(v, username): + id = get_user(username).id + + votes = g.db.query(Vote.user_id, func.count(Vote.user_id)).join(Submission, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==-1, Submission.author_id==id).group_by(Vote.user_id).order_by(func.count(Vote.user_id).desc()).all() + + votes2 = g.db.query(CommentVote.user_id, func.count(CommentVote.user_id)).join(Comment, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==-1, Comment.author_id==id).group_by(CommentVote.user_id).order_by(func.count(CommentVote.user_id).desc()).all() + + votes = Counter(dict(votes)) + Counter(dict(votes2)) + + users = g.db.query(User).filter(User.id.in_(votes.keys())).all() + users2 = [] + for user in users: users2.append((user, votes[user.id])) + + users = sorted(users2, key=lambda x: x[1], reverse=True) + + try: + pos = [x[0].id for x in users].index(v.id) + pos = (pos+1, users[pos][1]) + except: pos = (len(users)+1, 0) + + return render_template("voters.html", v=v, users=users[:25], pos=pos, name='Down', name2=f'@{username} biggest haters') + +@app.get("/@<username>/upvoting") +@auth_required +def upvoting(v, username): + id = get_user(username).id + + votes = g.db.query(Submission.author_id, func.count(Submission.author_id)).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==1, Vote.user_id==id).group_by(Submission.author_id).order_by(func.count(Submission.author_id).desc()).all() + + votes2 = g.db.query(Comment.author_id, func.count(Comment.author_id)).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==1, CommentVote.user_id==id).group_by(Comment.author_id).order_by(func.count(Comment.author_id).desc()).all() + + votes = Counter(dict(votes)) + Counter(dict(votes2)) + + users = g.db.query(User).filter(User.id.in_(votes.keys())).all() + users2 = [] + for user in users: users2.append((user, votes[user.id])) + + users = sorted(users2, key=lambda x: x[1], reverse=True) + + try: + pos = [x[0].id for x in users].index(v.id) + pos = (pos+1, users[pos][1]) + except: pos = (len(users)+1, 0) + + return render_template("voters.html", v=v, users=users[:25], pos=pos, name='Up', name2=f'Who @{username} simps for') + +@app.get("/@<username>/downvoting") +@auth_required +def downvoting(v, username): + id = get_user(username).id + + votes = g.db.query(Submission.author_id, func.count(Submission.author_id)).join(Vote, Vote.submission_id==Submission.id).filter(Submission.ghost == False, Submission.is_banned == False, Submission.deleted_utc == 0, Vote.vote_type==-1, Vote.user_id==id).group_by(Submission.author_id).order_by(func.count(Submission.author_id).desc()).all() + + votes2 = g.db.query(Comment.author_id, func.count(Comment.author_id)).join(CommentVote, CommentVote.comment_id==Comment.id).filter(Comment.ghost == False, Comment.is_banned == False, Comment.deleted_utc == 0, CommentVote.vote_type==-1, CommentVote.user_id==id).group_by(Comment.author_id).order_by(func.count(Comment.author_id).desc()).all() + + votes = Counter(dict(votes)) + Counter(dict(votes2)) + + users = g.db.query(User).filter(User.id.in_(votes.keys())).all() + users2 = [] + for user in users: users2.append((user, votes[user.id])) + + users = sorted(users2, key=lambda x: x[1], reverse=True) + + try: + pos = [x[0].id for x in users].index(v.id) + pos = (pos+1, users[pos][1]) + except: pos = (len(users)+1, 0) + + return render_template("voters.html", v=v, users=users[:25], pos=pos, name='Down', name2=f'Who @{username} hates') + + + +@app.post("/@<username>/suicide") +@limiter.limit("1/second;5/day") +@limiter.limit("1/second;5/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def suicide(v, username): + user = get_user(username) + suicide = f"Hi there,\n\nA [concerned user](/id/{v.id}) 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 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(user.id, suicide) + g.db.commit() + return {"message": "Help message sent!"} + + +@app.get("/@<username>/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("/@<username>/transfer_coins") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@is_not_permabanned +def transfer_coins(v, username): + receiver = g.db.query(User).filter_by(username=username).one_or_none() + + 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": "Invalid amount of coins."}, 400 + if v.coins < amount: return {"error": "You don't have enough coins."}, 400 + if amount < 100: return {"error": "You have to gift at least 100 coins."}, 400 + + if not v.patron and not receiver.patron and not v.alts_patron and not receiver.alts_patron: tax = math.ceil(amount*0.03) + else: tax = 0 + + log_message = f"@{v.username} has transferred {amount} coins to @{receiver.username}" + send_repeatable_notification(GIFT_NOTIF_ID, log_message) + + receiver.coins += amount-tax + v.coins -= amount + send_repeatable_notification(receiver.id, f":marseycapitalistmanlet: @{v.username} has gifted you {amount-tax} coins!") + g.db.add(receiver) + g.db.add(v) + + g.db.commit() + return {"message": f"{amount-tax} coins transferred!"}, 200 + + return {"message": "You can't transfer coins to yourself!"}, 400 + + +@app.post("/@<username>/transfer_bux") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@is_not_permabanned +def transfer_bux(v, username): + receiver = g.db.query(User).filter_by(username=username).one_or_none() + + if not receiver: 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 not amount or amount < 0: return {"error": "Invalid amount of marseybux."}, 400 + if v.procoins < amount: return {"error": "You don't have enough marseybux"}, 400 + if amount < 100: return {"error": "You have to gift at least 100 marseybux."}, 400 + + log_message = f"@{v.username} has transferred {amount} Marseybux to @{receiver.username}" + send_repeatable_notification(GIFT_NOTIF_ID, log_message) + + receiver.procoins += amount + v.procoins -= amount + send_repeatable_notification(receiver.id, f":marseycapitalistmanlet: @{v.username} has gifted you {amount} marseybux!") + g.db.add(receiver) + g.db.add(v) + + g.db.commit() + return {"message": f"{amount} marseybux transferred!"}, 200 + + return {"message": "You can't transfer marseybux to yourself!"}, 400 + + +@app.get("/leaderboard") +@auth_required +def leaderboard(v): + + users = g.db.query(User) + + users1 = users.order_by(User.coins.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.coins.desc()).label("rank")).subquery() + pos1 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + users2 = users.order_by(User.stored_subscriber_count.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.stored_subscriber_count.desc()).label("rank")).subquery() + pos2 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + users3 = users.order_by(User.post_count.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.post_count.desc()).label("rank")).subquery() + pos3 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + users4 = users.order_by(User.comment_count.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.comment_count.desc()).label("rank")).subquery() + pos4 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + users5 = users.order_by(User.received_award_count.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.received_award_count.desc()).label("rank")).subquery() + pos5 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + if request.host == 'pcmemes.net': + users6 = users.order_by(User.basedcount.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.basedcount.desc()).label("rank")).subquery() + pos6 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + else: + users6 = None + pos6 = None + + users7 = users.order_by(User.coins_spent.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.coins_spent.desc()).label("rank")).subquery() + pos7 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + try: + pos9 = [x[0].id for x in users9].index(v.id) + pos9 = (pos9+1, users9[pos9][1]) + except: pos9 = (len(users9)+1, 0) + + users10 = users.order_by(User.truecoins.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.truecoins.desc()).label("rank")).subquery() + pos10 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + sq = g.db.query(Badge.user_id, func.count(Badge.user_id).label("count"), func.rank().over(order_by=func.count(Badge.user_id).desc()).label("rank")).group_by(Badge.user_id).subquery() + users11 = g.db.query(User, sq.c.count).join(sq, User.id==sq.c.user_id).order_by(sq.c.count.desc()) + pos11 = g.db.query(User.id, sq.c.rank, sq.c.count).join(sq, User.id==sq.c.user_id).filter(User.id == v.id).one_or_none() + if pos11: pos11 = (pos11[1],pos11[2]) + else: pos11 = (users11.count()+1, 0) + users11 = users11.limit(25).all() + + if pos11[1] < 25 and v not in (x[0] for x in users11): + pos11 = (26, pos11[1]) + + if SITE_NAME == 'rDrama': + sq = g.db.query(Marsey.author_id, func.count(Marsey.author_id).label("count"), func.rank().over(order_by=func.count(Marsey.author_id).desc()).label("rank")).group_by(Marsey.author_id).subquery() + users12 = g.db.query(User, sq.c.count).join(sq, User.id==sq.c.author_id).order_by(sq.c.count.desc()) + pos12 = g.db.query(User.id, sq.c.rank, sq.c.count).join(sq, User.id==sq.c.author_id).filter(User.id == v.id).one_or_none() + if pos12: pos12 = (pos12[1],pos12[2]) + else: pos12 = (users12.count()+1, 0) + users12 = users12.limit(25).all() + else: + users12 = None + pos12 = None + + try: + pos13 = [x[0].id for x in users13].index(v.id) + pos13 = (pos13+1, users13[pos13][1]) + except: pos13 = (len(users13)+1, 0) + + users14 = users.order_by(User.winnings.desc()).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.winnings.desc()).label("rank")).subquery() + pos14 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + users15 = users.order_by(User.winnings).limit(25).all() + sq = g.db.query(User.id, func.rank().over(order_by=User.winnings).label("rank")).subquery() + pos15 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + + return render_template("leaderboard.html", v=v, users1=users1, pos1=pos1, users2=users2, pos2=pos2, users3=users3, pos3=pos3, users4=users4, pos4=pos4, users5=users5, pos5=pos5, users6=users6, pos6=pos6, users7=users7, pos7=pos7, users9=users9_25, pos9=pos9, users10=users10, pos10=pos10, users11=users11, pos11=pos11, users12=users12, pos12=pos12, users13=users13_25, pos13=pos13, users14=users14, pos14=pos14, users15=users15, pos15=pos15) + +@app.get("/@<username>/css") +def get_css(username): + user = get_user(username) + resp = make_response(user.css or "") + resp.headers["Content-Type"] = "text/css" + resp.headers["Referrer-Policy"] = "no-referrer" + return resp + +@app.get("/@<username>/profilecss") +def get_profilecss(username): + user = get_user(username) + if user.profilecss: profilecss = user.profilecss + else: profilecss = "" + resp = make_response(profilecss) + resp.headers["Content-Type"] = "text/css" + resp.headers["Referrer-Policy"] = "no-referrer" + return resp + +@app.get("/id/<id>/profilecss") +def get_profilecss_id(id): + user = get_account(id) + if user.profilecss: profilecss = user.profilecss + else: profilecss = "" + resp = make_response(profilecss) + resp.headers["Content-Type"] = "text/css" + resp.headers["Referrer-Policy"] = "no-referrer" + return resp + +@app.get("/@<username>/song") +def usersong(username): + user = get_user(username) + if user.song: return redirect(f"/song/{user.song}.mp3") + else: abort(404) + +@app.get("/song/<song>") +@app.get("/static/song/<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=3153600") + return resp + +@app.post("/subscribe/<post_id>") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@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/<post_id>") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def unsubscribe(v, post_id): + sub=g.db.query(Subscription).filter_by(user_id=v.id, submission_id=post_id).one_or_none() + if sub: + g.db.delete(sub) + g.db.commit() + return {"message": "Post unsubscribed!"} + +@app.get("/report_bugs") +@auth_required +def reportbugs(v): + return redirect(f'/post/{BUG_THREAD}') + +@app.post("/@<username>/message") +@limiter.limit("1/second;10/minute;20/hour;50/day") +@limiter.limit("1/second;10/minute;20/hour;50/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@is_not_permabanned +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 and hasattr(user, 'is_blocked') and user.is_blocked: + return {"error": "This user is blocking you."}, 403 + + message = request.values.get("message", "").strip()[:10000].strip() + + if not message: return {"error": "Message is empty!"} + + if 'linkedin.com' in message: return {"error": "This domain 'linkedin.com' is banned."}, 403 + + body_html = sanitize(message) + + existing = g.db.query(Comment.id).filter(Comment.author_id == v.id, + Comment.sentto == user.id, + Comment.body_html == body_html, + ).one_or_none() + + if existing: return {"error": "Message already exists."}, 403 + + c = Comment(author_id=v.id, + parent_submission=None, + level=1, + sentto=user.id, + body_html=body_html + ) + g.db.add(c) + + g.db.flush() + + if blackjack and any(i in c.body_html.lower() for i in blackjack.split()): + v.shadowbanned = 'AutoJanny' + g.db.add(v) + notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=CARP_ID).one_or_none() + if not notif: + notif = Notification(comment_id=c.id, user_id=CARP_ID) + g.db.add(notif) + g.db.flush() + + c.top_comment_id = c.id + + notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=user.id).one_or_none() + if not notif: + notif = Notification(comment_id=c.id, user_id=user.id) + g.db.add(notif) + + g.db.commit() + + if PUSHER_ID != 'blahblahblah' and not v.shadowbanned: + if len(message) > 500: notifbody = message[:500] + '...' + else: notifbody = message + + try: gevent.spawn(pusher_thread2, f'{request.host}{user.id}', notifbody, v.username) + except: pass + + return {"message": "Message sent!"} + + +@app.post("/reply") +@limiter.limit("1/second;6/minute;50/hour;200/day") +@limiter.limit("1/second;6/minute;50/hour;200/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def messagereply(v): + + message = request.values.get("body", "").strip()[:10000].strip() + + if not message and not request.files.get("file"): return {"error": "Message is empty!"} + + if 'linkedin.com' in message: return {"error": "this domain 'linkedin.com' is banned"} + + id = int(request.values.get("parent_id")) + parent = get_comment(id, v=v) + user_id = parent.author.id + + if parent.sentto == 2: user_id = None + elif v.id == user_id: user_id = parent.sentto + + body_html = sanitize(message) + + if parent.sentto == 2 and request.files.get("file") and request.headers.get("cf-ipcountry") != "T1": + file=request.files["file"] + if file.content_type.startswith('image/'): + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + url = process_image(v.patron, name) + body_html += f'<img data-bs-target="#expandImageModal" data-bs-toggle="modal" onclick="expandDesktopImage(this.src)" class="img" src="{url}" loading="lazy">' + elif file.content_type.startswith('video/'): + if file.content_type == 'video/webm': + file.save("video.mp4") + else: + file.save("unsanitized.mp4") + os.system(f'ffmpeg -y -loglevel warning -i unsanitized.mp4 -map_metadata -1 -c:v copy -c:a copy video.mp4') + with open("video.mp4", 'rb') as f: + try: req = requests.request("POST", "https://pomf2.lain.la/upload.php", files={'files[]': f}, timeout=5).json() + except requests.exceptions.ConnectionError: return {"error": "Video upload timed out, please try again!"} + try: url = req['files'][0]['url'] + except: return {"error": req['description']}, 400 + body_html += f"<p>{url}</p>" + else: return {"error": "Image/Video files only"}, 400 + + + c = Comment(author_id=v.id, + parent_submission=None, + parent_comment_id=id, + top_comment_id=parent.top_comment_id, + level=parent.level + 1, + sentto=user_id, + body_html=body_html, + ) + g.db.add(c) + g.db.flush() + + if blackjack and any(i in c.body_html.lower() for i in blackjack.split()): + v.shadowbanned = 'AutoJanny' + g.db.add(v) + notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=CARP_ID).one_or_none() + if not notif: + notif = Notification(comment_id=c.id, user_id=CARP_ID) + g.db.add(notif) + g.db.flush() + + if user_id and user_id != v.id and user_id != 2: + notif = g.db.query(Notification).filter_by(comment_id=c.id, user_id=user_id).one_or_none() + if not notif: + notif = Notification(comment_id=c.id, user_id=user_id) + g.db.add(notif) + ids = [c.top_comment.id] + [x.id for x in c.top_comment.replies] + notifications = g.db.query(Notification).filter(Notification.comment_id.in_(ids), Notification.user_id == user_id) + for n in notifications: + g.db.delete(n) + + if PUSHER_ID != 'blahblahblah' and not v.shadowbanned: + if len(message) > 500: notifbody = message[:500] + '...' + else: notifbody = message + + beams_client.publish_to_interests( + interests=[f'{request.host}{user_id}'], + publish_body={ + 'web': { + 'notification': { + 'title': f'New message from @{v.username}', + 'body': notifbody, + 'deep_link': f'{SITE_FULL}/notifications?messages=true', + 'icon': f'{SITE_FULL}/assets/images/{SITE_NAME}/icon.webp"a=1010', + } + }, + 'fcm': { + 'notification': { + 'title': f'New message from @{v.username}', + 'body': notifbody, + }, + 'data': { + 'url': '/notifications?messages=true', + } + } + }, + ) + + + if c.top_comment.sentto == 2: + admins = g.db.query(User).filter(User.admin_level > 2, User.id != v.id).all() + for admin in admins: + notif = Notification(comment_id=c.id, user_id=admin.id) + g.db.add(notif) + + ids = [c.top_comment.id] + [x.id for x in c.top_comment.replies] + uids = [x.id for x in admins] + notifications = g.db.query(Notification).filter(Notification.comment_id.in_(ids), Notification.user_id.in_(uids)) + for n in notifications: + g.db.delete(n) + + g.db.commit() + + return {"comment": render_template("comments.html", v=v, comments=[c], ajax=True)} + +@app.get("/2faqr/<secret>") +@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) + + try: f = send_file(mem, mimetype="image/png", as_attachment=False) + except: + print('/2faqr/<secret>', flush=True) + abort(404) + return f + + +@app.get("/is_available/<name>") +def api_is_available(name): + + name=name.strip() + + if len(name)<3 or len(name)>25: + return {name:False} + + name2 = name.replace('\\', '').replace('_','\_').replace('%','') + + x= g.db.query(User).filter( + or_( + User.username.ilike(name2), + User.original_username.ilike(name2) + ) + ).one_or_none() + + if x: + return {name: False} + else: + return {name: True} + +@app.get("/id/<id>") +@auth_required +def user_id(id, v): + user = get_account(id) + return redirect(user.url) + +@app.get("/u/<username>") +@auth_required +def redditor_moment_redirect(username, v): + return redirect(f"/@{username}") + +@app.get("/@<username>/followers") +@auth_required +def followers(username, v): + u = get_user(username, v=v) + users = g.db.query(User).join(Follow, Follow.target_id == u.id).filter(Follow.user_id == User.id).order_by(Follow.created_utc).all() + return render_template("followers.html", v=v, u=u, users=users) + +@app.get("/@<username>/following") +@auth_required +def following(username, v): + u = get_user(username, v=v) + users = g.db.query(User).join(Follow, Follow.user_id == u.id).filter(Follow.target_id == User.id).order_by(Follow.created_utc).all() + return render_template("following.html", v=v, u=u, users=users) + +@app.get("/views") +@auth_required +def visitors(v): + if v.admin_level < 2 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("/@<username>") +@app.get("/logged_out/@<username>") +@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'): return redirect(request.full_path.replace('/logged_out','')) + + u = get_user(username, v=v) + + + if username != u.username: + return redirect(SITE_FULL + request.full_path.replace(username, u.username)[:-1]) + + if u.reserved: + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": f"That username is reserved for: {u.reserved}"} + return render_template("userpage_reserved.html", u=u, v=v) + + if v and v.id not in (u.id,DAD_ID) and (u.patron or u.admin_level > 1): + view = g.db.query(ViewerRelationship).filter_by(viewer_id=v.id, user_id=u.id).one_or_none() + + if view: view.last_view_utc = int(time.time()) + else: view = ViewerRelationship(viewer_id=v.id, user_id=u.id) + + g.db.add(view) + g.db.commit() + + + if u.is_private and (not v or (v.id != u.id and v.admin_level < 2 and not v.eye)): + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "That userpage is private"} + return render_template("userpage_private.html", u=u, v=v) + + + if v and hasattr(u, 'is_blocking') and u.is_blocking: + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": f"You are blocking @{u.username}."} + return render_template("userpage_blocking.html", u=u, v=v) + + + if v and v.admin_level < 2 and hasattr(u, 'is_blocked') and u.is_blocked: + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "This person is blocking you."} + return render_template("userpage_blocked.html", u=u, v=v) + + + sort = request.values.get("sort", "new") + t = request.values.get("t", "all") + try: page = max(int(request.values.get("page", 1)), 1) + except: page = 1 + + ids = u.userpagelisting(site=SITE, v=v, page=page, sort=sort, t=t) + + next_exists = (len(ids) > 25) + ids = ids[:25] + + if page == 1: + sticky = [] + sticky = g.db.query(Submission).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]} + 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]} + 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("/@<username>/comments") +@app.get("/logged_out/@<username>/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'): return redirect(request.full_path.replace('/logged_out','')) + + user = get_user(username, v=v) + + if username != user.username: return redirect(f'/@{user.username}/comments') + + u = user + + if u.reserved: + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": f"That username is reserved for: {u.reserved}"} + 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 < 2 and not v.eye)): + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "That userpage is private"} + return render_template("userpage_private.html", u=u, v=v) + + if v and hasattr(u, 'is_blocking') and u.is_blocking: + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": f"You are blocking @{u.username}."} + return render_template("userpage_blocking.html", u=u, v=v) + + if v and v.admin_level < 2 and hasattr(u, 'is_blocked') and u.is_blocked: + if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "This person is blocking you."} + return render_template("userpage_blocked.html", u=u, v=v) + + + try: page = max(int(request.values.get("page", "1")), 1) + except: page = 1 + + sort=request.values.get("sort","new") + t=request.values.get("t","all") + + + comments = g.db.query(Comment.id).filter(Comment.author_id == u.id, Comment.parent_submission != None) + + if not v or (v.id != u.id and v.admin_level < 2): + comments = comments.filter(Comment.deleted_utc == 0, Comment.is_banned == False, Comment.ghost == 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) + elif sort == "controversial": + comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc()) + 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]} + 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("/@<username>/info") +@auth_required +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.get("/<id>/info") +@auth_required +def u_user_id_info(id, v=None): + + user=get_account(id, 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/<username>") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@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).filter_by(user_id=v.id, target_id=target.id).one_or_none(): 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).filter_by(target_id=target.id).count() + g.db.add(target) + + send_notification(target.id, f"@{v.username} has followed you!") + + g.db.commit() + + return {"message": "User followed!"} + +@app.post("/unfollow/<username>") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def unfollow_user(username, v): + + target = get_user(username) + + if target.fish: + send_notification(target.id, f"@{v.username} has tried to unfollow you and failed because of your fish award!") + g.db.commit() + return {"error": "You can't unfollow this user!"} + + follow = g.db.query(Follow).filter_by(user_id=v.id, target_id=target.id).one_or_none() + + if follow: + g.db.delete(follow) + + g.db.flush() + target.stored_subscriber_count = g.db.query(Follow).filter_by(target_id=target.id).count() + g.db.add(target) + + send_notification(target.id, f"@{v.username} has unfollowed you!") + + g.db.commit() + + return {"message": "User unfollowed!"} + +@app.post("/remove_follow/<username>") +@limiter.limit("1/second;30/minute;200/hour;1000/day") +@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def remove_follow(username, v): + target = get_user(username) + + follow = g.db.query(Follow).filter_by(user_id=target.id, target_id=v.id).one_or_none() + + if not follow: return {"message": "Follower removed!"} + + g.db.delete(follow) + + g.db.flush() + v.stored_subscriber_count = g.db.query(Follow).filter_by(target_id=v.id).count() + g.db.add(v) + + send_repeatable_notification(target.id, f"@{v.username} has removed your follow!") + + g.db.commit() + + return {"message": "Follower removed!"} + +@app.get("/pp/<id>") +@app.get("/uid/<id>/pic") +@app.get("/uid/<id>/pic/profile") +@app.get("/logged_out/pp/<id>") +@app.get("/logged_out/uid/<id>/pic") +@app.get("/logged_out/uid/<id>/pic/profile") +@limiter.exempt +@auth_desired +def user_profile_uid(v, id): + 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'): return redirect(request.full_path.replace('/logged_out','')) + + try: id = int(id) + except: + try: id = int(id, 36) + except: abort(404) + + x=get_account(id) + return redirect(x.profile_url) + +@app.get("/@<username>/pic") +@limiter.exempt +@auth_required +def user_profile_name(v, username): + x = get_user(username) + return redirect(x.profile_url) + +@app.get("/@<username>/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]} + return render_template("userpage.html", + u=v, + v=v, + listing=listing, + page=page, + next_exists=next_exists, + ) + + +@app.get("/@<username>/saved/comments") +@auth_required +def saved_comments(v, username): + + page=int(request.values.get("page",1)) + + ids=v.saved_comment_idlist(page=page) + + 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]} + return render_template("userpage_comments.html", + u=v, + v=v, + listing=listing, + page=page, + next_exists=next_exists, + standalone=True) + + +@app.post("/fp/<fp>") +@auth_required +def fp(v, fp): + v.fp = fp + users = g.db.query(User).filter(User.fp == fp, User.id != v.id).all() + if users: print(f'{v.username}: fp') + if v.email and v.is_activated: + alts = g.db.query(User).filter(User.email == v.email, User.is_activated, User.id != v.id).all() + if alts: + print(f'{v.username}: email') + users += alts + for u in users: + li = [v.id, u.id] + existing = g.db.query(Alt).filter(Alt.user1.in_(li), Alt.user2.in_(li)).one_or_none() + if existing: continue + new_alt = Alt(user1=v.id, user2=u.id) + g.db.add(new_alt) + g.db.flush() + print(v.username + ' + ' + u.username) + g.db.add(v) + g.db.commit() return '', 204 \ No newline at end of file diff --git a/files/routes/votes.py b/files/routes/votes.py index ebb8fbe0e..02a5da205 100644 --- a/files/routes/votes.py +++ b/files/routes/votes.py @@ -49,10 +49,9 @@ def admin_vote_info_get(v): downs=downs) - @app.post("/vote/post/<post_id>/<new>") -@limiter.limit("5/second;60/minute;600/hour;1000/day") -@limiter.limit("5/second;60/minute;600/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@limiter.limit("5/second;60/minute;1000/hour;2000/day") +@limiter.limit("5/second;60/minute;1000/hour;2000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') @is_not_permabanned def api_vote_post(post_id, new, v): @@ -113,17 +112,17 @@ def api_vote_post(post_id, new, v): g.db.add(vote) g.db.flush() - post.upvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, vote_type=1).count() - post.downvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, vote_type=-1).count() - post.realupvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, real=True).count() + post.upvotes = g.db.query(Vote).filter_by(submission_id=post.id, vote_type=1).count() + post.downvotes = g.db.query(Vote).filter_by(submission_id=post.id, vote_type=-1).count() + post.realupvotes = g.db.query(Vote).filter_by(submission_id=post.id, real=True).count() if post.author.progressivestack: post.realupvotes *= 2 g.db.add(post) g.db.commit() return "", 204 @app.post("/vote/comment/<comment_id>/<new>") -@limiter.limit("5/second;60/minute;600/hour;1000/day") -@limiter.limit("5/second;60/minute;600/hour;1000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@limiter.limit("5/second;60/minute;1000/hour;2000/day") +@limiter.limit("5/second;60/minute;1000/hour;2000/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') @is_not_permabanned def api_vote_comment(comment_id, new, v): @@ -190,9 +189,9 @@ def api_vote_comment(comment_id, new, v): g.db.add(vote) g.db.flush() - comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count() - comment.downvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=-1).count() - comment.realupvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, real=True).count() + comment.upvotes = g.db.query(CommentVote).filter_by(comment_id=comment.id, vote_type=1).count() + comment.downvotes = g.db.query(CommentVote).filter_by(comment_id=comment.id, vote_type=-1).count() + comment.realupvotes = g.db.query(CommentVote).filter_by(comment_id=comment.id, real=True).count() if comment.author.progressivestack: comment.realupvotes *= 2 g.db.add(comment) g.db.commit() @@ -225,7 +224,7 @@ def api_vote_poll(comment_id, v): g.db.add(vote) g.db.flush() - comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count() + comment.upvotes = g.db.query(CommentVote).filter_by(comment_id=comment.id, vote_type=1).count() g.db.add(comment) g.db.commit() return "", 204 @@ -283,12 +282,12 @@ def api_vote_choice(comment_id, v): else: parent = comment.post for vote in parent.total_choice_voted(v): - vote.comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=vote.comment.id, vote_type=1).count() - 1 + vote.comment.upvotes = g.db.query(CommentVote).filter_by(comment_id=vote.comment.id, vote_type=1).count() - 1 g.db.add(vote.comment) g.db.delete(vote) g.db.flush() - comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count() + comment.upvotes = g.db.query(CommentVote).filter_by(comment_id=comment.id, vote_type=1).count() g.db.add(comment) g.db.commit() return "", 204 \ No newline at end of file diff --git a/files/templates/admin/admin_home.html b/files/templates/admin/admin_home.html index c92491309..eae08bb84 100644 --- a/files/templates/admin/admin_home.html +++ b/files/templates/admin/admin_home.html @@ -1,91 +1,96 @@ -{% extends "default.html" %} - -{% block title %} -<title>{{SITE_NAME}} - -{% endblock %} - -{% block content %} -

-

-

 Admin Tools

- -

Content

-
- -

Users

- - -

Safety

- - -

Grant

- - -

API Access Control

- - -

Statistics

- - -{% if SITE_NAME == 'PCM' %} -

Configuration

- -{% endif %} - -{% if v.admin_level > 2 %} -

-	
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - -{% endif %} - +{% extends "default.html" %} + +{% block title %} +{{SITE_NAME}} + +{% endblock %} + +{% block content %} +

+

+

 Admin Tools

+ +

Content

+ + +

Users

+ + +

Safety

+ + +

Grant

+ + +

API Access Control

+ + +

Statistics

+ + +{% if SITE_NAME == 'PCM' %} +

Configuration

+ +{% endif %} + +{% if v.admin_level > 2 %} +

+	
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +{% endif %} + +

Server Status

+
+ Live Revision: {{ gitref }}
+
+ {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/alt_votes.html b/files/templates/admin/alt_votes.html index 0a3a7e774..051925725 100644 --- 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}} - -{% 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}} + +{% 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 index 0b31c23f4..46fd1d75f 100644 --- 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 index 57b4e4e89..188a18f3a 100644 --- 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 index f45ca98df..f60ccaab3 100644 --- a/files/templates/admin/badge_grant.html +++ b/files/templates/admin/badge_grant.html @@ -1,89 +1,89 @@ -{% 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 index e7447de7f..f5b666fd8 100644 --- 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 index 5bcb1eddc..1277cb5d8 100644 --- 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}} - -{% endblock %} - -{% block content %} -

-
- - - - - - -{% for entry in data %} - - - - -{% endfor %} -
StatisticValue
{{entry}}{{data[entry]}}
- +{% extends "default.html" %} + +{% block title %} +{{SITE_NAME}} + +{% 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 index 167cfe82a..47d17881a 100644 --- a/files/templates/admin/image_posts.html +++ b/files/templates/admin/image_posts.html @@ -1,56 +1,56 @@ -{% 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 %} - +{% 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 %} \ No newline at end of file diff --git a/files/templates/admin/new_users.html b/files/templates/admin/new_users.html index 5c4bc94d6..e58d56156 100644 --- a/files/templates/admin/new_users.html +++ b/files/templates/admin/new_users.html @@ -1,7 +1,7 @@ -{% 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 index 15eaf69b4..f4fb88eb1 100644 --- a/files/templates/admin/removed_posts.html +++ b/files/templates/admin/removed_posts.html @@ -1,66 +1,66 @@ -{% 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 fixedMobileBarJS %} -{% endblock %} - -{% block title %} -removed Posts - -{% endblock %} - -{% block content %} - - - -
- -
- - {% block listing %} -
- {% include "submission_listing.html" %} -
- {% endblock %} -
-
-{% endblock %} - -{% block pagenav %} - +{% 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 fixedMobileBarJS %} +{% endblock %} + +{% block title %} +removed Posts + +{% endblock %} + +{% block content %} + + + +
+ +
+ + {% block listing %} +
+ {% include "submission_listing.html" %} +
+ {% endblock %} +
+
+{% endblock %} + +{% block pagenav %} + {% endblock %} \ No newline at end of file diff --git a/files/templates/admin/reported_comments.html b/files/templates/admin/reported_comments.html index e9b23d6af..38c670360 100644 --- a/files/templates/admin/reported_comments.html +++ b/files/templates/admin/reported_comments.html @@ -1,26 +1,26 @@ -{% extends "admin/reported_posts.html" %} - -{% block title %} -Comments -{% endblock %} - -{% 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 title %} +Comments +{% endblock %} + +{% 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 index 7b06737b8..aaaccfeec 100644 --- a/files/templates/admin/reported_posts.html +++ b/files/templates/admin/reported_posts.html @@ -1,66 +1,66 @@ -{% 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 fixedMobileBarJS %} -{% endblock %} - -{% block title %} -Reported Posts - -{% endblock %} - -{% block content %} - - - -
- -
- - {% block listing %} -
- {% include "submission_listing.html" %} -
- {% endblock %} -
-
-{% endblock %} - -{% block pagenav %} - +{% 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 fixedMobileBarJS %} +{% endblock %} + +{% block title %} +Reported Posts + +{% endblock %} + +{% block content %} + + + +
+ +
+ + {% block listing %} +
+ {% include "submission_listing.html" %} +
+ {% endblock %} +
+
+{% endblock %} + +{% block pagenav %} + {% endblock %} \ No newline at end of file diff --git a/files/templates/admins.html b/files/templates/admins.html index 3622183e1..d35f54cab 100644 --- a/files/templates/admins.html +++ b/files/templates/admins.html @@ -1,31 +1,31 @@ -{% extends "settings2.html" %} - -{% block pagetitle %}Admins{% endblock %} - -{% block content %} - - - -

-
Admins
-

-
- - - - - - - - - -{% for user in admins %} - - - - - - -{% endfor %} -
#NameTruescoreMod actions
{{loop.index}}{{user.username}}{% if user.admin_level == 1 and v and v.admin_level > 1 %}{% endif %}{{user.truecoins}}{{user.modaction_num}}
-{% endblock %} +{% extends "settings2.html" %} + +{% block pagetitle %}Admins{% endblock %} + +{% block content %} + + + +

+
Admins
+

+
+ + + + + + + + + +{% for user in admins %} + + + + + + +{% endfor %} +
#NameTruescoreMod actions
{{loop.index}}{{user.username}}{% if user.admin_level == 1 and v and v.admin_level > 1 %}{% endif %}{{user.truecoins}}{{user.modaction_num}}
+{% endblock %} diff --git a/files/templates/api.html b/files/templates/api.html index 43713f978..1de23a226 100644 --- a/files/templates/api.html +++ b/files/templates/api.html @@ -1,112 +1,112 @@ -{% extends "default.html" %} - -{% block title %} -{{SITE_NAME}} - 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 {{SITE_NAME}} 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.

-

{{SITE_NAME}} 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 {{SITE_NAME}}: Authorization: access_token_goes_here

-

Python example:

-
	import requests
-
-	headers={"Authorization": "access_token_goes_here"}
-
-	url="{{SITE_FULL}}/?sort=comments"
-
-	r=requests.get(url, headers=headers)
-
-	print(r.json())
-
-

The expected result of this would be a large JSON representation of the posts on the frontpage sorted by the number of comments

- -
- -

Aother python example:

-
	import requests
-
-	headers={"Authorization": "access_token_goes_here"}
-
-	url="{{SITE_FULL}}/unread"
-
-	r=requests.get(url, headers=headers)
-
-	print(r.json())
-
-

The expected result of this would be a JSON representation of unread notifications for your account

-
-
-
-
-
-
-
-
-
-

API Guide for Applications

-

-

The OAuth2 authorization flow is used to enable users to authorize third-party applications to access their {{SITE_NAME}} 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 {{SITE_NAME}} 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.

-

{{SITE_NAME}} 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 {{SITE_FULL}}/authorize/?client_id=YOUR_CLIENT_ID

-

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

-

Step 3: Catch the redirect

-

The user clicks "Authorize". {{SITE_NAME}} 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 {{SITE_NAME}}: Authorization: access_token_goes_here

-

Python example:

-
	import requests
-
-	headers={"Authorization": "access_token_goes_here"}
-
-	url="{{SITE_FULL}}/?sort=comments"
-
-	r=requests.get(url, headers=headers)
-
-	print(r.json())
-
-

The expected result of this would be a large JSON representation of the posts on the frontpage sorted by the number of comments

- -
- -

Aother python example:

-
	import requests
-
-	headers={"Authorization": "access_token_goes_here"}
-
-	url="{{SITE_FULL}}/unread"
-
-	r=requests.get(url, headers=headers)
-
-	print(r.json())
-
-

The expected result of this would be a JSON representation of unread notifications for your account

+{% extends "default.html" %} + +{% block title %} +{{SITE_NAME}} - 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 {{SITE_NAME}} 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.

+

{{SITE_NAME}} 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 {{SITE_NAME}}: Authorization: access_token_goes_here

+

Python example:

+
	import requests
+
+	headers={"Authorization": "access_token_goes_here"}
+
+	url="{{SITE_FULL}}/?sort=comments"
+
+	r=requests.get(url, headers=headers)
+
+	print(r.json())
+
+

The expected result of this would be a large JSON representation of the posts on the frontpage sorted by the number of comments

+ +
+ +

Aother python example:

+
	import requests
+
+	headers={"Authorization": "access_token_goes_here"}
+
+	url="{{SITE_FULL}}/unread"
+
+	r=requests.get(url, headers=headers)
+
+	print(r.json())
+
+

The expected result of this would be a JSON representation of unread notifications for your account

+
+
+
+
+
+
+
+
+
+

API Guide for Applications

+

+

The OAuth2 authorization flow is used to enable users to authorize third-party applications to access their {{SITE_NAME}} 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 {{SITE_NAME}} 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.

+

{{SITE_NAME}} 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 {{SITE_FULL}}/authorize/?client_id=YOUR_CLIENT_ID

+

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

+

Step 3: Catch the redirect

+

The user clicks "Authorize". {{SITE_NAME}} 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 {{SITE_NAME}}: Authorization: access_token_goes_here

+

Python example:

+
	import requests
+
+	headers={"Authorization": "access_token_goes_here"}
+
+	url="{{SITE_FULL}}/?sort=comments"
+
+	r=requests.get(url, headers=headers)
+
+	print(r.json())
+
+

The expected result of this would be a large JSON representation of the posts on the frontpage sorted by the number of comments

+ +
+ +

Aother python example:

+
	import requests
+
+	headers={"Authorization": "access_token_goes_here"}
+
+	url="{{SITE_FULL}}/unread"
+
+	r=requests.get(url, headers=headers)
+
+	print(r.json())
+
+

The expected result of this would be a JSON representation of unread notifications for your account

{% endblock %} \ No newline at end of file diff --git a/files/templates/authforms.html b/files/templates/authforms.html index 5078c9ff0..eb0b3ea8e 100644 --- a/files/templates/authforms.html +++ b/files/templates/authforms.html @@ -1,125 +1,125 @@ - - - - - - - - - - - - - {% block pagetitle %}{{SITE_NAME}}{% 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 %} - -
- -
- -
- -
- -
- -
- - cover -
- -
- -
-
- - - + + + + + + + + + + + + + {% block pagetitle %}{{SITE_NAME}}{% 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 %} + +
+ +
+ +
+ +
+ +
+ +
+ + cover +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/files/templates/award_modal.html b/files/templates/award_modal.html index 5a9e55fc7..38efd67cf 100644 --- a/files/templates/award_modal.html +++ b/files/templates/award_modal.html @@ -1,47 +1,52 @@ - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/files/templates/badges.html b/files/templates/badges.html index aebeb87b4..774945855 100644 --- a/files/templates/badges.html +++ b/files/templates/badges.html @@ -1,39 +1,39 @@ -{% extends "default.html" %} -{% block content %} - -
-
-
-
-

User Badges

-
This page describes the requirements for obtaining all profile badges.
-
-
-
-
-
- - - - - - - - - - - -{% for badge in badges %} - - - - - {%- set ct = counts[badge.id] if badge.id in counts else (0, 0) %} - - - -{% endfor %} -
#NameImageDescription#Rarity
{{loop.index}}{{badge.name}}{{badge.name}} - {{badge.description}}{{ ct[0] }}{{ "{:0.3f}".format(ct[1]) }}%
- +{% extends "default.html" %} +{% block content %} + +
+
+
+
+

User Badges

+
This page describes the requirements for obtaining all profile badges.
+
+
+
+
+
+ + + + + + + + + + + +{% for badge in badges %} + + + + + {%- set ct = counts[badge.id] if badge.id in counts else (0, 0) %} + + + +{% endfor %} +
#NameImageDescription#Rarity
{{loop.index}}{{badge.name}}{{badge.name}} + {{badge.description}}{{ ct[0] }}{{ "{:0.3f}".format(ct[1]) }}%
+ {% endblock %} \ No newline at end of file diff --git a/files/templates/ban_modal.html b/files/templates/ban_modal.html index 2904474be..10c5832aa 100644 --- a/files/templates/ban_modal.html +++ b/files/templates/ban_modal.html @@ -1,38 +1,38 @@ - - - -