Merge branch 'master' into regex-censor

# Conflicts:
#	.gitignore
#	docker-compose.yml
#	files/classes/comment.py
#	files/classes/submission.py
#	files/helpers/const.py
#	requirements.txt
remotes/1693045480750635534/spooky-22
Yo Mama 2021-10-16 21:29:36 +02:00
commit a831a24aa4
171 changed files with 27714 additions and 27682 deletions

8
.gitattributes vendored 100644 → 100755
View File

@ -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

30
Dockerfile 100644 → 100755
View File

@ -1,15 +1,15 @@
FROM ubuntu:20.04
COPY supervisord.conf /etc/supervisord.conf
RUN apt update && apt install -y python3.8 python3-pip supervisor
RUN mkdir -p ./service
COPY requirements.txt ./service/requirements.txt
RUN cd ./service && pip3 install -r requirements.txt
EXPOSE 80/tcp
CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]
FROM ubuntu:20.04
COPY supervisord.conf /etc/supervisord.conf
RUN apt update && apt install -y python3.8 python3-pip supervisor
RUN mkdir -p ./service
COPY requirements.txt ./service/requirements.txt
RUN cd ./service && pip3 install -r requirements.txt
EXPOSE 80/tcp
CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]

746
LICENSE 100644 → 100755
View File

@ -1,373 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

24
appspec.yml 100644 → 100755
View File

@ -1,13 +1,13 @@
version: 0.0
os: linux
files:
- source: /
destination: files
permissions:
- object: files/*
mode: 4755
hooks:
AfterInstall:
- location: scripts/install_pip
ApplicationStart:
version: 0.0
os: linux
files:
- source: /
destination: files
permissions:
- object: files/*
mode: 4755
hooks:
AfterInstall:
- location: scripts/install_pip
ApplicationStart:
- location: scripts/start_files

14
buildspec.yml 100644 → 100755
View File

@ -1,8 +1,8 @@
version: 0.2
phases:
install:
runtime-versions:
python: 3.7
artifacts:
files:
version: 0.2
phases:
install:
runtime-versions:
python: 3.7
artifacts:
files:
- '**/*'

View File

@ -1,7 +0,0 @@
for theme in ['transparent', 'win98', 'midnight', 'dark', 'light', 'coffee', 'tron', '4chan']:
with open(f"./files/assets/css/{theme}_ff66ac.css", encoding='utf-8') as t:
text = t.read()
for color in ['ff66ac','805ad5','62ca56','38a169','80ffff','2a96f3','62ca56','eb4963','ff0000','f39731','30409f','3e98a7','e4432d','7b9ae4','ec72de','7f8fa6', 'f8db58']:
newtext = text.replace("ff66ac", color).replace("ff4097", color).replace("ff1a83", color).replace("ff3390", color).replace("rgba(255, 102, 172, 0.25)", color)
with open(f"./files/assets/css/{theme}_{color}.css", encoding='utf-8', mode='w') as nt:
nt.write(newtext)

10
dependabot.yml 100644 → 100755
View File

@ -1,6 +1,6 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"

0
disablesignups 100644 → 100755
View File

View File

@ -7,10 +7,9 @@ services:
volumes:
- "./:/service"
environment:
- DATABASE_URL=postgresql://postgres@postgres:5432/postgres
- DATABASE_URL=postgresql://postgres@127.0.0.1:5432/postgres
- MASTER_KEY=${MASTER_KEY:-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA=}
- REDIS_URL=redis://redis
- DOMAIN=localhost
- DOMAIN=localhost
- SITE_NAME=Drama
- GIPHY_KEY=3435tdfsdudebussylmaoxxt43
- FORCE_HTTPS=0
@ -32,7 +31,7 @@ services:
- BOT_DISABLE=0
- COINS_NAME=Dramacoins
- DEFAULT_TIME_FILTER=all
- DEFAULT_THEME=dark
- DEFAULT_THEME=midnight
- DEFAULT_COLOR=ff66ac #YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58
- SLOGAN=Dude bussy lmao
- GUMROAD_TOKEN=3435tdfsdudebussylmaoxxt43

64
env 100644 → 100755
View File

@ -1,33 +1,33 @@
export DATABASE_URL="postgresql://postgres@postgres:5432/postgres"
export MASTER_KEY="-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA="
export DOMAIN="localhost"
export SITE_NAME="Drama"
export GIPHY_KEY="3435tdfsdudebussylmaoxxt43"
export FORCE_HTTPS="0"
export DISCORD_SERVER_ID="3435tdfsdudebussylmaoxxt43"
export DISCORD_CLIENT_ID="3435tdfsdudebussylmaoxxt43"
export DISCORD_CLIENT_SECRET="3435tdfsdudebussylmaoxxt43"
export DISCORD_BOT_TOKEN="3435tdfsdudebussylmaoxxt43"
export HCAPTCHA_SECRET="3435tdfsdudebussylmaoxxt43"
export YOUTUBE_KEY="3435tdfsdudebussylmaoxxt43"
export PUSHER_KEY="3435tdfsdudebussylmaoxxt43"
export CATBOX_KEY="3435tdfsdudebussylmaoxxt43"
export SPAM_SIMILARITY_THRESHOLD="0.5"
export SPAM_SIMILAR_COUNT_THRESHOLD="5"
export SPAM_URL_SIMILARITY_THRESHOLD="0.1"
export COMMENT_SPAM_SIMILAR_THRESHOLD="0.5"
export COMMENT_SPAM_COUNT_THRESHOLD="5"
export READ_ONLY="0"
export BOT_DISABLE="0"
export COINS_NAME="Dramacoins"
export DEFAULT_TIME_FILTER="all"
export SLOGAN="Dude bussy lmao"
export GUMROAD_TOKEN="3435tdfsdudebussylmaoxxt43"
export GUMROAD_LINK="https://marsey1.gumroad.com/l/tfcvri"
export CARD_VIEW="1"
export DISABLE_DOWNVOTES="0"
export DUES="0"
export DEFAULT_THEME="dark"
export DEFAULT_COLOR="ff66ac" # YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58
export MAIL_USERNAME="blahblahblah@gmail.com"
export DATABASE_URL="postgresql://postgres@127.0.0.1:5432/postgres"
export MASTER_KEY="-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA="
export DOMAIN="localhost"
export SITE_NAME="Drama"
export GIPHY_KEY="3435tdfsdudebussylmaoxxt43"
export FORCE_HTTPS="0"
export DISCORD_SERVER_ID="3435tdfsdudebussylmaoxxt43"
export DISCORD_CLIENT_ID="3435tdfsdudebussylmaoxxt43"
export DISCORD_CLIENT_SECRET="3435tdfsdudebussylmaoxxt43"
export DISCORD_BOT_TOKEN="3435tdfsdudebussylmaoxxt43"
export HCAPTCHA_SECRET="3435tdfsdudebussylmaoxxt43"
export YOUTUBE_KEY="3435tdfsdudebussylmaoxxt43"
export PUSHER_KEY="3435tdfsdudebussylmaoxxt43"
export CATBOX_KEY="3435tdfsdudebussylmaoxxt43"
export SPAM_SIMILARITY_THRESHOLD="0.5"
export SPAM_SIMILAR_COUNT_THRESHOLD="5"
export SPAM_URL_SIMILARITY_THRESHOLD="0.1"
export COMMENT_SPAM_SIMILAR_THRESHOLD="0.5"
export COMMENT_SPAM_COUNT_THRESHOLD="5"
export READ_ONLY="0"
export BOT_DISABLE="0"
export COINS_NAME="Dramacoins"
export DEFAULT_TIME_FILTER="all"
export SLOGAN="Dude bussy lmao"
export GUMROAD_TOKEN="3435tdfsdudebussylmaoxxt43"
export GUMROAD_LINK="https://marsey1.gumroad.com/l/tfcvri"
export CARD_VIEW="1"
export DISABLE_DOWNVOTES="0"
export DUES="0"
export DEFAULT_THEME="midnight"
export DEFAULT_COLOR="ff66ac" # YOU HAVE TO PICK ONE OF THOSE COLORS OR SHIT WILL BREAK: ff66ac, 805ad5, 62ca56, 38a169, 80ffff, 2a96f3, eb4963, ff0000, f39731, 30409f, 3e98a7, e4432d, 7b9ae4, ec72de, 7f8fa6, f8db58
export MAIL_USERNAME="blahblahblah@gmail.com"
export MAIL_PASSWORD="3435tdfsdudebussylmaoxxt43"

260
files/__main__.py 100644 → 100755
View File

@ -1,131 +1,131 @@
import gevent.monkey
gevent.monkey.patch_all()
from os import environ
import secrets
from flask import *
from flask_caching import Cache
from flask_limiter import Limiter
from flask_compress import Compress
from flask_limiter.util import get_ipaddr
from flask_mail import Mail
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import *
import gevent
from werkzeug.middleware.proxy_fix import ProxyFix
import redis
app = Flask(__name__, template_folder='./templates')
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=3)
app.url_map.strict_slashes = False
app.jinja_env.cache = {}
import faulthandler
faulthandler.enable()
app.config["SITE_NAME"]=environ.get("SITE_NAME").strip()
app.config["COINS_NAME"]=environ.get("COINS_NAME").strip()
app.config["GUMROAD_LINK"]=environ.get("GUMROAD_LINK", "https://marsey1.gumroad.com/l/tfcvri").strip()
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['DATABASE_URL'] = environ.get("DATABASE_URL")
app.config['SECRET_KEY'] = environ.get('MASTER_KEY')
app.config["SERVER_NAME"] = environ.get("DOMAIN").strip()
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 86400
app.config["SESSION_COOKIE_NAME"] = "session_" + environ.get("SITE_NAME").strip().lower()
app.config["VERSION"] = "1.0.0"
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
app.config["SESSION_COOKIE_SECURE"] = bool(int(environ.get("FORCE_HTTPS", 1)))
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 365
app.config["SESSION_REFRESH_EACH_REQUEST"] = True
app.config["SLOGAN"] = environ.get("SLOGAN", "").strip()
app.config["DEFAULT_COLOR"] = environ.get("DEFAULT_COLOR", "ff0000").strip()
app.config["DEFAULT_THEME"] = environ.get("DEFAULT_THEME", "light").strip() + "_" + environ.get("DEFAULT_COLOR", "ff0000").strip()
app.config["FORCE_HTTPS"] = int(environ.get("FORCE_HTTPS", 1)) if ("localhost" not in app.config["SERVER_NAME"] and "127.0.0.1" not in app.config["SERVER_NAME"]) else 0
app.config["UserAgent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
app.config["HCAPTCHA_SITEKEY"] = environ.get("HCAPTCHA_SITEKEY","").strip()
app.config["HCAPTCHA_SECRET"] = environ.get("HCAPTCHA_SECRET","").strip()
app.config["SPAM_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_SIMILARITY_THRESHOLD", 0.5))
app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", 5))
app.config["SPAM_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", 0.5))
app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"] = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD", 0.5))
app.config["COMMENT_SPAM_COUNT_THRESHOLD"] = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD", 0.5))
app.config["VIDEO_COIN_REQUIREMENT"] = int(environ.get("VIDEO_COIN_REQUIREMENT", 0))
app.config["READ_ONLY"]=bool(int(environ.get("READ_ONLY", "0")))
app.config["BOT_DISABLE"]=bool(int(environ.get("BOT_DISABLE", False)))
app.config["RATELIMIT_KEY_PREFIX"] = "flask_limiting_"
app.config["RATELIMIT_ENABLED"] = True
app.config["RATELIMIT_DEFAULTS_DEDUCT_WHEN"]=lambda:True
app.config["RATELIMIT_DEFAULTS_EXEMPT_WHEN"]=lambda:False
app.config["RATELIMIT_HEADERS_ENABLED"]=True
app.config["CACHE_TYPE"] = "filesystem"
app.config["CACHE_DIR"] = "cache"
app.config["RATELIMIT_STORAGE_URL"] = environ.get("REDIS_URL", "redis://127.0.0.1")
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = environ.get("MAIL_USERNAME", "").strip()
app.config['MAIL_PASSWORD'] = environ.get("MAIL_PASSWORD", "").strip()
r=redis.Redis(host=environ.get("REDIS_URL", "redis://127.0.0.1"), decode_responses=True, ssl_cert_reqs=None)
limiter = Limiter(
app,
key_func=get_ipaddr,
default_limits=["50/minute"],
headers_enabled=True,
strategy="fixed-window"
)
Base = declarative_base()
engine = create_engine(app.config['DATABASE_URL'])
db_session = scoped_session(sessionmaker(bind=engine, autoflush=False))
cache = Cache(app)
Compress(app)
mail = Mail(app)
@app.before_request
def before_request():
if request.method.lower() != "get" and app.config["READ_ONLY"]: return {"error":f"{app.config['SITE_NAME']} is currently in read-only mode."}, 500
if app.config["BOT_DISABLE"] and request.headers.get("X-User-Type")=="Bot": abort(503)
g.db = db_session()
g.timestamp = int(time.time())
if not request.path.startswith("/assets") and not request.path.startswith("/images") and not request.path.startswith("/hostedimages"):
session.permanent = True
if not session.get("session_id"): session["session_id"] = secrets.token_hex(16)
if app.config["FORCE_HTTPS"] and request.url.startswith("http://") and "localhost" not in app.config["SERVER_NAME"]:
url = request.url.replace("http://", "https://", 1)
return redirect(url, code=301)
ua=request.headers.get("User-Agent","")
if "CriOS/" in ua: g.system="ios/chrome"
elif "Version/" in ua: g.system="android/webview"
elif "Mobile Safari/" in ua: g.system="android/chrome"
elif "Safari/" in ua: g.system="ios/safari"
elif "Mobile/" in ua: g.system="ios/webview"
else: g.system="other/other"
@app.teardown_appcontext
def teardown_request(error):
if hasattr(g, 'db') and g.db:
g.db.close()
@app.after_request
def after_request(response):
response.headers.add("Strict-Transport-Security", "max-age=31536000")
response.headers.add("Referrer-Policy", "same-origin")
response.headers.add("X-Frame-Options", "deny")
return response
import gevent.monkey
gevent.monkey.patch_all()
from os import environ
import secrets
from flask import *
from flask_caching import Cache
from flask_limiter import Limiter
from flask_compress import Compress
from flask_limiter.util import get_ipaddr
from flask_mail import Mail
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import *
import gevent
from werkzeug.middleware.proxy_fix import ProxyFix
import redis
app = Flask(__name__, template_folder='./templates')
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=3)
app.url_map.strict_slashes = False
app.jinja_env.cache = {}
import faulthandler
faulthandler.enable()
app.config["SITE_NAME"]=environ.get("SITE_NAME").strip()
app.config["COINS_NAME"]=environ.get("COINS_NAME").strip()
app.config["GUMROAD_LINK"]=environ.get("GUMROAD_LINK", "https://marsey1.gumroad.com/l/tfcvri").strip()
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['DATABASE_URL'] = environ.get("DATABASE_URL")
app.config['SECRET_KEY'] = environ.get('MASTER_KEY')
app.config["SERVER_NAME"] = environ.get("DOMAIN").strip()
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 86400
app.config["SESSION_COOKIE_NAME"] = "session_" + environ.get("SITE_NAME").strip().lower()
app.config["VERSION"] = "1.0.0"
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
app.config["SESSION_COOKIE_SECURE"] = bool(int(environ.get("FORCE_HTTPS", 1)))
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 365
app.config["SESSION_REFRESH_EACH_REQUEST"] = True
app.config["SLOGAN"] = environ.get("SLOGAN", "").strip()
app.config["DEFAULT_COLOR"] = environ.get("DEFAULT_COLOR", "ff0000").strip()
app.config["DEFAULT_THEME"] = environ.get("DEFAULT_THEME", "midnight").strip()
app.config["FORCE_HTTPS"] = int(environ.get("FORCE_HTTPS", 1)) if ("localhost" not in app.config["SERVER_NAME"] and "127.0.0.1" not in app.config["SERVER_NAME"]) else 0
app.config["UserAgent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
app.config["HCAPTCHA_SITEKEY"] = environ.get("HCAPTCHA_SITEKEY","").strip()
app.config["HCAPTCHA_SECRET"] = environ.get("HCAPTCHA_SECRET","").strip()
app.config["SPAM_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_SIMILARITY_THRESHOLD", 0.5))
app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", 5))
app.config["SPAM_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", 0.5))
app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"] = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD", 0.5))
app.config["COMMENT_SPAM_COUNT_THRESHOLD"] = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD", 0.5))
app.config["VIDEO_COIN_REQUIREMENT"] = int(environ.get("VIDEO_COIN_REQUIREMENT", 0))
app.config["READ_ONLY"]=bool(int(environ.get("READ_ONLY", "0")))
app.config["BOT_DISABLE"]=bool(int(environ.get("BOT_DISABLE", False)))
app.config["RATELIMIT_KEY_PREFIX"] = "flask_limiting_"
app.config["RATELIMIT_ENABLED"] = True
app.config["RATELIMIT_DEFAULTS_DEDUCT_WHEN"]=lambda:True
app.config["RATELIMIT_DEFAULTS_EXEMPT_WHEN"]=lambda:False
app.config["RATELIMIT_HEADERS_ENABLED"]=True
app.config["CACHE_TYPE"] = "filesystem"
app.config["CACHE_DIR"] = "cache"
app.config["RATELIMIT_STORAGE_URL"] = environ.get("REDIS_URL", "redis://127.0.0.1")
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = environ.get("MAIL_USERNAME", "").strip()
app.config['MAIL_PASSWORD'] = environ.get("MAIL_PASSWORD", "").strip()
r=redis.Redis(host=environ.get("REDIS_URL", "redis://127.0.0.1"), decode_responses=True, ssl_cert_reqs=None)
limiter = Limiter(
app,
key_func=get_ipaddr,
default_limits=["50/minute"],
headers_enabled=True,
strategy="fixed-window"
)
Base = declarative_base()
engine = create_engine(app.config['DATABASE_URL'])
db_session = scoped_session(sessionmaker(bind=engine, autoflush=False))
cache = Cache(app)
Compress(app)
mail = Mail(app)
@app.before_request
def before_request():
if request.method.lower() != "get" and app.config["READ_ONLY"]: return {"error":f"{app.config['SITE_NAME']} is currently in read-only mode."}, 500
if app.config["BOT_DISABLE"] and request.headers.get("X-User-Type")=="Bot": abort(503)
g.db = db_session()
g.timestamp = int(time.time())
if not request.path.startswith("/assets") and not request.path.startswith("/images") and not request.path.startswith("/hostedimages"):
session.permanent = True
if not session.get("session_id"): session["session_id"] = secrets.token_hex(16)
if app.config["FORCE_HTTPS"] and request.url.startswith("http://") and "localhost" not in app.config["SERVER_NAME"]:
url = request.url.replace("http://", "https://", 1)
return redirect(url, code=301)
ua=request.headers.get("User-Agent","")
if "CriOS/" in ua: g.system="ios/chrome"
elif "Version/" in ua: g.system="android/webview"
elif "Mobile Safari/" in ua: g.system="android/chrome"
elif "Safari/" in ua: g.system="ios/safari"
elif "Mobile/" in ua: g.system="ios/webview"
else: g.system="other/other"
@app.teardown_appcontext
def teardown_request(error):
if hasattr(g, 'db') and g.db:
g.db.close()
@app.after_request
def after_request(response):
response.headers.add("Strict-Transport-Security", "max-age=31536000")
response.headers.add("Referrer-Policy", "same-origin")
response.headers.add("X-Frame-Options", "deny")
return response
from files.routes import *

28
files/classes/__init__.py 100644 → 100755
View File

@ -1,15 +1,15 @@
from .alts import *
from .badges import *
from .clients import *
from .comment import *
from .domains import *
from .flags import *
from .user import *
from .userblock import *
from .submission import *
from .votes import *
from .domains import *
from .subscriptions import *
from files.__main__ import app
from .mod_logs import *
from .alts import *
from .badges import *
from .clients import *
from .comment import *
from .domains import *
from .flags import *
from .user import *
from .userblock import *
from .submission import *
from .votes import *
from .domains import *
from .subscriptions import *
from files.__main__ import app
from .mod_logs import *
from .award import *

30
files/classes/alts.py 100644 → 100755
View File

@ -1,15 +1,15 @@
from sqlalchemy import *
from files.__main__ import Base
class Alt(Base):
__tablename__ = "alts"
id = Column(Integer, primary_key=True)
user1 = Column(Integer, ForeignKey("users.id"))
user2 = Column(Integer, ForeignKey("users.id"))
is_manual = Column(Boolean, default=False)
def __repr__(self):
return f"<Alt(id={self.id})>"
from sqlalchemy import *
from files.__main__ import Base
class Alt(Base):
__tablename__ = "alts"
id = Column(Integer, primary_key=True)
user1 = Column(Integer, ForeignKey("users.id"))
user2 = Column(Integer, ForeignKey("users.id"))
is_manual = Column(Boolean, default=False)
def __repr__(self):
return f"<Alt(id={self.id})>"

174
files/classes/award.py 100644 → 100755
View File

@ -1,87 +1,87 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from os import environ
from files.helpers.lazy import lazy
site_name = environ.get("SITE_NAME").strip()
if site_name == "Drama":
AWARDS = {
"ban": {
"kind": "ban",
"title": "One-Day Ban",
"description": "Bans the author for a day.",
"icon": "fas fa-gavel",
"color": "text-danger",
"price": 5000
},
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 1000
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 1000
}
}
else:
AWARDS = {
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 1000
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 1000
}
}
class AwardRelationship(Base):
__tablename__ = "award_relationships"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
submission_id = Column(Integer, ForeignKey("submissions.id"))
comment_id = Column(Integer, ForeignKey("comments.id"))
kind = Column(String)
user = relationship("User", primaryjoin="AwardRelationship.user_id==User.id", viewonly=True)
post = relationship("Submission", primaryjoin="AwardRelationship.submission_id==Submission.id", viewonly=True)
comment = relationship("Comment", primaryjoin="AwardRelationship.comment_id==Comment.id", viewonly=True)
@property
@lazy
def type(self):
return AWARDS[self.kind]
@property
@lazy
def title(self):
return self.type['title']
@property
@lazy
def class_list(self):
return self.type['icon']+' '+self.type['color']
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from os import environ
from files.helpers.lazy import lazy
site_name = environ.get("SITE_NAME").strip()
if site_name == "Drama":
AWARDS = {
"ban": {
"kind": "ban",
"title": "One-Day Ban",
"description": "Bans the author for a day.",
"icon": "fas fa-gavel",
"color": "text-danger",
"price": 5000
},
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 500
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 500
}
}
else:
AWARDS = {
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 500
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 500
}
}
class AwardRelationship(Base):
__tablename__ = "award_relationships"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
submission_id = Column(Integer, ForeignKey("submissions.id"))
comment_id = Column(Integer, ForeignKey("comments.id"))
kind = Column(String)
user = relationship("User", primaryjoin="AwardRelationship.user_id==User.id", viewonly=True)
post = relationship("Submission", primaryjoin="AwardRelationship.submission_id==Submission.id", viewonly=True)
comment = relationship("Comment", primaryjoin="AwardRelationship.comment_id==Comment.id", viewonly=True)
@property
@lazy
def type(self):
return AWARDS[self.kind]
@property
@lazy
def title(self):
return self.type['title']
@property
@lazy
def class_list(self):
return self.type['icon']+' '+self.type['color']

187
files/classes/badges.py 100644 → 100755
View File

@ -1,94 +1,93 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base, app
from os import environ
from files.helpers.lazy import lazy
site_name = environ.get("SITE_NAME").strip()
class BadgeDef(Base):
__tablename__ = "badge_defs"
id = Column(BigInteger, primary_key=True)
name = Column(String)
description = Column(String)
icon = Column(String)
kind = Column(Integer, default=1)
qualification_expr = Column(String)
def __repr__(self):
return f"<BadgeDef(badge_id={self.id})>"
@property
@lazy
def path(self):
return f"/assets/images/badges/{self.icon}"
@property
@lazy
def json_core(self):
return {
"name": self.name,
"description": self.description,
"icon": self.icon
}
class Badge(Base):
__tablename__ = "badges"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
badge_id = Column(Integer, ForeignKey("badge_defs.id"))
description = Column(String)
url = Column(String)
badge = relationship("BadgeDef", viewonly=True)
def __repr__(self):
return f"<Badge(user_id={self.user_id}, badge_id={self.badge_id})>"
@property
@lazy
def text(self):
if self.description:
return self.description
else:
return self.badge.description
@property
@lazy
def type(self):
return self.badge.id
@property
@lazy
def name(self):
return self.badge.name
@property
@lazy
def path(self):
return self.badge.path
@property
@lazy
def json_core(self):
return {'text': self.text,
'name': self.name,
'created_utc': self.created_utc,
'url': self.url,
'icon_url':f"https://{app.config['SERVER_NAME']}{self.path}"
}
@property
@lazy
def json(self):
return self.json_core
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base, app
from os import environ
from files.helpers.lazy import lazy
site_name = environ.get("SITE_NAME").strip()
class BadgeDef(Base):
__tablename__ = "badge_defs"
id = Column(BigInteger, primary_key=True)
name = Column(String)
description = Column(String)
icon = Column(String)
kind = Column(Integer, default=1)
qualification_expr = Column(String)
def __repr__(self):
return f"<BadgeDef(badge_id={self.id})>"
@property
@lazy
def path(self):
return f"/assets/images/badges/{self.icon}.gif"
@property
@lazy
def json_core(self):
return {
"name": self.name,
"description": self.description,
"icon": self.icon
}
class Badge(Base):
__tablename__ = "badges"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
badge_id = Column(Integer, ForeignKey("badge_defs.id"))
description = Column(String)
url = Column(String)
badge = relationship("BadgeDef", viewonly=True)
def __repr__(self):
return f"<Badge(user_id={self.user_id}, badge_id={self.badge_id})>"
@property
@lazy
def text(self):
if self.description:
return self.description
else:
return self.badge.description
@property
@lazy
def type(self):
return self.badge.id
@property
@lazy
def name(self):
return self.badge.name
@property
@lazy
def path(self):
return self.badge.path
@property
@lazy
def json_core(self):
return {'text': self.text,
'name': self.name,
'url': self.url,
'icon_url':f"https://{app.config['SERVER_NAME']}{self.path}"
}
@property
@lazy
def json(self):
return self.json_core

162
files/classes/clients.py 100644 → 100755
View File

@ -1,82 +1,82 @@
from flask import *
from sqlalchemy import *
from sqlalchemy.orm import relationship, lazyload
from .submission import Submission
from .comment import Comment
from files.__main__ import Base
from files.helpers.lazy import lazy
import time
class OauthApp(Base):
__tablename__ = "oauth_apps"
id = Column(Integer, primary_key=True)
client_id = Column(String)
app_name = Column(String)
redirect_uri = Column(String)
description = Column(String)
author_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", viewonly=True)
def __repr__(self): return f"<OauthApp(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
@property
@lazy
def permalink(self): return f"/admin/app/{self.id}"
@lazy
def idlist(self, page=1):
posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(app_id=self.id)
posts=posts.order_by(Submission.created_utc.desc())
posts=posts.offset(100*(page-1)).limit(101)
return [x[0] for x in posts.all()]
@lazy
def comments_idlist(self, page=1):
posts = g.db.query(Comment.id).options(lazyload('*')).filter_by(app_id=self.id)
posts=posts.order_by(Comment.created_utc.desc())
posts=posts.offset(100*(page-1)).limit(101)
return [x[0] for x in posts.all()]
class ClientAuth(Base):
__tablename__ = "client_auths"
id = Column(Integer, primary_key=True)
oauth_client = Column(Integer, ForeignKey("oauth_apps.id"))
access_token = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", viewonly=True)
application = relationship("OauthApp", viewonly=True)
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
from flask import *
from sqlalchemy import *
from sqlalchemy.orm import relationship, lazyload
from .submission import Submission
from .comment import Comment
from files.__main__ import Base
from files.helpers.lazy import lazy
import time
class OauthApp(Base):
__tablename__ = "oauth_apps"
id = Column(Integer, primary_key=True)
client_id = Column(String)
app_name = Column(String)
redirect_uri = Column(String)
description = Column(String)
author_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", viewonly=True)
def __repr__(self): return f"<OauthApp(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
@property
@lazy
def permalink(self): return f"/admin/app/{self.id}"
@lazy
def idlist(self, page=1):
posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(app_id=self.id)
posts=posts.order_by(Submission.created_utc.desc())
posts=posts.offset(100*(page-1)).limit(101)
return [x[0] for x in posts.all()]
@lazy
def comments_idlist(self, page=1):
posts = g.db.query(Comment.id).options(lazyload('*')).filter_by(app_id=self.id)
posts=posts.order_by(Comment.created_utc.desc())
posts=posts.offset(100*(page-1)).limit(101)
return [x[0] for x in posts.all()]
class ClientAuth(Base):
__tablename__ = "client_auths"
id = Column(Integer, primary_key=True)
oauth_client = Column(Integer, ForeignKey("oauth_apps.id"))
access_token = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", viewonly=True)
application = relationship("OauthApp", viewonly=True)
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))

View File

@ -348,16 +348,13 @@ class Comment(Base):
@lazy
def collapse_for_user(self, v):
if self.over_18 and not (v and v.over_18) and not self.post.over_18:
return True
if self.over_18 and not (v and v.over_18) and not self.post.over_18: return True
if not v:
return False
if not v: return False
if any([x in self.body for x in v.filter_words]):
return True
if v.filter_words and any([x in self.body for x in v.filter_words]): return True
if self.is_banned: return True
if self.is_banned or (self.author and self.author.shadowbanned): return True
return False

16
files/classes/domains.py 100644 → 100755
View File

@ -1,9 +1,9 @@
from sqlalchemy import *
from files.__main__ import Base
class BannedDomain(Base):
__tablename__ = "banneddomains"
id = Column(Integer, primary_key=True)
domain = Column(String)
from sqlalchemy import *
from files.__main__ import Base
class BannedDomain(Base):
__tablename__ = "banneddomains"
id = Column(Integer, primary_key=True)
domain = Column(String)
reason = Column(String)

110
files/classes/flags.py 100644 → 100755
View File

@ -1,56 +1,56 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
import time
class Flag(Base):
__tablename__ = "flags"
id = Column(Integer, primary_key=True)
post_id = Column(Integer, ForeignKey("submissions.id"))
user_id = Column(Integer, ForeignKey("users.id"))
reason = Column(String)
user = relationship("User", primaryjoin = "Flag.user_id == User.id", uselist = False, viewonly=True)
def __repr__(self):
return f"<Flag(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
class CommentFlag(Base):
__tablename__ = "commentflags"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
comment_id = Column(Integer, ForeignKey("comments.id"))
reason = Column(String)
user = relationship("User", primaryjoin = "CommentFlag.user_id == User.id", uselist = False, viewonly=True)
def __repr__(self):
return f"<CommentFlag(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
import time
class Flag(Base):
__tablename__ = "flags"
id = Column(Integer, primary_key=True)
post_id = Column(Integer, ForeignKey("submissions.id"))
user_id = Column(Integer, ForeignKey("users.id"))
reason = Column(String)
user = relationship("User", primaryjoin = "Flag.user_id == User.id", uselist = False, viewonly=True)
def __repr__(self):
return f"<Flag(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
class CommentFlag(Base):
__tablename__ = "commentflags"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
comment_id = Column(Integer, ForeignKey("comments.id"))
reason = Column(String)
user = relationship("User", primaryjoin = "CommentFlag.user_id == User.id", uselist = False, viewonly=True)
def __repr__(self):
return f"<CommentFlag(id={self.id})>"
@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)))

472
files/classes/mod_logs.py 100644 → 100755
View File

@ -1,236 +1,236 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
import time
from files.helpers.lazy import lazy
class ModAction(Base):
__tablename__ = "modactions"
id = Column(BigInteger, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
kind = Column(String)
target_user_id = Column(Integer, ForeignKey("users.id"), default=0)
target_submission_id = Column(Integer, ForeignKey("submissions.id"), default=0)
target_comment_id = Column(Integer, ForeignKey("comments.id"), default=0)
_note=Column(String)
created_utc = Column(Integer, default=0)
user = relationship("User", primaryjoin="User.id==ModAction.user_id", viewonly=True)
target_user = relationship("User", primaryjoin="User.id==ModAction.target_user_id", viewonly=True)
target_post = relationship("Submission", viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs:
kwargs["created_utc"] = int(time.time())
if "note" in kwargs:
kwargs["_note"]=kwargs["note"]
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<ModAction(id={self.id})>"
@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 <a href="{self.target_post.permalink}">post</a>'
elif self.target_comment_id: return f'for <a href="/comment/{self.target_comment_id}">comment</a>'
else: return self._note
else:
return self._note or ""
@note.setter
def note(self, x):
self._note=x
@property
@lazy
def string(self):
output = ACTIONTYPES[self.kind]["str"].format(self=self)
if self.note: output += f" <i>({self.note})</i>"
return output
@property
@lazy
def target_link(self):
if self.target_user: return f'<a href="{self.target_user.url}">{self.target_user.username}</a>'
elif self.target_post: return f'<a href="{self.target_post.permalink}">{self.target_post.title.replace("<","").replace(">","")}</a>'
elif self.target_comment_id: return f'<a href="/comment/{self.target_comment_id}">comment</a>'
@property
@lazy
def icon(self):
return ACTIONTYPES[self.kind]['icon']
@property
@lazy
def color(self):
return ACTIONTYPES[self.kind]['color']
@property
@lazy
def permalink(self):
return f"/log/{self.id}"
ACTIONTYPES={
"ban_user":{
"str":'banned user {self.target_link}',
"icon":"fa-user-slash",
"color": "bg-danger",
},
"unban_user":{
"str":'unbanned user {self.target_link}',
"icon": "fa-user-slash",
"color": "bg-muted",
},
"club_allow":{
"str":'allowed user {self.target_link} into the country club',
"icon":"fa-user-slash",
"color": "bg-danger",
},
"club_ban":{
"str":'disallowed user {self.target_link} from the country club',
"icon": "fa-user-slash",
"color": "bg-muted",
},
"nuke_user":{
"str":'removed all content of {self.target_link}',
"icon":"fa-user-slash",
"color": "bg-danger",
},
"unnuke_user":{
"str":'approved all content of {self.target_link}',
"icon": "fa-user-slash",
"color": "bg-muted",
},
"shadowban": {
"str": 'shadowbanned {self.target_link}',
"icon": "fa-user-slash",
"color": "bg-danger",
},
"unshadowban": {
"str": 'unshadowbanned {self.target_link}',
"icon": "fa-user-slash",
"color": "bg-muted",
},
"agendaposter": {
"str": "set agendaposter theme on {self.target_link}",
"icon": "fa-user-slash",
"color": "bg-muted",
},
"unagendaposter": {
"str": "removed agendaposter theme from {self.target_link}",
"icon": "fa-user-slash",
"color": "bg-muted",
},
"set_flair_locked":{
"str":"set {self.target_link}'s flair (locked)",
"icon": "fa-user-slash",
"color": "bg-muted",
},
"set_flair_notlocked":{
"str":"set {self.target_link}'s flair (not locked)",
"icon": "fa-user-slash",
"color": "bg-muted",
},
"pin_comment":{
"str":'pinned a {self.target_link}',
"icon":"fa-thumbtack fa-rotate--45",
"color": "bg-info",
},
"unpin_comment":{
"str":'un-pinned a {self.target_link}',
"icon":"fa-thumbtack fa-rotate--45",
"color": "bg-muted",
},
"pin_post":{
"str":'pinned post {self.target_link}',
"icon":"fa-thumbtack fa-rotate--45",
"color": "bg-success",
},
"unpin_post":{
"str":'un-pinned post {self.target_link}',
"icon":"fa-thumbtack fa-rotate--45",
"color": "bg-muted",
},
"set_nsfw":{
"str":'set nsfw on post {self.target_link}',
"icon":"fa-eye-evil",
"color": "bg-danger",
},
"unset_nsfw":{
"str":'un-set nsfw on post {self.target_link}',
"icon":"fa-eye-evil",
"color": "bg-muted",
},
"ban_post":{
"str": 'removed post {self.target_link}',
"icon":"fa-feather-alt",
"color": "bg-danger",
},
"unban_post":{
"str": 'reinstated post {self.target_link}',
"icon":"fa-feather-alt",
"color": "bg-muted",
},
"club":{
"str": 'marked post {self.target_link} as club-only',
"icon":"fa-eye-slash",
"color": "bg-danger",
},
"unclub":{
"str": 'unmarked post {self.target_link} as club-only',
"icon":"fa-eye",
"color": "bg-muted",
},
"ban_comment":{
"str": 'removed {self.target_link}',
"icon":"fa-comment",
"color": "bg-danger",
},
"unban_comment":{
"str": 'reinstated {self.target_link}',
"icon":"fa-comment",
"color": "bg-muted",
},
}
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
import time
from files.helpers.lazy import lazy
class ModAction(Base):
__tablename__ = "modactions"
id = Column(BigInteger, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
kind = Column(String)
target_user_id = Column(Integer, ForeignKey("users.id"), default=0)
target_submission_id = Column(Integer, ForeignKey("submissions.id"), default=0)
target_comment_id = Column(Integer, ForeignKey("comments.id"), default=0)
_note=Column(String)
created_utc = Column(Integer, default=0)
user = relationship("User", primaryjoin="User.id==ModAction.user_id", viewonly=True)
target_user = relationship("User", primaryjoin="User.id==ModAction.target_user_id", viewonly=True)
target_post = relationship("Submission", viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs:
kwargs["created_utc"] = int(time.time())
if "note" in kwargs:
kwargs["_note"]=kwargs["note"]
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<ModAction(id={self.id})>"
@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 <a href="{self.target_post.permalink}">post</a>'
elif self.target_comment_id: return f'for <a href="/comment/{self.target_comment_id}">comment</a>'
else: return self._note
else:
return self._note or ""
@note.setter
def note(self, x):
self._note=x
@property
@lazy
def string(self):
output = ACTIONTYPES[self.kind]["str"].format(self=self)
if self.note: output += f" <i>({self.note})</i>"
return output
@property
@lazy
def target_link(self):
if self.target_user: return f'<a href="{self.target_user.url}">{self.target_user.username}</a>'
elif self.target_post: return f'<a href="{self.target_post.permalink}">{self.target_post.title.replace("<","").replace(">","")}</a>'
elif self.target_comment_id: return f'<a href="/comment/{self.target_comment_id}">comment</a>'
@property
@lazy
def icon(self):
return ACTIONTYPES[self.kind]['icon']
@property
@lazy
def color(self):
return ACTIONTYPES[self.kind]['color']
@property
@lazy
def permalink(self):
return f"/log/{self.id}"
ACTIONTYPES={
"ban_user":{
"str":'banned user {self.target_link}',
"icon":"fa-user-slash",
"color": "bg-danger",
},
"unban_user":{
"str":'unbanned user {self.target_link}',
"icon": "fa-user-slash",
"color": "bg-muted",
},
"club_allow":{
"str":'allowed user {self.target_link} into the country club',
"icon":"fa-user-slash",
"color": "bg-danger",
},
"club_ban":{
"str":'disallowed user {self.target_link} from the country club',
"icon": "fa-user-slash",
"color": "bg-muted",
},
"nuke_user":{
"str":'removed all content of {self.target_link}',
"icon":"fa-user-slash",
"color": "bg-danger",
},
"unnuke_user":{
"str":'approved all content of {self.target_link}',
"icon": "fa-user-slash",
"color": "bg-muted",
},
"shadowban": {
"str": 'shadowbanned {self.target_link}',
"icon": "fa-user-slash",
"color": "bg-danger",
},
"unshadowban": {
"str": 'unshadowbanned {self.target_link}',
"icon": "fa-user-slash",
"color": "bg-muted",
},
"agendaposter": {
"str": "set agendaposter theme on {self.target_link}",
"icon": "fa-user-slash",
"color": "bg-muted",
},
"unagendaposter": {
"str": "removed agendaposter theme from {self.target_link}",
"icon": "fa-user-slash",
"color": "bg-muted",
},
"set_flair_locked":{
"str":"set {self.target_link}'s flair (locked)",
"icon": "fa-user-slash",
"color": "bg-muted",
},
"set_flair_notlocked":{
"str":"set {self.target_link}'s flair (not locked)",
"icon": "fa-user-slash",
"color": "bg-muted",
},
"pin_comment":{
"str":'pinned a {self.target_link}',
"icon":"fa-thumbtack fa-rotate--45",
"color": "bg-info",
},
"unpin_comment":{
"str":'un-pinned a {self.target_link}',
"icon":"fa-thumbtack fa-rotate--45",
"color": "bg-muted",
},
"pin_post":{
"str":'pinned post {self.target_link}',
"icon":"fa-thumbtack fa-rotate--45",
"color": "bg-success",
},
"unpin_post":{
"str":'un-pinned post {self.target_link}',
"icon":"fa-thumbtack fa-rotate--45",
"color": "bg-muted",
},
"set_nsfw":{
"str":'set nsfw on post {self.target_link}',
"icon":"fa-eye-evil",
"color": "bg-danger",
},
"unset_nsfw":{
"str":'un-set nsfw on post {self.target_link}',
"icon":"fa-eye-evil",
"color": "bg-muted",
},
"ban_post":{
"str": 'removed post {self.target_link}',
"icon":"fa-feather-alt",
"color": "bg-danger",
},
"unban_post":{
"str": 'reinstated post {self.target_link}',
"icon":"fa-feather-alt",
"color": "bg-muted",
},
"club":{
"str": 'marked post {self.target_link} as club-only',
"icon":"fa-eye-slash",
"color": "bg-danger",
},
"unclub":{
"str": 'unmarked post {self.target_link} as club-only',
"icon":"fa-eye",
"color": "bg-muted",
},
"ban_comment":{
"str": 'removed {self.target_link}',
"icon":"fa-comment",
"color": "bg-danger",
},
"unban_comment":{
"str": 'reinstated {self.target_link}',
"icon":"fa-comment",
"color": "bg-muted",
},
}

View File

@ -233,11 +233,11 @@ class Submission(Base):
@property
@lazy
def thumb_url(self):
if self.over_18: return f"https://{site}/assets/images/nsfw.webp"
elif not self.url: return f"https://{site}/assets/images/{site_name}/default_thumb_text.webp"
if self.over_18: return f"https://{site}/assets/images/nsfw.gif"
elif not self.url: return f"https://{site}/assets/images/{site_name}/default_thumb_text.gif"
elif self.thumburl: return self.thumburl
elif "youtu.be" in self.domain or "youtube.com" in self.domain: return f"https://{site}/assets/images/default_thumb_yt.webp"
else: return f"https://{site}/assets/images/default_thumb_link.webp"
elif "youtu.be" in self.domain or "youtube.com" in self.domain: return f"https://{site}/assets/images/default_thumb_yt.gif"
else: return f"https://{site}/assets/images/default_thumb_link.gif"
@property
@lazy
@ -266,6 +266,7 @@ class Submission(Base):
'upvotes': self.upvotes,
'downvotes': self.downvotes,
'stickied': self.stickied,
'private' : self.private,
'distinguish_level': self.distinguish_level,
'voted': self.voted if hasattr(self, 'voted') else 0,
'flags': flags,
@ -398,7 +399,8 @@ class SaveRelationship(Base):
__tablename__="save_relationship"
id=Column(Integer, primary_key=true)
user_id=Column(Integer, ForeignKey("users.id"))
submission_id=Column(Integer, ForeignKey("submissions.id"))
id=Column(Integer, primary_key=True)
user_id=Column(Integer)
submission_id=Column(Integer)
comment_id=Column(Integer)
type=Column(Integer)

66
files/classes/subscriptions.py 100644 → 100755
View File

@ -1,34 +1,34 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
class Subscription(Base):
__tablename__ = "subscriptions"
id = Column(BigInteger, primary_key=True)
user_id = Column(BigInteger, ForeignKey("users.id"))
submission_id = Column(BigInteger, default=0)
user = relationship("User", uselist=False, viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Subscription(id={self.id})>"
class Follow(Base):
__tablename__ = "follows"
id = Column(BigInteger, primary_key=True)
user_id = Column(BigInteger, ForeignKey("users.id"))
target_id = Column(BigInteger, ForeignKey("users.id"))
user = relationship("User", uselist=False, primaryjoin="User.id==Follow.user_id", viewonly=True)
target = relationship("User", primaryjoin="User.id==Follow.target_id", viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
class Subscription(Base):
__tablename__ = "subscriptions"
id = Column(BigInteger, primary_key=True)
user_id = Column(BigInteger, ForeignKey("users.id"))
submission_id = Column(BigInteger, default=0)
user = relationship("User", uselist=False, viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Subscription(id={self.id})>"
class Follow(Base):
__tablename__ = "follows"
id = Column(BigInteger, primary_key=True)
user_id = Column(BigInteger, ForeignKey("users.id"))
target_id = Column(BigInteger, ForeignKey("users.id"))
user = relationship("User", uselist=False, primaryjoin="User.id==Follow.user_id", viewonly=True)
target = relationship("User", primaryjoin="User.id==Follow.target_id", viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Follow(id={self.id})>"

1247
files/classes/user.py 100644 → 100755

File diff suppressed because it is too large Load Diff

40
files/classes/userblock.py 100644 → 100755
View File

@ -1,24 +1,16 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
import time
class UserBlock(Base):
__tablename__ = "userblocks"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
target_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", primaryjoin="User.id==UserBlock.user_id", viewonly=True)
target = relationship("User", primaryjoin="User.id==UserBlock.target_id", viewonly=True)
def __repr__(self):
return f"<UserBlock(user={user.username}, target={target.username})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %b %Y", time.gmtime(self.created_utc))
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
class UserBlock(Base):
__tablename__ = "userblocks"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
target_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", primaryjoin="User.id==UserBlock.user_id", viewonly=True)
target = relationship("User", primaryjoin="User.id==UserBlock.target_id", viewonly=True)
def __repr__(self):
return f"<UserBlock(user={self.user_id}, target={self.target_id})>"

166
files/classes/votes.py 100644 → 100755
View File

@ -1,84 +1,84 @@
from flask import *
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
class Vote(Base):
__tablename__ = "votes"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
vote_type = Column(Integer)
submission_id = Column(Integer, ForeignKey("submissions.id"))
app_id = Column(Integer, ForeignKey("oauth_apps.id"))
user = relationship("User", lazy="subquery", viewonly=True)
post = relationship("Submission", lazy="subquery", viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Vote(id={self.id})>"
@property
@lazy
def json_core(self):
data={
"user_id": self.user_id,
"submission_id":self.submission_id,
"vote_type":self.vote_type
}
return data
@property
@lazy
def json(self):
data=self.json_core
data["user"]=self.user.json_core
data["post"]=self.post.json_core
return data
class CommentVote(Base):
__tablename__ = "commentvotes"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
vote_type = Column(Integer)
comment_id = Column(Integer, ForeignKey("comments.id"))
app_id = Column(Integer, ForeignKey("oauth_apps.id"))
user = relationship("User", lazy="subquery", viewonly=True)
comment = relationship("Comment", lazy="subquery", viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<CommentVote(id={self.id})>"
@property
@lazy
def json_core(self):
data={
"user_id": self.user_id,
"comment_id":self.comment_id,
"vote_type":self.vote_type
}
return data
@property
@lazy
def json(self):
data=self.json_core
data["user"]=self.user.json_core
data["comment"]=self.comment.json_core
from flask import *
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
class Vote(Base):
__tablename__ = "votes"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
vote_type = Column(Integer)
submission_id = Column(Integer, ForeignKey("submissions.id"))
app_id = Column(Integer, ForeignKey("oauth_apps.id"))
user = relationship("User", lazy="subquery", viewonly=True)
post = relationship("Submission", lazy="subquery", viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Vote(id={self.id})>"
@property
@lazy
def json_core(self):
data={
"user_id": self.user_id,
"submission_id":self.submission_id,
"vote_type":self.vote_type
}
return data
@property
@lazy
def json(self):
data=self.json_core
data["user"]=self.user.json_core
data["post"]=self.post.json_core
return data
class CommentVote(Base):
__tablename__ = "commentvotes"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
vote_type = Column(Integer)
comment_id = Column(Integer, ForeignKey("comments.id"))
app_id = Column(Integer, ForeignKey("oauth_apps.id"))
user = relationship("User", lazy="subquery", viewonly=True)
comment = relationship("Comment", lazy="subquery", viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<CommentVote(id={self.id})>"
@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

274
files/helpers/alerts.py 100644 → 100755
View File

@ -1,137 +1,137 @@
import mistletoe
from files.classes import *
from flask import g
from .markdown import *
from .sanitize import *
from .const import *
def send_notification(vid, user, text):
if isinstance(user, int):
uid = user
else:
uid = user.id
text = text.replace('r/', 'r\/').replace('u/', 'u\/')
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=vid,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
notifiedto=uid
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=uid)
g.db.add(notif)
def send_follow_notif(vid, user, text):
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=user,
followsender=vid)
g.db.add(notif)
def send_unfollow_notif(vid, user, text):
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=user,
unfollowsender=vid)
g.db.add(notif)
def send_block_notif(vid, user, text):
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=user,
blocksender=vid)
g.db.add(notif)
def send_unblock_notif(vid, user, text):
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=user,
unblocksender=vid)
g.db.add(notif)
def send_admin(vid, text):
text = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', text)
text_html = Renderer().render(mistletoe.Document(text))
text_html = sanitize(text_html, True)
new_comment = Comment(author_id=vid,
parent_submission=None,
level=1,
sentto=0,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
admins = g.db.query(User).options(lazyload('*')).filter(User.admin_level > 0).all()
for admin in admins:
notif = Notification(comment_id=new_comment.id, user_id=admin.id)
g.db.add(notif)
import mistletoe
from files.classes import *
from flask import g
from .markdown import *
from .sanitize import *
from .const import *
def send_notification(vid, user, text):
if isinstance(user, int):
uid = user
else:
uid = user.id
text = text.replace('r/', 'r\/').replace('u/', 'u\/')
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=vid,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
notifiedto=uid
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=uid)
g.db.add(notif)
def send_follow_notif(vid, user, text):
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=user,
followsender=vid)
g.db.add(notif)
def send_unfollow_notif(vid, user, text):
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=user,
unfollowsender=vid)
g.db.add(notif)
def send_block_notif(vid, user, text):
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=user,
blocksender=vid)
g.db.add(notif)
def send_unblock_notif(vid, user, text):
text_html = CustomRenderer().render(mistletoe.Document(text))
text_html = sanitize(text_html)
new_comment = Comment(author_id=NOTIFICATIONS_ACCOUNT,
parent_submission=None,
distinguish_level=6,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
notif = Notification(comment_id=new_comment.id,
user_id=user,
unblocksender=vid)
g.db.add(notif)
def send_admin(vid, text):
text = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', text)
text_html = Renderer().render(mistletoe.Document(text))
text_html = sanitize(text_html, True)
new_comment = Comment(author_id=vid,
parent_submission=None,
level=1,
sentto=0,
body=text,
body_html=text_html,
)
g.db.add(new_comment)
g.db.flush()
admins = g.db.query(User).options(lazyload('*')).filter(User.admin_level > 0).all()
for admin in admins:
notif = Notification(comment_id=new_comment.id, user_id=admin.id)
g.db.add(notif)

View File

@ -4,9 +4,9 @@ site = environ.get("DOMAIN", '').strip()
SLURS = {
"faggot": "cute twink",
"fag": " cute twink",
"fag": "cute twink",
"pedophile": "libertarian",
"pedo": " libertarian",
"pedo": "libertarian",
"kill yourself": "keep yourself safe",
"nigger": "🏀",
"rapist": "male feminist",
@ -14,18 +14,139 @@ SLURS = {
"trannie": "🚂🚃🚃",
"tranny": "🚂🚃🚃",
"troon": "🚂🚃🚃",
"NoNewNormal": " HorseDewormerAddicts",
"kike": " https://sciencedirect.com/science/article/abs/pii/S016028960600033X",
"NoNewNormal": "HorseDewormerAddicts",
"nonewnormal": "HorseDewormerAddicts",
"kike": "https://sciencedirect.com/science/article/abs/pii/S016028960600033X",
"retard": "r-slur",
"janny": " j-slur",
"jannie": " j-slur",
"janny": " j-slur",
"janny": "j-slur",
"jannie": "j-slur",
"janny": "j-slur",
"latinos": "latinx",
"latino": "latinx",
"latinas": "latinx",
"latina": "latinx",
"hispanics": "latinx",
"hispanic": "latinx",
" uss liberty incident":" tragic accident aboard the USS Liberty",
" USS Liberty Incident":" tragic accident aboard the USS Liberty",
" USS Liberty incident":" tragic accident aboard the USS Liberty",
" USS Liberty Incident":" tragic accident aboard the USS Liberty",
" uss Liberty incident":" tragic accident aboard the USS Liberty",
" uss liberty Incident":" tragic accident aboard the USS Liberty",
" USS LIBERTY INCIDENT":" TRAGIC ACCIDENT ABOARD THE USS LIBERTY",
" lavon affair":" Lavon Misunderstanding",
" Lavon affair":" Lavon Misunderstanding",
" Lavon Affair":" Lavon Misunderstanding",
" lavon Affair":" Lavon Misunderstanding",
" shylock":" Israeli friend",
" Shylock":" Israeli friend",
" SHYLOCK":" ISRAELI FRIEND",
" yid":" Israeli friend",
" Yid":" Israeli friend",
" YID":" ISRAELI FRIEND",
" heeb":" Israeli friend",
" Heeb":" Israeli friend",
" HEEB":" ISRAELI FRIEND",
" sheeny":" Israeli friend",
" Sheeny":" Israeli friend",
" SHEENY":" ISRAELI FRIEND",
" sheenies":" Israeli friends",
" Sheenies":" Israeli friends",
" SHEENIES":" ISRAELI FRIENDS",
" hymie":" Israeli friend",
" Hymie":" Israeli friend",
" HYMIES":" ISRAELI FRIENDS",
" allah":" Allah (SWT)",
" Allah":" Allah (SWT)",
" ALLAH":" ALLAH (SWT)",
" Mohammad":" Mohammad (PBUH)",
" Muhammad":" Mohammad (PBUH)",
" Mohammed":" Mohammad (PBUH)",
" Muhammed":" Mohammad (PBUH)",
" mohammad":" Mohammad (PBUH)",
" mohammed":" Mohammad (PBUH)",
" muhammad":" Mohammad (PBUH)",
" muhammed":" Mohammad (PBUH)",
" I HATE MARSEY":" I LOVE MARSEY",
" i hate marsey":" i love marsey",
" I hate Marsey":" I love Marsey",
" I hate marsey":" I love Marsey",
" libertarian":" pedophile",
" Libertarian":" Pedophile",
" LIBERTARIAN":" PEDOPHILE",
" Billie Eilish":" Billie Eilish (fat cow)",
" billie eilish":" bilie eilish (fat cow)",
" BILLIE EILISH":" BILIE EILISH (FAT COW)",
" dancing Israelis":" I love Israel",
" dancing israelis":" i love israel",
" DANCING ISRAELIS":" I LOVE ISRAEL",
" Dancing Israelis":" I love Israel",
" sodomite":" total dreamboat",
" Sodomite":" Total dreamboat",
" pajeet":" sexy Indian dude",
" Pajeet":" Sexy Indian dude",
" PAJEET":" SEXY INDIAN DUDE",
" female":" birthing person",
" Female":" Womb-haver",
" FEMALE":" birthing person",
" landlord":" landchad",
" Landlord":" Landchad",
" LANDLORD":" LANDCHAD",
" tenant":" renthog",
" Tenant":" Renthog",
" TENANT":" RENTHOG",
" renter":" rentoid",
" Renter":" Rentoid",
" RENTER":" RENTOID",
" autistic":" neurodivergent",
" Autistic":" Neurodivergent",
" AUTISTIC":" NEURODIVERGENT",
" anime":" p-dophilic japanese cartoons",
" Anime":" P-dophilic Japanese cartoons",
" ANIME":" P-DOPHILIC JAPANESE CARTOONS",
" holohoax":" I tried to claim the Holocaust didn't happen because I am a pencil-dicked imbecile and the word filter caught me lol",
" Holohoax":" I tried to claim the Holocaust didn't happen because I am a pencil-dicked imbecile and the word filter caught me lol",
" HOLOHOAX":" I tried to claim the Holocaust didn't happen because I am a pencil-dicked imbecile and the word filter caught me lol",
" groomercord":" discord (actually a pretty cool service)",
" Groomercord":" Discord (actually a pretty cool service)",
" GROOMERCORD":" DISCORD (ACTUALLY A PRETTY COOL SERVICE)",
" pedocord":" discord (actually a pretty cool service)",
" Pedocord":" Discord (actually a pretty cool service)",
" PEDOCORD":" DISCORD (ACTUALLY A PRETTY COOL SERVICE)",
" i hate carp":" i love carp",
" I hate carp":" I love carp",
" I HATE CARP":" I LOVE CARP",
" I hate Carp":" I love Carp",
" manlet":" little king",
" Manlet":" Little king",
" MANLET":" LITTLE KING",
" gamer":" g*mer",
" Gamer":" G*mer",
" GAMER":" G*MER",
" journalist":" journ*list",
" Journalist":" Journ*list",
" JOURNALIST":" JOURN*LIST",
" journalism":" journ*lism",
" Journalism":" Journ*lism",
" JOURNALISM":" JOURN*LISM",
" buttcheeks":" bulva",
" Buttcheeks":" Bulva",
" BUTTCHEEKS":" BULVA",
" asscheeks":" bulva",
" Asscheeks":" bulva",
" ASSCHEEKS":" BULVA",
" wuhan flu":" SARS-CoV-2 syndemic",
" Wuhan flu":" SARS-CoV-2 syndemic",
" Wuhan Flu":" SARS-CoV-2 syndemic",
" china flu":" SARS-CoV-2 syndemic",
" China flu":" SARS-CoV-2 syndemic",
" China Flu":" SARS-CoV-2 syndemic",
" china virus":" SARS-CoV-2 syndemic",
" China virus":" SARS-CoV-2 syndemic",
" China Virus":" SARS-CoV-2 syndemic",
" kung flu":" SARS-CoV-2 syndemic",
" Kung flu":" SARS-CoV-2 syndemic",
" Kung Flu":" SARS-CoV-2 syndemic",
# if the word has spaces in the beginning and the end it will only censor this word without prefixes or suffixes
" nig ": "🏀",
@ -53,12 +174,28 @@ Thank you."""
BASED_MSG = "@{username}'s Based Count has increased by 1. Their Based Count is now {basedcount}.\n\nPills: {pills}"
BASEDBOT_ACCOUNT = 800
NOTIFICATIONS_ACCOUNT = 1046
if site == "pcmemes.net": AUTOJANNY_ACCOUNT = 1050
else: AUTOJANNY_ACCOUNT = 2360
LONGPOSTBOT_ACCOUNT = 1832
AUTOPOLLER_ACCOUNT = 3369
if site == "pcmemes.net":
BASEDBOT_ACCOUNT = 800
NOTIFICATIONS_ACCOUNT = 1046
AUTOJANNY_ACCOUNT = 1050
SNAPPY_ACCOUNT = 261
LONGPOSTBOT_ACCOUNT = 1832
ZOZBOT_ACCOUNT = 1833
AUTOPOLLER_ACCOUNT = 3369
elif site == 'rdrama.net':
NOTIFICATIONS_ACCOUNT = 1046
AUTOJANNY_ACCOUNT = 2360
SNAPPY_ACCOUNT = 261
LONGPOSTBOT_ACCOUNT = 1832
ZOZBOT_ACCOUNT = 1833
AUTOPOLLER_ACCOUNT = 3369
else:
NOTIFICATIONS_ACCOUNT = 1
AUTOJANNY_ACCOUNT = 2
SNAPPY_ACCOUNT = 3
LONGPOSTBOT_ACCOUNT = 4
ZOZBOT_ACCOUNT = 5
AUTOPOLLER_ACCOUNT = 6
PUSHER_INSTANCE_ID = '02ddcc80-b8db-42be-9022-44c546b4dce6'
PUSHER_KEY = environ.get("PUSHER_KEY", "").strip()

126
files/helpers/discord.py 100644 → 100755
View File

@ -1,64 +1,64 @@
from os import environ
import requests
import threading
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN",'').strip()
AUTH = environ.get("DISCORD_AUTH",'').strip()
ROLES={
"shrigma": "864612849199480914",
"admin": "879459632656048180" if environ.get("DOMAIN") == "pcmemes.net" else "846509661288267776",
"linked": "890342909390520382",
"1": "868129042346414132",
"2": "875569477671067688",
"3": "869434199575236649",
"4": "868140288013664296",
"5": "880445545771044884",
"8": "886781932430565418",
}
def discord_wrap(f):
def wrapper(*args, **kwargs):
user=args[0]
if not user.discord_id:
return
thread=threading.Thread(target=f, args=args, kwargs=kwargs)
thread.start()
wrapper.__name__=f.__name__
return wrapper
@discord_wrap
def add_role(user, role_name):
role_id = ROLES[role_name]
url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.put(url, headers=headers)
@discord_wrap
def remove_user(user):
url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.delete(url, headers=headers)
@discord_wrap
def set_nick(user, nick):
url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"nick": nick}
requests.patch(url, headers=headers, json=data)
def send_message(message):
url=f"https://discordapp.com/api/channels/851846904283267094/messages"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"content": message}
from os import environ
import requests
import threading
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN",'').strip()
AUTH = environ.get("DISCORD_AUTH",'').strip()
ROLES={
"shrigma": "864612849199480914",
"admin": "879459632656048180" if environ.get("DOMAIN") == "pcmemes.net" else "846509661288267776",
"linked": "890342909390520382",
"1": "868129042346414132",
"2": "875569477671067688",
"3": "869434199575236649",
"4": "868140288013664296",
"5": "880445545771044884",
"8": "886781932430565418",
}
def discord_wrap(f):
def wrapper(*args, **kwargs):
user=args[0]
if not user.discord_id:
return
thread=threading.Thread(target=f, args=args, kwargs=kwargs)
thread.start()
wrapper.__name__=f.__name__
return wrapper
@discord_wrap
def add_role(user, role_name):
role_id = ROLES[role_name]
url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.put(url, headers=headers)
@discord_wrap
def remove_user(user):
url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.delete(url, headers=headers)
@discord_wrap
def set_nick(user, nick):
url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"nick": nick}
requests.patch(url, headers=headers, json=data)
def send_message(message):
url=f"https://discordapp.com/api/channels/851846904283267094/messages"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"content": message}
requests.post(url, headers=headers, data=data)

69
files/helpers/filters.py 100644 → 100755
View File

@ -1,36 +1,33 @@
from bs4 import BeautifulSoup
from flask import *
from urllib.parse import urlparse
from files.classes import BannedDomain
from sqlalchemy.orm import lazyload
def filter_comment_html(html_text):
soup = BeautifulSoup(html_text, features="html.parser")
links = soup.find_all("a")
domain_list = set()
for link in links:
href=link.get("href", None)
if not href:
continue
domain = urlparse(href).netloc
parts = domain.split(".")
for i in range(len(parts)):
new_domain = parts[i]
for j in range(i + 1, len(parts)):
new_domain += "." + parts[j]
domain_list.add(new_domain)
bans = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(list(domain_list))).all()]
if bans:
return bans
else:
return []
from bs4 import BeautifulSoup
from flask import *
from urllib.parse import urlparse
from files.classes import BannedDomain
from sqlalchemy.orm import lazyload
def filter_comment_html(html_text):
soup = BeautifulSoup(html_text, features="html.parser")
links = soup.find_all("a")
domain_list = set()
for link in links:
href = link.get("href")
if not href: continue
domain = urlparse(href).netloc
parts = domain.split(".")
for i in range(len(parts)):
new_domain = parts[i]
for j in range(i + 1, len(parts)):
new_domain += "." + parts[j]
domain_list.add(new_domain)
bans = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(list(domain_list))).all()]
if bans: return bans
else: return []

540
files/helpers/get.py 100644 → 100755
View File

@ -1,271 +1,271 @@
from files.classes import *
from flask import g
def get_user(username, v=None, graceful=False):
username = username.replace('\\', '')
username = username.replace('_', '\_')
username = username.replace('%', '')
user = g.db.query(
User
).filter(
or_(
User.username.ilike(username),
User.original_username.ilike(username)
)
).first()
if not user:
if not graceful:
abort(404)
else:
return None
if v:
block = g.db.query(UserBlock).options(lazyload('*')).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == user.id
),
and_(UserBlock.user_id == user.id,
UserBlock.target_id == v.id
)
)
).first()
user.is_blocking = block and block.user_id == v.id
user.is_blocked = block and block.target_id == v.id
return user
def get_account(id, v=None):
user = g.db.query(User).options(lazyload('*')).filter_by(id = id).first()
if not user:
try: id = int(str(id), 36)
except: abort(404)
user = g.db.query(User).options(lazyload('*')).filter_by(id = id).first()
if not user: abort(404)
if v:
block = g.db.query(UserBlock).options(lazyload('*')).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == user.id
),
and_(UserBlock.user_id == user.id,
UserBlock.target_id == v.id
)
)
).first()
user.is_blocking = block and block.user_id == v.id
user.is_blocked = block and block.target_id == v.id
return user
def get_post(i, v=None, graceful=False):
if v:
vt = g.db.query(Vote).options(lazyload('*')).filter_by(
user_id=v.id, submission_id=i).subquery()
blocking = v.blocking.subquery()
items = g.db.query(
Submission,
vt.c.vote_type,
blocking.c.id,
)
items=items.filter(Submission.id == i
).join(
vt,
vt.c.submission_id == Submission.id,
isouter=True
).join(
blocking,
blocking.c.target_id == Submission.author_id,
isouter=True
)
items=items.first()
if not items and not graceful:
abort(404)
x = items[0]
x.voted = items[1] or 0
x.is_blocking = items[2] or 0
else:
items = g.db.query(
Submission
).filter(Submission.id == i).first()
if not items and not graceful:
abort(404)
x=items
return x
def get_posts(pids, v=None):
if not pids:
return []
pids=tuple(pids)
if v:
vt = g.db.query(Vote).options(lazyload('*')).filter(
Vote.submission_id.in_(pids),
Vote.user_id==v.id
).subquery()
blocking = v.blocking.subquery()
blocked = v.blocked.subquery()
query = g.db.query(
Submission,
vt.c.vote_type,
blocking.c.id,
blocked.c.id,
).filter(
Submission.id.in_(pids)
).join(
vt, vt.c.submission_id==Submission.id, isouter=True
).join(
blocking,
blocking.c.target_id == Submission.author_id,
isouter=True
).join(
blocked,
blocked.c.user_id == Submission.author_id,
isouter=True
).all()
output = [p[0] for p in query]
for i in range(len(output)):
output[i].voted = query[i][1] or 0
output[i].is_blocking = query[i][2] or 0
output[i].is_blocked = query[i][3] or 0
else:
output = g.db.query(Submission,).options(lazyload('*')).filter(Submission.id.in_(pids)).all()
return sorted(output, key=lambda x: pids.index(x.id))
def get_comment(i, v=None, graceful=False):
if v:
comment=g.db.query(Comment).options(lazyload('*')).filter(Comment.id == i).first()
if not comment and not graceful: abort(404)
block = g.db.query(UserBlock).options(lazyload('*')).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == comment.author_id
),
and_(UserBlock.user_id == comment.author_id,
UserBlock.target_id == v.id
)
)
).first()
vts = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id)
vt = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first()
comment.is_blocking = block and block.user_id == v.id
comment.is_blocked = block and block.target_id == v.id
comment.voted = vt.vote_type if vt else 0
else:
comment = g.db.query(Comment).options(lazyload('*')).filter(Comment.id == i).first()
if not comment and not graceful:abort(404)
return comment
def get_comments(cids, v=None, load_parent=False):
if not cids: return []
cids=tuple(cids)
if v:
votes = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id).subquery()
blocking = v.blocking.subquery()
blocked = v.blocked.subquery()
comments = g.db.query(
Comment,
votes.c.vote_type,
blocking.c.id,
blocked.c.id,
).filter(Comment.id.in_(cids))
if not (v and v.shadowbanned) and not (v and v.admin_level == 6):
shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()]
comments = comments.filter(Comment.author_id.notin_(shadowbanned))
comments = comments.join(
votes,
votes.c.comment_id == Comment.id,
isouter=True
).join(
blocking,
blocking.c.target_id == Comment.author_id,
isouter=True
).join(
blocked,
blocked.c.user_id == Comment.author_id,
isouter=True
).all()
output = []
for c in comments:
comment = c[0]
comment.voted = c[1] or 0
comment.is_blocking = c[2] or 0
comment.is_blocked = c[3] or 0
output.append(comment)
else:
shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()]
output = g.db.query(Comment).options(lazyload('*')).filter(Comment.id.in_(cids), Comment.author_id.notin_(shadowbanned)).all()
if load_parent:
parents = [x.parent_comment_id for x in output if x.parent_comment_id]
parents = get_comments(parents, v=v)
parents = {x.id: x for x in parents}
for c in output: c.sex = parents.get(c.parent_comment_id)
return sorted(output, key=lambda x: cids.index(x.id))
def get_domain(s):
parts = s.split(".")
domain_list = set([])
for i in range(len(parts)):
new_domain = parts[i]
for j in range(i + 1, len(parts)):
new_domain += "." + parts[j]
domain_list.add(new_domain)
domain_list = tuple(list(domain_list))
doms = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(domain_list)).all()]
if not doms:
return None
doms = sorted(doms, key=lambda x: len(x.domain), reverse=True)
from files.classes import *
from flask import g
def get_user(username, v=None, graceful=False):
username = username.replace('\\', '')
username = username.replace('_', '\_')
username = username.replace('%', '')
user = g.db.query(
User
).filter(
or_(
User.username.ilike(username),
User.original_username.ilike(username)
)
).first()
if not user:
if not graceful:
abort(404)
else:
return None
if v:
block = g.db.query(UserBlock).options(lazyload('*')).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == user.id
),
and_(UserBlock.user_id == user.id,
UserBlock.target_id == v.id
)
)
).first()
user.is_blocking = block and block.user_id == v.id
user.is_blocked = block and block.target_id == v.id
return user
def get_account(id, v=None):
user = g.db.query(User).options(lazyload('*')).filter_by(id = id).first()
if not user:
try: id = int(str(id), 36)
except: abort(404)
user = g.db.query(User).options(lazyload('*')).filter_by(id = id).first()
if not user: abort(404)
if v:
block = g.db.query(UserBlock).options(lazyload('*')).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == user.id
),
and_(UserBlock.user_id == user.id,
UserBlock.target_id == v.id
)
)
).first()
user.is_blocking = block and block.user_id == v.id
user.is_blocked = block and block.target_id == v.id
return user
def get_post(i, v=None, graceful=False):
if v:
vt = g.db.query(Vote).options(lazyload('*')).filter_by(
user_id=v.id, submission_id=i).subquery()
blocking = v.blocking.subquery()
items = g.db.query(
Submission,
vt.c.vote_type,
blocking.c.id,
)
items=items.filter(Submission.id == i
).join(
vt,
vt.c.submission_id == Submission.id,
isouter=True
).join(
blocking,
blocking.c.target_id == Submission.author_id,
isouter=True
)
items=items.first()
if not items and not graceful:
abort(404)
x = items[0]
x.voted = items[1] or 0
x.is_blocking = items[2] or 0
else:
items = g.db.query(
Submission
).filter(Submission.id == i).first()
if not items and not graceful:
abort(404)
x=items
return x
def get_posts(pids, v=None):
if not pids:
return []
pids=tuple(pids)
if v:
vt = g.db.query(Vote).options(lazyload('*')).filter(
Vote.submission_id.in_(pids),
Vote.user_id==v.id
).subquery()
blocking = v.blocking.subquery()
blocked = v.blocked.subquery()
query = g.db.query(
Submission,
vt.c.vote_type,
blocking.c.id,
blocked.c.id,
).filter(
Submission.id.in_(pids)
).join(
vt, vt.c.submission_id==Submission.id, isouter=True
).join(
blocking,
blocking.c.target_id == Submission.author_id,
isouter=True
).join(
blocked,
blocked.c.user_id == Submission.author_id,
isouter=True
).all()
output = [p[0] for p in query]
for i in range(len(output)):
output[i].voted = query[i][1] or 0
output[i].is_blocking = query[i][2] or 0
output[i].is_blocked = query[i][3] or 0
else:
output = g.db.query(Submission,).options(lazyload('*')).filter(Submission.id.in_(pids)).all()
return sorted(output, key=lambda x: pids.index(x.id))
def get_comment(i, v=None, graceful=False):
if v:
comment=g.db.query(Comment).options(lazyload('*')).filter(Comment.id == i).first()
if not comment and not graceful: abort(404)
block = g.db.query(UserBlock).options(lazyload('*')).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == comment.author_id
),
and_(UserBlock.user_id == comment.author_id,
UserBlock.target_id == v.id
)
)
).first()
vts = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id)
vt = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first()
comment.is_blocking = block and block.user_id == v.id
comment.is_blocked = block and block.target_id == v.id
comment.voted = vt.vote_type if vt else 0
else:
comment = g.db.query(Comment).options(lazyload('*')).filter(Comment.id == i).first()
if not comment and not graceful:abort(404)
return comment
def get_comments(cids, v=None, load_parent=False):
if not cids: return []
cids=tuple(cids)
if v:
votes = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id).subquery()
blocking = v.blocking.subquery()
blocked = v.blocked.subquery()
comments = g.db.query(
Comment,
votes.c.vote_type,
blocking.c.id,
blocked.c.id,
).filter(Comment.id.in_(cids))
if not (v and v.shadowbanned) and not (v and v.admin_level == 6):
shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()]
comments = comments.filter(Comment.author_id.notin_(shadowbanned))
comments = comments.join(
votes,
votes.c.comment_id == Comment.id,
isouter=True
).join(
blocking,
blocking.c.target_id == Comment.author_id,
isouter=True
).join(
blocked,
blocked.c.user_id == Comment.author_id,
isouter=True
).all()
output = []
for c in comments:
comment = c[0]
comment.voted = c[1] or 0
comment.is_blocking = c[2] or 0
comment.is_blocked = c[3] or 0
output.append(comment)
else:
shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()]
output = g.db.query(Comment).options(lazyload('*')).filter(Comment.id.in_(cids), Comment.author_id.notin_(shadowbanned)).all()
if load_parent:
parents = [x.parent_comment_id for x in output if x.parent_comment_id]
parents = get_comments(parents, v=v)
parents = {x.id: x for x in parents}
for c in output: c.sex = parents.get(c.parent_comment_id)
return sorted(output, key=lambda x: cids.index(x.id))
def get_domain(s):
parts = s.split(".")
domain_list = set([])
for i in range(len(parts)):
new_domain = parts[i]
for j in range(i + 1, len(parts)):
new_domain += "." + parts[j]
domain_list.add(new_domain)
domain_list = tuple(list(domain_list))
doms = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(domain_list)).all()]
if not doms:
return None
doms = sorted(doms, key=lambda x: len(x.domain), reverse=True)
return doms[0]

52
files/helpers/images.py 100644 → 100755
View File

@ -1,27 +1,27 @@
from PIL import Image as IImage, ImageSequence
from webptools import gifwebp
def process_image(filename=None, resize=False):
i = IImage.open(filename)
if resize:
size = 100, 100
frames = ImageSequence.Iterator(i)
def thumbnails(frames):
for frame in frames:
thumbnail = frame.copy()
thumbnail.thumbnail(size)
yield thumbnail
frames = thumbnails(frames)
om = next(frames)
om.info = i.info
om.save(filename, format="WEBP", save_all=True, append_images=list(frames), loop=0)
elif i.format.lower() != "webp":
if i.format.lower() == "gif": gifwebp(input_image=filename, output_image=filename, option="-q 80")
else: i.save(filename, format="WEBP")
from PIL import Image as IImage, ImageSequence
from webptools import gifwebp
def process_image(filename=None, resize=False):
i = IImage.open(filename)
if resize:
size = 100, 100
frames = ImageSequence.Iterator(i)
def thumbnails(frames):
for frame in frames:
thumbnail = frame.copy()
thumbnail.thumbnail(size)
yield thumbnail
frames = thumbnails(frames)
om = next(frames)
om.info = i.info
om.save(filename, format="WEBP", save_all=True, append_images=list(frames), loop=0)
elif i.format.lower() != "webp":
if i.format.lower() == "gif": gifwebp(input_image=filename, output_image=filename, option="-q 80")
else: i.save(filename, format="WEBP")
return filename

70
files/helpers/jinja2.py 100644 → 100755
View File

@ -1,36 +1,36 @@
from files.__main__ import app
from .get import *
from files.helpers import const
@app.template_filter("full_link")
def full_link(url):
return f"https://{app.config['SERVER_NAME']}{url}"
@app.template_filter("app_config")
def app_config(x):
return app.config.get(x)
@app.template_filter("post_embed")
def post_embed(id, v):
try: id = int(id)
except: return None
p = get_post(id, v, graceful=True)
return render_template("submission_listing.html", listing=[p], v=v)
@app.template_filter("favorite_emojis")
def favorite_emojis(x):
str = ""
emojis = sorted(x.items(), key=lambda x: x[1], reverse=True)[:25]
for k, v in emojis:
str += f'<button class="btn m-1 px-0 emoji2" onclick="getEmoji(\'{k}\')" style="background: None!important; width:60px; overflow: hidden; border: none;" data-bs-toggle="tooltip" title=":{k}:" delay:="0"><img loading="lazy" width=50 src="/assets/images/emojis/{k}.webp" alt="{k}-emoji"/></button>'
return str
@app.context_processor
def inject_constants():
constants = [c for c in dir(const) if not c.startswith("_")]
from files.__main__ import app
from .get import *
from files.helpers import const
@app.template_filter("full_link")
def full_link(url):
return f"https://{app.config['SERVER_NAME']}{url}"
@app.template_filter("app_config")
def app_config(x):
return app.config.get(x)
@app.template_filter("post_embed")
def post_embed(id, v):
try: id = int(id)
except: return None
p = get_post(id, v, graceful=True)
return render_template("submission_listing.html", listing=[p], v=v)
@app.template_filter("favorite_emojis")
def favorite_emojis(x):
str = ""
emojis = sorted(x.items(), key=lambda x: x[1], reverse=True)[:25]
for k, v in emojis:
str += f'<button class="btn m-1 px-0 emoji2" onclick="getEmoji(\'{k}\')" style="background: None!important; width:60px; overflow: hidden; border: none;" data-bs-toggle="tooltip" title=":{k}:" delay:="0"><img loading="lazy" width=50 src="/assets/images/emojis/{k}.webp" alt="{k}-emoji"/></button>'
return str
@app.context_processor
def inject_constants():
constants = [c for c in dir(const) if not c.startswith("_")]
return {c:getattr(const, c) for c in constants}

36
files/helpers/lazy.py 100644 → 100755
View File

@ -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

270
files/helpers/markdown.py 100644 → 100755
View File

@ -1,136 +1,136 @@
from .get import *
from mistletoe.span_token import SpanToken
from mistletoe.html_renderer import HTMLRenderer
import re
from flask import g
# add token/rendering for @username mentions
class UserMention(SpanToken):
pattern = re.compile("(^|\s|\n)@((\w|-){1,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class SubMention(SpanToken):
pattern = re.compile("(^|\s|\n)r/(\w{3,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class RedditorMention(SpanToken):
pattern = re.compile("(^|\s|\n)u/((\w|-){3,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class SubMention2(SpanToken):
pattern = re.compile("(^|\s|\n)/r/(\w{3,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class RedditorMention2(SpanToken):
pattern = re.compile("(^|\s|\n)/u/((\w|-){3,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class CustomRenderer(HTMLRenderer):
def __init__(self, **kwargs):
super().__init__(UserMention,
SubMention,
RedditorMention,
SubMention2,
RedditorMention2,
)
for i in kwargs:
self.__dict__[i] = kwargs[i]
def render_user_mention(self, token):
space = token.target[0]
target = token.target[1]
user = get_user(target, graceful=True)
try:
if g.v.admin_level == 0 and g.v.any_block_exists(user):
return f"{space}@{target}"
except BaseException:
pass
if not user: return f"{space}@{target}"
return f'{space}<a href="{user.url}" class="d-inline-block mention-user" data-bs-original-name="{user.original_username}"><img loading="lazy" src="/uid/{user.id}/pic/profile" class="profile-pic-20 mr-1">@{user.username}</a>'
def render_sub_mention(self, token):
space = token.target[0]
target = token.target[1]
return f'{space}<a href="https://old.reddit.com/r/{target}" rel="nofollow noopener noreferrer" class="d-inline-block">r/{target}</a>'
def render_redditor_mention(self, token):
space = token.target[0]
target = token.target[1]
return f'{space}<a href="https://old.reddit.com/u/{target}" rel="nofollow noopener noreferrer" class="d-inline-block">u/{target}</a>'
class Renderer(HTMLRenderer):
def __init__(self, **kwargs):
super().__init__(UserMention,
SubMention,
RedditorMention,
SubMention2,
RedditorMention2,
)
for i in kwargs:
self.__dict__[i] = kwargs[i]
def render_user_mention(self, token):
space = token.target[0]
target = token.target[1]
user = get_user(target, graceful=True)
try:
if g.v.admin_level == 0 and g.v.any_block_exists(user):
return f"{space}@{target}"
except BaseException:
pass
if not user: return f"{space}@{target}"
return f'{space}<a href="{user.url}" class="d-inline-block mention-user" data-bs-original-name="{user.original_username}">@{user.username}</a>'
def render_sub_mention(self, token):
space = token.target[0]
target = token.target[1]
return f'{space}<a href="https://old.reddit.com/r/{target}" rel="nofollow noopener noreferrer" class="d-inline-block">r/{target}</a>'
def render_redditor_mention(self, token):
space = token.target[0]
target = token.target[1]
from .get import *
from mistletoe.span_token import SpanToken
from mistletoe.html_renderer import HTMLRenderer
import re
from flask import g
# add token/rendering for @username mentions
class UserMention(SpanToken):
pattern = re.compile("(^|\s|\n)@((\w|-){1,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class SubMention(SpanToken):
pattern = re.compile("(^|\s|\n)r/(\w{3,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class RedditorMention(SpanToken):
pattern = re.compile("(^|\s|\n)u/((\w|-){3,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class SubMention2(SpanToken):
pattern = re.compile("(^|\s|\n)/r/(\w{3,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class RedditorMention2(SpanToken):
pattern = re.compile("(^|\s|\n)/u/((\w|-){3,25})")
parse_inner = False
def __init__(self, match_obj):
self.target = (match_obj.group(1), match_obj.group(2))
class CustomRenderer(HTMLRenderer):
def __init__(self, **kwargs):
super().__init__(UserMention,
SubMention,
RedditorMention,
SubMention2,
RedditorMention2,
)
for i in kwargs:
self.__dict__[i] = kwargs[i]
def render_user_mention(self, token):
space = token.target[0]
target = token.target[1]
user = get_user(target, graceful=True)
try:
if g.v.admin_level == 0 and g.v.any_block_exists(user):
return f"{space}@{target}"
except BaseException:
pass
if not user: return f"{space}@{target}"
return f'{space}<a href="{user.url}" class="d-inline-block mention-user" data-bs-original-name="{user.original_username}"><img loading="lazy" src="/uid/{user.id}/pic/profile" class="profile-pic-20 mr-1">@{user.username}</a>'
def render_sub_mention(self, token):
space = token.target[0]
target = token.target[1]
return f'{space}<a href="https://old.reddit.com/r/{target}" rel="nofollow noopener noreferrer" class="d-inline-block">r/{target}</a>'
def render_redditor_mention(self, token):
space = token.target[0]
target = token.target[1]
return f'{space}<a href="https://old.reddit.com/u/{target}" rel="nofollow noopener noreferrer" class="d-inline-block">u/{target}</a>'
class Renderer(HTMLRenderer):
def __init__(self, **kwargs):
super().__init__(UserMention,
SubMention,
RedditorMention,
SubMention2,
RedditorMention2,
)
for i in kwargs:
self.__dict__[i] = kwargs[i]
def render_user_mention(self, token):
space = token.target[0]
target = token.target[1]
user = get_user(target, graceful=True)
try:
if g.v.admin_level == 0 and g.v.any_block_exists(user):
return f"{space}@{target}"
except BaseException:
pass
if not user: return f"{space}@{target}"
return f'{space}<a href="{user.url}" class="d-inline-block mention-user" data-bs-original-name="{user.original_username}">@{user.username}</a>'
def render_sub_mention(self, token):
space = token.target[0]
target = token.target[1]
return f'{space}<a href="https://old.reddit.com/r/{target}" rel="nofollow noopener noreferrer" class="d-inline-block">r/{target}</a>'
def render_redditor_mention(self, token):
space = token.target[0]
target = token.target[1]
return f'{space}<a href="https://old.reddit.com/u/{target}" rel="nofollow noopener noreferrer" class="d-inline-block">u/{target}</a>'

456
files/helpers/sanitize.py 100644 → 100755
View File

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

46
files/helpers/security.py 100644 → 100755
View File

@ -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)

40
files/helpers/session.py 100644 → 100755
View File

@ -1,20 +1,20 @@
from flask import *
import time
from .security import *
def make_logged_out_formkey(t):
s = f"{t}+{session['session_id']}"
return generate_hash(s)
def validate_logged_out_formkey(t, k):
now = int(time.time())
if now - t > 3600:
return False
s = f"{t}+{session['session_id']}"
return validate_hash(s, k)
from flask import *
import time
from .security import *
def make_logged_out_formkey(t):
s = f"{t}+{session['session_id']}"
return generate_hash(s)
def validate_logged_out_formkey(t, k):
now = int(time.time())
if now - t > 3600:
return False
s = f"{t}+{session['session_id']}"
return validate_hash(s, k)

66
files/helpers/sqla_values.py 100644 → 100755
View File

@ -1,33 +1,33 @@
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import FromClause
class values(FromClause):
named_with_column = True
def __init__(self, columns, *args, **kwargs):
self._column_args = columns
self.list = args
self.alias_name = self.name = kwargs.pop('alias_name', None)
def _populate_column_collection(self):
for c in self._column_args:
c._make_proxy(self)
@compiles(values)
def compile_values(element, compiler, asfrom=False):
columns = element.columns
v = "VALUES %s" % ", ".join(
"(%s)" % ", ".join(
compiler.render_literal_value(elem, column.type)
for elem, column in zip(tup, columns))
for tup in element.list
)
if asfrom:
if element.alias_name:
v = "(%s) AS %s (%s)" % (v, element.alias_name,
(", ".join(c.name for c in element.columns)))
else:
v = "(%s)" % v
return v
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import FromClause
class values(FromClause):
named_with_column = True
def __init__(self, columns, *args, **kwargs):
self._column_args = columns
self.list = args
self.alias_name = self.name = kwargs.pop('alias_name', None)
def _populate_column_collection(self):
for c in self._column_args:
c._make_proxy(self)
@compiles(values)
def compile_values(element, compiler, asfrom=False):
columns = element.columns
v = "VALUES %s" % ", ".join(
"(%s)" % ", ".join(
compiler.render_literal_value(elem, column.type)
for elem, column in zip(tup, columns))
for tup in element.list
)
if asfrom:
if element.alias_name:
v = "(%s) AS %s (%s)" % (v, element.alias_name,
(", ".join(c.name for c in element.columns)))
else:
v = "(%s)" % v
return v

388
files/helpers/wrappers.py 100644 → 100755
View File

@ -1,200 +1,190 @@
from .get import *
from .alerts import send_notification
from files.helpers.const import *
def get_logged_in_user():
if request.headers.get("Authorization"):
token = request.headers.get("Authorization")
if not token: return None
client = g.db.query(ClientAuth).options(lazyload('*')).filter(ClientAuth.access_token == token).first()
x = (client.user, client) if client else (None, None)
else:
uid = session.get("user_id")
nonce = session.get("login_nonce", 0)
if not uid: x= (None, None)
try:
if g.db: v = g.db.query(User).options(lazyload('*')).filter_by(id=uid).first()
else: v = None
except: v = None
if v and v.agendaposter_expires_utc and v.agendaposter_expires_utc < g.timestamp:
v.agendaposter_expires_utc = 0
v.agendaposter = False
g.db.add(v)
if v and (nonce < v.login_nonce):
x= (None, None)
else:
x=(v, None)
if x[0]: x[0].client=x[1]
return x[0]
def check_ban_evade(v):
if not v or not v.ban_evade or v.admin_level > 0:
return
if random.randint(0,30) < v.ban_evade:
v.ban(reason="permaban evasion")
send_notification(NOTIFICATIONS_ACCOUNT, v, "Your account has been permanently suspended for the following reason:\n\n> permaban evasion")
for post in g.db.query(Submission).options(lazyload('*')).filter_by(author_id=v.id).all():
if post.is_banned:
continue
post.is_banned=True
post.ban_reason="permaban evasion"
g.db.add(post)
ma=ModAction(
kind="ban_post",
user_id=AUTOJANNY_ACCOUNT,
target_submission_id=post.id,
note="permaban evasion"
)
g.db.add(ma)
for comment in g.db.query(Comment).options(lazyload('*')).filter_by(author_id=v.id).all():
if comment.is_banned:
continue
comment.is_banned=True
comment.ban_reason="permaban evasion"
g.db.add(comment)
try:
ma=ModAction(
kind="ban_comment",
user_id=AUTOJANNY_ACCOUNT,
target_comment_id=comment.id,
note="ban evasion"
)
g.db.add(ma)
except: pass
else:
v.ban_evade +=1
g.db.add(v)
g.db.commit()
# Wrappers
def auth_desired(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
check_ban_evade(v)
resp = make_response(f(*args, v=v, **kwargs))
return resp
wrapper.__name__ = f.__name__
return wrapper
def auth_required(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v:
abort(401)
check_ban_evade(v)
g.v = v
resp = make_response(f(*args, v=v, **kwargs))
return resp
wrapper.__name__ = f.__name__
return wrapper
def is_not_banned(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v:
abort(401)
check_ban_evade(v)
if v.is_suspended:
abort(403)
g.v = v
resp = make_response(f(*args, v=v, **kwargs))
return resp
wrapper.__name__ = f.__name__
return wrapper
# this wrapper takes args and is a bit more complicated
def admin_level_required(x):
def wrapper_maker(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v:
abort(401)
if v.admin_level < x:
abort(403)
g.v = v
response = f(*args, v=v, **kwargs)
if isinstance(response, tuple):
resp = make_response(response[0])
else:
resp = make_response(response)
return resp
wrapper.__name__ = f.__name__
return wrapper
return wrapper_maker
def validate_formkey(f):
"""Always use @auth_required or @admin_level_required above @validate_form"""
def wrapper(*args, v, **kwargs):
if not request.headers.get("Authorization"):
submitted_key = request.values.get("formkey", None)
if not submitted_key: abort(401)
elif not v.validate_formkey(submitted_key): abort(401)
return f(*args, v=v, **kwargs)
wrapper.__name__ = f.__name__
from .get import *
from .alerts import send_notification
from files.helpers.const import *
def get_logged_in_user():
if request.headers.get("Authorization"):
token = request.headers.get("Authorization")
if not token: return None
client = g.db.query(ClientAuth).options(lazyload('*')).filter(ClientAuth.access_token == token).first()
x = (client.user, client) if client else (None, None)
else:
uid = session.get("user_id")
nonce = session.get("login_nonce", 0)
if not uid: x= (None, None)
try:
if g.db: v = g.db.query(User).options(lazyload('*')).filter_by(id=uid).first()
else: v = None
except: v = None
if v and v.agendaposter_expires_utc and v.agendaposter_expires_utc < g.timestamp:
v.agendaposter_expires_utc = 0
v.agendaposter = False
g.db.add(v)
if v and (nonce < v.login_nonce):
x= (None, None)
else:
x=(v, None)
if x[0]: x[0].client=x[1]
return x[0]
def check_ban_evade(v):
if not v or not v.ban_evade or v.admin_level > 0:
return
if random.randint(0,30) < v.ban_evade:
v.ban(reason="permaban evasion")
send_notification(NOTIFICATIONS_ACCOUNT, v, "Your account has been permanently suspended for the following reason:\n\n> permaban evasion")
for post in g.db.query(Submission).options(lazyload('*')).filter_by(author_id=v.id).all():
if post.is_banned:
continue
post.is_banned=True
post.ban_reason="permaban evasion"
g.db.add(post)
ma=ModAction(
kind="ban_post",
user_id=AUTOJANNY_ACCOUNT,
target_submission_id=post.id,
note="permaban evasion"
)
g.db.add(ma)
for comment in g.db.query(Comment).options(lazyload('*')).filter_by(author_id=v.id).all():
if comment.is_banned:
continue
comment.is_banned=True
comment.ban_reason="permaban evasion"
g.db.add(comment)
try:
ma=ModAction(
kind="ban_comment",
user_id=AUTOJANNY_ACCOUNT,
target_comment_id=comment.id,
note="ban evasion"
)
g.db.add(ma)
except: pass
else:
v.ban_evade +=1
g.db.add(v)
g.db.commit()
def auth_desired(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
check_ban_evade(v)
resp = make_response(f(*args, v=v, **kwargs))
return resp
wrapper.__name__ = f.__name__
return wrapper
def auth_required(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
check_ban_evade(v)
g.v = v
resp = make_response(f(*args, v=v, **kwargs))
return resp
wrapper.__name__ = f.__name__
return wrapper
def is_not_banned(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
check_ban_evade(v)
if v.is_suspended:
abort(403)
g.v = v
resp = make_response(f(*args, v=v, **kwargs))
return resp
wrapper.__name__ = f.__name__
return wrapper
def admin_level_required(x):
def wrapper_maker(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
if v.admin_level < x: abort(403)
g.v = v
response = f(*args, v=v, **kwargs)
if isinstance(response, tuple): resp = make_response(response[0])
else: resp = make_response(response)
return resp
wrapper.__name__ = f.__name__
return wrapper
return wrapper_maker
def validate_formkey(f):
def wrapper(*args, v, **kwargs):
if not request.headers.get("Authorization"):
submitted_key = request.values.get("formkey", None)
if not submitted_key: abort(401)
elif not v.validate_formkey(submitted_key): abort(401)
return f(*args, v=v, **kwargs)
wrapper.__name__ = f.__name__
return wrapper

176
files/mail/__init__.py 100644 → 100755
View File

@ -1,88 +1,88 @@
from os import environ
import time
from flask import *
from urllib.parse import quote
from files.helpers.security import *
from files.helpers.wrappers import *
from files.classes import *
from files.__main__ import app, mail, limiter
from flask_mail import Message
site = environ.get("DOMAIN").strip()
name = environ.get("SITE_NAME").strip()
def send_mail(to_address, subject, html):
msg = Message(html=html, subject=subject, sender=f"{name}@{site}", recipients=[to_address])
mail.send(msg)
def send_verification_email(user, email=None):
if not email:
email = user.email
url = f"https://{app.config['SERVER_NAME']}/activate"
now = int(time.time())
token = generate_hash(f"{email}+{user.id}+{now}")
params = f"?email={quote(email)}&id={user.id}&time={now}&token={token}"
link = url + params
send_mail(to_address=email,
html=render_template("email/email_verify.html",
action_url=link,
v=user),
subject=f"Validate your {name} account email."
)
@app.post("/verify_email")
@limiter.limit("1/second")
@auth_required
def api_verify_email(v):
send_verification_email(v)
return {"message": "Email has been sent (ETA ~5 minutes)"}
@app.get("/activate")
@auth_desired
def activate(v):
email = request.values.get("email", "")
id = request.values.get("id", "")
timestamp = int(request.values.get("time", "0"))
token = request.values.get("token", "")
if int(time.time()) - timestamp > 3600:
return render_template("message.html", v=v, title="Verification link expired.",
message="That link has expired. Visit your settings to send yourself another verification email."), 410
if not validate_hash(f"{email}+{id}+{timestamp}", token):
abort(403)
user = g.db.query(User).options(lazyload('*')).filter_by(id=id).first()
if not user:
abort(404)
if user.is_activated and user.email == email:
return render_template("message_success.html", v=v,
title="Email already verified.", message="Email already verified."), 404
user.email = email
user.is_activated = True
if not any([b.badge_id == 2 for b in user.badges]):
mail_badge = Badge(user_id=user.id,
badge_id=2)
g.db.add(mail_badge)
g.db.add(user)
g.db.commit()
return render_template("message_success.html", v=v, title="Email verified.", message=f"Your email {email} has been verified. Thank you.")
from os import environ
import time
from flask import *
from urllib.parse import quote
from files.helpers.security import *
from files.helpers.wrappers import *
from files.classes import *
from files.__main__ import app, mail, limiter
from flask_mail import Message
site = environ.get("DOMAIN").strip()
name = environ.get("SITE_NAME").strip()
def send_mail(to_address, subject, html):
msg = Message(html=html, subject=subject, sender=f"{name}@{site}", recipients=[to_address])
mail.send(msg)
def send_verification_email(user, email=None):
if not email:
email = user.email
url = f"https://{app.config['SERVER_NAME']}/activate"
now = int(time.time())
token = generate_hash(f"{email}+{user.id}+{now}")
params = f"?email={quote(email)}&id={user.id}&time={now}&token={token}"
link = url + params
send_mail(to_address=email,
html=render_template("email/email_verify.html",
action_url=link,
v=user),
subject=f"Validate your {name} account email."
)
@app.post("/verify_email")
@limiter.limit("1/second")
@auth_required
def api_verify_email(v):
send_verification_email(v)
return {"message": "Email has been sent (ETA ~5 minutes)"}
@app.get("/activate")
@auth_desired
def activate(v):
email = request.values.get("email", "").strip()
id = request.values.get("id", "").strip()
timestamp = int(request.values.get("time", "0"))
token = request.values.get("token", "").strip()
if int(time.time()) - timestamp > 3600:
return render_template("message.html", v=v, title="Verification link expired.",
message="That link has expired. Visit your settings to send yourself another verification email."), 410
if not validate_hash(f"{email}+{id}+{timestamp}", token):
abort(403)
user = g.db.query(User).options(lazyload('*')).filter_by(id=id).first()
if not user:
abort(404)
if user.is_activated and user.email == email:
return render_template("message_success.html", v=v,
title="Email already verified.", message="Email already verified."), 404
user.email = email
user.is_activated = True
if not any([b.badge_id == 2 for b in user.badges]):
mail_badge = Badge(user_id=user.id,
badge_id=2)
g.db.add(mail_badge)
g.db.add(user)
g.db.commit()
return render_template("message_success.html", v=v, title="Email verified.", message=f"Your email {email} has been verified. Thank you.")

32
files/routes/__init__.py 100644 → 100755
View File

@ -1,17 +1,17 @@
from .admin import *
from .comments import *
from .discord import *
from .errors import *
from .reporting import *
from .front import *
from .login import *
from .oauth import *
from .posts import *
from .search import *
from .settings import *
from .static import *
from .users import *
from .votes import *
from .feeds import *
from .awards import *
from .admin import *
from .comments import *
from .discord import *
from .errors import *
from .reporting import *
from .front import *
from .login import *
from .oauth import *
from .posts import *
from .search import *
from .settings import *
from .static import *
from .users import *
from .votes import *
from .feeds import *
from .awards import *
from .giphy import *

2533
files/routes/admin.py 100644 → 100755

File diff suppressed because it is too large Load Diff

743
files/routes/awards.py 100644 → 100755
View File

@ -1,372 +1,373 @@
from files.__main__ import app, limiter
from files.helpers.wrappers import *
from files.helpers.alerts import *
from files.helpers.get import *
from files.helpers.const import *
from files.classes.award import *
from flask import g, request
@app.get("/shop")
@app.get("/settings/shop")
@auth_required
def shop(v):
if site_name == "Drama":
AWARDS = {
"ban": {
"kind": "ban",
"title": "One-Day Ban",
"description": "Bans the author for a day.",
"icon": "fas fa-gavel",
"color": "text-danger",
"price": 5000
},
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 1000
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 1000
}
}
else:
AWARDS = {
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 1000
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 1000
}
}
query = g.db.query(
User.id, User.username, User.patron, User.namecolor,
AwardRelationship.kind.label('last_award_kind'), func.count(AwardRelationship.id).label('last_award_count')
).filter(AwardRelationship.submission_id==None, AwardRelationship.comment_id==None, User.patron > 0) \
.group_by(User.username, User.patron, User.id, User.namecolor, AwardRelationship.kind) \
.order_by(User.patron.desc(), AwardRelationship.kind.desc()) \
.join(User).filter(User.id == v.id).all()
owned = []
for row in (r._asdict() for r in query):
kind = row['last_award_kind']
if kind in AWARDS.keys():
award = AWARDS[kind]
award["owned_num"] = row['last_award_count']
owned.append(award)
if v.patron:
for val in AWARDS.values():
if v.patron == 1: val["price"] = int(val["price"]*0.90)
elif v.patron == 2: val["price"] = int(val["price"]*0.85)
elif v.patron == 3: val["price"] = int(val["price"]*0.80)
elif v.patron == 4: val["price"] = int(val["price"]*0.75)
else: val["price"] = int(val["price"]*0.70)
return render_template("settings_shop.html", owned=owned, awards=list(AWARDS.values()), v=v)
@app.post("/buy/<award>")
@auth_required
def buy(v, award):
if site_name == "Drama":
AWARDS = {
"ban": {
"kind": "ban",
"title": "One-Day Ban",
"description": "Bans the author for a day.",
"icon": "fas fa-gavel",
"color": "text-danger",
"price": 5000
},
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 1000
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 1000
}
}
else:
AWARDS = {
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 1000
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 1000
}
}
if award not in AWARDS: abort(400)
price = AWARDS[award]["price"]
if v.patron:
if v.patron == 1: price = int(price*0.90)
elif v.patron == 2: price = int(price*0.85)
elif v.patron == 3: price = int(price*0.80)
elif v.patron == 4: price = int(price*0.75)
else: price = int(price*0.70)
if v.coins < price: return {"error": "Not enough coins."}, 400
v.coins -= price
g.db.add(v)
thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id
thing += 1
award = AwardRelationship(id=thing, user_id=v.id, kind=award)
g.db.add(award)
g.db.commit()
return {"message": "Award bought!"}
def banaward_trigger(post=None, comment=None):
author = post.author if post else comment.author
link = f"[this post]({post.permalink})" if post else f"[this comment]({comment.permalink})"
if not author.is_suspended:
author.ban(reason="one-day ban award used", days=1)
send_notification(NOTIFICATIONS_ACCOUNT, author, f"Your account has been suspended for a day for {link}. It sucked and you should feel bad.")
elif author.unban_utc > 0:
author.unban_utc += 24*60*60
g.db.add(author)
send_notification(NOTIFICATIONS_ACCOUNT, author, f"Your account has been suspended for yet another day for {link}. Seriously man?")
ACTIONS = {
"ban": banaward_trigger
}
ALLOW_MULTIPLE = (
"ban",
"shit",
"fireflies"
)
@app.post("/post/<pid>/awards")
@limiter.limit("1/second")
@auth_required
def award_post(pid, v):
if v.is_suspended and v.unban_utc == 0: return {"error": "forbidden."}, 403
kind = request.values.get("kind", "")
if kind not in AWARDS:
return {"error": "That award doesn't exist."}, 404
post_award = g.db.query(AwardRelationship).options(lazyload('*')).filter(
and_(
AwardRelationship.kind == kind,
AwardRelationship.user_id == v.id,
AwardRelationship.submission_id == None,
AwardRelationship.comment_id == None
)
).first()
if not post_award:
return {"error": "You don't have that award."}, 404
post = g.db.query(Submission).options(lazyload('*')).filter_by(id=pid).first()
if not post or post.is_banned or post.deleted_utc > 0:
return {"error": "That post doesn't exist or has been deleted or removed."}, 404
if post.author_id == v.id:
return {"error": "You can't award yourself."}, 403
existing_award = g.db.query(AwardRelationship).options(lazyload('*')).filter(
and_(
AwardRelationship.submission_id == post.id,
AwardRelationship.user_id == v.id,
AwardRelationship.kind == kind
)
).first()
if existing_award and kind not in ALLOW_MULTIPLE:
return {"error": "You can't give that award multiple times to the same post."}, 409
post_award.submission_id = post.id
g.db.add(post_award)
msg = f"@{v.username} has given your [post]({post.permalink}) the {AWARDS[kind]['title']} Award!"
note = request.values.get("note", "")
if note:
msg += f"\n\n> {note}"
send_notification(NOTIFICATIONS_ACCOUNT, post.author, msg)
if kind in ACTIONS: ACTIONS[kind](post=post)
post.author.received_award_count += 1
g.db.add(post.author)
g.db.commit()
if request.referrer and len(request.referrer) > 1: return redirect(request.referrer)
else: return redirect("/")
@app.post("/comment/<cid>/awards")
@limiter.limit("1/second")
@auth_required
def award_comment(cid, v):
if v.is_suspended and v.unban_utc == 0: return {"error": "forbidden"}, 403
kind = request.values.get("kind", "")
if kind not in AWARDS:
return {"error": "That award doesn't exist."}, 404
comment_award = g.db.query(AwardRelationship).options(lazyload('*')).filter(
and_(
AwardRelationship.kind == kind,
AwardRelationship.user_id == v.id,
AwardRelationship.submission_id == None,
AwardRelationship.comment_id == None
)
).first()
if not comment_award:
return {"error": "You don't have that award."}, 404
c = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first()
if not c or c.is_banned or c.deleted_utc > 0:
return {"error": "That comment doesn't exist or has been deleted or removed."}, 404
if c.author_id == v.id:
return {"error": "You can't award yourself."}, 403
existing_award = g.db.query(AwardRelationship).options(lazyload('*')).filter(
and_(
AwardRelationship.comment_id == c.id,
AwardRelationship.user_id == v.id,
AwardRelationship.kind == kind
)
).first()
if existing_award and kind not in ALLOW_MULTIPLE:
return {"error": "You can't give that award multiple times to the same comment."}, 409
comment_award.comment_id = c.id
g.db.add(comment_award)
msg = f"@{v.username} has given your [comment]({c.permalink}) the {AWARDS[kind]['title']} Award!"
note = request.values.get("note", "")
if note:
msg += f"\n\n> {note}"
send_notification(NOTIFICATIONS_ACCOUNT, c.author, msg)
if kind in ACTIONS:
ACTIONS[kind](comment=c)
c.author.received_award_count += 1
g.db.add(c.author)
g.db.commit()
if request.referrer and len(request.referrer) > 1: return redirect(request.referrer)
else: return redirect("/")
@app.get("/admin/user_award")
@auth_required
def admin_userawards_get(v):
if v.admin_level < 6:
abort(403)
return render_template("admin/user_award.html", awards=list(AWARDS.values()), v=v)
@app.post("/admin/user_award")
@limiter.limit("1/second")
@auth_required
@validate_formkey
def admin_userawards_post(v):
if v.admin_level < 6:
abort(403)
try: u = request.values.get("username").strip()
except: abort(404)
u = get_user(u, graceful=False, v=v)
notify_awards = {}
latest = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first()
thing = latest.id
for key, value in request.values.items():
if key not in AWARDS:
continue
if value:
if int(value) > 0:
notify_awards[key] = int(value)
for x in range(int(value)):
thing += 1
award = AwardRelationship(
id=thing,
user_id=u.id,
kind=key
)
g.db.add(award)
text = "You were given the following awards:\n\n"
for key, value in notify_awards.items():
text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n"
send_notification(NOTIFICATIONS_ACCOUNT, u, text)
g.db.commit()
from files.__main__ import app, limiter
from files.helpers.wrappers import *
from files.helpers.alerts import *
from files.helpers.get import *
from files.helpers.const import *
from files.classes.award import *
from flask import g, request
@app.get("/shop")
@app.get("/settings/shop")
@auth_required
def shop(v):
if site_name == "Drama":
AWARDS = {
"ban": {
"kind": "ban",
"title": "One-Day Ban",
"description": "Bans the author for a day.",
"icon": "fas fa-gavel",
"color": "text-danger",
"price": 5000
},
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 500
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 500
}
}
else:
AWARDS = {
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 500
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 500
}
}
query = g.db.query(
User.id, User.username, User.patron, User.namecolor,
AwardRelationship.kind.label('last_award_kind'), func.count(AwardRelationship.id).label('last_award_count')
).filter(AwardRelationship.submission_id==None, AwardRelationship.comment_id==None, User.patron > 0) \
.group_by(User.username, User.patron, User.id, User.namecolor, AwardRelationship.kind) \
.order_by(User.patron.desc(), AwardRelationship.kind.desc()) \
.join(User).filter(User.id == v.id).all()
owned = []
for row in (r._asdict() for r in query):
kind = row['last_award_kind']
if kind in AWARDS.keys():
award = AWARDS[kind]
award["owned_num"] = row['last_award_count']
owned.append(award)
if v.patron:
for val in AWARDS.values():
if v.patron == 1: val["price"] = int(val["price"]*0.90)
elif v.patron == 2: val["price"] = int(val["price"]*0.85)
elif v.patron == 3: val["price"] = int(val["price"]*0.80)
elif v.patron == 4: val["price"] = int(val["price"]*0.75)
else: val["price"] = int(val["price"]*0.70)
return render_template("settings_shop.html", owned=owned, awards=list(AWARDS.values()), v=v)
@app.post("/buy/<award>")
@auth_required
def buy(v, award):
if site_name == "Drama":
AWARDS = {
"ban": {
"kind": "ban",
"title": "One-Day Ban",
"description": "Bans the author for a day.",
"icon": "fas fa-gavel",
"color": "text-danger",
"price": 5000
},
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 500
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 500
}
}
else:
AWARDS = {
"shit": {
"kind": "shit",
"title": "Shit",
"description": "Makes flies swarm a post.",
"icon": "fas fa-poop",
"color": "text-black-50",
"price": 500
},
"fireflies": {
"kind": "fireflies",
"title": "Fireflies",
"description": "Puts stars on the post.",
"icon": "fas fa-sparkles",
"color": "text-warning",
"price": 500
}
}
if award not in AWARDS: abort(400)
price = AWARDS[award]["price"]
if v.patron:
if v.patron == 1: price = int(price*0.90)
elif v.patron == 2: price = int(price*0.85)
elif v.patron == 3: price = int(price*0.80)
elif v.patron == 4: price = int(price*0.75)
else: price = int(price*0.70)
if v.coins < price: return {"error": "Not enough coins."}, 400
v.coins -= price
g.db.add(v)
g.db.flush()
thing = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first().id
thing += 1
award = AwardRelationship(id=thing, user_id=v.id, kind=award)
g.db.add(award)
g.db.commit()
return {"message": "Award bought!"}
def banaward_trigger(post=None, comment=None):
author = post.author if post else comment.author
link = f"[this post]({post.permalink})" if post else f"[this comment]({comment.permalink})"
if not author.is_suspended:
author.ban(reason="one-day ban award used", days=1)
send_notification(NOTIFICATIONS_ACCOUNT, author, f"Your account has been suspended for a day for {link}. It sucked and you should feel bad.")
elif author.unban_utc > 0:
author.unban_utc += 24*60*60
g.db.add(author)
send_notification(NOTIFICATIONS_ACCOUNT, author, f"Your account has been suspended for yet another day for {link}. Seriously man?")
ACTIONS = {
"ban": banaward_trigger
}
ALLOW_MULTIPLE = (
"ban",
"shit",
"fireflies"
)
@app.post("/post/<pid>/awards")
@limiter.limit("1/second")
@auth_required
def award_post(pid, v):
if v.is_suspended and v.unban_utc == 0: return {"error": "forbidden."}, 403
kind = request.values.get("kind", "").strip()
if kind not in AWARDS:
return {"error": "That award doesn't exist."}, 404
post_award = g.db.query(AwardRelationship).options(lazyload('*')).filter(
and_(
AwardRelationship.kind == kind,
AwardRelationship.user_id == v.id,
AwardRelationship.submission_id == None,
AwardRelationship.comment_id == None
)
).first()
if not post_award:
return {"error": "You don't have that award."}, 404
post = g.db.query(Submission).options(lazyload('*')).filter_by(id=pid).first()
if not post or post.is_banned or post.deleted_utc > 0:
return {"error": "That post doesn't exist or has been deleted or removed."}, 404
if post.author_id == v.id:
return {"error": "You can't award yourself."}, 403
existing_award = g.db.query(AwardRelationship).options(lazyload('*')).filter(
and_(
AwardRelationship.submission_id == post.id,
AwardRelationship.user_id == v.id,
AwardRelationship.kind == kind
)
).first()
if existing_award and kind not in ALLOW_MULTIPLE:
return {"error": "You can't give that award multiple times to the same post."}, 409
post_award.submission_id = post.id
g.db.add(post_award)
msg = f"@{v.username} has given your [post]({post.permalink}) the {AWARDS[kind]['title']} Award!"
note = request.values.get("note", "").strip()
if note:
msg += f"\n\n> {note}"
send_notification(NOTIFICATIONS_ACCOUNT, post.author, msg)
if kind in ACTIONS: ACTIONS[kind](post=post)
post.author.received_award_count += 1
g.db.add(post.author)
g.db.commit()
if request.referrer and len(request.referrer) > 1: return redirect(request.referrer)
else: return redirect("/")
@app.post("/comment/<cid>/awards")
@limiter.limit("1/second")
@auth_required
def award_comment(cid, v):
if v.is_suspended and v.unban_utc == 0: return {"error": "forbidden"}, 403
kind = request.values.get("kind", "").strip()
if kind not in AWARDS:
return {"error": "That award doesn't exist."}, 404
comment_award = g.db.query(AwardRelationship).options(lazyload('*')).filter(
and_(
AwardRelationship.kind == kind,
AwardRelationship.user_id == v.id,
AwardRelationship.submission_id == None,
AwardRelationship.comment_id == None
)
).first()
if not comment_award:
return {"error": "You don't have that award."}, 404
c = g.db.query(Comment).options(lazyload('*')).filter_by(id=cid).first()
if not c or c.is_banned or c.deleted_utc > 0:
return {"error": "That comment doesn't exist or has been deleted or removed."}, 404
if c.author_id == v.id:
return {"error": "You can't award yourself."}, 403
existing_award = g.db.query(AwardRelationship).options(lazyload('*')).filter(
and_(
AwardRelationship.comment_id == c.id,
AwardRelationship.user_id == v.id,
AwardRelationship.kind == kind
)
).first()
if existing_award and kind not in ALLOW_MULTIPLE:
return {"error": "You can't give that award multiple times to the same comment."}, 409
comment_award.comment_id = c.id
g.db.add(comment_award)
msg = f"@{v.username} has given your [comment]({c.permalink}) the {AWARDS[kind]['title']} Award!"
note = request.values.get("note", "").strip()
if note:
msg += f"\n\n> {note}"
send_notification(NOTIFICATIONS_ACCOUNT, c.author, msg)
if kind in ACTIONS:
ACTIONS[kind](comment=c)
c.author.received_award_count += 1
g.db.add(c.author)
g.db.commit()
if request.referrer and len(request.referrer) > 1: return redirect(request.referrer)
else: return redirect("/")
@app.get("/admin/user_award")
@auth_required
def admin_userawards_get(v):
if v.admin_level < 6:
abort(403)
return render_template("admin/user_award.html", awards=list(AWARDS.values()), v=v)
@app.post("/admin/user_award")
@limiter.limit("1/second")
@auth_required
@validate_formkey
def admin_userawards_post(v):
if v.admin_level < 6:
abort(403)
try: u = request.values.get("username").strip()
except: abort(404)
u = get_user(u, graceful=False, v=v)
notify_awards = {}
latest = g.db.query(AwardRelationship).order_by(AwardRelationship.id.desc()).first()
thing = latest.id
for key, value in request.values.items():
if key not in AWARDS:
continue
if value:
if int(value) > 0:
notify_awards[key] = int(value)
for x in range(int(value)):
thing += 1
award = AwardRelationship(
id=thing,
user_id=u.id,
kind=key
)
g.db.add(award)
text = "You were given the following awards:\n\n"
for key, value in notify_awards.items():
text += f" - **{value}** {AWARDS[key]['title']} {'Awards' if value != 1 else 'Award'}\n"
send_notification(NOTIFICATIONS_ACCOUNT, u, text)
g.db.commit()
return render_template("admin/user_award.html", awards=list(AWARDS.values()), v=v)

1731
files/routes/comments.py 100644 → 100755

File diff suppressed because it is too large Load Diff

290
files/routes/discord.py 100644 → 100755
View File

@ -1,146 +1,146 @@
from files.helpers.wrappers import *
from files.helpers.security import *
from files.helpers.discord import add_role
from files.__main__ import app
import requests
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN").strip()
COINS_NAME = environ.get("COINS_NAME").strip()
DISCORD_ENDPOINT = "https://discordapp.com/api/v6"
WELCOME_CHANNEL="846509313941700618"
@app.get("/discord")
@auth_required
def join_discord(v):
if v.is_suspended != 0: return "You're banned"
if 'rdrama' in request.host and v.admin_level == 0 and v.patron == 0 and v.truecoins < 150: return f"You must earn 150 {COINS_NAME} before entering the Discord server. You earn {COINS_NAME} by making posts/comments and getting upvoted."
if v.shadowbanned or v.agendaposter: return ""
now=int(time.time())
state=generate_hash(f"{now}+{v.id}+discord")
state=f"{now}.{state}"
return redirect(f"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri=https%3A%2F%2F{app.config['SERVER_NAME']}%2Fdiscord_redirect&response_type=code&scope=identify%20guilds.join&state={state}")
@app.get("/discord_redirect")
@auth_required
def discord_redirect(v):
now=int(time.time())
state=request.values.get('state','').split('.')
timestamp=state[0]
state=state[1]
if int(timestamp) < now-600:
abort(400)
if not validate_hash(f"{timestamp}+{v.id}+discord", state):
abort(400)
code = request.values.get("code","")
if not code:
abort(400)
data={
"client_id":CLIENT_ID,
'client_secret': CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': f"https://{app.config['SERVER_NAME']}/discord_redirect",
'scope': 'identify guilds.join'
}
headers={
'Content-Type': 'application/x-www-form-urlencoded'
}
url="https://discord.com/api/oauth2/token"
x=requests.post(url, headers=headers, data=data)
x=x.json()
try:
token=x["access_token"]
except KeyError:
abort(403)
url="https://discord.com/api/users/@me"
headers={
'Authorization': f"Bearer {token}"
}
x=requests.get(url, headers=headers)
x=x.json()
headers={
'Authorization': f"Bot {BOT_TOKEN}",
'Content-Type': "application/json"
}
if v.discord_id and v.discord_id != x['id']:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
requests.delete(url, headers=headers)
if g.db.query(User).options(lazyload('*')).filter(User.id!=v.id, User.discord_id==x["id"]).first():
return render_template("message.html", title="Discord account already linked.", error="That Discord account is already in use by another user.", v=v)
v.discord_id=x["id"]
g.db.add(v)
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{x['id']}"
name=v.username
data={
"access_token":token,
"nick":name,
}
x=requests.put(url, headers=headers, json=data)
if x.status_code in [201, 204]:
if v.id == 1:
add_role(v, "shrigma")
time.sleep(0.1)
if v.admin_level > 0: add_role(v, "admin")
time.sleep(0.1)
add_role(v, "linked")
if v.patron:
time.sleep(0.1)
add_role(v, str(v.patron))
else:
return x.json()
if x.status_code==204:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
data={
"nick": name
}
requests.patch(url, headers=headers, json=data)
g.db.commit()
from files.helpers.wrappers import *
from files.helpers.security import *
from files.helpers.discord import add_role
from files.__main__ import app
import requests
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN").strip()
COINS_NAME = environ.get("COINS_NAME").strip()
DISCORD_ENDPOINT = "https://discordapp.com/api/v6"
WELCOME_CHANNEL="846509313941700618"
@app.get("/discord")
@auth_required
def join_discord(v):
if v.is_suspended != 0: return "You're banned"
if 'rama' in request.host and v.admin_level == 0 and v.patron == 0 and v.truecoins < 150: return f"You must earn 150 {COINS_NAME} before entering the Discord server. You earn {COINS_NAME} by making posts/comments and getting upvoted."
if v.shadowbanned or v.agendaposter: return ""
now=int(time.time())
state=generate_hash(f"{now}+{v.id}+discord")
state=f"{now}.{state}"
return redirect(f"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri=https%3A%2F%2F{app.config['SERVER_NAME']}%2Fdiscord_redirect&response_type=code&scope=identify%20guilds.join&state={state}")
@app.get("/discord_redirect")
@auth_required
def discord_redirect(v):
now=int(time.time())
state=request.values.get('state','').split('.')
timestamp=state[0]
state=state[1]
if int(timestamp) < now-600:
abort(400)
if not validate_hash(f"{timestamp}+{v.id}+discord", state):
abort(400)
code = request.values.get("code","")
if not code:
abort(400)
data={
"client_id":CLIENT_ID,
'client_secret': CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': f"https://{app.config['SERVER_NAME']}/discord_redirect",
'scope': 'identify guilds.join'
}
headers={
'Content-Type': 'application/x-www-form-urlencoded'
}
url="https://discord.com/api/oauth2/token"
x=requests.post(url, headers=headers, data=data)
x=x.json()
try:
token=x["access_token"]
except KeyError:
abort(403)
url="https://discord.com/api/users/@me"
headers={
'Authorization': f"Bearer {token}"
}
x=requests.get(url, headers=headers)
x=x.json()
headers={
'Authorization': f"Bot {BOT_TOKEN}",
'Content-Type': "application/json"
}
if v.discord_id and v.discord_id != x['id']:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
requests.delete(url, headers=headers)
if g.db.query(User).options(lazyload('*')).filter(User.id!=v.id, User.discord_id==x["id"]).first():
return render_template("message.html", title="Discord account already linked.", error="That Discord account is already in use by another user.", v=v)
v.discord_id=x["id"]
g.db.add(v)
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{x['id']}"
name=v.username
data={
"access_token":token,
"nick":name,
}
x=requests.put(url, headers=headers, json=data)
if x.status_code in [201, 204]:
if v.id == 1:
add_role(v, "shrigma")
time.sleep(0.1)
if v.admin_level > 0: add_role(v, "admin")
time.sleep(0.1)
add_role(v, "linked")
if v.patron:
time.sleep(0.1)
add_role(v, str(v.patron))
else:
return x.json()
if x.status_code==204:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
data={
"nick": name
}
requests.patch(url, headers=headers, json=data)
g.db.commit()
return redirect(f"https://discord.com/channels/{SERVER_ID}/{WELCOME_CHANNEL}")

168
files/routes/errors.py 100644 → 100755
View File

@ -1,84 +1,84 @@
import jinja2.exceptions
from files.helpers.wrappers import *
from files.helpers.session import *
from flask import *
from urllib.parse import quote, urlencode
import time
from files.__main__ import app, limiter
# Errors
@app.errorhandler(400)
@auth_desired
def error_400(e, v):
if request.headers.get("Authorization"): return {"error": "400 Bad Request"}, 400
else: return render_template('errors/400.html', v=v), 400
@app.errorhandler(401)
def error_401(e):
path = request.path
qs = urlencode(dict(request.values))
argval = quote(f"{path}?{qs}", safe='')
output = f"/login?redirect={argval}"
if request.headers.get("Authorization"): return {"error": "401 Not Authorized"}, 401
else: return redirect(output)
@app.errorhandler(403)
@auth_desired
def error_403(e, v):
if request.headers.get("Authorization"): return {"error": "403 Forbidden"}, 403
else: return render_template('errors/403.html', v=v), 403
@app.errorhandler(404)
@auth_desired
def error_404(e, v):
if request.headers.get("Authorization"): return {"error": "404 Not Found"}, 404
else: return render_template('errors/404.html', v=v), 404
@app.errorhandler(405)
@auth_desired
def error_405(e, v):
if request.headers.get("Authorization"): return {"error": "405 Method Not Allowed"}, 405
else: return render_template('errors/405.html', v=v), 405
@app.errorhandler(429)
@auth_desired
def error_429(e, v):
if request.headers.get("Authorization"): return {"error": "429 Too Many Requests"}, 429
else: return render_template('errors/429.html', v=v), 429
@app.errorhandler(500)
@auth_desired
def error_500(e, v):
g.db.rollback()
if request.headers.get("Authorization"): return {"error": "500 Internal Server Error"}, 500
else: return render_template('errors/500.html', v=v), 500
@app.post("/allow_nsfw")
def allow_nsfw():
session["over_18"] = int(time.time()) + 3600
return redirect(request.values.get("redir", "/"))
@app.get("/error/<error>")
@auth_desired
def error_all_preview(error, v):
try:
return render_template(f"errors/{error}.html", v=v)
except jinja2.exceptions.TemplateNotFound:
abort(400)
import jinja2.exceptions
from files.helpers.wrappers import *
from files.helpers.session import *
from flask import *
from urllib.parse import quote, urlencode
import time
from files.__main__ import app, limiter
# Errors
@app.errorhandler(400)
@auth_desired
def error_400(e, v):
if request.headers.get("Authorization"): return {"error": "400 Bad Request"}, 400
else: return render_template('errors/400.html', v=v), 400
@app.errorhandler(401)
def error_401(e):
path = request.path
qs = urlencode(dict(request.values))
argval = quote(f"{path}?{qs}", safe='')
output = f"/login?redirect={argval}"
if request.headers.get("Authorization"): return {"error": "401 Not Authorized"}, 401
else: return redirect(output)
@app.errorhandler(403)
@auth_desired
def error_403(e, v):
if request.headers.get("Authorization"): return {"error": "403 Forbidden"}, 403
else: return render_template('errors/403.html', v=v), 403
@app.errorhandler(404)
@auth_desired
def error_404(e, v):
if request.headers.get("Authorization"): return {"error": "404 Not Found"}, 404
else: return render_template('errors/404.html', v=v), 404
@app.errorhandler(405)
@auth_desired
def error_405(e, v):
if request.headers.get("Authorization"): return {"error": "405 Method Not Allowed"}, 405
else: return render_template('errors/405.html', v=v), 405
@app.errorhandler(429)
@auth_desired
def error_429(e, v):
if request.headers.get("Authorization"): return {"error": "429 Too Many Requests"}, 429
else: return render_template('errors/429.html', v=v), 429
@app.errorhandler(500)
@auth_desired
def error_500(e, v):
g.db.rollback()
if request.headers.get("Authorization"): return {"error": "500 Internal Server Error"}, 500
else: return render_template('errors/500.html', v=v), 500
@app.post("/allow_nsfw")
def allow_nsfw():
session["over_18"] = int(time.time()) + 3600
return redirect(request.values.get("redir", "/"))
@app.get("/error/<error>")
@auth_desired
def error_all_preview(error, v):
try:
return render_template(f"errors/{error}.html", v=v)
except jinja2.exceptions.TemplateNotFound:
abort(400)

130
files/routes/feeds.py 100644 → 100755
View File

@ -1,66 +1,66 @@
import html
from .front import frontlist
from datetime import datetime
from files.helpers.jinja2 import full_link
from files.helpers.get import *
from yattag import Doc
from files.__main__ import app
@app.get('/rss/<sort>/<t>')
def feeds_user(sort='hot', t='all'):
page = int(request.values.get("page", 1))
ids, next_exists = frontlist(
sort=sort,
page=page,
t=t,
v=None,
)
posts = get_posts(ids)
domain = environ.get("DOMAIN").strip()
doc, tag, text = Doc().tagtext()
with tag("feed", ("xmlns:media","http://search.yahoo.com/mrss/"), xmlns="http://www.w3.org/2005/Atom",):
with tag("title", type="text"):
text(f"{sort} posts from {domain}")
doc.stag("link", href=request.url)
doc.stag("link", href=request.url_root)
for post in posts:
with tag("entry", ("xml:base", request.url)):
with tag("title", type="text"):
text(post.title)
with tag("id"):
text(post.fullname)
if (post.edited_utc > 0):
with tag("updated"):
text(datetime.utcfromtimestamp(post.edited_utc).isoformat())
with tag("published"):
text(datetime.utcfromtimestamp(post.created_utc).isoformat())
with tag("author"):
with tag("name"):
text(post.author.username)
with tag("uri"):
text(f'https://{site}/@{post.author.username}')
doc.stag("link", href=full_link(post.permalink))
image_url = post.thumb_url or post.embed_url or post.url
doc.stag("media:thumbnail", url=image_url)
if len(post.body_html) > 0:
with tag("content", type="html"):
doc.cdata(f'<img loading="lazy" src={image_url}/><br/>{post.body_html}')
import html
from .front import frontlist
from datetime import datetime
from files.helpers.jinja2 import full_link
from files.helpers.get import *
from yattag import Doc
from files.__main__ import app
@app.get('/rss/<sort>/<t>')
def feeds_user(sort='hot', t='all'):
page = int(request.values.get("page", 1))
ids, next_exists = frontlist(
sort=sort,
page=page,
t=t,
v=None,
)
posts = get_posts(ids)
domain = environ.get("DOMAIN").strip()
doc, tag, text = Doc().tagtext()
with tag("feed", ("xmlns:media","http://search.yahoo.com/mrss/"), xmlns="http://www.w3.org/2005/Atom",):
with tag("title", type="text"):
text(f"{sort} posts from {domain}")
doc.stag("link", href=request.url)
doc.stag("link", href=request.url_root)
for post in posts:
with tag("entry", ("xml:base", request.url)):
with tag("title", type="text"):
text(post.title)
with tag("id"):
text(post.fullname)
if (post.edited_utc > 0):
with tag("updated"):
text(datetime.utcfromtimestamp(post.edited_utc).isoformat())
with tag("published"):
text(datetime.utcfromtimestamp(post.created_utc).isoformat())
with tag("author"):
with tag("name"):
text(post.author.username)
with tag("uri"):
text(f'https://{site}/@{post.author.username}')
doc.stag("link", href=full_link(post.permalink))
image_url = post.thumb_url or post.embed_url or post.url
doc.stag("media:thumbnail", url=image_url)
if len(post.body_html) > 0:
with tag("content", type="html"):
doc.cdata(f'<img loading="lazy" src={image_url}/><br/>{post.body_html}')
return Response( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+ doc.getvalue(), mimetype="application/xml")

794
files/routes/front.py 100644 → 100755
View File

@ -1,396 +1,398 @@
from files.helpers.wrappers import *
from files.helpers.get import *
from files.__main__ import app, cache
from files.classes.submission import Submission
defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip()
@app.get("/post/")
def slash_post():
return redirect("/")
@app.get("/notifications")
@auth_required
def notifications(v):
try: page = int(request.values.get('page', 1))
except: page = 1
messages = request.values.get('messages', False)
modmail = request.values.get('modmail', False)
posts = request.values.get('posts', False)
if modmail and v.admin_level == 6:
comments = g.db.query(Comment).filter(Comment.sentto==0).order_by(Comment.created_utc.desc()).offset(25*(page-1)).limit(26).all()
next_exists = (len(comments) > 25)
comments = comments[:25]
elif messages:
comments = g.db.query(Comment).filter(or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None).order_by(Comment.created_utc.desc(), not_(Comment.child_comments.any())).offset(25*(page-1)).limit(26).all()
next_exists = (len(comments) > 25)
comments = comments[:25]
elif posts:
notifications = v.notifications.join(Notification.comment).filter(Comment.author_id == AUTOJANNY_ACCOUNT).order_by(Notification.id.desc()).offset(25 * (page - 1)).limit(100).all()
comments = []
for index, x in enumerate(notifications):
c = x.comment
if x.read and index > 26: break
elif not x.read:
c.unread = True
x.read = True
g.db.add(x)
comments.append(c)
g.db.commit()
next_exists = (len(comments) > 25)
listing = comments[:25]
else:
notifications = v.notifications.join(Notification.comment).filter(
Comment.is_banned == False,
Comment.deleted_utc == 0,
Comment.author_id != AUTOJANNY_ACCOUNT,
).order_by(Notification.id.desc()).offset(50 * (page - 1)).limit(51).all()
next_exists = (len(notifications) > 50)
notifications = notifications[:50]
cids = [x.comment_id for x in notifications]
comments = get_comments(cids, v=v, load_parent=True)
i = 0
for x in notifications:
try:
if not x.read:
comments[i].unread = True
x.read = True
g.db.add(x)
except: continue
i += 1
g.db.commit()
if not posts:
listing = []
for c in comments:
c.is_blocked = False
c.is_blocking = False
if c.parent_submission and c.parent_comment and c.parent_comment.author_id == v.id:
c.replies = []
while c.parent_comment and c.parent_comment.author_id == v.id:
parent = c.parent_comment
if c not in parent.replies2:
parent.replies2 = parent.replies2 + [c]
parent.replies = parent.replies2
c = parent
if c not in listing:
listing.append(c)
c.replies = c.replies2
elif c.parent_submission:
c.replies = []
if c not in listing:
listing.append(c)
else:
if c.parent_comment:
while c.level > 1:
c = c.parent_comment
if c not in listing:
listing.append(c)
return render_template("notifications.html",
v=v,
notifications=listing,
next_exists=next_exists,
page=page,
standalone=True,
render_replies=True
)
@app.get("/")
@app.get("/logged_out")
@auth_desired
def front_all(v):
if not v and request.path == "/" and not request.headers.get("Authorization"): return redirect(f"/logged_out{request.full_path}")
if v and request.path.startswith('/logged_out'): v = None
try: page = int(request.values.get("page") or 1)
except: abort(400)
page = max(page, 1)
if v:
defaultsorting = v.defaultsorting
defaulttime = v.defaulttime
else:
defaultsorting = "hot"
defaulttime = defaulttimefilter
sort=request.values.get("sort", defaultsorting)
t=request.values.get('t', defaulttime)
ids, next_exists = frontlist(sort=sort,
page=page,
t=t,
v=v,
filter_words=v.filter_words if v else [],
)
posts = get_posts(ids, v=v)
if v and v.hidevotedon: posts = [x for x in posts if not hasattr(x, 'voted') or not x.voted]
if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists}
else: return render_template("home.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page)
@cache.memoize(timeout=86400)
def frontlist(v=None, sort="hot", page=1, t="all", ids_only=True, filter_words=''):
posts = g.db.query(Submission.id).options(lazyload('*'))
if 'rdrama' in request.host and sort == "hot":
cutoff = int(time.time()) - 86400
posts = posts.filter(Submission.created_utc >= cutoff)
elif t != 'all':
now = int(time.time())
if t == 'hour': cutoff = now - 3600
elif t == 'week': cutoff = now - 604800
elif t == 'month': cutoff = now - 2592000
elif t == 'year': cutoff = now - 31536000
else: cutoff = now - 86400
posts = posts.filter(Submission.created_utc >= cutoff)
posts = posts.filter_by(is_banned=False, stickied=None, private=False, deleted_utc = 0)
if v and v.admin_level == 0:
blocking = [x[0] for x in g.db.query(
UserBlock.target_id).filter_by(
user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(
UserBlock.user_id).filter_by(
target_id=v.id).all()]
posts = posts.filter(
Submission.author_id.notin_(blocking),
Submission.author_id.notin_(blocked)
)
if not (v and v.changelogsub):
posts=posts.filter(not_(Submission.title.ilike(f'[changelog]%')))
if v and filter_words:
for word in filter_words:
posts=posts.filter(not_(Submission.title.ilike(f'%{word}%')))
if not (v and v.shadowbanned):
shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()]
posts = posts.filter(Submission.author_id.notin_(shadowbanned))
if sort == "hot":
ti = int(time.time()) + 3600
posts = posts.order_by(-1000000*(Submission.upvotes - Submission.downvotes + 1 + Submission.comment_count/5)/(func.power(((ti - Submission.created_utc)/1000), 1.35)))
elif sort == "new":
posts = posts.order_by(Submission.created_utc.desc())
elif sort == "old":
posts = posts.order_by(Submission.created_utc.asc())
elif sort == "controversial":
posts = posts.order_by(-1 * Submission.upvotes * (Submission.downvotes+1))
elif sort == "top":
posts = posts.order_by(Submission.downvotes - Submission.upvotes)
elif sort == "bottom":
posts = posts.order_by(Submission.upvotes - Submission.downvotes)
elif sort == "comments":
posts = posts.order_by(Submission.comment_count.desc())
if v:
if v.agendaposter: size = 5
else: size = v.frontsize
else: size = 25
posts = posts.offset(size * (page - 1)).limit(size+1).all()
next_exists = (len(posts) > size)
posts = posts[:size]
if page == 1: posts = g.db.query(Submission.id).options(lazyload('*')).filter(Submission.stickied != None).all() + posts
if ids_only: posts = [x[0] for x in posts]
return posts, next_exists
@app.get("/changelog")
@auth_desired
def changelog(v):
page = int(request.values.get("page") or 1)
page = max(page, 1)
sort=request.values.get("sort", "new")
t=request.values.get('t', "all")
ids = changeloglist(sort=sort,
page=page,
t=t,
v=v,
)
next_exists = (len(ids) > 25)
ids = ids[:25]
posts = get_posts(ids, v=v)
if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists}
else: return render_template("changelog.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page)
@cache.memoize(timeout=86400)
def changeloglist(v=None, sort="new", page=1 ,t="all"):
posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False, private=False,).filter(Submission.deleted_utc == 0)
if v and v.admin_level == 0:
blocking = [x[0] for x in g.db.query(
UserBlock.target_id).filter_by(
user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(
UserBlock.user_id).filter_by(
target_id=v.id).all()]
posts = posts.filter(
Submission.author_id.notin_(blocking),
Submission.author_id.notin_(blocked)
)
admins = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.admin_level == 6).all()]
posts = posts.filter(Submission.title.ilike('_changelog%'), Submission.author_id.in_(admins))
if t != 'all':
cutoff = 0
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
posts = posts.filter(Submission.created_utc >= cutoff)
if sort == "new":
posts = posts.order_by(Submission.created_utc.desc())
elif sort == "old":
posts = posts.order_by(Submission.created_utc.asc())
elif sort == "controversial":
posts = posts.order_by(-1 * Submission.upvotes * (Submission.downvotes+1))
elif sort == "top":
posts = posts.order_by(Submission.downvotes - Submission.upvotes)
elif sort == "bottom":
posts = posts.order_by(Submission.upvotes - Submission.downvotes)
elif sort == "comments":
posts = posts.order_by(Submission.comment_count.desc())
posts = posts.offset(25 * (page - 1)).limit(26).all()
return [x[0] for x in posts]
@app.get("/random")
@auth_desired
def random_post(v):
x = g.db.query(Submission).options(lazyload('*')).filter(Submission.deleted_utc == 0, Submission.is_banned == False)
total = x.count()
n = random.randint(1, total - 2)
post = x.offset(n).limit(1).first()
return redirect(f"/post/{post.id}")
@cache.memoize(timeout=86400)
def comment_idlist(page=1, v=None, nsfw=False, sort="new", t="all"):
posts = g.db.query(Submission).options(lazyload('*'))
cc_idlist = [x[0] for x in g.db.query(Submission.id).options(lazyload('*')).filter(Submission.club == True).all()]
posts = posts.subquery()
comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.parent_submission.notin_(cc_idlist))
if v and v.admin_level <= 3:
blocking = [x[0] for x in g.db.query(
UserBlock.target_id).filter_by(
user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(
UserBlock.user_id).filter_by(
target_id=v.id).all()]
comments = comments.filter(
Comment.author_id.notin_(blocking),
Comment.author_id.notin_(blocked)
)
if not v or not v.admin_level >= 3:
comments = comments.filter_by(is_banned=False).filter(Comment.deleted_utc == 0)
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
comments = comments.filter(Comment.created_utc >= cutoff)
if sort == "new":
comments = comments.order_by(Comment.created_utc.desc())
elif sort == "old":
comments = comments.order_by(Comment.created_utc.asc())
elif sort == "controversial":
comments = comments.order_by(-1 * Comment.upvotes * (Comment.downvotes+1))
elif sort == "top":
comments = comments.order_by(Comment.downvotes - Comment.upvotes)
elif sort == "bottom":
comments = comments.order_by(Comment.upvotes - Comment.downvotes)
comments = comments.offset(25 * (page - 1)).limit(26).all()
return [x[0] for x in comments]
@app.get("/comments")
@auth_desired
def all_comments(v):
page = int(request.values.get("page", 1))
sort=request.values.get("sort", "new")
t=request.values.get("t", defaulttimefilter)
idlist = comment_idlist(v=v,
page=page,
sort=sort,
t=t,
)
comments = get_comments(idlist, v=v)
next_exists = len(idlist) > 25
idlist = idlist[:25]
if request.headers.get("Authorization"): return {"data": [x.json for x in comments]}
else: return render_template("home_comments.html", v=v, sort=sort, t=t, page=page, comments=comments, standalone=True, next_exists=next_exists)
from files.helpers.wrappers import *
from files.helpers.get import *
from files.__main__ import app, cache
from files.classes.submission import Submission
defaulttimefilter = environ.get("DEFAULT_TIME_FILTER", "all").strip()
@app.get("/post/")
def slash_post():
return redirect("/")
@app.get("/notifications")
@auth_required
def notifications(v):
try: page = int(request.values.get('page', 1))
except: page = 1
messages = request.values.get('messages', False)
modmail = request.values.get('modmail', False)
posts = request.values.get('posts', False)
if modmail and v.admin_level == 6:
comments = g.db.query(Comment).filter(Comment.sentto==0).order_by(Comment.created_utc.desc()).offset(25*(page-1)).limit(26).all()
next_exists = (len(comments) > 25)
comments = comments[:25]
elif messages:
comments = g.db.query(Comment).filter(or_(Comment.author_id==v.id, Comment.sentto==v.id), Comment.parent_submission == None).order_by(Comment.created_utc.desc(), not_(Comment.child_comments.any())).offset(25*(page-1)).limit(26).all()
next_exists = (len(comments) > 25)
comments = comments[:25]
elif posts:
notifications = v.notifications.join(Notification.comment).filter(Comment.author_id == AUTOJANNY_ACCOUNT).order_by(Notification.id.desc()).offset(25 * (page - 1)).limit(101).all()
listing = []
for index, x in enumerate(notifications[:100]):
c = x.comment
if x.read and index > 25: break
elif not x.read:
x.read = True
c.unread = True
g.db.add(x)
listing.append(c)
g.db.commit()
next_exists = (len(notifications) > len(listing))
else:
notifications = v.notifications.join(Notification.comment).filter(Comment.author_id != AUTOJANNY_ACCOUNT).order_by(Notification.id.desc()).offset(25 * (page - 1)).limit(101).all()
listing = []
for index, x in enumerate(notifications[:100]):
c = x.comment
if x.read and index > 25: break
elif not x.read:
x.read = True
g.db.add(x)
listing.append(c.id)
g.db.commit()
comments = get_comments(listing, v=v, load_parent=True)
next_exists = (len(notifications) > len(comments))
if not posts:
listing = []
for c in comments:
c.is_blocked = False
c.is_blocking = False
if c.parent_submission and c.parent_comment and c.parent_comment.author_id == v.id:
c.replies = []
while c.parent_comment and c.parent_comment.author_id == v.id:
parent = c.parent_comment
if c not in parent.replies2:
parent.replies2 = parent.replies2 + [c]
parent.replies = parent.replies2
c = parent
if c not in listing:
listing.append(c)
c.replies = c.replies2
elif c.parent_submission:
c.replies = []
if c not in listing:
listing.append(c)
else:
if c.parent_comment:
while c.level > 1:
c = c.parent_comment
if c not in listing:
listing.append(c)
return render_template("notifications.html",
v=v,
notifications=listing,
next_exists=next_exists,
page=page,
standalone=True,
render_replies=True
)
@app.get("/")
@app.get("/logged_out")
@auth_desired
def front_all(v):
if not v and request.path == "/" and not request.headers.get("Authorization"): return redirect(f"/logged_out{request.full_path}")
if v and request.path.startswith('/logged_out'): v = None
try: page = max(int(request.values.get("page", 1)), 1)
except: abort(400)
if v:
defaultsorting = v.defaultsorting
defaulttime = v.defaulttime
else:
defaultsorting = "hot"
defaulttime = defaulttimefilter
sort=request.values.get("sort", defaultsorting)
t=request.values.get('t', defaulttime)
ids, next_exists = frontlist(sort=sort,
page=page,
t=t,
v=v,
filter_words=v.filter_words if v else [],
gt=int(request.values.get("utc_greater_than", 0)),
lt=int(request.values.get("utc_less_than", 0)),
)
posts = get_posts(ids, v=v)
if v and v.hidevotedon: posts = [x for x in posts if not hasattr(x, 'voted') or not x.voted]
if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists}
else: return render_template("home.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page)
@cache.memoize(timeout=86400)
def frontlist(v=None, sort="hot", page=1, t="all", ids_only=True, filter_words='', gt=None, lt=None):
posts = g.db.query(Submission.id).options(lazyload('*'))
if 'rama' in request.host and sort == "hot":
cutoff = int(time.time()) - 86400
posts = posts.filter(Submission.created_utc >= cutoff)
elif t != 'all':
now = int(time.time())
if t == 'hour': cutoff = now - 3600
elif t == 'week': cutoff = now - 604800
elif t == 'month': cutoff = now - 2592000
elif t == 'year': cutoff = now - 31536000
else: cutoff = now - 86400
posts = posts.filter(Submission.created_utc >= cutoff)
posts = posts.filter_by(is_banned=False, stickied=None, private=False, deleted_utc = 0)
if v and v.admin_level == 0:
blocking = [x[0] for x in g.db.query(
UserBlock.target_id).filter_by(
user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(
UserBlock.user_id).filter_by(
target_id=v.id).all()]
posts = posts.filter(
Submission.author_id.notin_(blocking),
Submission.author_id.notin_(blocked)
)
if not (v and v.changelogsub):
posts=posts.filter(not_(Submission.title.ilike(f'[changelog]%')))
if v and filter_words:
for word in filter_words:
posts=posts.filter(not_(Submission.title.ilike(f'%{word}%')))
if gt: posts = posts.filter(Submission.created_utc > gt)
if lt: posts = posts.filter(Submission.created_utc < lt)
if not (v and v.shadowbanned):
shadowbanned = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.shadowbanned != None).all()]
posts = posts.filter(Submission.author_id.notin_(shadowbanned))
if sort == "hot":
ti = int(time.time()) + 3600
posts = posts.order_by(-1000000*(Submission.upvotes + Submission.downvotes + 1 + Submission.comment_count/5)/(func.power(((ti - Submission.created_utc)/1000), 1.35)))
elif sort == "new":
posts = posts.order_by(Submission.created_utc.desc())
elif sort == "old":
posts = posts.order_by(Submission.created_utc.asc())
elif sort == "controversial":
posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes)
elif sort == "top":
posts = posts.order_by(Submission.downvotes - Submission.upvotes)
elif sort == "bottom":
posts = posts.order_by(Submission.upvotes - Submission.downvotes)
elif sort == "comments":
posts = posts.order_by(Submission.comment_count.desc())
if v:
if v.agendaposter: size = 5
else: size = v.frontsize
else: size = 25
posts = posts.offset(size * (page - 1)).limit(size+1).all()
next_exists = (len(posts) > size)
posts = posts[:size]
pins = g.db.query(Submission.id).options(lazyload('*')).filter(Submission.stickied != None)
if v and v.admin_level == 0:
blocking = [x[0] for x in g.db.query(UserBlock.target_id).filter_by(user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(UserBlock.user_id).filter_by(target_id=v.id).all()]
pins = pins.filter(Submission.author_id.notin_(blocking), Submission.author_id.notin_(blocked))
if page == 1 and not gt and not lt: posts = pins.all() + posts
if ids_only: posts = [x[0] for x in posts]
return posts, next_exists
@app.get("/changelog")
@auth_desired
def changelog(v):
page = int(request.values.get("page") or 1)
page = max(page, 1)
sort=request.values.get("sort", "new")
t=request.values.get('t', "all")
ids = changeloglist(sort=sort,
page=page,
t=t,
v=v,
)
next_exists = (len(ids) > 25)
ids = ids[:25]
posts = get_posts(ids, v=v)
if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists}
else: return render_template("changelog.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page)
@cache.memoize(timeout=86400)
def changeloglist(v=None, sort="new", page=1 ,t="all"):
posts = g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False, private=False,).filter(Submission.deleted_utc == 0)
if v and v.admin_level == 0:
blocking = [x[0] for x in g.db.query(
UserBlock.target_id).filter_by(
user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(
UserBlock.user_id).filter_by(
target_id=v.id).all()]
posts = posts.filter(
Submission.author_id.notin_(blocking),
Submission.author_id.notin_(blocked)
)
admins = [x[0] for x in g.db.query(User.id).options(lazyload('*')).filter(User.admin_level == 6).all()]
posts = posts.filter(Submission.title.ilike('_changelog%'), Submission.author_id.in_(admins))
if t != 'all':
cutoff = 0
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
posts = posts.filter(Submission.created_utc >= cutoff)
if sort == "new":
posts = posts.order_by(Submission.created_utc.desc())
elif sort == "old":
posts = posts.order_by(Submission.created_utc.asc())
elif sort == "controversial":
posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes)
elif sort == "top":
posts = posts.order_by(Submission.downvotes - Submission.upvotes)
elif sort == "bottom":
posts = posts.order_by(Submission.upvotes - Submission.downvotes)
elif sort == "comments":
posts = posts.order_by(Submission.comment_count.desc())
posts = posts.offset(25 * (page - 1)).limit(26).all()
return [x[0] for x in posts]
@app.get("/random")
@auth_desired
def random_post(v):
x = g.db.query(Submission).options(lazyload('*')).filter(Submission.deleted_utc == 0, Submission.is_banned == False)
total = x.count()
n = random.randint(1, total - 2)
post = x.offset(n).limit(1).first()
return redirect(f"/post/{post.id}")
@cache.memoize(timeout=86400)
def comment_idlist(page=1, v=None, nsfw=False, sort="new", t="all"):
posts = g.db.query(Submission).options(lazyload('*'))
cc_idlist = [x[0] for x in g.db.query(Submission.id).options(lazyload('*')).filter(Submission.club == True).all()]
posts = posts.subquery()
comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.parent_submission.notin_(cc_idlist))
if v and v.admin_level <= 3:
blocking = [x[0] for x in g.db.query(
UserBlock.target_id).filter_by(
user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(
UserBlock.user_id).filter_by(
target_id=v.id).all()]
comments = comments.filter(
Comment.author_id.notin_(blocking),
Comment.author_id.notin_(blocked)
)
if not v or not v.admin_level >= 3:
comments = comments.filter_by(is_banned=False).filter(Comment.deleted_utc == 0)
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
comments = comments.filter(Comment.created_utc >= cutoff)
if sort == "new":
comments = comments.order_by(Comment.created_utc.desc())
elif sort == "old":
comments = comments.order_by(Comment.created_utc.asc())
elif sort == "controversial":
comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes)
elif sort == "top":
comments = comments.order_by(Comment.downvotes - Comment.upvotes)
elif sort == "bottom":
comments = comments.order_by(Comment.upvotes - Comment.downvotes)
comments = comments.offset(25 * (page - 1)).limit(26).all()
return [x[0] for x in comments]
@app.get("/comments")
@auth_desired
def all_comments(v):
page = int(request.values.get("page", 1))
sort=request.values.get("sort", "new")
t=request.values.get("t", defaulttimefilter)
idlist = comment_idlist(v=v,
page=page,
sort=sort,
t=t,
)
comments = get_comments(idlist, v=v)
next_exists = len(idlist) > 25
idlist = idlist[:25]
if request.headers.get("Authorization"): return {"data": [x.json for x in comments]}
else: return render_template("home_comments.html", v=v, sort=sort, t=t, page=page, comments=comments, standalone=True, next_exists=next_exists)

44
files/routes/giphy.py 100644 → 100755
View File

@ -1,22 +1,22 @@
from flask import *
from os import environ
import requests
from files.__main__ import app
GIPHY_KEY = environ.get('GIPHY_KEY').rstrip()
@app.get("/giphy")
@app.get("/giphy<path>")
def giphy(path=None):
searchTerm = request.values.get("searchTerm", "")
limit = int(request.values.get("limit", 48))
if searchTerm and limit:
url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit={limit}"
elif searchTerm and not limit:
url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit=48"
else:
url = f"https://api.giphy.com/v1/gifs?api_key={GIPHY_KEY}&limit=48"
return jsonify(requests.get(url).json())
from flask import *
from os import environ
import requests
from files.__main__ import app
GIPHY_KEY = environ.get('GIPHY_KEY').rstrip()
@app.get("/giphy")
@app.get("/giphy<path>")
def giphy(path=None):
searchTerm = request.values.get("searchTerm", "").strip()
limit = int(request.values.get("limit", 48))
if searchTerm and limit:
url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit={limit}"
elif searchTerm and not limit:
url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit=48"
else:
url = f"https://api.giphy.com/v1/gifs?api_key={GIPHY_KEY}&limit=48"
return jsonify(requests.get(url).json())

1127
files/routes/login.py 100644 → 100755

File diff suppressed because it is too large Load Diff

496
files/routes/oauth.py 100644 → 100755
View File

@ -1,248 +1,248 @@
from files.helpers.wrappers import *
from files.helpers.alerts import *
from files.helpers.get import *
from files.helpers.const import *
from files.classes import *
from flask import *
from files.__main__ import app, limiter
from sqlalchemy.orm import joinedload
@app.get("/authorize")
@auth_required
def authorize_prompt(v):
client_id = request.values.get("client_id")
application = g.db.query(OauthApp).options(lazyload('*')).filter_by(client_id=client_id).first()
if not application: return {"oauth_error": "Invalid `client_id`"}, 401
return render_template("oauth.html", v=v, application=application)
@app.post("/authorize")
@limiter.limit("1/second")
@auth_required
@validate_formkey
def authorize(v):
client_id = request.values.get("client_id")
application = g.db.query(OauthApp).options(lazyload('*')).filter_by(client_id=client_id).first()
if not application: return {"oauth_error": "Invalid `client_id`"}, 401
access_token = secrets.token_urlsafe(128)[:128]
new_auth = ClientAuth(
oauth_client = application.id,
user_id = v.id,
access_token=access_token
)
g.db.add(new_auth)
g.db.commit()
return redirect(f"{application.redirect_uri}?token={access_token}")
@app.post("/api_keys")
@limiter.limit("1/second")
@is_not_banned
def request_api_keys(v):
new_app = OauthApp(
app_name=request.values.get('name'),
redirect_uri=request.values.get('redirect_uri'),
author_id=v.id,
description=request.values.get("description")[:256]
)
g.db.add(new_app)
send_admin(NOTIFICATIONS_ACCOUNT, f"{v.username} has requested API keys for `{request.values.get('name')}`. You can approve or deny the request [here](/admin/apps).")
g.db.commit()
return redirect('/settings/apps')
@app.post("/delete_app/<aid>")
@limiter.limit("1/second")
@auth_required
@validate_formkey
def delete_oauth_app(v, aid):
aid = int(aid)
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all():
g.db.delete(auth)
g.db.delete(app)
g.db.commit()
return redirect('/apps')
@app.post("/edit_app/<aid>")
@limiter.limit("1/second")
@is_not_banned
@validate_formkey
def edit_oauth_app(v, aid):
aid = int(aid)
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
app.redirect_uri = request.values.get('redirect_uri')
app.app_name = request.values.get('name')
app.description = request.values.get("description")[:256]
g.db.add(app)
g.db.commit()
return redirect('/settings/apps')
@app.post("/admin/app/approve/<aid>")
@limiter.limit("1/second")
@admin_level_required(3)
@validate_formkey
def admin_app_approve(v, aid):
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
user = app.author
app.client_id = secrets.token_urlsafe(64)[:64]
g.db.add(app)
access_token = secrets.token_urlsafe(128)[:128]
new_auth = ClientAuth(
oauth_client = app.id,
user_id = user.id,
access_token=access_token
)
g.db.add(new_auth)
send_notification(NOTIFICATIONS_ACCOUNT, user, f"Your application `{app.app_name}` has been approved. Here's your access token: `{access_token}`\nPlease check the guide [here](/api) if you don't know what to do next.")
g.db.commit()
return {"message": f"{app.app_name} approved"}
@app.post("/admin/app/revoke/<aid>")
@limiter.limit("1/second")
@admin_level_required(3)
@validate_formkey
def admin_app_revoke(v, aid):
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
if app.id:
for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): g.db.delete(auth)
send_notification(NOTIFICATIONS_ACCOUNT, app.author, f"Your application `{app.app_name}` has been revoked.")
g.db.delete(app)
g.db.commit()
return {"message": f"App revoked"}
@app.post("/admin/app/reject/<aid>")
@limiter.limit("1/second")
@admin_level_required(3)
@validate_formkey
def admin_app_reject(v, aid):
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): g.db.delete(auth)
send_notification(NOTIFICATIONS_ACCOUNT, app.author, f"Your application `{app.app_name}` has been rejected.")
g.db.delete(app)
g.db.commit()
return {"message": f"App rejected"}
@app.get("/admin/app/<aid>")
@admin_level_required(3)
def admin_app_id(v, aid):
aid=aid
oauth = g.db.query(OauthApp).options(
joinedload(
OauthApp.author)).filter_by(
id=aid).first()
pids=oauth.idlist(page=int(request.values.get("page",1)),
)
next_exists=len(pids)==101
pids=pids[:100]
posts=get_posts(pids, v=v)
return render_template("admin/app.html",
v=v,
app=oauth,
listing=posts,
next_exists=next_exists
)
@app.get("/admin/app/<aid>/comments")
@admin_level_required(3)
def admin_app_id_comments(v, aid):
aid=aid
oauth = g.db.query(OauthApp).options(
joinedload(
OauthApp.author)).filter_by(
id=aid).first()
cids=oauth.comments_idlist(page=int(request.values.get("page",1)),
)
next_exists=len(cids)==101
cids=cids[:100]
comments=get_comments(cids, v=v)
return render_template("admin/app.html",
v=v,
app=oauth,
comments=comments,
next_exists=next_exists,
standalone=True
)
@app.get("/admin/apps")
@admin_level_required(3)
def admin_apps_list(v):
apps = g.db.query(OauthApp).all()
return render_template("admin/apps.html", v=v, apps=apps)
@app.post("/oauth/reroll/<aid>")
@limiter.limit("1/second")
@auth_required
def reroll_oauth_tokens(aid, v):
aid = aid
a = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
if a.author_id != v.id: abort(403)
a.client_id = secrets.token_urlsafe(64)[:64]
g.db.add(a)
g.db.commit()
return {"message": "Client ID Rerolled", "id": a.client_id}
from files.helpers.wrappers import *
from files.helpers.alerts import *
from files.helpers.get import *
from files.helpers.const import *
from files.classes import *
from flask import *
from files.__main__ import app, limiter
from sqlalchemy.orm import joinedload
@app.get("/authorize")
@auth_required
def authorize_prompt(v):
client_id = request.values.get("client_id")
application = g.db.query(OauthApp).options(lazyload('*')).filter_by(client_id=client_id).first()
if not application: return {"oauth_error": "Invalid `client_id`"}, 401
return render_template("oauth.html", v=v, application=application)
@app.post("/authorize")
@limiter.limit("1/second")
@auth_required
@validate_formkey
def authorize(v):
client_id = request.values.get("client_id")
application = g.db.query(OauthApp).options(lazyload('*')).filter_by(client_id=client_id).first()
if not application: return {"oauth_error": "Invalid `client_id`"}, 401
access_token = secrets.token_urlsafe(128)[:128]
new_auth = ClientAuth(oauth_client = application.id, user_id = v.id, access_token=access_token)
g.db.add(new_auth)
g.db.commit()
return redirect(f"{application.redirect_uri}?token={access_token}")
@app.post("/api_keys")
@limiter.limit("1/second")
@is_not_banned
def request_api_keys(v):
new_app = OauthApp(
app_name=request.values.get('name'),
redirect_uri=request.values.get('redirect_uri'),
author_id=v.id,
description=request.values.get("description")[:256]
)
g.db.add(new_app)
send_admin(NOTIFICATIONS_ACCOUNT, f"{v.username} has requested API keys for `{request.values.get('name')}`. You can approve or deny the request [here](/admin/apps).")
g.db.commit()
return redirect('/settings/apps')
@app.post("/delete_app/<aid>")
@limiter.limit("1/second")
@auth_required
@validate_formkey
def delete_oauth_app(v, aid):
aid = int(aid)
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
if app.author_id != v.id: abort(403)
for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all():
g.db.delete(auth)
g.db.delete(app)
g.db.commit()
return redirect('/apps')
@app.post("/edit_app/<aid>")
@limiter.limit("1/second")
@is_not_banned
@validate_formkey
def edit_oauth_app(v, aid):
aid = int(aid)
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
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/<aid>")
@limiter.limit("1/second")
@admin_level_required(3)
@validate_formkey
def admin_app_approve(v, aid):
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
user = app.author
app.client_id = secrets.token_urlsafe(64)[:64]
g.db.add(app)
access_token = secrets.token_urlsafe(128)[:128]
new_auth = ClientAuth(
oauth_client = app.id,
user_id = user.id,
access_token=access_token
)
g.db.add(new_auth)
send_notification(NOTIFICATIONS_ACCOUNT, user, f"Your application `{app.app_name}` has been approved. Here's your access token: `{access_token}`\nPlease check the guide [here](/api) if you don't know what to do next.")
g.db.commit()
return {"message": f"{app.app_name} approved"}
@app.post("/admin/app/revoke/<aid>")
@limiter.limit("1/second")
@admin_level_required(3)
@validate_formkey
def admin_app_revoke(v, aid):
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
if app.id:
for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): g.db.delete(auth)
send_notification(NOTIFICATIONS_ACCOUNT, app.author, f"Your application `{app.app_name}` has been revoked.")
g.db.delete(app)
g.db.commit()
return {"message": f"App revoked"}
@app.post("/admin/app/reject/<aid>")
@limiter.limit("1/second")
@admin_level_required(3)
@validate_formkey
def admin_app_reject(v, aid):
app = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
for auth in g.db.query(ClientAuth).options(lazyload('*')).filter_by(oauth_client=app.id).all(): g.db.delete(auth)
send_notification(NOTIFICATIONS_ACCOUNT, app.author, f"Your application `{app.app_name}` has been rejected.")
g.db.delete(app)
g.db.commit()
return {"message": f"App rejected"}
@app.get("/admin/app/<aid>")
@admin_level_required(3)
def admin_app_id(v, aid):
aid=aid
oauth = g.db.query(OauthApp).options(
joinedload(
OauthApp.author)).filter_by(
id=aid).first()
pids=oauth.idlist(page=int(request.values.get("page",1)),
)
next_exists=len(pids)==101
pids=pids[:100]
posts=get_posts(pids, v=v)
return render_template("admin/app.html",
v=v,
app=oauth,
listing=posts,
next_exists=next_exists
)
@app.get("/admin/app/<aid>/comments")
@admin_level_required(3)
def admin_app_id_comments(v, aid):
aid=aid
oauth = g.db.query(OauthApp).options(
joinedload(
OauthApp.author)).filter_by(
id=aid).first()
cids=oauth.comments_idlist(page=int(request.values.get("page",1)),
)
next_exists=len(cids)==101
cids=cids[:100]
comments=get_comments(cids, v=v)
return render_template("admin/app.html",
v=v,
app=oauth,
comments=comments,
next_exists=next_exists,
standalone=True
)
@app.get("/admin/apps")
@admin_level_required(3)
def admin_apps_list(v):
apps = g.db.query(OauthApp).all()
return render_template("admin/apps.html", v=v, apps=apps)
@app.post("/oauth/reroll/<aid>")
@limiter.limit("1/second")
@auth_required
def reroll_oauth_tokens(aid, v):
aid = aid
a = g.db.query(OauthApp).options(lazyload('*')).filter_by(id=aid).first()
if a.author_id != v.id: abort(403)
a.client_id = secrets.token_urlsafe(64)[:64]
g.db.add(a)
g.db.commit()
return {"message": "Client ID Rerolled", "id": a.client_id}

2038
files/routes/posts.py 100644 → 100755

File diff suppressed because it is too large Load Diff

178
files/routes/reporting.py 100644 → 100755
View File

@ -1,90 +1,90 @@
from files.helpers.wrappers import *
from files.helpers.get import *
from flask import g
from files.__main__ import app, limiter
from os import path
@app.post("/flag/post/<pid>")
@limiter.limit("1/second")
@auth_desired
def api_flag_post(pid, v):
post = get_post(pid)
if v and not v.shadowbanned:
existing = g.db.query(Flag).options(lazyload('*')).filter_by(user_id=v.id, post_id=post.id).first()
if existing: return "", 409
reason = request.values.get("reason", "").strip()[:100]
if "<" in reason: return {"error": f"Reasons can't contain <"}
for i in re.finditer(':(.{1,30}?):', reason):
if path.isfile(f'./files/assets/images/emojis/{i.group(1)}.webp'):
reason = reason.replace(f':{i.group(1)}:', f'<img loading="lazy" data-bs-toggle="tooltip" title="{i.group(1)}" delay="0" height=20 src="https://{site}/assets/images/emojis/{i.group(1)}.webp">')
flag = Flag(post_id=post.id,
user_id=v.id,
reason=reason,
)
g.db.add(flag)
g.db.commit()
return {"message": "Post reported!"}
@app.post("/flag/comment/<cid>")
@limiter.limit("1/second")
@auth_desired
def api_flag_comment(cid, v):
comment = get_comment(cid)
if v and not v.shadowbanned:
existing = g.db.query(CommentFlag).options(lazyload('*')).filter_by(
user_id=v.id, comment_id=comment.id).first()
if existing: return "", 409
reason = request.values.get("reason", "").strip()[:100]
if "<" in reason: return {"error": f"Reasons can't contain <"}
for i in re.finditer(':(.{1,30}?):', reason):
if path.isfile(f'./files/assets/images/emojis/{i.group(1)}.webp'):
reason = reason.replace(f':{i.group(1)}:', f'<img loading="lazy" data-bs-toggle="tooltip" title="{i.group(1)}" delay="0" height=20 src="https://{site}/assets/images/emojis/{i.group(1)}.webp">')
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/<report_fn>')
@limiter.limit("1/second")
@auth_required
@validate_formkey
def remove_report(report_fn, v):
if v.admin_level < 6:
return {"error": "go outside"}, 403
if report_fn.startswith('c'):
report = g.db.query(CommentFlag).options(lazyload('*')).filter_by(id=int(report_fn.lstrip('c'))).first()
elif report_fn.startswith('p'):
report = g.db.query(Flag).options(lazyload('*')).filter_by(id=int(report_fn.lstrip('p'))).first()
else:
return {"error": "Invalid report ID"}, 400
g.db.delete(report)
g.db.commit()
from files.helpers.wrappers import *
from files.helpers.get import *
from flask import g
from files.__main__ import app, limiter
from os import path
@app.post("/flag/post/<pid>")
@limiter.limit("1/second")
@auth_desired
def api_flag_post(pid, v):
post = get_post(pid)
if v and not v.shadowbanned:
existing = g.db.query(Flag).options(lazyload('*')).filter_by(user_id=v.id, post_id=post.id).first()
if existing: return "", 409
reason = request.values.get("reason", "").strip()[:100]
if "<" in reason: return {"error": f"Reasons can't contain <"}
for i in re.finditer(':(.{1,30}?):', reason):
if path.isfile(f'./files/assets/images/emojis/{i.group(1)}.webp'):
reason = reason.replace(f':{i.group(1)}:', f'<img loading="lazy" data-bs-toggle="tooltip" title="{i.group(1)}" delay="0" height=20 src="https://{site}/assets/images/emojis/{i.group(1)}.webp">')
flag = Flag(post_id=post.id,
user_id=v.id,
reason=reason,
)
g.db.add(flag)
g.db.commit()
return {"message": "Post reported!"}
@app.post("/flag/comment/<cid>")
@limiter.limit("1/second")
@auth_desired
def api_flag_comment(cid, v):
comment = get_comment(cid)
if v and not v.shadowbanned:
existing = g.db.query(CommentFlag).options(lazyload('*')).filter_by(
user_id=v.id, comment_id=comment.id).first()
if existing: return "", 409
reason = request.values.get("reason", "").strip()[:100]
if "<" in reason: return {"error": f"Reasons can't contain <"}
for i in re.finditer(':(.{1,30}?):', reason):
if path.isfile(f'./files/assets/images/emojis/{i.group(1)}.webp'):
reason = reason.replace(f':{i.group(1)}:', f'<img loading="lazy" data-bs-toggle="tooltip" title="{i.group(1)}" delay="0" height=20 src="https://{site}/assets/images/emojis/{i.group(1)}.webp">')
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/<report_fn>')
@limiter.limit("1/second")
@auth_required
@validate_formkey
def remove_report(report_fn, v):
if v.admin_level < 6:
return {"error": "go outside"}, 403
if report_fn.startswith('c'):
report = g.db.query(CommentFlag).options(lazyload('*')).filter_by(id=int(report_fn.lstrip('c'))).first()
elif report_fn.startswith('p'):
report = g.db.query(Flag).options(lazyload('*')).filter_by(id=int(report_fn.lstrip('p'))).first()
else:
return {"error": "Invalid report ID"}, 400
g.db.delete(report)
g.db.commit()
return {"message": "Removed report"}

568
files/routes/search.py 100644 → 100755
View File

@ -1,283 +1,287 @@
from files.helpers.wrappers import *
import re
from sqlalchemy import *
from flask import *
from files.__main__ import app
query_regex=re.compile("(\w+):(\S+)")
valid_params=[
'author',
'domain',
'over18'
]
def searchparse(text):
criteria = {x[0]:x[1] for x in query_regex.findall(text)}
for x in criteria:
if x in valid_params:
text = text.replace(f"{x}:{criteria[x]}", "")
text=text.strip()
if text:
criteria['q']=text
return criteria
@app.get("/search/posts")
@auth_desired
def searchposts(v):
query = request.values.get("q", '').strip()
page = max(1, int(request.values.get("page", 1)))
sort = request.values.get("sort", "top").lower()
t = request.values.get('t', 'all').lower()
criteria=searchparse(query)
posts = g.db.query(Submission.id).options(lazyload('*'))
if not (v and v.admin_level == 6): posts = posts.filter(Submission.private == False)
if 'q' in criteria:
words=criteria['q'].split()
words=[Submission.title.ilike('%'+x+'%') for x in words]
words=tuple(words)
posts=posts.filter(*words)
if 'over18' in criteria: posts = posts.filter(Submission.over_18==True)
if 'author' in criteria: posts = posts.filter(Submission.author_id == get_user(criteria['author']).id)
if 'domain' in criteria:
domain=criteria['domain']
posts=posts.filter(
or_(
Submission.url.ilike("https://"+domain+'/%'),
Submission.url.ilike("https://"+domain+'/%'),
Submission.url.ilike("https://"+domain),
Submission.url.ilike("https://"+domain),
Submission.url.ilike("https://www."+domain+'/%'),
Submission.url.ilike("https://www."+domain+'/%'),
Submission.url.ilike("https://www."+domain),
Submission.url.ilike("https://www."+domain),
Submission.url.ilike("https://old." + domain + '/%'),
Submission.url.ilike("https://old." + domain + '/%'),
Submission.url.ilike("https://old." + domain),
Submission.url.ilike("https://old." + domain)
)
)
if not(v and v.admin_level >= 3):
posts = posts.filter(
Submission.deleted_utc == 0,
Submission.is_banned == False,
)
if v and v.admin_level >= 4:
pass
elif v:
blocking = [x[0] for x in g.db.query(
UserBlock.target_id).filter_by(
user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(
UserBlock.user_id).filter_by(
target_id=v.id).all()]
posts = posts.filter(
Submission.author_id.notin_(blocking),
Submission.author_id.notin_(blocked),
)
if t:
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
posts = posts.filter(Submission.created_utc >= cutoff)
if sort == "new":
posts = posts.order_by(Submission.created_utc.desc())
elif sort == "old":
posts = posts.order_by(Submission.created_utc.asc())
elif sort == "controversial":
posts = posts.order_by(-1 * Submission.upvotes * (Submission.downvotes+1))
elif sort == "top":
posts = posts.order_by(Submission.downvotes - Submission.upvotes)
elif sort == "bottom":
posts = posts.order_by(Submission.upvotes - Submission.downvotes)
elif sort == "comments":
posts = posts.order_by(Submission.comment_count.desc())
total = posts.count()
posts = posts.offset(25 * (page - 1)).limit(26).all()
ids = [x[0] for x in posts]
next_exists = (len(ids) > 25)
ids = ids[:25]
posts = get_posts(ids, v=v)
if v and v.admin_level>3 and "domain" in criteria:
domain=criteria['domain']
domain_obj=get_domain(domain)
else:
domain=None
domain_obj=None
if request.headers.get("Authorization"): return {"data":[x.json for x in posts]}
else: return render_template("search.html",
v=v,
query=query,
total=total,
page=page,
listing=posts,
sort=sort,
t=t,
next_exists=next_exists,
domain=domain,
domain_obj=domain_obj
)
@app.get("/search/comments")
@auth_desired
def searchcomments(v):
query = request.values.get("q", '').strip()
try: page = max(1, int(request.values.get("page", 1)))
except: page = 1
sort = request.values.get("sort", "top").lower()
t = request.values.get('t', 'all').lower()
criteria=searchparse(query)
comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.parent_submission != None)
if 'q' in criteria:
words=criteria['q'].split()
words=[Comment.body.ilike('%'+x+'%') for x in words]
words=tuple(words)
comments=comments.filter(*words)
if not(v and v.admin_level >= 3):
comments = comments.filter(
Comment.deleted_utc == 0,
Comment.is_banned == False)
if t:
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
comments = comments.filter(Comment.created_utc >= cutoff)
if sort == "new":
comments = comments.order_by(Comment.created_utc.desc())
elif sort == "old":
comments = comments.order_by(Comment.created_utc.asc())
elif sort == "controversial":
comments = comments.order_by(-1 * Comment.upvotes * (Comment.downvotes+1))
elif sort == "top":
comments = comments.order_by(Comment.downvotes - Comment.upvotes)
elif sort == "bottom":
comments = comments.order_by(Comment.upvotes - Comment.downvotes)
total = comments.count()
comments = comments.offset(25 * (page - 1)).limit(26).all()
ids = [x[0] for x in comments]
next_exists = (len(ids) > 25)
ids = ids[:25]
comments = get_comments(ids, v=v)
if request.headers.get("Authorization"): return [x.json for x in comments]
else: return render_template("search_comments.html", v=v, query=query, total=total, page=page, comments=comments, sort=sort, t=t, next_exists=next_exists)
@app.get("/search/users")
@auth_desired
def searchusers(v):
query = request.values.get("q", '').strip()
page = max(1, int(request.values.get("page", 1)))
sort = request.values.get("sort", "top").lower()
t = request.values.get('t', 'all').lower()
term=query.lstrip('@')
term=term.replace('\\','')
term=term.replace('_','\_')
users=g.db.query(User).options(lazyload('*')).filter(User.username.ilike(f'%{term}%'))
users=users.order_by(User.username.ilike(term).desc(), User.stored_subscriber_count.desc())
total=users.count()
users=[x for x in users.offset(25 * (page-1)).limit(26)]
next_exists=(len(users)==26)
users=users[:25]
if request.headers.get("Authorization"): return [x.json for x in users]
from files.helpers.wrappers import *
import re
from sqlalchemy import *
from flask import *
from files.__main__ import app
query_regex=re.compile("(\w+):(\S+)")
valid_params=[
'author',
'domain',
'over18'
]
def searchparse(text):
criteria = {x[0]:x[1] for x in query_regex.findall(text)}
for x in criteria:
if x in valid_params:
text = text.replace(f"{x}:{criteria[x]}", "")
text=text.strip()
if text:
criteria['q']=text
return criteria
@app.get("/search/posts")
@auth_desired
def searchposts(v):
query = request.values.get("q", '').strip()
page = max(1, int(request.values.get("page", 1)))
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
criteria=searchparse(query)
posts = g.db.query(Submission.id).options(lazyload('*'))
if not (v and v.admin_level == 6): posts = posts.filter(Submission.private == False)
if 'q' in criteria:
words=criteria['q'].split()
words=[Submission.title.ilike('%'+x+'%') for x in words]
words=tuple(words)
posts=posts.filter(*words)
if 'over18' in criteria: posts = posts.filter(Submission.over_18==True)
if 'author' in criteria: posts = posts.filter(Submission.author_id == get_user(criteria['author']).id)
if 'domain' in criteria:
domain=criteria['domain']
posts=posts.filter(
or_(
Submission.url.ilike("https://"+domain+'/%'),
Submission.url.ilike("https://"+domain+'/%'),
Submission.url.ilike("https://"+domain),
Submission.url.ilike("https://"+domain),
Submission.url.ilike("https://www."+domain+'/%'),
Submission.url.ilike("https://www."+domain+'/%'),
Submission.url.ilike("https://www."+domain),
Submission.url.ilike("https://www."+domain),
Submission.url.ilike("https://old." + domain + '/%'),
Submission.url.ilike("https://old." + domain + '/%'),
Submission.url.ilike("https://old." + domain),
Submission.url.ilike("https://old." + domain)
)
)
if not(v and v.admin_level >= 3):
posts = posts.filter(
Submission.deleted_utc == 0,
Submission.is_banned == False,
)
if v and v.admin_level >= 4:
pass
elif v:
blocking = [x[0] for x in g.db.query(
UserBlock.target_id).filter_by(
user_id=v.id).all()]
blocked = [x[0] for x in g.db.query(
UserBlock.user_id).filter_by(
target_id=v.id).all()]
posts = posts.filter(
Submission.author_id.notin_(blocking),
Submission.author_id.notin_(blocked),
)
if t:
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
posts = posts.filter(Submission.created_utc >= cutoff)
if sort == "new":
posts = posts.order_by(Submission.created_utc.desc())
elif sort == "old":
posts = posts.order_by(Submission.created_utc.asc())
elif sort == "controversial":
posts = posts.order_by(-1 * Submission.upvotes * Submission.downvotes * Submission.downvotes)
elif sort == "top":
posts = posts.order_by(Submission.downvotes - Submission.upvotes)
elif sort == "bottom":
posts = posts.order_by(Submission.upvotes - Submission.downvotes)
elif sort == "comments":
posts = posts.order_by(Submission.comment_count.desc())
total = posts.count()
posts = posts.offset(25 * (page - 1)).limit(26).all()
ids = [x[0] for x in posts]
next_exists = (len(ids) > 25)
ids = ids[:25]
posts = get_posts(ids, v=v)
if v and v.admin_level>3 and "domain" in criteria:
domain=criteria['domain']
domain_obj=get_domain(domain)
else:
domain=None
domain_obj=None
if request.headers.get("Authorization"): return {"data":[x.json for x in posts]}
else: return render_template("search.html",
v=v,
query=query,
total=total,
page=page,
listing=posts,
sort=sort,
t=t,
next_exists=next_exists,
domain=domain,
domain_obj=domain_obj
)
@app.get("/search/comments")
@auth_desired
def searchcomments(v):
query = request.values.get("q", '').strip()
try: page = max(1, int(request.values.get("page", 1)))
except: page = 1
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
criteria=searchparse(query)
comments = g.db.query(Comment.id).options(lazyload('*')).filter(Comment.parent_submission != None)
if 'q' in criteria:
words=criteria['q'].split()
words=[Comment.body.ilike('%'+x+'%') for x in words]
words=tuple(words)
comments=comments.filter(*words)
if 'over18' in criteria: comments = comments.filter(Comment.over_18==True)
if 'author' in criteria: comments = comments.filter(Comment.author_id == get_user(criteria['author']).id)
if not(v and v.admin_level >= 3):
comments = comments.filter(
Comment.deleted_utc == 0,
Comment.is_banned == False)
if t:
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
comments = comments.filter(Comment.created_utc >= cutoff)
if sort == "new":
comments = comments.order_by(Comment.created_utc.desc())
elif sort == "old":
comments = comments.order_by(Comment.created_utc.asc())
elif sort == "controversial":
comments = comments.order_by(-1 * Comment.upvotes * Comment.downvotes * Comment.downvotes)
elif sort == "top":
comments = comments.order_by(Comment.downvotes - Comment.upvotes)
elif sort == "bottom":
comments = comments.order_by(Comment.upvotes - Comment.downvotes)
total = comments.count()
comments = comments.offset(25 * (page - 1)).limit(26).all()
ids = [x[0] for x in comments]
next_exists = (len(ids) > 25)
ids = ids[:25]
comments = get_comments(ids, v=v)
if request.headers.get("Authorization"): return [x.json for x in comments]
else: return render_template("search_comments.html", v=v, query=query, total=total, page=page, comments=comments, sort=sort, t=t, next_exists=next_exists)
@app.get("/search/users")
@auth_desired
def searchusers(v):
query = request.values.get("q", '').strip()
page = max(1, int(request.values.get("page", 1)))
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
term=query.lstrip('@')
term=term.replace('\\','')
term=term.replace('_','\_')
users=g.db.query(User).options(lazyload('*')).filter(User.username.ilike(f'%{term}%'))
users=users.order_by(User.username.ilike(term).desc(), User.stored_subscriber_count.desc())
total=users.count()
users=[x for x in users.offset(25 * (page-1)).limit(26)]
next_exists=(len(users)>25)
users=users[:25]
if request.headers.get("Authorization"): return [x.json for x in users]
else: return render_template("search_users.html", v=v, query=query, total=total, page=page, users=users, sort=sort, t=t, next_exists=next_exists)

1740
files/routes/settings.py 100644 → 100755

File diff suppressed because it is too large Load Diff

682
files/routes/static.py 100644 → 100755
View File

@ -1,341 +1,341 @@
from files.mail import *
from files.__main__ import app, limiter, mail
from files.helpers.alerts import *
from files.classes.award import AWARDS
from sqlalchemy import func
from os import path
import calendar
import matplotlib.pyplot as plt
site = environ.get("DOMAIN").strip()
site_name = environ.get("SITE_NAME").strip()
@app.get('/rules')
@auth_desired
def static_rules(v):
if not path.exists(f'./{site_name} rules.html'):
if v and v.admin_level == 6:
return render_template('norules.html', v=v)
else:
abort(404)
with open(f'./{site_name} rules.html', 'r') as f:
rules = f.read()
return render_template('rules.html', rules=rules, v=v)
@app.get("/stats")
@auth_required
def participation_stats(v):
now = int(time.time())
day = now - 86400
data = {"valid_users": g.db.query(User.id).count(),
"private_users": g.db.query(User.id).options(lazyload('*')).filter_by(is_private=True).count(),
"banned_users": g.db.query(User.id).options(lazyload('*')).filter(User.is_banned > 0).count(),
"verified_email_users": g.db.query(User.id).options(lazyload('*')).filter_by(is_activated=True).count(),
"total_coins": g.db.query(func.sum(User.coins)).scalar(),
"signups_last_24h": g.db.query(User.id).options(lazyload('*')).filter(User.created_utc > day).count(),
"total_posts": g.db.query(Submission.id).count(),
"posting_users": g.db.query(Submission.author_id).distinct().count(),
"listed_posts": g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False).filter(Submission.deleted_utc == 0).count(),
"removed_posts": g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=True).count(),
"deleted_posts": g.db.query(Submission.id).options(lazyload('*')).filter(Submission.deleted_utc > 0).count(),
"posts_last_24h": g.db.query(Submission.id).options(lazyload('*')).filter(Submission.created_utc > day).count(),
"total_comments": g.db.query(Comment.id).count(),
"commenting_users": g.db.query(Comment.author_id).distinct().count(),
"removed_comments": g.db.query(Comment.id).options(lazyload('*')).filter_by(is_banned=True).count(),
"deleted_comments": g.db.query(Comment.id).options(lazyload('*')).filter(Comment.deleted_utc>0).count(),
"comments_last_24h": g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc > day).count(),
"post_votes": g.db.query(Vote.id).count(),
"post_voting_users": g.db.query(Vote.user_id).distinct().count(),
"comment_votes": g.db.query(CommentVote.id).count(),
"comment_voting_users": g.db.query(CommentVote.user_id).distinct().count(),
"total_awards": g.db.query(AwardRelationship.id).count(),
"awards_given": g.db.query(AwardRelationship.id).options(lazyload('*')).filter(or_(AwardRelationship.submission_id != None, AwardRelationship.comment_id != None)).count()
}
return render_template("admin/content_stats.html", v=v, title="Content Statistics", data=data)
@app.get("/chart")
@auth_required
def chart(v):
file = cached_chart()
return send_file(f"../{file}")
@cache.memoize(timeout=86400)
def cached_chart():
days = int(request.values.get("days", 25))
now = time.gmtime()
midnight_this_morning = time.struct_time((now.tm_year,
now.tm_mon,
now.tm_mday,
0,
0,
0,
now.tm_wday,
now.tm_yday,
0)
)
today_cutoff = calendar.timegm(midnight_this_morning)
day = 3600 * 24
day_cutoffs = [today_cutoff - day * i for i in range(days)]
day_cutoffs.insert(0, calendar.timegm(now))
daily_times = [time.strftime("%d", time.gmtime(day_cutoffs[i + 1])) for i in range(len(day_cutoffs) - 1)][2:][::-1]
daily_signups = [g.db.query(User.id).options(lazyload('*')).filter(User.created_utc < day_cutoffs[i], User.created_utc > day_cutoffs[i + 1]).count() for i in range(len(day_cutoffs) - 1)][2:][::-1]
post_stats = [g.db.query(Submission.id).options(lazyload('*')).filter(Submission.created_utc < day_cutoffs[i], Submission.created_utc > day_cutoffs[i + 1], Submission.is_banned == False).count() for i in range(len(day_cutoffs) - 1)][2:][::-1]
comment_stats = [g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id != 1).count() for i in range(len(day_cutoffs) - 1)][2:][::-1]
signup_chart = plt.subplot2grid((20, 4), (0, 0), rowspan=5, colspan=4)
posts_chart = plt.subplot2grid((20, 4), (7, 0), rowspan=5, colspan=4)
comments_chart = plt.subplot2grid((20, 4), (14, 0), rowspan=5, colspan=4)
signup_chart.grid(), posts_chart.grid(), comments_chart.grid()
signup_chart.plot(
daily_times,
daily_signups,
color='red')
posts_chart.plot(
daily_times,
post_stats,
color='green')
comments_chart.plot(
daily_times,
comment_stats,
color='gold')
signup_chart.set_ylabel("Signups")
posts_chart.set_ylabel("Posts")
comments_chart.set_ylabel("Comments")
comments_chart.set_xlabel("Time (UTC)")
signup_chart.legend(loc='upper left', frameon=True)
posts_chart.legend(loc='upper left', frameon=True)
comments_chart.legend(loc='upper left', frameon=True)
file = "chart.png"
plt.savefig(file)
plt.clf()
return file
@app.get("/patrons")
@app.get("/paypigs")
@auth_desired
def patrons(v):
query = g.db.query(
User.id, User.username, User.patron, User.namecolor,
AwardRelationship.kind.label('last_award_kind'), func.count(AwardRelationship.id).label('last_award_count')
).filter(AwardRelationship.submission_id==None, AwardRelationship.comment_id==None, User.patron > 0) \
.group_by(User.username, User.patron, User.id, User.namecolor, AwardRelationship.kind) \
.order_by(User.patron.desc(), AwardRelationship.kind.desc()) \
.join(User).all()
result = {}
for row in (r._asdict() for r in query):
user_id = row['id']
if user_id not in result:
result[user_id] = row
result[user_id]['awards'] = {}
kind = row['last_award_kind']
if kind in AWARDS.keys():
result[user_id]['awards'][kind] = (AWARDS[kind], row['last_award_count'])
return render_template("patrons.html", v=v, result=result)
@app.get("/admins")
@auth_desired
def admins(v):
admins = g.db.query(User).options(lazyload('*')).filter_by(admin_level=6).order_by(User.coins.desc()).all()
return render_template("admins.html", v=v, admins=admins)
@app.get("/log")
@auth_desired
def log(v):
page=int(request.args.get("page",1))
if v and v.admin_level == 6: actions = g.db.query(ModAction).order_by(ModAction.id.desc()).offset(25 * (page - 1)).limit(26).all()
else: actions=g.db.query(ModAction).filter(ModAction.kind!="shadowban", ModAction.kind!="unshadowban", ModAction.kind!="club", ModAction.kind!="unclub").order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all()
next_exists=len(actions)==26
actions=actions[:25]
return render_template("log.html", v=v, actions=actions, next_exists=next_exists, page=page)
@app.get("/log/<id>")
@auth_desired
def log_item(id, v):
try: id = int(id)
except:
try: id = int(id, 36)
except: abort(404)
action=g.db.query(ModAction).options(lazyload('*')).filter_by(id=id).first()
if not action:
abort(404)
if request.path != action.permalink:
return redirect(action.permalink)
return render_template("log.html",
v=v,
actions=[action],
next_exists=False,
page=1,
action=action
)
@app.get("/assets/favicon.ico")
def favicon():
return send_file(f"./assets/images/{site_name}/icon.webp")
@app.get("/api")
@auth_desired
def api(v):
return render_template("api.html", v=v)
@app.get("/contact")
@auth_required
def contact(v):
return render_template("contact.html", v=v)
@app.post("/contact")
@limiter.limit("1/second")
@auth_required
def submit_contact(v):
message = f'This message has been sent automatically to all admins via https://{site}/contact, user email is "{v.email}"\n\nMessage:\n\n' + request.values.get("message", "")
send_admin(v.id, message)
g.db.commit()
return render_template("contact.html", v=v, msg="Your message has been sent.")
@app.get('/archives')
def archivesindex():
return redirect("/archives/index.html")
@app.get('/archives/<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('/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('.woff') or request.path.endswith('.woff2'):
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=2628000")
return resp
@app.get('/images/<path:path>')
@app.get('/hostedimages/<path:path>')
@limiter.exempt
def images(path):
resp = make_response(send_from_directory('/images', path))
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=2628000")
return resp
@app.get("/robots.txt")
def robots_txt():
return send_file("./assets/robots.txt")
@app.get("/settings")
@auth_required
def settings(v):
return redirect("/settings/profile")
@app.get("/settings/profile")
@auth_required
def settings_profile(v):
return render_template("settings_profile.html",
v=v)
@app.get("/badges")
@auth_desired
def badges(v):
badges = g.db.query(BadgeDef).all()
return render_template("badges.html", v=v, badges=badges)
@app.get("/blocks")
@auth_desired
def blocks(v):
blocks=g.db.query(UserBlock).all()
users = []
targets = []
for x in blocks:
users.append(get_account(x.user_id))
targets.append(get_account(x.target_id))
return render_template("blocks.html", v=v, users=users, targets=targets)
@app.get("/banned")
@auth_desired
def banned(v):
users = [x for x in g.db.query(User).options(lazyload('*')).filter(User.is_banned > 0, User.unban_utc == 0).all()]
return render_template("banned.html", v=v, users=users)
@app.get("/formatting")
@auth_desired
def formatting(v):
return render_template("formatting.html", v=v)
@app.get("/service-worker.js")
def serviceworker():
with open("files/assets/js/service-worker.js", "r") as f: return Response(f.read(), mimetype='application/javascript')
@app.get("/settings/security")
@auth_required
def settings_security(v):
return render_template("settings_security.html",
v=v,
mfa_secret=pyotp.random_base32() if not v.mfa_secret else None,
error=request.values.get("error") or None,
msg=request.values.get("msg") or None
)
@app.post("/dismiss_mobile_tip")
@limiter.limit("1/second")
def dismiss_mobile_tip():
session["tooltip_last_dismissed"]=int(time.time())
session.modified=True
return "", 204
from files.mail import *
from files.__main__ import app, limiter, mail
from files.helpers.alerts import *
from files.classes.award import AWARDS
from sqlalchemy import func
from os import path
import calendar
import matplotlib.pyplot as plt
site = environ.get("DOMAIN").strip()
site_name = environ.get("SITE_NAME").strip()
@app.get('/rules')
@auth_desired
def static_rules(v):
if not path.exists(f'./{site_name} rules.html'):
if v and v.admin_level == 6:
return render_template('norules.html', v=v)
else:
abort(404)
with open(f'./{site_name} rules.html', 'r') as f:
rules = f.read()
return render_template('rules.html', rules=rules, v=v)
@app.get("/stats")
@auth_required
def participation_stats(v):
now = int(time.time())
day = now - 86400
data = {"valid_users": g.db.query(User.id).count(),
"private_users": g.db.query(User.id).options(lazyload('*')).filter_by(is_private=True).count(),
"banned_users": g.db.query(User.id).options(lazyload('*')).filter(User.is_banned > 0).count(),
"verified_email_users": g.db.query(User.id).options(lazyload('*')).filter_by(is_activated=True).count(),
"total_coins": g.db.query(func.sum(User.coins)).scalar(),
"signups_last_24h": g.db.query(User.id).options(lazyload('*')).filter(User.created_utc > day).count(),
"total_posts": g.db.query(Submission.id).count(),
"posting_users": g.db.query(Submission.author_id).distinct().count(),
"listed_posts": g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=False).filter(Submission.deleted_utc == 0).count(),
"removed_posts": g.db.query(Submission.id).options(lazyload('*')).filter_by(is_banned=True).count(),
"deleted_posts": g.db.query(Submission.id).options(lazyload('*')).filter(Submission.deleted_utc > 0).count(),
"posts_last_24h": g.db.query(Submission.id).options(lazyload('*')).filter(Submission.created_utc > day).count(),
"total_comments": g.db.query(Comment.id).count(),
"commenting_users": g.db.query(Comment.author_id).distinct().count(),
"removed_comments": g.db.query(Comment.id).options(lazyload('*')).filter_by(is_banned=True).count(),
"deleted_comments": g.db.query(Comment.id).options(lazyload('*')).filter(Comment.deleted_utc>0).count(),
"comments_last_24h": g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc > day).count(),
"post_votes": g.db.query(Vote.id).count(),
"post_voting_users": g.db.query(Vote.user_id).distinct().count(),
"comment_votes": g.db.query(CommentVote.id).count(),
"comment_voting_users": g.db.query(CommentVote.user_id).distinct().count(),
"total_awards": g.db.query(AwardRelationship.id).count(),
"awards_given": g.db.query(AwardRelationship.id).options(lazyload('*')).filter(or_(AwardRelationship.submission_id != None, AwardRelationship.comment_id != None)).count()
}
return render_template("admin/content_stats.html", v=v, title="Content Statistics", data=data)
@app.get("/chart")
@auth_required
def chart(v):
file = cached_chart()
return send_file(f"../{file}")
@cache.memoize(timeout=86400)
def cached_chart():
days = int(request.values.get("days", 25))
now = time.gmtime()
midnight_this_morning = time.struct_time((now.tm_year,
now.tm_mon,
now.tm_mday,
0,
0,
0,
now.tm_wday,
now.tm_yday,
0)
)
today_cutoff = calendar.timegm(midnight_this_morning)
day = 3600 * 24
day_cutoffs = [today_cutoff - day * i for i in range(days)]
day_cutoffs.insert(0, calendar.timegm(now))
daily_times = [time.strftime("%d", time.gmtime(day_cutoffs[i + 1])) for i in range(len(day_cutoffs) - 1)][2:][::-1]
daily_signups = [g.db.query(User.id).options(lazyload('*')).filter(User.created_utc < day_cutoffs[i], User.created_utc > day_cutoffs[i + 1]).count() for i in range(len(day_cutoffs) - 1)][2:][::-1]
post_stats = [g.db.query(Submission.id).options(lazyload('*')).filter(Submission.created_utc < day_cutoffs[i], Submission.created_utc > day_cutoffs[i + 1], Submission.is_banned == False).count() for i in range(len(day_cutoffs) - 1)][2:][::-1]
comment_stats = [g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id != 1).count() for i in range(len(day_cutoffs) - 1)][2:][::-1]
signup_chart = plt.subplot2grid((20, 4), (0, 0), rowspan=5, colspan=4)
posts_chart = plt.subplot2grid((20, 4), (7, 0), rowspan=5, colspan=4)
comments_chart = plt.subplot2grid((20, 4), (14, 0), rowspan=5, colspan=4)
signup_chart.grid(), posts_chart.grid(), comments_chart.grid()
signup_chart.plot(
daily_times,
daily_signups,
color='red')
posts_chart.plot(
daily_times,
post_stats,
color='green')
comments_chart.plot(
daily_times,
comment_stats,
color='gold')
signup_chart.set_ylabel("Signups")
posts_chart.set_ylabel("Posts")
comments_chart.set_ylabel("Comments")
comments_chart.set_xlabel("Time (UTC)")
signup_chart.legend(loc='upper left', frameon=True)
posts_chart.legend(loc='upper left', frameon=True)
comments_chart.legend(loc='upper left', frameon=True)
file = "chart.png"
plt.savefig(file)
plt.clf()
return file
@app.get("/patrons")
@app.get("/paypigs")
@auth_desired
def patrons(v):
query = g.db.query(
User.id, User.username, User.patron, User.namecolor,
AwardRelationship.kind.label('last_award_kind'), func.count(AwardRelationship.id).label('last_award_count')
).filter(AwardRelationship.submission_id==None, AwardRelationship.comment_id==None, User.patron > 0) \
.group_by(User.username, User.patron, User.id, User.namecolor, AwardRelationship.kind) \
.order_by(User.patron.desc(), AwardRelationship.kind.desc()) \
.join(User).all()
result = {}
for row in (r._asdict() for r in query):
user_id = row['id']
if user_id not in result:
result[user_id] = row
result[user_id]['awards'] = {}
kind = row['last_award_kind']
if kind in AWARDS.keys():
result[user_id]['awards'][kind] = (AWARDS[kind], row['last_award_count'])
return render_template("patrons.html", v=v, result=result)
@app.get("/admins")
@auth_desired
def admins(v):
admins = g.db.query(User).options(lazyload('*')).filter_by(admin_level=6).order_by(User.coins.desc()).all()
return render_template("admins.html", v=v, admins=admins)
@app.get("/log")
@auth_desired
def log(v):
page=int(request.args.get("page",1))
if v and v.admin_level == 6: actions = g.db.query(ModAction).order_by(ModAction.id.desc()).offset(25 * (page - 1)).limit(26).all()
else: actions=g.db.query(ModAction).filter(ModAction.kind!="shadowban", ModAction.kind!="unshadowban", ModAction.kind!="club", ModAction.kind!="unclub").order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all()
next_exists=len(actions)>25
actions=actions[:25]
return render_template("log.html", v=v, actions=actions, next_exists=next_exists, page=page)
@app.get("/log/<id>")
@auth_desired
def log_item(id, v):
try: id = int(id)
except:
try: id = int(id, 36)
except: abort(404)
action=g.db.query(ModAction).options(lazyload('*')).filter_by(id=id).first()
if not action:
abort(404)
if request.path != action.permalink:
return redirect(action.permalink)
return render_template("log.html",
v=v,
actions=[action],
next_exists=False,
page=1,
action=action
)
@app.get("/assets/favicon.ico")
def favicon():
return send_file(f"./assets/images/{site_name}/icon.gif")
@app.get("/api")
@auth_desired
def api(v):
return render_template("api.html", v=v)
@app.get("/contact")
@auth_required
def contact(v):
return render_template("contact.html", v=v)
@app.post("/contact")
@limiter.limit("1/second")
@auth_required
def submit_contact(v):
message = f'This message has been sent automatically to all admins via https://{site}/contact, user email is "{v.email}"\n\nMessage:\n\n' + request.values.get("message", "")
send_admin(v.id, message)
g.db.commit()
return render_template("contact.html", v=v, msg="Your message has been sent.")
@app.get('/archives')
def archivesindex():
return redirect("/archives/index.html")
@app.get('/archives/<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('/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('.woff') or request.path.endswith('.woff2'):
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=2628000")
return resp
@app.get('/images/<path:path>')
@app.get('/hostedimages/<path:path>')
@limiter.exempt
def images(path):
resp = make_response(send_from_directory('/images', path))
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=2628000")
return resp
@app.get("/robots.txt")
def robots_txt():
return send_file("./assets/robots.txt")
@app.get("/settings")
@auth_required
def settings(v):
return redirect("/settings/profile")
@app.get("/settings/profile")
@auth_required
def settings_profile(v):
return render_template("settings_profile.html",
v=v)
@app.get("/badges")
@auth_desired
def badges(v):
badges = g.db.query(BadgeDef).all()
return render_template("badges.html", v=v, badges=badges)
@app.get("/blocks")
@auth_desired
def blocks(v):
blocks=g.db.query(UserBlock).all()
users = []
targets = []
for x in blocks:
users.append(get_account(x.user_id))
targets.append(get_account(x.target_id))
return render_template("blocks.html", v=v, users=users, targets=targets)
@app.get("/banned")
@auth_desired
def banned(v):
users = [x for x in g.db.query(User).options(lazyload('*')).filter(User.is_banned > 0, User.unban_utc == 0).all()]
return render_template("banned.html", v=v, users=users)
@app.get("/formatting")
@auth_desired
def formatting(v):
return render_template("formatting.html", v=v)
@app.get("/service-worker.js")
def serviceworker():
with open("files/assets/js/service-worker.js", "r") as f: return Response(f.read(), mimetype='application/javascript')
@app.get("/settings/security")
@auth_required
def settings_security(v):
return render_template("settings_security.html",
v=v,
mfa_secret=pyotp.random_base32() if not v.mfa_secret else None,
error=request.values.get("error") or None,
msg=request.values.get("msg") or None
)
@app.post("/dismiss_mobile_tip")
@limiter.limit("1/second")
def dismiss_mobile_tip():
session["tooltip_last_dismissed"]=int(time.time())
session.modified=True
return "", 204

1486
files/routes/users.py 100644 → 100755

File diff suppressed because it is too large Load Diff

397
files/routes/votes.py 100644 → 100755
View File

@ -1,200 +1,199 @@
from files.helpers.wrappers import *
from files.helpers.get import *
from files.classes import *
from flask import *
from files.__main__ import app, limiter
from sqlalchemy.orm import joinedload
@app.get("/votes")
@auth_desired
def admin_vote_info_get(v):
link = request.values.get("link")
if not link: return render_template("votes.html", v=v)
try:
if "t2_" in link: thing = get_post(int(link.split("t2_")[1]), v=v)
elif "t3_" in link: thing = get_comment(int(link.split("t3_")[1]), v=v)
else: abort(400)
except: abort(400)
if isinstance(thing, Submission):
ups = g.db.query(Vote
).options(joinedload(Vote.user)
).filter_by(submission_id=thing.id, vote_type=1
).all()
downs = g.db.query(Vote
).options(joinedload(Vote.user)
).filter_by(submission_id=thing.id, vote_type=-1
).all()
elif isinstance(thing, Comment):
ups = g.db.query(CommentVote
).options(joinedload(CommentVote.user)
).filter_by(comment_id=thing.id, vote_type=1
).all()
downs = g.db.query(CommentVote
).options(joinedload(CommentVote.user)
).filter_by(comment_id=thing.id, vote_type=-1
).all()
else:
abort(400)
return render_template("votes.html",
v=v,
thing=thing,
ups=ups,
downs=downs,)
@app.post("/vote/post/<post_id>/<new>")
@auth_required
@validate_formkey
def api_vote_post(post_id, new, v):
if new not in ["-1", "0", "1"]: abort(400)
if request.headers.get("X-User-Type","") == "Bot": abort(403)
new = int(new)
post = get_post(post_id)
existing = g.db.query(Vote).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id).first()
if existing and existing.vote_type == new: return "", 204
if existing:
if existing.vote_type == 0 and new != 0:
post.author.coins += 1
post.author.truecoins += 1
g.db.add(post.author)
existing.vote_type = new
g.db.add(existing)
elif existing.vote_type != 0 and new == 0:
post.author.coins -= 1
post.author.truecoins -= 1
g.db.add(post.author)
g.db.delete(existing)
else:
existing.vote_type = new
g.db.add(existing)
elif new != 0:
post.author.coins += 1
post.author.truecoins += 1
g.db.add(post.author)
vote = Vote(user_id=v.id,
vote_type=new,
submission_id=post_id,
app_id=v.client.application.id if v.client else None
)
g.db.add(vote)
try:
g.db.flush()
post.upvotes = g.db.query(Vote.id).options(lazyload('*')).filter_by(submission_id=post.id, vote_type=1).count()
post.downvotes = g.db.query(Vote.id).options(lazyload('*')).filter_by(submission_id=post.id, vote_type=-1).count()
g.db.add(post)
g.db.commit()
except: g.db.rollback()
return "", 204
@app.post("/vote/comment/<comment_id>/<new>")
@auth_required
@validate_formkey
def api_vote_comment(comment_id, new, v):
if new not in ["-1", "0", "1"]: abort(400)
if request.headers.get("X-User-Type","") == "Bot": abort(403)
new = int(new)
try: comment_id = int(comment_id)
except:
try: comment_id = int(comment_id, 36)
except: abort(404)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first()
if existing and existing.vote_type == new: return "", 204
if existing:
if existing.vote_type == 0 and new != 0:
comment.author.coins += 1
comment.author.truecoins += 1
g.db.add(comment.author)
existing.vote_type = new
g.db.add(existing)
elif existing.vote_type != 0 and new == 0:
comment.author.coins -= 1
comment.author.truecoins -= 1
g.db.add(comment.author)
g.db.delete(existing)
else:
existing.vote_type = new
g.db.add(existing)
elif new != 0:
comment.author.coins += 1
comment.author.truecoins += 1
g.db.add(comment.author)
vote = CommentVote(user_id=v.id,
vote_type=new,
comment_id=comment_id,
app_id=v.client.application.id if v.client else None
)
g.db.add(vote)
try:
g.db.flush()
comment.upvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=1).count()
comment.downvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=-1).count()
g.db.add(comment)
g.db.commit()
except: g.db.rollback()
return "", 204
@app.post("/vote/poll/<comment_id>")
@auth_required
def api_vote_poll(comment_id, v):
vote = request.values.get("vote")
if vote == "true": new = 1
elif vote == "false": new = 0
else: abort(400)
comment_id = int(comment_id)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first()
if existing and existing.vote_type == new: return "", 204
if existing:
if new == 1:
existing.vote_type = new
g.db.add(existing)
else: g.db.delete(existing)
elif new == 1:
vote = CommentVote(user_id=v.id, vote_type=new, comment_id=comment.id)
g.db.add(vote)
try:
g.db.flush()
comment.upvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=1).count()
g.db.add(comment)
g.db.commit()
except: g.db.rollback()
from files.helpers.wrappers import *
from files.helpers.get import *
from files.classes import *
from flask import *
from files.__main__ import app, limiter
from sqlalchemy.orm import joinedload
@app.get("/votes")
@auth_desired
def admin_vote_info_get(v):
link = request.values.get("link")
if not link: return render_template("votes.html", v=v)
try:
if "t2_" in link: thing = get_post(int(link.split("t2_")[1]), v=v)
elif "t3_" in link: thing = get_comment(int(link.split("t3_")[1]), v=v)
else: abort(400)
except: abort(400)
if isinstance(thing, Submission):
ups = g.db.query(Vote
).options(joinedload(Vote.user)
).filter_by(submission_id=thing.id, vote_type=1
).order_by(Vote.id).all()
downs = g.db.query(Vote
).options(joinedload(Vote.user)
).filter_by(submission_id=thing.id, vote_type=-1
).order_by(Vote.id).all()
elif isinstance(thing, Comment):
ups = g.db.query(CommentVote
).options(joinedload(CommentVote.user)
).filter_by(comment_id=thing.id, vote_type=1
).order_by(CommentVote.id).all()
downs = g.db.query(CommentVote
).options(joinedload(CommentVote.user)
).filter_by(comment_id=thing.id, vote_type=-1
).order_by(CommentVote.id).all()
else: abort(400)
return render_template("votes.html",
v=v,
thing=thing,
ups=ups,
downs=downs,)
@app.post("/vote/post/<post_id>/<new>")
@auth_required
@validate_formkey
def api_vote_post(post_id, new, v):
if new not in ["-1", "0", "1"]: abort(400)
if request.headers.get("X-User-Type","") == "Bot": abort(403)
new = int(new)
post = get_post(post_id)
existing = g.db.query(Vote).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id).first()
if existing and existing.vote_type == new: return "", 204
if existing:
if existing.vote_type == 0 and new != 0:
post.author.coins += 1
post.author.truecoins += 1
g.db.add(post.author)
existing.vote_type = new
g.db.add(existing)
elif existing.vote_type != 0 and new == 0:
post.author.coins -= 1
post.author.truecoins -= 1
g.db.add(post.author)
g.db.delete(existing)
else:
existing.vote_type = new
g.db.add(existing)
elif new != 0:
post.author.coins += 1
post.author.truecoins += 1
g.db.add(post.author)
vote = Vote(user_id=v.id,
vote_type=new,
submission_id=post_id,
app_id=v.client.application.id if v.client else None
)
g.db.add(vote)
try:
g.db.flush()
post.upvotes = g.db.query(Vote.id).options(lazyload('*')).filter_by(submission_id=post.id, vote_type=1).count()
post.downvotes = g.db.query(Vote.id).options(lazyload('*')).filter_by(submission_id=post.id, vote_type=-1).count()
g.db.add(post)
g.db.commit()
except: g.db.rollback()
return "", 204
@app.post("/vote/comment/<comment_id>/<new>")
@auth_required
@validate_formkey
def api_vote_comment(comment_id, new, v):
if new not in ["-1", "0", "1"]: abort(400)
if request.headers.get("X-User-Type","") == "Bot": abort(403)
new = int(new)
try: comment_id = int(comment_id)
except:
try: comment_id = int(comment_id, 36)
except: abort(404)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first()
if existing and existing.vote_type == new: return "", 204
if existing:
if existing.vote_type == 0 and new != 0:
comment.author.coins += 1
comment.author.truecoins += 1
g.db.add(comment.author)
existing.vote_type = new
g.db.add(existing)
elif existing.vote_type != 0 and new == 0:
comment.author.coins -= 1
comment.author.truecoins -= 1
g.db.add(comment.author)
g.db.delete(existing)
else:
existing.vote_type = new
g.db.add(existing)
elif new != 0:
comment.author.coins += 1
comment.author.truecoins += 1
g.db.add(comment.author)
vote = CommentVote(user_id=v.id,
vote_type=new,
comment_id=comment_id,
app_id=v.client.application.id if v.client else None
)
g.db.add(vote)
try:
g.db.flush()
comment.upvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=1).count()
comment.downvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=-1).count()
g.db.add(comment)
g.db.commit()
except: g.db.rollback()
return "", 204
@app.post("/vote/poll/<comment_id>")
@auth_required
def api_vote_poll(comment_id, v):
vote = request.values.get("vote")
if vote == "true": new = 1
elif vote == "false": new = 0
else: abort(400)
comment_id = int(comment_id)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first()
if existing and existing.vote_type == new: return "", 204
if existing:
if new == 1:
existing.vote_type = new
g.db.add(existing)
else: g.db.delete(existing)
elif new == 1:
vote = CommentVote(user_id=v.id, vote_type=new, comment_id=comment.id)
g.db.add(vote)
try:
g.db.flush()
comment.upvotes = g.db.query(CommentVote.id).options(lazyload('*')).filter_by(comment_id=comment.id, vote_type=1).count()
g.db.add(comment)
g.db.commit()
except: g.db.rollback()
return "", 204

View File

@ -1,62 +1,61 @@
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}}</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<pre></pre>
<pre></pre>
<h3>&nbsp;Admin Tools</h3>
<h4>Content</h4>
<ul>
<li><a href="/admin/reported/posts">Reported Posts</a></li>
<li><a href="/admin/reported/comments">Reported Comments</a></li>
<li><a href="/admin/image_posts">Image Posts</a></li>
<li><a href="/admin/removed">Removed Posts</a></li>
</ul>
<h4>Users</h4>
<ul>
<li><a href="/admin/shadowbanned">Shadowbanned Users</a></li>
<li><a href="/admin/agendaposters">Users with Agendaposter Theme</a></li>
<li><a href="/admin/users">Users Feed</a></li>
</ul>
<h4>Safety</h4>
<ul>
<li><a href="/admin/banned_domains">Banned Domains</a></li>
<li><a href="/admin/image_ban">Perceptive Hash Image Ban</a></li>
<li><a href="/admin/alt_votes">Multi Vote Analysis</a></li>
</ul>
<h4>Grant</h4>
<ul>
<li><a href="/admin/user_award">Give User Award</a></li>
<li><a href="/admin/badge_grant">Badges</a></li>
</ul>
<h4>API Access Control</h4>
<ul>
<li><a href="/admin/apps">Apps</a></li>
</ul>
<h4>Statistics</h4>
<ul>
<li><a href="/stats">Content Stats</a></li>
<li><a href="/chart">Stat Chart</a></li>
</ul>
<h4>Configuration</h4>
<ul>
<li><a href="/admin/rules">Site Rules</a></li>
</ul>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="disablesignups" name="disablesignups" {% if x == "yes" %}checked{% endif %} onchange="post_toast('/admin/disablesignups');">
<label class="custom-control-label" for="disablesignups">Disable signups</label>
</div>
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}}</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<pre></pre>
<pre></pre>
<h3>&nbsp;Admin Tools</h3>
<h4>Content</h4>
<ul>
<li><a href="/admin/reported/posts">Reported Posts</a></li>
<li><a href="/admin/reported/comments">Reported Comments</a></li>
<li><a href="/admin/image_posts">Image Posts</a></li>
<li><a href="/admin/removed">Removed Posts</a></li>
</ul>
<h4>Users</h4>
<ul>
<li><a href="/admin/shadowbanned">Shadowbanned Users</a></li>
<li><a href="/admin/agendaposters">Users with Agendaposter Theme</a></li>
<li><a href="/admin/users">Users Feed</a></li>
</ul>
<h4>Safety</h4>
<ul>
<li><a href="/admin/banned_domains">Banned Domains</a></li>
<li><a href="/admin/alt_votes">Multi Vote Analysis</a></li>
</ul>
<h4>Grant</h4>
<ul>
<li><a href="/admin/user_award">Give User Award</a></li>
<li><a href="/admin/badge_grant">Badges</a></li>
</ul>
<h4>API Access Control</h4>
<ul>
<li><a href="/admin/apps">Apps</a></li>
</ul>
<h4>Statistics</h4>
<ul>
<li><a href="/stats">Content Stats</a></li>
<li><a href="/chart">Stat Chart</a></li>
</ul>
<h4>Configuration</h4>
<ul>
<li><a href="/admin/rules">Site Rules</a></li>
</ul>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="disablesignups" name="disablesignups" {% if x == "yes" %}checked{% endif %} onchange="post_toast('/admin/disablesignups');">
<label class="custom-control-label" for="disablesignups">Disable signups</label>
</div>
{% endblock %}

174
files/templates/admin/alt_votes.html 100644 → 100755
View File

@ -1,88 +1,88 @@
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}}</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<pre>
</pre>
<h5>Vote Info</h5>
<form action="/admin/alt_votes" method="get" class="mb-6">
<label for="link-input">Usernames</label>
<input id="link-input" type="text" class="form-control mb-2" name="u1" value="{{u1.username if u1 else ''}}" placeholder="User 1">
<input id="link-input" type="text" class="form-control mb-2" name="u2" value="{{u2.username if u2 else ''}}" placeholder="User 2">
<input type="submit" value="Submit" class="btn btn-primary">
</form>
{% if u1 and u2 %}
<h2>Analysis</h2>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th></th>
<th>@{{u1.username}} only(% unique)</th>
<th>Both</th>
<th>@{{u2.username}} only (% unique)</th>
</tr>
</thead>
<tr>
<td><b>Post Upvotes</b></td>
<td>{{data['u1_only_post_ups']}} ({{data['u1_post_ups_unique']}}%)</td>
<td>{{data['both_post_ups']}}</td>
<td>{{data['u2_only_post_ups']}} ({{data['u2_post_ups_unique']}}%)</td>
</tr>
<tr>
<td><b>Post Downvotes</b></td>
<td>{{data['u1_only_post_downs']}} ({{data['u1_post_downs_unique']}}%)</td>
<td>{{data['both_post_downs']}}</td>
<td>{{data['u2_only_post_downs']}} ({{data['u2_post_downs_unique']}}%)</td>
</tr>
<tr>
<td><b>Comment Upvotes</b></td>
<td>{{data['u1_only_comment_ups']}} ({{data['u1_comment_ups_unique']}}%)</td>
<td>{{data['both_comment_ups']}}</td>
<td>{{data['u2_only_comment_ups']}} ({{data['u2_comment_ups_unique']}}%)</td>
</tr>
<tr>
<td><b>Comment Downvotes</b></td>
<td>{{data['u1_only_comment_downs']}} ({{data['u1_comment_downs_unique']}}%)</td>
<td>{{data['both_comment_downs']}}</td>
<td>{{data['u2_only_comment_downs']}} ({{data['u2_comment_downs_unique']}}%)</td>
</tr>
</table>
<h2>Link Accounts</h2>
{% if u2 in u1.alts %}
<p>Accounts are known alts of eachother.</p>
{% else %}
<p>Two accounts controlled by different people should have most uniqueness percentages at or above 70-80%</p>
<p>A sockpuppet account will have its uniqueness percentages significantly lower.</p>
<a href="javascript:void(0)" class="btn btn-secondary" onclick="document.getElementById('linkbtn').classList.toggle('d-none');">Link Accounts</a>
<form action="/admin/link_accounts" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input type="hidden" name="u1" value="{{u1.id}}">
<input type="hidden" name="u2" value="{{u2.id}}">
<input type="submit" id="linkbtn" class="btn btn-primary d-none" value="Confirm Link: {{u1.username}} and {{u2.username}}">
</form>
{% endif %}
{% endif %}
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}}</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<pre>
</pre>
<h5>Vote Info</h5>
<form action="/admin/alt_votes" method="get" class="mb-6">
<label for="link-input">Usernames</label>
<input id="link-input" type="text" class="form-control mb-2" name="u1" value="{{u1.username if u1 else ''}}" placeholder="User 1">
<input id="link-input" type="text" class="form-control mb-2" name="u2" value="{{u2.username if u2 else ''}}" placeholder="User 2">
<input type="submit" value="Submit" class="btn btn-primary">
</form>
{% if u1 and u2 %}
<h2>Analysis</h2>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th></th>
<th>@{{u1.username}} only(% unique)</th>
<th>Both</th>
<th>@{{u2.username}} only (% unique)</th>
</tr>
</thead>
<tr>
<td><b>Post Upvotes</b></td>
<td>{{data['u1_only_post_ups']}} ({{data['u1_post_ups_unique']}}%)</td>
<td>{{data['both_post_ups']}}</td>
<td>{{data['u2_only_post_ups']}} ({{data['u2_post_ups_unique']}}%)</td>
</tr>
<tr>
<td><b>Post Downvotes</b></td>
<td>{{data['u1_only_post_downs']}} ({{data['u1_post_downs_unique']}}%)</td>
<td>{{data['both_post_downs']}}</td>
<td>{{data['u2_only_post_downs']}} ({{data['u2_post_downs_unique']}}%)</td>
</tr>
<tr>
<td><b>Comment Upvotes</b></td>
<td>{{data['u1_only_comment_ups']}} ({{data['u1_comment_ups_unique']}}%)</td>
<td>{{data['both_comment_ups']}}</td>
<td>{{data['u2_only_comment_ups']}} ({{data['u2_comment_ups_unique']}}%)</td>
</tr>
<tr>
<td><b>Comment Downvotes</b></td>
<td>{{data['u1_only_comment_downs']}} ({{data['u1_comment_downs_unique']}}%)</td>
<td>{{data['both_comment_downs']}}</td>
<td>{{data['u2_only_comment_downs']}} ({{data['u2_comment_downs_unique']}}%)</td>
</tr>
</table>
<h2>Link Accounts</h2>
{% if u2 in u1.alts %}
<p>Accounts are known alts of eachother.</p>
{% else %}
<p>Two accounts controlled by different people should have most uniqueness percentages at or above 70-80%</p>
<p>A sockpuppet account will have its uniqueness percentages significantly lower.</p>
<a href="javascript:void(0)" class="btn btn-secondary" onclick="document.getElementById('linkbtn').classList.toggle('d-none');">Link Accounts</a>
<form action="/admin/link_accounts" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input type="hidden" name="u1" value="{{u1.id}}">
<input type="hidden" name="u2" value="{{u2.id}}">
<input type="submit" id="linkbtn" class="btn btn-primary d-none" value="Confirm Link: {{u1.username}} and {{u2.username}}">
</form>
{% endif %}
{% endif %}
{% endblock %}

142
files/templates/admin/app.html 100644 → 100755
View File

@ -1,72 +1,72 @@
{% extends "default.html" %}
{% block title %}
<title>API App Administration</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="over18">{{app.app_name}}</label>
</div>
<div class="body w-lg-100">
<label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label>
<input id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly>
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label>
<input id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly>
<label for="edit-{{app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
<input id="edit-{{app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{app.redirect_uri}}" readonly="readonly">
<label for="edit-{{app.id}}-desc" class="mb-0 w-lg-25">Description</label>
<textarea form="edit-app-{{app.id}}" class="form-control" name="description" id="edit-{{app.id}}-desc" maxlength="256" readonly="readonly">{{app.description}}</textarea>
</div>
</div>
<div class="footer">
<div class="d-flex">
{% if not app.client_id%}
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/approve/{{app.id}}')">Approve</a>
<a href="javascript:void(0)" class="btn btn-secondary mr-0" onclick="post_toast('/admin/app/reject/{{app.id}}')">Reject</a>
{% else %}
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/revoke/{{app.id}}')">Revoke</a>
{% endif %}
</div>
</div>
</div>
</div>
{% if listing %}
{% include "submission_listing.html" %}
{% elif comments %}
{% include "comments.html" %}
{% endif %}
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text"></span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text"></span>
</div>
</div>
{% extends "default.html" %}
{% block title %}
<title>API App Administration</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="over18">{{app.app_name}}</label>
</div>
<div class="body w-lg-100">
<label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label>
<input id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly>
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label>
<input id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly>
<label for="edit-{{app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
<input id="edit-{{app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{app.redirect_uri}}" readonly="readonly">
<label for="edit-{{app.id}}-desc" class="mb-0 w-lg-25">Description</label>
<textarea form="edit-app-{{app.id}}" class="form-control" name="description" id="edit-{{app.id}}-desc" maxlength="256" readonly="readonly">{{app.description}}</textarea>
</div>
</div>
<div class="footer">
<div class="d-flex">
{% if not app.client_id%}
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/approve/{{app.id}}')">Approve</a>
<a href="javascript:void(0)" class="btn btn-secondary mr-0" onclick="post_toast('/admin/app/reject/{{app.id}}')">Reject</a>
{% else %}
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/revoke/{{app.id}}')">Revoke</a>
{% endif %}
</div>
</div>
</div>
</div>
{% if listing %}
{% include "submission_listing.html" %}
{% elif comments %}
{% include "comments.html" %}
{% endif %}
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text"></span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
{% endblock %}

142
files/templates/admin/apps.html 100644 → 100755
View File

@ -1,72 +1,72 @@
{% extends "default.html" %}
{% block title %}
<title>API App Administration</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
{% for app in apps %}
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="over18"><a href="{{app.permalink}}" target="_blank">{{app.app_name}}</a></label>
</div>
<div class="body w-lg-100">
<label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label>
<input id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly>
<label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label>
<input id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly>
{% if app.client_id %}
<label for="edit-{{app.id}}-client-id" class="mb-0 w-lg-25">Client ID</label>
<input id="edit-{{app.id}}-client-id" class="form-control" type="text" name="name" value="{{app.client_id}}" readonly="readonly">
{% endif %}
<label for="edit-{{app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
<input id="edit-{{app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{app.redirect_uri}}" readonly="readonly">
<label for="edit-{{app.id}}-desc" class="mb-0 w-lg-25">Description</label>
<textarea form="edit-app-{{app.id}}" class="form-control" name="description" id="edit-{{app.id}}-desc" maxlength="256" readonly="readonly">{{app.description}}</textarea>
</div>
</div>
<div class="footer">
<div class="d-flex">
{% if not app.client_id %}
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/approve/{{app.id}}')">Approve</a>
<a href="javascript:void(0)" class="btn btn-secondary mr-0" onclick="post_toast('/admin/app/reject/{{app.id}}')">Reject</a>
{% else %}
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/revoke/{{app.id}}')">Revoke</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text"></span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text"></span>
</div>
</div>
{% extends "default.html" %}
{% block title %}
<title>API App Administration</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
{% for app in apps %}
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="over18"><a href="{{app.permalink}}" target="_blank">{{app.app_name}}</a></label>
</div>
<div class="body w-lg-100">
<label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label>
<input id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly>
<label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label>
<input id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly>
{% if app.client_id %}
<label for="edit-{{app.id}}-client-id" class="mb-0 w-lg-25">Client ID</label>
<input id="edit-{{app.id}}-client-id" class="form-control" type="text" name="name" value="{{app.client_id}}" readonly="readonly">
{% endif %}
<label for="edit-{{app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
<input id="edit-{{app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{app.redirect_uri}}" readonly="readonly">
<label for="edit-{{app.id}}-desc" class="mb-0 w-lg-25">Description</label>
<textarea form="edit-app-{{app.id}}" class="form-control" name="description" id="edit-{{app.id}}-desc" maxlength="256" readonly="readonly">{{app.description}}</textarea>
</div>
</div>
<div class="footer">
<div class="d-flex">
{% if not app.client_id %}
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/approve/{{app.id}}')">Approve</a>
<a href="javascript:void(0)" class="btn btn-secondary mr-0" onclick="post_toast('/admin/app/reject/{{app.id}}')">Reject</a>
{% else %}
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/revoke/{{app.id}}')">Revoke</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text"></span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
{% endblock %}

View File

@ -1,74 +1,74 @@
{% extends "default.html" %}
{% block title %}
<title>Badge Grant</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
{% if error %}
<div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<pre></pre>
<pre></pre>
<h5>Badge Grant</h5>
<form action="/admin/badge_grant", method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="input-username">Username</label><br>
<input id="input-username" class="form-control" type="text" name="username" required>
<table class="table table-striped">
<thead class="bg-primary text-white">
<tr>
<th scope="col">Select</th>
<th scope="col">Image</th>
<th scope="col">Name</th>
<th scope="col">Default Description</th>
</tr>
</thead>
<tbody>
{% for badge in badge_types %}
<tr>
<td><input type="radio" id="badge-{{badge.id}}" name="badge_id" value="{{badge.id}}"></td>
<td><label for="badge-{{badge.id}}"><img loading="lazy" src="{{badge.path}}" width="70px" height="70px"></label></td>
<td>{{badge.name}}</td>
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
<label for="input-url">URL</label><br>
<input id="input-url" class="form-control" type="text" name="url" placeholder="Optional">
<label for="input-description">Custom description</label><br>
<input id="input-description" class="form-control" type="text" name="description" placeholder="Leave blank for badge default">
<input class="btn btn-primary" type="submit">
</form>
{% endblock %}
{% extends "default.html" %}
{% block title %}
<title>Badge Grant</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
{% if error %}
<div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<pre></pre>
<pre></pre>
<h5>Badge Grant</h5>
<form action="/admin/badge_grant", method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="input-username">Username</label><br>
<input id="input-username" class="form-control" type="text" name="username" required>
<table class="table table-striped">
<thead class="bg-primary text-white">
<tr>
<th scope="col">Select</th>
<th scope="col">Image</th>
<th scope="col">Name</th>
<th scope="col">Default Description</th>
</tr>
</thead>
<tbody>
{% for badge in badge_types %}
<tr>
<td><input type="radio" id="badge-{{badge.id}}" name="badge_id" value="{{badge.id}}"></td>
<td><label for="badge-{{badge.id}}"><img loading="lazy" src="{{badge.path}}" width="70px" height="70px"></label></td>
<td>{{badge.name}}</td>
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
<label for="input-url">URL</label><br>
<input id="input-url" class="form-control" type="text" name="url" placeholder="Optional">
<label for="input-description">Custom description</label><br>
<input id="input-description" class="form-control" type="text" name="description" placeholder="Leave blank for badge default">
<input class="btn btn-primary" type="submit">
</form>
{% endblock %}

View File

@ -1,37 +1,37 @@
{% extends "default.html" %}
{% block title %}
<title>Banned Domains</title>
{% endblock %}
{% block content %}
<pre>
</pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">Domain</th>
<th style="font-weight:bold;">Ban reason</th>
</tr>
</thead>
{% for domain in banned_domains %}
<tr>
<td style="font-weight:bold;">{{domain.domain}}</td>
<td>{{domain.reason}}</td>
</tr>
{% endfor %}
</table>
<form action="/admin/banned_domains" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input name="domain" placeholder="Enter domain here.." class="form-control" required>
<input name="reason" placeholder="Enter ban reason here.." class="form-control">
<input id="ban-submit" type="submit" class="btn btn-primary" value="Toggle ban">
</form>
{% extends "default.html" %}
{% block title %}
<title>Banned Domains</title>
{% endblock %}
{% block content %}
<pre>
</pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">Domain</th>
<th style="font-weight:bold;">Ban reason</th>
</tr>
</thead>
{% for domain in banned_domains %}
<tr>
<td style="font-weight:bold;">{{domain.domain}}</td>
<td>{{domain.reason}}</td>
</tr>
{% endfor %}
</table>
<form action="/admin/banned_domains" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input name="domain" placeholder="Enter domain here.." class="form-control" required>
<input name="reason" placeholder="Enter ban reason here.." onchange="document.getElementById('ban-submit').disabled=false" class="form-control">
<input id="ban-submit" type="submit" class="btn btn-primary" value="Toggle ban" disabled>
</form>
{% endblock %}

View File

@ -1,25 +1,25 @@
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}}</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Statistic</th>
<th>Value</th>
</tr>
</thead>
{% for entry in data %}
<tr>
<td>{{entry}}</td>
<td>{{data[entry]}}</td>
</tr>
{% endfor %}
</table>
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}}</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} Help">
{% endblock %}
{% block content %}
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Statistic</th>
<th>Value</th>
</tr>
</thead>
{% for entry in data %}
<tr>
<td>{{entry}}</td>
<td>{{data[entry]}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -1,42 +0,0 @@
{% extends "default.html" %}
{% block title %}
<title>Image Ban</title>
<meta name="description" content="Image Ban">
{% endblock %}
{% block content %}
{% if existing %}
<p class="text-danger">Image already banned for: {{existing.ban_reason}}</p>
{% elif success %}
<p class="text-success">Image banned.</p>
{% endif %}
<pre>
</pre>
<h5>Perceptive Hash Image Ban</h5>
<p>Upload an image to add its hash to the ban list.</p>
<form action="/admin/image_ban" method="post" class="mb-6" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="img-input">Image Upload</label>
<input id="img-input" type="file" class="form-control-file mb-2" name="file">
<label for="img-input" class="mt-3">Ban Reason</label>
<select name="ban_reason" class="form-control" id="ban_reason">
<option disabled selected>Select Ban Reason</option>
<option value="Bestiality">Bestiality</option>
<option value="Child Sexual Abuse Material">CSAM</option>
<option value="Involuntary Pornography">Involuntary Pornography</option>
</select>
<label for="time-input" class="mt-3">Penalty</label>
<small>Enter the number of days to ban a user who attempts to upload this image</small>
<input id="time-input" class="form-control" type="text" name="ban_length" placeholder="Enter 0 for permanent ban" required>
<input type="submit" value="Ban Image" class="btn btn-primary mt-3">
</form>
{% endblock %}

View File

@ -1,77 +1,77 @@
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block postNav %}{% endblock %}
{% block fixedMobileBarJS %}
<script>
var prevScrollpos = window.pageYOffset;
window.onscroll = function () {
var currentScrollPos = window.pageYOffset;
if (prevScrollpos > currentScrollPos) {
document.getElementById("fixed-bar-mobile").style.top = "48px";
document.getElementById("navbar").classList.remove("shadow");
}
else if (currentScrollPos <= 125) {
document.getElementById("fixed-bar-mobile").style.top = "48px";
document.getElementById("navbar").classList.remove("shadow");
}
else {
document.getElementById("fixed-bar-mobile").style.top = "-48px";
document.getElementById("dropdownMenuSortBy").classList.remove('show');
document.getElementById("dropdownMenuFrom").classList.remove('show');
document.getElementById("navbar").classList.add("shadow");
}
prevScrollpos = currentScrollPos;
}
</script>
{% endblock %}
{% block title %}
<title>Image feed</title>
<meta name="description" content="on {{'SITE_NAME' | app_config}}">
{% endblock %}
{% block content %}
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block postNav %}{% endblock %}
{% block fixedMobileBarJS %}
<script>
var prevScrollpos = window.pageYOffset;
window.onscroll = function () {
var currentScrollPos = window.pageYOffset;
if (prevScrollpos > currentScrollPos) {
document.getElementById("fixed-bar-mobile").style.top = "48px";
document.getElementById("navbar").classList.remove("shadow");
}
else if (currentScrollPos <= 125) {
document.getElementById("fixed-bar-mobile").style.top = "48px";
document.getElementById("navbar").classList.remove("shadow");
}
else {
document.getElementById("fixed-bar-mobile").style.top = "-48px";
document.getElementById("dropdownMenuSortBy").classList.remove('show');
document.getElementById("dropdownMenuFrom").classList.remove('show');
document.getElementById("navbar").classList.add("shadow");
}
prevScrollpos = currentScrollPos;
}
</script>
{% endblock %}
{% block title %}
<title>Image feed</title>
<meta name="description" content="on {{'SITE_NAME' | app_config}}">
{% endblock %}
{% block content %}
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}

View File

@ -1,9 +1,9 @@
{% extends "mine.html" %}
{% block maincontent %}
<img loading="lazy" src="{{single_plot}}">
<img loading="lazy" src="{{multi_plot}}">
{% include "user_listing.html" %}
{% endblock %}
{% extends "mine.html" %}
{% block maincontent %}
<img loading="lazy" src="{{single_plot}}">
<img loading="lazy" src="{{multi_plot}}">
{% include "user_listing.html" %}
{% endblock %}
{% block navbar %}{% endblock %}

View File

@ -1,59 +1,59 @@
{% extends "admin/image_posts.html" %}
{% block title %}
<title>Removed Content</title>
<meta name="description" content="on {{'SITE_NAME' | app_config}}">
{% endblock %}
{% block content %}
<div class="row justify-content-around mx-lg-5 d-lg-none no-gutters">
<div class="col bg-light border-bottom rounded-md p-3">
<div class="profile-details">
<div class="media">
<div class="media-body">
<pre></pre>
<h5 class="h6 d-inline-block">Removed Posts</h5>
</div>
</div>
</div>
</div>
</div>
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}
{% extends "admin/image_posts.html" %}
{% block title %}
<title>Removed Content</title>
<meta name="description" content="on {{'SITE_NAME' | app_config}}">
{% endblock %}
{% block content %}
<div class="row justify-content-around mx-lg-5 d-lg-none no-gutters">
<div class="col bg-light border-bottom rounded-md p-3">
<div class="profile-details">
<div class="media">
<div class="media-body">
<pre></pre>
<h5 class="h6 d-inline-block">Removed Posts</h5>
</div>
</div>
</div>
</div>
</div>
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}

View File

@ -1,24 +1,24 @@
{% extends "admin/reported_posts.html" %}
{% block listing %}
<div class="posts">
{% with comments=listing %}
{% include "comments.html" %}
{% endwith %}
{% if not listing %}
<div class="row no-gutters">
<div class="col">
<div class="text-center py-7">
<div class="h4 p-2">There are no comments here (yet).</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% extends "admin/reported_posts.html" %}
{% block listing %}
<div class="posts">
{% with comments=listing %}
{% include "comments.html" %}
{% endwith %}
{% if not listing %}
<div class="row no-gutters">
<div class="col">
<div class="text-center py-7">
<div class="h4 p-2">There are no comments here (yet).</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,104 +1,104 @@
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block postNav %}
<div class="container-fluid bg-white sticky">
<div class="row border-bottom">
<div class="col">
<div class="container">
<div class="row bg-white">
<div class="col">
<div class="d-flex">
<ul class="nav post-nav mr-auto">
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/reported/posts" %} active{% endif %}" href="/admin/reported/posts">
<div>Posts</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/reported/comments" %} active{% endif %}" href="/admin/reported/comments">
<div>Comments</div>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block fixedMobileBarJS %}
<script>
var prevScrollpos = window.pageYOffset;
window.onscroll = function () {
var currentScrollPos = window.pageYOffset;
if (prevScrollpos > currentScrollPos) {
document.getElementById("fixed-bar-mobile").style.top = "48px";
document.getElementById("navbar").classList.remove("shadow");
}
else if (currentScrollPos <= 125) {
document.getElementById("fixed-bar-mobile").style.top = "48px";
document.getElementById("navbar").classList.remove("shadow");
}
else {
document.getElementById("fixed-bar-mobile").style.top = "-48px";
document.getElementById("dropdownMenuSortBy").classList.remove('show');
document.getElementById("dropdownMenuFrom").classList.remove('show');
document.getElementById("navbar").classList.add("shadow");
}
prevScrollpos = currentScrollPos;
}
</script>
{% endblock %}
{% block title %}
<title>Reported Posts</title>
<meta name="description" content="on {{'SITE_NAME' | app_config}}">
{% endblock %}
{% block content %}
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block postNav %}
<div class="container-fluid bg-white sticky">
<div class="row border-bottom">
<div class="col">
<div class="container">
<div class="row bg-white">
<div class="col">
<div class="d-flex">
<ul class="nav post-nav mr-auto">
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/reported/posts" %} active{% endif %}" href="/admin/reported/posts">
<div>Posts</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/reported/comments" %} active{% endif %}" href="/admin/reported/comments">
<div>Comments</div>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block fixedMobileBarJS %}
<script>
var prevScrollpos = window.pageYOffset;
window.onscroll = function () {
var currentScrollPos = window.pageYOffset;
if (prevScrollpos > currentScrollPos) {
document.getElementById("fixed-bar-mobile").style.top = "48px";
document.getElementById("navbar").classList.remove("shadow");
}
else if (currentScrollPos <= 125) {
document.getElementById("fixed-bar-mobile").style.top = "48px";
document.getElementById("navbar").classList.remove("shadow");
}
else {
document.getElementById("fixed-bar-mobile").style.top = "-48px";
document.getElementById("dropdownMenuSortBy").classList.remove('show');
document.getElementById("dropdownMenuFrom").classList.remove('show');
document.getElementById("navbar").classList.add("shadow");
}
prevScrollpos = currentScrollPos;
}
</script>
{% endblock %}
{% block title %}
<title>Reported Posts</title>
<meta name="description" content="on {{'SITE_NAME' | app_config}}">
{% endblock %}
{% block content %}
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}

60
files/templates/admin/rules.html 100644 → 100755
View File

@ -1,31 +1,31 @@
{% extends "default.html" %}
{% block pagetitle %}Edit {{'SITE_NAME' | app_config}} rules{% endblock %}
{% block content %}
<div class="row my-5">
<div class="col col-md-8">
<div class="settings">
<div id="description">
<h2>Edit rules</h2>
<p>Your rules page will be publicly visible at <a href="/rules">{{'/rules'|full_link}}</a>.</p>
<p class="text-small text-muted">Supports <a href="https://www.markdownguide.org/basic-syntax">markdown syntax</a>.</p>
</div>
<div class="body d-lg-flex">
<div class="w-lg-100">
<form id="profile-settings" action="/admin/rules" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea class="form-control rounded" id="bio-text" aria-label="With textarea" placeholder="Site rules" rows="50" name="rules" form="profile-settings">{% if rules %}{{ rules }}{% endif %}</textarea>
<div class="d-flex mt-2">
<input class="btn btn-primary ml-auto" type="submit" value="Save">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% extends "default.html" %}
{% block pagetitle %}Edit {{'SITE_NAME' | app_config}} rules{% endblock %}
{% block content %}
<div class="row my-5">
<div class="col col-md-8">
<div class="settings">
<div id="description">
<h2>Edit rules</h2>
<p>Your rules page will be publicly visible at <a href="/rules">{{'/rules'|full_link}}</a>.</p>
<p class="text-small text-muted">Supports <a href="https://www.markdownguide.org/basic-syntax">markdown syntax</a>.</p>
</div>
<div class="body d-lg-flex">
<div class="w-lg-100">
<form id="profile-settings" action="/admin/rules" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea class="form-control rounded" id="bio-text" aria-label="With textarea" placeholder="Site rules" rows="50" name="rules" form="profile-settings">{% if rules %}{{ rules }}{% endif %}</textarea>
<div class="d-flex mt-2">
<input class="btn btn-primary ml-auto" type="submit" value="Save">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,71 +1,71 @@
{% extends "default.html" %}
{% block title %}
<title>Grant User Award</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
{% if error %}
<div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<pre></pre>
<pre></pre>
<h5>User Award Grant</h5>
<form action="/admin/user_award", method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="input-username">Username</label><br>
<input id="input-username" class="form-control mb-3" type="text" name="username" required>
<table class="table table-striped">
<thead class="bg-primary text-white">
<tr>
<th scope="col">Icon</th>
<th scope="col">Title</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
{% for a in awards %}
<tr>
<td><i class="{{a['icon']}} {{a['color']}}" style="font-size: 30px"></i></td>
<td style="font-weight: bold">{{a['title']}}</td>
<td><input type="number" class="form-control" name="{{a['kind']}}" value="0" placeholder="enter amount" /></td>
</tr>
{% endfor %}
</table>
<input class="btn btn-primary mt-3" type="submit" value="Grant Awards">
</form>
<pre></pre>
{% if v.id in [1,12,28,29,747,995,1480] %}
<div><a class="btn btn-success" href="javascript:void(0)" onclick="post_toast('/admin/monthly')">Grant Monthly Awards</a></div>
{% endif %}
{% extends "default.html" %}
{% block title %}
<title>Grant User Award</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
{% if error %}
<div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<pre></pre>
<pre></pre>
<h5>User Award Grant</h5>
<form action="/admin/user_award", method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="input-username">Username</label><br>
<input id="input-username" class="form-control mb-3" type="text" name="username" required>
<table class="table table-striped">
<thead class="bg-primary text-white">
<tr>
<th scope="col">Icon</th>
<th scope="col">Title</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
{% for a in awards %}
<tr>
<td><i class="{{a['icon']}} {{a['color']}}" style="font-size: 30px"></i></td>
<td style="font-weight: bold">{{a['title']}}</td>
<td><input type="number" class="form-control" name="{{a['kind']}}" value="0" placeholder="enter amount" /></td>
</tr>
{% endfor %}
</table>
<input class="btn btn-primary mt-3" type="submit" value="Grant Awards">
</form>
<pre></pre>
{% if v.id in [1,12,28,29,747,995,1480] %}
<div><a class="btn btn-success" href="javascript:void(0)" onclick="post_toast('/admin/monthly')">Grant Monthly Awards</a></div>
{% endif %}
{% endblock %}

44
files/templates/admins.html 100644 → 100755
View File

@ -1,23 +1,23 @@
{% extends "settings2.html" %}
{% block pagetitle %}Admins{% endblock %}
{% block content %}
<pre class="d-none d-md-inline-block"></pre>
<h5 style="font-weight:bold;">Admins</h5>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">Name</th>
<th style="font-weight:bold; text-align:right;">Coins</th>
</tr>
</thead>
{% for user in admins %}
<tr>
<td><a style="color:#{{user.namecolor}}; font-weight:bold;" href="/@{{user.username}}"><img loading="lazy" src="/uid/{{user.id}}/pic/profile" class="profile-pic-20 mr-1"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}};"{% endif %}>{{user.username}}</span></a></td>
<td style="font-weight:bold; text-align:right;">{{user.coins}}</td>
</tr>
{% endfor %}
</table>
{% extends "settings2.html" %}
{% block pagetitle %}Admins{% endblock %}
{% block content %}
<pre class="d-none d-md-inline-block"></pre>
<h5 style="font-weight:bold;">Admins</h5>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">Name</th>
<th style="font-weight:bold; text-align:right;">Coins</th>
</tr>
</thead>
{% for user in admins %}
<tr>
<td><a style="color:#{{user.namecolor}}; font-weight:bold;" href="/@{{user.username}}"><img loading="lazy" src="/uid/{{user.id}}/pic/profile" class="profile-pic-20 mr-1"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}};"{% endif %}>{{user.username}}</span></a></td>
<td style="font-weight:bold; text-align:right;">{{user.coins}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

164
files/templates/api.html 100644 → 100755
View File

@ -1,84 +1,82 @@
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}} - API</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} API Guide">
{% endblock %}
{% block content %}
<img class="in-comment-image rounded-sm my-2" data-src="https://media.giphy.com/media/c6Wwc9cT05vMdhyTcM/giphy.webp" loading="lazy" height="100px" rel="nofollow noopener noreferrer" data-placeholder-background="red" style="max-height: 100px; max-width: 100%;">
<pre>
</pre>
<h1>API Guide for Bots</h1>
<pre></pre>
<p>This page explains how to obtain and use an access token. </p>
<h2>Step 1: Create your Application</h2>
<p>In the <a href="/settings/apps">apps tab of Drama settings</a>, fill in and submit the form to request an access token. You will need:</p>
<ul>
<li>an application name</li>
<li>a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).</li>
<li>a brief description of what your bot is intended to do</li>
</ul>
<p>Don't worry too much about accuracy; you will be able to change all of these later.</p>
<p>Drama administrators will review and approve or deny your request for an access token. You'll know when your request has been approved when you get a private message with an access token tied to your account.</p>
<p>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!</p>
<h2>Step 2: Using the Access Token</h2>
<p>To use the access token, include the following header in subsequent API requests to Drama: <code>Authorization: access_token_goes_here</code></p>
<p>Python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here", "User-Agent": "sex"}
url="https://rdrama.net/@carpathianflorist"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a large JSON representation of the posts posted by @carpathianflorist</p>
<pre>
</pre>
<h1>API Guide for Applications</h1>
<pre></pre>
<p>The OAuth2 authorization flow is used to enable users to authorize third-party applications to access their Drama account without having to provide their login information to the application.</p>
<p>This page explains how to obtain API application keys, how to prompt a user for authorization, and how to obtain and use access tokens. </p>
<h2>Step 1: Create your Application</h2>
<p>In the <a href="/settings/apps">apps tab of Drama settings</a>, fill in and submit the form to request new API keys. You will need:</p>
<ul>
<li>an application name</li>
<li>a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).</li>
<li>a brief description of what your application is intended to do</li>
</ul>
<p>Don't worry too much about accuracy; you will be able to change all of these later.</p>
<p>Drama administrators will review and approve or deny your request for API keys. You'll know when your request has been approved when you get a private message with an access token tied to your account.</p>
<p>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!</p>
<h2>Step 2: Prompt Your User for Authorization</h2>
<p>Send your user to <code>https://rdrama.net/authorize/?client_id=YOUR_CLIENT_ID</code></p>
<p>If done correctly, the user will see that your application wants to access their Drama account, and be prompted to approve or deny the request.</p>
<h2>Step 3: Catch the redirect</h2>
<p>The user clicks "Authorize". Drama will redirect the user's browser to GET the designated redirect URI. The access token URL parameter will be included in the redirect, which your server should process.</p>
<h2>Step 4: Using the Access Token</h2>
<p>To use the access token, include the following header in subsequent API requests to Drama: <code>Authorization: access_token_goes_here</code></p>
<p>Python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here", "User-Agent": "sex"}
url="https://rdrama.net/@carpathianflorist"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a large JSON representation of the submissions submitted by @carpathianflorist</p>
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}} - API</title>
<meta name="description" content="{{'SITE_NAME' | app_config}} API Guide">
{% endblock %}
{% block content %}
<pre>
</pre>
<h1>API Guide for Bots</h1>
<pre></pre>
<p>This page explains how to obtain and use an access token. </p>
<h2>Step 1: Create your Application</h2>
<p>In the <a href="/settings/apps">apps tab of Drama settings</a>, fill in and submit the form to request an access token. You will need:</p>
<ul>
<li>an application name</li>
<li>a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).</li>
<li>a brief description of what your bot is intended to do</li>
</ul>
<p>Don't worry too much about accuracy; you will be able to change all of these later.</p>
<p>Drama administrators will review and approve or deny your request for an access token. You'll know when your request has been approved when you get a private message with an access token tied to your account.</p>
<p>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!</p>
<h2>Step 2: Using the Access Token</h2>
<p>To use the access token, include the following header in subsequent API requests to Drama: <code>Authorization: access_token_goes_here</code></p>
<p>Python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="https://rdrama.net/@carpathianflorist"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a large JSON representation of the posts posted by @carpathianflorist</p>
<pre>
</pre>
<h1>API Guide for Applications</h1>
<pre></pre>
<p>The OAuth2 authorization flow is used to enable users to authorize third-party applications to access their Drama account without having to provide their login information to the application.</p>
<p>This page explains how to obtain API application keys, how to prompt a user for authorization, and how to obtain and use access tokens. </p>
<h2>Step 1: Create your Application</h2>
<p>In the <a href="/settings/apps">apps tab of Drama settings</a>, fill in and submit the form to request new API keys. You will need:</p>
<ul>
<li>an application name</li>
<li>a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).</li>
<li>a brief description of what your application is intended to do</li>
</ul>
<p>Don't worry too much about accuracy; you will be able to change all of these later.</p>
<p>Drama administrators will review and approve or deny your request for API keys. You'll know when your request has been approved when you get a private message with an access token tied to your account.</p>
<p>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!</p>
<h2>Step 2: Prompt Your User for Authorization</h2>
<p>Send your user to <code>https://rdrama.net/authorize/?client_id=YOUR_CLIENT_ID</code></p>
<p>If done correctly, the user will see that your application wants to access their Drama account, and be prompted to approve or deny the request.</p>
<h2>Step 3: Catch the redirect</h2>
<p>The user clicks "Authorize". Drama will redirect the user's browser to GET the designated redirect URI. The access token URL parameter will be included in the redirect, which your server should process.</p>
<h2>Step 4: Using the Access Token</h2>
<p>To use the access token, include the following header in subsequent API requests to Drama: <code>Authorization: access_token_goes_here</code></p>
<p>Python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="https://rdrama.net/@carpathianflorist"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a large JSON representation of the submissions submitted by @carpathianflorist</p>
{% endblock %}

204
files/templates/authforms.html 100644 → 100755
View File

@ -1,102 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="{% block pagedesc %}{{'SITE_NAME' | app_config}}{% endblock %}">
<meta name="author" content="">
<title>{% block pagetitle %}{{'SITE_NAME' | app_config}}{% endblock %}</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600&display=swap" rel="stylesheet">
{% if v %}
<link rel="stylesheet" href="/assets/css/{{v.theme}}_{{v.themecolor}}.css?v=61">
{% if v.agendaposter %}<link rel="stylesheet" href="/assets/css/agendaposter.css?v=61">{% endif %}
{% else %}
<link rel="stylesheet" href="/assets/css/{{'DEFAULT_THEME' | app_config}}.css?v=61">
{% endif %}
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid">
<button class="navbar-toggler d-none" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="mb-5">
<a href="/" class="text-decoration-none"><span class="h3 text-primary">{{'SITE_NAME' | app_config}}</span></a>
</div>
<h1 class="h2">{% block authtitle %}{% endblock %}</h1>
<p class="text-muted mb-md-5">{% block authtext %}{% endblock %}</p>
{% if error %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-info-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img loading="lazy" class="splash-img" src="/assets/images/{{'SITE_NAME' | app_config}}/cover.webp"></img>
</div>
</div>
</div>
</div>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="{% block pagedesc %}{{'SITE_NAME' | app_config}}{% endblock %}">
<meta name="author" content="">
<title>{% block pagetitle %}{{'SITE_NAME' | app_config}}{% endblock %}</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600&display=swap" rel="stylesheet">
{% if v %}
<style>:root{--primary:#{{v.themecolor}}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=80"><link rel="stylesheet" href="/assets/css/{{v.theme}}.css?v=80">
{% if v.agendaposter %}<link rel="stylesheet" href="/assets/css/agendaposter.css?v=80">{% elif v.css %}<link rel="stylesheet" href="/@{{v.username}}/css">{% endif %}
{% else %}
<style>:root{--primary:#{{'DEFAULT_COLOR' | app_config}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=80"><link rel="stylesheet" href="/assets/css/{{'DEFAULT_THEME' | app_config}}.css?v=80">
{% endif %}
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid">
<button class="navbar-toggler d-none" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="mb-5">
<a href="/" class="text-decoration-none"><span class="h3 text-primary">{{'SITE_NAME' | app_config}}</span></a>
</div>
<h1 class="h2">{% block authtitle %}{% endblock %}</h1>
<p class="text-muted mb-md-5">{% block authtext %}{% endblock %}</p>
{% if error %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-info-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img loading="lazy" class="splash-img" src="/assets/images/{{'SITE_NAME' | app_config}}/cover.gif"></img>
</div>
</div>
</div>
</div>
</body>
</html>

138
files/templates/award_modal.html 100644 → 100755
View File

@ -1,70 +1,70 @@
<script src="/assets/js/award_modal.js?v=50"></script>
<div class="modal fade" id="awardModal" tabindex="-1" role="dialog" aria-labelledby="awardModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Give Award</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div id="awardModalBody" class="modal-body">
<form id="awardTarget" class="pt-3 pb-0" action="" method="post">
<div class="card-columns award-columns awards-wrapper">
{% for award in v.user_awards %}
{% if award.owned %}
<a href="javascript:void(0)" id="{{award.kind}}" class="card" onclick="bruh('{{award.kind}}')">
{% else %}
<a href="javascript:void(0)" id="{{award.kind}}" class="card disabled">
{% endif %}
<i class="{{award.icon}} {{award.color}}"></i>
<div class="pt-2" style="font-weight: bold; font-size: 14px; color:#E1E1E1">{{award.title}}</div>
<div class="text-muted">{{award.owned}} owned</div>
</a>
{% endfor %}
</div>
<label for="note" class="pt-4">Note (optional):</label>
<input id="kind" name="kind" value="" hidden>
<textarea id="note" name="note" class="form-control" placeholder="Note to include in award notification"></textarea>
<input id="giveaward" class="btn btn-primary" style="float:right" type="submit" value="Give Award" disabled>
</form>
</div>
</div>
</div>
</div>
<style>
.awards-wrapper input[type="radio"] {
display: none;
}
.awards-wrapper a {
cursor: pointer;
padding: 15px !important;
text-align: center;
text-transform: none!important;
}
.awards-wrapper a i {
font-size: 25px;
}
.awards-wrapper a.disabled {
opacity: 0.6;
}
.awards-wrapper a:hover, .picked {
background-color: var(--primary)!important;
}
.awards-wrapper input[type="radio"]:checked+a {
background-color: var(--primary)!important;
}
@media (min-width: 767.98px) {
.award-columns {
column-count: 3 !important;
}
}
<script src="/assets/js/award_modal.js?v=53"></script>
<div class="modal fade" id="awardModal" tabindex="-1" role="dialog" aria-labelledby="awardModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Give Award</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div id="awardModalBody" class="modal-body">
<form id="awardTarget" class="pt-3 pb-0" action="" method="post">
<div class="card-columns award-columns awards-wrapper">
{% for award in v.user_awards %}
{% if award.owned %}
<a href="javascript:void(0)" id="{{award.kind}}" class="card" onclick="bruh('{{award.kind}}')">
{% else %}
<a href="javascript:void(0)" id="{{award.kind}}" class="card disabled">
{% endif %}
<i class="{{award.icon}} {{award.color}}"></i>
<div class="pt-2" style="font-weight: bold; font-size: 14px; color:#E1E1E1">{{award.title}}</div>
<div class="text-muted">{{award.owned}} owned</div>
</a>
{% endfor %}
</div>
<label for="note" class="pt-4">Note (optional):</label>
<input id="kind" name="kind" value="" hidden>
<textarea id="note" name="note" class="form-control" placeholder="Note to include in award notification"></textarea>
<input id="giveaward" class="btn btn-primary" style="float:right" type="submit" value="Give Award" disabled>
</form>
</div>
</div>
</div>
</div>
<style>
.awards-wrapper input[type="radio"] {
display: none;
}
.awards-wrapper a {
cursor: pointer;
padding: 15px !important;
text-align: center;
text-transform: none!important;
}
.awards-wrapper a i {
font-size: 25px;
}
.awards-wrapper a.disabled {
opacity: 0.6;
}
.awards-wrapper a:hover, .picked {
background-color: var(--primary)!important;
}
.awards-wrapper input[type="radio"]:checked+a {
background-color: var(--primary)!important;
}
@media (min-width: 767.98px) {
.award-columns {
column-count: 3 !important;
}
}
</style>

142
files/templates/badges.html 100644 → 100755
View File

@ -1,72 +1,72 @@
{% extends "default.html" %}
{% block content %}
<pre>
</pre>
<h1>User Badges</h1>
<div>This page describes the requirements for obtaining all profile badges.</div>
<div>Badges are sorted into bronze, silver, gold, and diamond tiers, based on the relative difficulty of obtaining them.</div>
<h2 class="mt-3">Unlockable Badges</h2>
<div>These badges are automatically granted through different kinds of activity on {{'SITE_NAME' | app_config}}.</div>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Name</th>
<th>Image</th>
<th>Description</th>
</tr>
</thead>
{% for badge in badges if badge.kind==1 %}
<tr>
<td>{{badge.name}}</td>
<td><img loading="lazy" src="{{badge.path}}" style="width:50px;height:50px">
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
<h2 class="mt-3">Granted Badges</h2>
<div>These badges can be granted by admins.</div>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Name</th>
<th>Image</th>
<th>Description</th>
</tr>
</thead>
{% for badge in badges if badge.kind==3 %}
<tr>
<td>{{badge.name}}</td>
<td><img loading="lazy" src="{{badge.path}}" style="width:50px;height:50px">
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
<h2 class="mt-3">Unobtainable Badges</h2>
<div>There is no way to acquire these badges if you don't already have them.</div>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Name</th>
<th>Image</th>
<th>Description</th>
</tr>
</thead>
{% for badge in badges if badge.kind==4 %}
<tr>
<td>{{badge.name}}</td>
<td><img loading="lazy" src="{{badge.path}}" style="width:50px;height:50px">
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
{% extends "default.html" %}
{% block content %}
<pre>
</pre>
<h1>User Badges</h1>
<div>This page describes the requirements for obtaining all profile badges.</div>
<div>Badges are sorted into bronze, silver, gold, and diamond tiers, based on the relative difficulty of obtaining them.</div>
<h2 class="mt-3">Unlockable Badges</h2>
<div>These badges are automatically granted through different kinds of activity on {{'SITE_NAME' | app_config}}.</div>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Name</th>
<th>Image</th>
<th>Description</th>
</tr>
</thead>
{% for badge in badges if badge.kind==1 %}
<tr>
<td>{{badge.name}}</td>
<td><img loading="lazy" src="{{badge.path}}" width=50 height=50>
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
<h2 class="mt-3">Granted Badges</h2>
<div>These badges can be granted by admins.</div>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Name</th>
<th>Image</th>
<th>Description</th>
</tr>
</thead>
{% for badge in badges if badge.kind==3 %}
<tr>
<td>{{badge.name}}</td>
<td><img loading="lazy" src="{{badge.path}}" width=50 height=50>
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
<h2 class="mt-3">Unobtainable Badges</h2>
<div>There is no way to acquire these badges if you don't already have them.</div>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Name</th>
<th>Image</th>
<th>Description</th>
</tr>
</thead>
{% for badge in badges if badge.kind==4 %}
<tr>
<td>{{badge.name}}</td>
<td><img loading="lazy" src="{{badge.path}}" width=50 height=50>
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

120
files/templates/ban_modal.html 100644 → 100755
View File

@ -1,61 +1,61 @@
<script>
const banModal = function(link, id, name) {
document.getElementById("banModalTitle").innerHTML = `Ban @${name}`;
document.getElementById("ban-modal-link").value = link;
document.getElementById("banUserButton").innerHTML = `Ban @${name}`;
document.getElementById("banUserButton").onclick = function() {
let fd = new FormData(document.getElementById("banModalForm"));
fd.append("formkey", formkey());
let xhr = new XMLHttpRequest();
xhr.open("POST", `/ban_user/${id}?form`, true);
xhr.withCredentials = true;
xhr.onload = function(){
var myToast = new bootstrap.Toast(document.getElementById('toast-post-success'));
myToast.show();
document.getElementById('toast-post-success-text').innerHTML = `@${name} banned`;
}
xhr.send(fd);
}
};
</script>
<div class="modal fade" id="banModal" tabindex="-1" role="dialog" aria-labelledby="banModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header pt-3">
<h5 id="banModalTitle"></h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div class="modal-body" id="ban-modal-body">
<form id="banModalForm">
<input type="hidden" name="formkey" value="{{v.formkey}}" />
<label for="ban-modal-link">Public ban reason (optional)</label>
<textarea name="reason" form="banModalForm" class="form-control" id="ban-modal-link" aria-label="With textarea" placeholder="Enter reason"></textarea>
<label for="days" class="mt-3">Duration days</label>
<input type="number" step="any" name="days" id="days" class="form-control" placeholder="leave blank for permanent" />
<div class="custom-control custom-switch mt-3">
<input type="checkbox" class="custom-control-input" id="alts" name="alts">
<label class="custom-control-label" for="alts">Ban known alts</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="banUserButton" class="btn btn-danger" data-bs-dismiss="modal"></button>
</div>
</div>
</div>
<script>
const banModal = function(link, id, name) {
document.getElementById("banModalTitle").innerHTML = `Ban @${name}`;
document.getElementById("ban-modal-link").value = link;
document.getElementById("banUserButton").innerHTML = `Ban @${name}`;
document.getElementById("banUserButton").onclick = function() {
let fd = new FormData(document.getElementById("banModalForm"));
fd.append("formkey", formkey());
let xhr = new XMLHttpRequest();
xhr.open("POST", `/ban_user/${id}?form`, true);
xhr.withCredentials = true;
xhr.onload = function(){
var myToast = new bootstrap.Toast(document.getElementById('toast-post-success'));
myToast.show();
document.getElementById('toast-post-success-text').innerHTML = `@${name} banned`;
}
xhr.send(fd);
}
};
</script>
<div class="modal fade" id="banModal" tabindex="-1" role="dialog" aria-labelledby="banModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header pt-3">
<h5 id="banModalTitle"></h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div class="modal-body" id="ban-modal-body">
<form id="banModalForm">
<input type="hidden" name="formkey" value="{{v.formkey}}" />
<label for="ban-modal-link">Public ban reason (optional)</label>
<textarea name="reason" form="banModalForm" class="form-control" id="ban-modal-link" aria-label="With textarea" placeholder="Enter reason"></textarea>
<label for="days" class="mt-3">Duration days</label>
<input type="number" step="any" name="days" id="days" class="form-control" placeholder="leave blank for permanent" />
<div class="custom-control custom-switch mt-3">
<input type="checkbox" class="custom-control-input" id="alts" name="alts">
<label class="custom-control-label" for="alts">Ban known alts</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="banUserButton" class="btn btn-danger" data-bs-dismiss="modal"></button>
</div>
</div>
</div>
</div>

46
files/templates/banned.html 100644 → 100755
View File

@ -1,23 +1,23 @@
{% extends "settings2.html" %}
{% block content %}
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">#</th>
<th style="font-weight:bold;">Name</th>
<th style="font-weight:bold;">Ban reason</th>
<th style="font-weight:bold;">Banned by</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td style="font-weight:bold;">{{users.index(user)+1}}</td>
<td><a style="color:#{{user.namecolor}}; font-weight:bold;" href="/@{{user.username}}"><img loading="lazy" src="/uid/{{user.id}}/pic/profile" class="profile-pic-20 mr-1"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}};"{% endif %}>{{user.username}}</span></a></td>
<td style="font-weight:bold;">{% if user.ban_reason %}{{user.ban_reason}}{% endif %}</td>
<td style="font-weight:bold;" href="/@{{user.banned_by.username}}"><img loading="lazy" src="/uid/{{user.banned_by.id}}/pic/profile" class="profile-pic-20 mr-1"><span {% if user.banned_by.patron %}class="patron" style="background-color:#{{user.banned_by.namecolor}};"{% endif %}>{{user.banned_by.username}}</span></a></td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% extends "settings2.html" %}
{% block content %}
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">#</th>
<th style="font-weight:bold;">Name</th>
<th style="font-weight:bold;">Ban reason</th>
<th style="font-weight:bold;">Banned by</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td style="font-weight:bold;">{{users.index(user)+1}}</td>
<td><a style="color:#{{user.namecolor}}; font-weight:bold;" href="/@{{user.username}}"><img loading="lazy" src="/uid/{{user.id}}/pic/profile" class="profile-pic-20 mr-1"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}};"{% endif %}>{{user.username}}</span></a></td>
<td style="font-weight:bold;">{% if user.ban_reason %}{{user.ban_reason}}{% endif %}</td>
<td style="font-weight:bold;" href="/@{{user.banned_by.username}}"><img loading="lazy" src="/uid/{{user.banned_by.id}}/pic/profile" class="profile-pic-20 mr-1"><span {% if user.banned_by.patron %}class="patron" style="background-color:#{{user.banned_by.namecolor}};"{% endif %}>{{user.banned_by.username}}</span></a></td>
</tr>
{% endfor %}
</table>
{% endblock %}

44
files/templates/blocks.html 100644 → 100755
View File

@ -1,22 +1,22 @@
{% extends "settings2.html" %}
{% block pagetitle %}Blocks{% endblock %}
{% block content %}
<h1> Blocks</h1>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">User</th>
<th style="font-weight:bold;">Target</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td><a style="font-weight:bold;color:#{{user.namecolor}}; " href="/@{{user.username}}"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}};"{% endif %}>{{user.username}}</span></a></td>
<td><a style="font-weight:bold;color:#{{targets[loop.index-1].namecolor}}; " href="/@{{targets[loop.index-1].username}}"><span {% if targets[loop.index-1].patron %}class="patron" style="background-color:#{{targets[loop.index-1].namecolor}};"{% endif %}>{{targets[loop.index-1].username}}</span></a></td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% extends "settings2.html" %}
{% block pagetitle %}Blocks{% endblock %}
{% block content %}
<h1> Blocks</h1>
<pre></pre>
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th style="font-weight:bold;">User</th>
<th style="font-weight:bold;">Target</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td><a style="font-weight:bold;color:#{{user.namecolor}}; " href="/@{{user.username}}"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}};"{% endif %}>{{user.username}}</span></a></td>
<td><a style="font-weight:bold;color:#{{targets[loop.index-1].namecolor}}; " href="/@{{targets[loop.index-1].username}}"><span {% if targets[loop.index-1].patron %}class="patron" style="background-color:#{{targets[loop.index-1].namecolor}};"{% endif %}>{{targets[loop.index-1].username}}</span></a></td>
</tr>
{% endfor %}
</table>
{% endblock %}

214
files/templates/changelog.html 100644 → 100755
View File

@ -1,108 +1,108 @@
{% extends "settings2.html" %}
{% block pagetitle %}Changelog{% endblock %}
{% block desktopBanner %}
<script src="/assets/js/changelog.js?v=50"></script>
<div class="row" style="overflow: visible;padding-top:5px;">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
{% block navbar %}
<div class="font-weight-bold py-3"></div>
<div class="d-flex align-items-center sortingbarmargin">
<div class="text-small font-weight-bold mr-2"></div>
<div class="dropdown dropdown-actions">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% if t=="day" %}<i class="fas fa-calendar-day mr-1"></i>{% endif %}
{% if t=="week" %}<i class="fas fa-calendar-week mr-1"></i>{% endif %}
{% if t=="month" %}<i class="fas fa-calendar-alt mr-1"></i>{% endif %}
{% if t=="year" %}<i class="fas fa-calendar mr-1"></i>{% endif %}
{% if t=="all" %}<i class="fas fa-infinity mr-1"></i>{% endif %}
{{t | capitalize}}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
{% if not t=="hour" %}<a class="dropdown-item" href="?sort={{sort}}&t=hour"><i class="fas fa-clock mr-2"></i>Hour</a>{% endif %}
{% if not t=="day" %}<a class="dropdown-item" href="?sort={{sort}}&t=day"><i class="fas fa-calendar-day mr-2"></i>Day</a>{% endif %}
{% if not t=="week" %}<a class="dropdown-item" href="?sort={{sort}}&t=week"><i class="fas fa-calendar-week mr-2"></i>Week</a>{% endif %}
{% if not t=="month" %}<a class="dropdown-item" href="?sort={{sort}}&t=month"><i class="fas fa-calendar-alt mr-2"></i>Month</a>{% endif %}
{% if not t=="year" %}<a class="dropdown-item" href="?sort={{sort}}&t=year"><i class="fas fa-calendar mr-2"></i>Year</a>{% endif %}
{% if not t=="all" %}<a class="dropdown-item" href="?sort={{sort}}&t=all"><i class="fas fa-infinity mr-2"></i>All</a>{% endif %}
</div>
</div>
<div class="text-small font-weight-bold ml-3 mr-2"></div>
<div class="dropdown dropdown-actions">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% if sort=="hot" %}<i class="fas fa-fire mr-1"></i>{% endif %}
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
{% if sort=="comments" %}<i class="fas fa-comments mr-1"></i>{% endif %}
{{sort | capitalize}}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
{% if sort != "hot" %}<a class="dropdown-item" href="?sort=hot&t={{t}}"><i class="fas fa-fire mr-2"></i>Hot</a>{% endif %}
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
{% if sort != "comments" %}<a class="dropdown-item" href="?sort=comments&t={{t}}"><i class="fas fa-comments mr-2"></i>Comments</a>{% endif %}
</div>
</div>
</div>
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
{% if v %}
<a id="subscribe" class="{% if v.changelogsub %}d-none{% endif %} btn btn-primary followbutton " href="javascript:void(0)" onclick="post_toast2('/changelogsub','subscribe','unsubscribe')">Subscribe</a>
<a id="unsubscribe" class="{% if not v.changelogsub %}d-none{% endif %} btn btn-primary followbutton " href="javascript:void(0)" onclick="post_toast2('/changelogsub','subscribe','unsubscribe')">Unsubscribe</a>
{% endif %}
<div class="row no-gutters {% if listing %}mt-md-3{% elif not listing %}my-md-3{% endif %}">
<div class="col-12">
<div class="posts" id="posts">
{% include "submission_listing.html" %}
</div>
</div>
</div>
{% endblock %}
{% block pagenav %}
{% if listing %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?sort={{sort}}&page={{page-1}}&t={{t}}{% if only %}&only={{only}}{% endif %}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?sort={{sort}}&page={{page+1}}&t={{t}}{% if only %}&only={{only}}{% endif %}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endif %}
{% extends "settings2.html" %}
{% block pagetitle %}Changelog{% endblock %}
{% block desktopBanner %}
<script src="/assets/js/changelog.js?v=54"></script>
<div class="row" style="overflow: visible;padding-top:5px;">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
{% block navbar %}
<div class="font-weight-bold py-3"></div>
<div class="d-flex align-items-center sortingbarmargin">
<div class="text-small font-weight-bold mr-2"></div>
<div class="dropdown dropdown-actions">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% if t=="day" %}<i class="fas fa-calendar-day mr-1"></i>{% endif %}
{% if t=="week" %}<i class="fas fa-calendar-week mr-1"></i>{% endif %}
{% if t=="month" %}<i class="fas fa-calendar-alt mr-1"></i>{% endif %}
{% if t=="year" %}<i class="fas fa-calendar mr-1"></i>{% endif %}
{% if t=="all" %}<i class="fas fa-infinity mr-1"></i>{% endif %}
{{t | capitalize}}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
{% if not t=="hour" %}<a class="dropdown-item" href="?sort={{sort}}&t=hour"><i class="fas fa-clock mr-2"></i>Hour</a>{% endif %}
{% if not t=="day" %}<a class="dropdown-item" href="?sort={{sort}}&t=day"><i class="fas fa-calendar-day mr-2"></i>Day</a>{% endif %}
{% if not t=="week" %}<a class="dropdown-item" href="?sort={{sort}}&t=week"><i class="fas fa-calendar-week mr-2"></i>Week</a>{% endif %}
{% if not t=="month" %}<a class="dropdown-item" href="?sort={{sort}}&t=month"><i class="fas fa-calendar-alt mr-2"></i>Month</a>{% endif %}
{% if not t=="year" %}<a class="dropdown-item" href="?sort={{sort}}&t=year"><i class="fas fa-calendar mr-2"></i>Year</a>{% endif %}
{% if not t=="all" %}<a class="dropdown-item" href="?sort={{sort}}&t=all"><i class="fas fa-infinity mr-2"></i>All</a>{% endif %}
</div>
</div>
<div class="text-small font-weight-bold ml-3 mr-2"></div>
<div class="dropdown dropdown-actions">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% if sort=="hot" %}<i class="fas fa-fire mr-1"></i>{% endif %}
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
{% if sort=="comments" %}<i class="fas fa-comments mr-1"></i>{% endif %}
{{sort | capitalize}}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
{% if sort != "hot" %}<a class="dropdown-item" href="?sort=hot&t={{t}}"><i class="fas fa-fire mr-2"></i>Hot</a>{% endif %}
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
{% if sort != "comments" %}<a class="dropdown-item" href="?sort=comments&t={{t}}"><i class="fas fa-comments mr-2"></i>Comments</a>{% endif %}
</div>
</div>
</div>
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
{% if v %}
<a id="subscribe" class="{% if v.changelogsub %}d-none{% endif %} btn btn-primary followbutton " href="javascript:void(0)" onclick="post_toast2('/changelogsub','subscribe','unsubscribe')">Subscribe</a>
<a id="unsubscribe" class="{% if not v.changelogsub %}d-none{% endif %} btn btn-primary followbutton " href="javascript:void(0)" onclick="post_toast2('/changelogsub','subscribe','unsubscribe')">Unsubscribe</a>
{% endif %}
<div class="row no-gutters {% if listing %}mt-md-3{% elif not listing %}my-md-3{% endif %}">
<div class="col-12">
<div class="posts" id="posts">
{% include "submission_listing.html" %}
</div>
</div>
</div>
{% endblock %}
{% block pagenav %}
{% if listing %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?sort={{sort}}&page={{page-1}}&t={{t}}{% if only %}&only={{only}}{% endif %}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?sort={{sort}}&page={{page+1}}&t={{t}}{% if only %}&only={{only}}{% endif %}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@ -1,43 +1,43 @@
{% extends "default.html" %}
{% block title %}
<title>Unable to post comment</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
<div class="">
<p>Please remove the following link(s) from your comment, and then you will be able to post it:</p>
<ul>
{% for site in badlinks %}
<li>{{site}}</li>
{% endfor %}
</ul>
<div>
<div class="comment-write collapsed child p-4">
<form id="reply" action="{{action}}" method="post" class="input-group">
<input type="hidden" name="formkey" value="{{v.formkey}}">
{% if parent_fullname %}<input type="hidden" name="parent_fullname" value="{{parent_fullname}}">{% endif %}
{% if parent_submission %}<input type="hidden" name="submission" value="{{parent_submission}}">{% endif %}
<textarea name="body" form="reply" class="comment-box form-control rounded" id="reply-form" aria-label="With textarea" placeholder="Add your comment..." rows="10">{{body}}</textarea>
<div class="comment-format">
<small class="format pl-0"><i class="fas fa-bold" aria-hidden="true" onclick="makeReplyBold()" data-bs-toggle="tooltip" data-bs-placement="bottom" title="" data-bs-original-title="Bold"></i></small>
<small class="format"><i class="fas fa-italic" aria-hidden="true" onclick="makeReplyItalics()" data-bs-toggle="tooltip" data-bs-placement="bottom" title="" data-bs-original-title="Italicize"></i></small>
<small class="format"><i class="fas fa-quote-right" aria-hidden="true" onclick="makeReplyQuote()" data-bs-toggle="tooltip" data-bs-placement="bottom" title="" data-bs-original-title="Quote"></i></small>
<small class="format"><i class="fas fa-link" aria-hidden="true"></i></small>
<button form="reply" class="btn btn-primary ml-auto">Comment</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% extends "default.html" %}
{% block title %}
<title>Unable to post comment</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
<div class="">
<p>Please remove the following link(s) from your comment, and then you will be able to post it:</p>
<ul>
{% for site in badlinks %}
<li>{{site}}</li>
{% endfor %}
</ul>
<div>
<div class="comment-write collapsed child p-4">
<form id="reply" action="{{action}}" method="post" class="input-group">
<input type="hidden" name="formkey" value="{{v.formkey}}">
{% if parent_fullname %}<input type="hidden" name="parent_fullname" value="{{parent_fullname}}">{% endif %}
{% if parent_submission %}<input type="hidden" name="submission" value="{{parent_submission}}">{% endif %}
<textarea name="body" form="reply" class="comment-box form-control rounded" id="reply-form" aria-label="With textarea" placeholder="Add your comment..." rows="10">{{body}}</textarea>
<div class="comment-format">
<small class="format pl-0"><i class="fas fa-bold" aria-hidden="true" onclick="makeReplyBold()" data-bs-toggle="tooltip" data-bs-placement="bottom" title="" data-bs-original-title="Bold"></i></small>
<a class="format" href="javscript:void(0)"><i class="fas fa-italic" aria-hidden="true" onclick="makeReplyItalics()" data-bs-toggle="tooltip" data-bs-placement="bottom" title="" data-bs-original-title="Italicize"></i></a>
<a class="format" href="javscript:void(0)"><i class="fas fa-quote-right" aria-hidden="true" onclick="makeReplyQuote()" data-bs-toggle="tooltip" data-bs-placement="bottom" title="" data-bs-original-title="Quote"></i></a>
<a class="format" href="javscript:void(0)"><i class="fas fa-link" aria-hidden="true"></i></small>
<button form="reply" class="btn btn-primary ml-auto">Comment</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

1386
files/templates/comments.html 100644 → 100755

File diff suppressed because it is too large Load Diff

136
files/templates/contact.html 100644 → 100755
View File

@ -1,68 +1,68 @@
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}} - Contact</title>
<meta name="description" content="Contact {{'SITE_NAME' | app_config}} Admins">
{% endblock %}
{% block content %}
{% if request.values.get('error') or error %}
<div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error if error else request.values.get('error')}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if request.values.get('msg') or msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg if msg else request.values.get('msg')}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<h1 class="article-title">Contact {{'SITE_NAME' | app_config}} Admins</h1>
{% if v and v.is_activated and not v.is_suspended %}
<p>Use this form to contact {{'SITE_NAME' | app_config}} Admins.</p>
<label class="mt-3">Your Email</label>
<input class="form-control" value="{{v.email}}" readonly="readonly" disabled>
<form id="contactform" action="/contact" method="post">
<label for="input-message" class="mt-3">Your message</label>
<textarea id="input-message" form="contactform" name="message" class="form-control" required></textarea>
<input type="submit" value="Submit" class="btn btn-primary mt-3">
</form>
{% elif v %}
<p>Please <a target="_blank" href="/settings/security">verify your email address</a> in order to ensure we can respond to your message if needed. Then, refresh this page.</p>
{% else %}
<p>In order to ensure that we can respond to your message, please first <a href="/signup" target="_blank">sign up</a> or <a href="/login" target="_blank">log in</a> and make sure you have <a target="_blank" href="/settings/security">verified your email address</a>. Then, refresh this page.</p>
{% endif %}
<pre>
</pre>
<p>If you can see this line, we haven't been contacted by any law enforcement or governmental organizations in 2021 yet.</p>
{% endblock %}
{% extends "default.html" %}
{% block title %}
<title>{{'SITE_NAME' | app_config}} - Contact</title>
<meta name="description" content="Contact {{'SITE_NAME' | app_config}} Admins">
{% endblock %}
{% block content %}
{% if request.values.get('error') or error %}
<div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error if error else request.values.get('error')}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if request.values.get('msg') or msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg if msg else request.values.get('msg')}}
</span>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<h1 class="article-title">Contact {{'SITE_NAME' | app_config}} Admins</h1>
{% if v and v.is_activated and not v.is_suspended %}
<p>Use this form to contact {{'SITE_NAME' | app_config}} Admins.</p>
<label class="mt-3">Your Email</label>
<input class="form-control" value="{{v.email}}" readonly="readonly" disabled>
<form id="contactform" action="/contact" method="post">
<label for="input-message" class="mt-3">Your message</label>
<textarea id="input-message" form="contactform" name="message" class="form-control" required></textarea>
<input type="submit" value="Submit" class="btn btn-primary mt-3">
</form>
{% elif v %}
<p>Please <a target="_blank" href="/settings/security">verify your email address</a> in order to ensure we can respond to your message if needed. Then, refresh this page.</p>
{% else %}
<p>In order to ensure that we can respond to your message, please first <a href="/signup" target="_blank">sign up</a> or <a href="/login" target="_blank">log in</a> and make sure you have <a target="_blank" href="/settings/security">verified your email address</a>. Then, refresh this page.</p>
{% endif %}
<pre>
</pre>
<p>If you can see this line, we haven't been contacted by any law enforcement or governmental organizations in 2021 yet.</p>
{% endblock %}

717
files/templates/default.html 100644 → 100755
View File

@ -1,357 +1,360 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/assets/js/lozad.js?v=50"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"></script>
{% if v and v.agendaposter %}
<script>
var BugDispatch={options:{minDelay:500,maxDelay:1E4,minBugs:2,maxBugs:20,minSpeed:5,maxSpeed:10,maxLargeTurnDeg:150,maxSmallTurnDeg:10,maxWiggleDeg:5,imageSprite:"fly-sprite.webp",bugWidth:13,bugHeight:14,num_frames:5,zoom:10,canFly:!0,canDie:!0,numDeathTypes:3,monitorMouseMovement:!1,eventDistanceToBug:40,minTimeBetweenMultipy:1E3,mouseOver:"random"},initialize:function(a){this.options=mergeOptions(this.options,a);this.options.minBugs>this.options.maxBugs&&(this.options.minBugs=this.options.maxBugs);
this.modes=["multiply","nothing"];this.options.canFly&&this.modes.push("fly","flyoff");this.options.canDie&&this.modes.push("die");-1==this.modes.indexOf(this.options.mouseOver)&&(this.options.mouseOver="random");this.transform=null;this.transforms={Moz:function(a){this.bug.style.MozTransform=a},webkit:function(a){this.bug.style.webkitTransform=a},O:function(a){this.bug.style.OTransform=a},ms:function(a){this.bug.style.msTransform=a},Khtml:function(a){this.bug.style.KhtmlTransform=a},w3c:function(a){this.bug.style.transform=
a}};if("transform"in document.documentElement.style)this.transform=this.transforms.w3c;else{var b=["Moz","webkit","O","ms","Khtml"],c=0;for(c=0;c<b.length;c++)if(b[c]+"Transform"in document.documentElement.style){this.transform=this.transforms[b[c]];break}}if(this.transform){this.bugs=[];b="multiply"===this.options.mouseOver?this.options.minBugs:this.random(this.options.minBugs,this.options.maxBugs,!0);c=0;var d=this;for(c=0;c<b;c++){a=JSON.parse(JSON.stringify(this.options));var e=SpawnBug();a.wingsOpen=
this.options.canFly?.5<Math.random()?!0:!1:!0;a.walkSpeed=this.random(this.options.minSpeed,this.options.maxSpeed);e.initialize(this.transform,a);this.bugs.push(e)}this.spawnDelay=[];for(c=0;c<b;c++)a=this.random(this.options.minDelay,this.options.maxDelay,!0),e=this.bugs[c],this.spawnDelay[c]=setTimeout(function(a){return function(){d.options.canFly?a.flyIn():a.walkIn()}}(e),a),d.add_events_to_bug(e);this.options.monitorMouseMovement&&(window.onmousemove=function(){d.check_if_mouse_close_to_bug()})}},
stop:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].stop()},end:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].stop(),this.bugs[a].remove()},reset:function(){this.stop();for(var a=0;a<this.bugs.length;a++)this.bugs[a].reset(),this.bugs[a].walkIn()},killAll:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].die()},
add_events_to_bug:function(a){var b=this;a.bug&&(a.bug.addEventListener?a.bug.addEventListener("mouseover",function(c){b.on_bug(a)}):a.bug.attachEvent&&a.bug.attachEvent("onmouseover",function(c){b.on_bug(a)}))},check_if_mouse_close_to_bug:function(a){if(a=a||window.event){var b=0,c=0;a.client&&a.client.x?(b=a.client.x,c=a.client.y):a.clientX?(b=a.clientX,c=a.clientY):a.page&&a.page.x?(b=a.page.x-(document.body.scrollLeft+document.documentElement.scrollLeft),c=a.page.y-(document.body.scrollTop+document.documentElement.scrollTop)):
a.pageX&&(b=a.pageX-(document.body.scrollLeft+document.documentElement.scrollLeft),c=a.pageY-(document.body.scrollTop+document.documentElement.scrollTop));a=this.bugs.length;var d;for(d=0;d<a;d++){var e=this.bugs[d].getPos();e&&Math.abs(e.top-c)+Math.abs(e.left-b)<this.options.eventDistanceToBug&&!this.bugs[d].flyperiodical&&this.near_bug(this.bugs[d])}}},near_bug:function(a){this.on_bug(a)},on_bug:function(a){if(a.alive){var b=this.options.mouseOver;"random"===b&&(b=this.modes[this.random(0,this.modes.length-
1,!0)]);if("fly"===b)a.stop(),a.flyRand();else if("nothing"!==b)if("flyoff"===b)a.stop(),a.flyOff();else if("die"===b)a.die();else if("multiply"===b&&!this.multiplyDelay&&this.bugs.length<this.options.maxBugs){var c=SpawnBug();b=JSON.parse(JSON.stringify(this.options));var d=a.getPos(),e=this;b.wingsOpen=this.options.canFly?.5<Math.random()?!0:!1:!0;b.walkSpeed=this.random(this.options.minSpeed,this.options.maxSpeed);c.initialize(this.transform,b);c.drawBug(d.top,d.left);b.canFly?(c.flyRand(),a.flyRand()):
(c.go(),a.go());this.bugs.push(c);this.multiplyDelay=!0;setTimeout(function(){e.add_events_to_bug(c);e.multiplyDelay=!1},this.options.minTimeBetweenMultipy)}}},random:function(a,b,c){if(a==b)return c?Math.round(a):a;var d=a-.5+Math.random()*(b-a+1);d>b?d=b:d<a&&(d=a);return c?Math.round(d):d}},BugController=function(){this.initialize.apply(this,arguments)};BugController.prototype=BugDispatch;
var SpiderController=function(){this.options=mergeOptions(this.options,{imageSprite:"spider-sprite.webp",bugWidth:69,bugHeight:90,num_frames:7,canFly:!1,canDie:!0,numDeathTypes:2,zoom:6,minDelay:200,maxDelay:3E3,minSpeed:6,maxSpeed:13,minBugs:3,maxBugs:10});this.initialize.apply(this,arguments)};SpiderController.prototype=BugDispatch;
var Bug={options:{wingsOpen:!1,walkSpeed:2,flySpeed:40,edge_resistance:50,zoom:10},initialize:function(a,b){this.options=mergeOptions(this.options,b);this.NEAR_TOP_EDGE=1;this.NEAR_BOTTOM_EDGE=2;this.NEAR_LEFT_EDGE=4;this.NEAR_RIGHT_EDGE=8;this.directions={};this.directions[this.NEAR_TOP_EDGE]=270;this.directions[this.NEAR_BOTTOM_EDGE]=90;this.directions[this.NEAR_LEFT_EDGE]=0;this.directions[this.NEAR_RIGHT_EDGE]=180;this.directions[this.NEAR_TOP_EDGE+this.NEAR_LEFT_EDGE]=315;this.directions[this.NEAR_TOP_EDGE+
this.NEAR_RIGHT_EDGE]=225;this.directions[this.NEAR_BOTTOM_EDGE+this.NEAR_LEFT_EDGE]=45;this.directions[this.NEAR_BOTTOM_EDGE+this.NEAR_RIGHT_EDGE]=135;this.large_turn_angle_deg=this.angle_rad=this.angle_deg=0;this.near_edge=!1;this.edge_test_counter=10;this.fly_counter=this.large_turn_counter=this.small_turn_counter=0;this.toggle_stationary_counter=50*Math.random();this.zoom=this.random(this.options.zoom,10)/10;this.stationary=!1;this.bug=null;this.active=!0;this.wingsOpen=this.options.wingsOpen;
this.transform=a;this.flyIndex=this.walkIndex=0;this.alive=!0;this.twitchTimer=null;this.rad2deg_k=180/Math.PI;this.deg2rad_k=Math.PI/180;this.makeBug();this.angle_rad=this.deg2rad(this.angle_deg);this.angle_deg=this.random(0,360,!0)},go:function(){if(this.transform){this.drawBug();var a=this;this.animating=!0;this.going=requestAnimFrame(function(b){a.animate(b)})}},stop:function(){this.animating=!1;this.going&&(clearTimeout(this.going),this.going=null);this.flyperiodical&&(clearTimeout(this.flyperiodical),
this.flyperiodical=null);this.twitchTimer&&(clearTimeout(this.twitchTimer),this.twitchTimer=null)},remove:function(){this.active=!1;this.inserted&&this.bug.parentNode&&(this.bug.parentNode.removeChild(this.bug),this.inserted=!1)},reset:function(){this.active=this.alive=!0;this.bug.style.bottom="";this.bug.style.top=0;this.bug.style.left=0;this.bug.classList.remove("bug-dead")},animate:function(a){if(this.animating&&this.alive&&this.active){var b=this;this.going=requestAnimFrame(function(a){b.animate(a)});
"_lastTimestamp"in this||(this._lastTimestamp=a);var c=a-this._lastTimestamp;if(!(40>c||(200<c&&(c=200),this._lastTimestamp=a,0>=--this.toggle_stationary_counter&&this.toggleStationary(),this.stationary))){if(0>=--this.edge_test_counter&&this.bug_near_window_edge()&&(this.angle_deg%=360,0>this.angle_deg&&(this.angle_deg+=360),15<Math.abs(this.directions[this.near_edge]-this.angle_deg))){a=this.directions[this.near_edge]-this.angle_deg;var d=360-this.angle_deg+this.directions[this.near_edge];this.large_turn_angle_deg=
Math.abs(a)<Math.abs(d)?a:d;this.edge_test_counter=10;this.large_turn_counter=100;this.small_turn_counter=30}0>=--this.large_turn_counter&&(this.large_turn_angle_deg=this.random(1,this.options.maxLargeTurnDeg,!0),this.next_large_turn());if(0>=--this.small_turn_counter)this.angle_deg+=this.random(1,this.options.maxSmallTurnDeg),this.next_small_turn();else{a=this.random(1,this.options.maxWiggleDeg,!0);if(0<this.large_turn_angle_deg&&0>a||0>this.large_turn_angle_deg&&0<a)a=-a;this.large_turn_angle_deg-=
a;this.angle_deg+=a}this.angle_rad=this.deg2rad(this.angle_deg);this.moveBug(this.bug.left+c/100*this.options.walkSpeed*Math.cos(this.angle_rad),this.bug.top+c/100*this.options.walkSpeed*-Math.sin(this.angle_rad),90-this.angle_deg);this.walkFrame()}}},makeBug:function(){if(!this.bug&&this.active){var a=this.wingsOpen?"0":"-"+this.options.bugHeight+"px",b=document.createElement("div");b.className="bug";b.style.background="transparent url("+this.options.imageSprite+") no-repeat 0 "+a;b.style.width=
this.options.bugWidth+"px";b.style.height=this.options.bugHeight+"px";b.style.position="fixed";b.style.top=0;b.style.left=0;b.style.zIndex="9999999";this.bug=b;this.setPos()}},setPos:function(a,b){this.bug.top=a||this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);this.bug.left=b||this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.moveBug(this.bug.left,this.bug.top,90-this.angle_deg)},
moveBug:function(a,b,c){this.bug.left=a;this.bug.top=b;a="translate("+parseInt(a)+"px,"+parseInt(b)+"px)";c&&(a+=" rotate("+c+"deg)");a+=" scale("+this.zoom+")";this.transform(a)},drawBug:function(a,b){this.bug||this.makeBug();this.bug&&(a&&b?this.setPos(a,b):this.setPos(this.bug.top,this.bug.left),this.inserted||(this.inserted=!0,document.body.appendChild(this.bug)))},toggleStationary:function(){this.stationary=!this.stationary;this.next_stationary();var a=this.wingsOpen?"0":"-"+this.options.bugHeight+
"px";this.bug.style.backgroundPosition=this.stationary?"0 "+a:"-"+this.options.bugWidth+"px "+a},walkFrame:function(){this.bug.style.backgroundPosition=-1*this.walkIndex*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.walkIndex++;this.walkIndex>=this.options.num_frames&&(this.walkIndex=0)},fly:function(a){var b=this.bug.top,c=this.bug.left,d=c-a.left,e=b-a.top,f=Math.atan(e/d);50>Math.abs(d)+Math.abs(e)&&(this.bug.style.backgroundPosition=-2*this.options.bugWidth+
"px -"+2*this.options.bugHeight+"px");30>Math.abs(d)+Math.abs(e)&&(this.bug.style.backgroundPosition=-1*this.options.bugWidth+"px -"+2*this.options.bugHeight+"px");if(10>Math.abs(d)+Math.abs(e))this.bug.style.backgroundPosition="0 0",this.stop(),this.go();else{var g=Math.cos(f)*this.options.flySpeed;f=Math.sin(f)*this.options.flySpeed;if(c>a.left&&0<g||c>a.left&&0>g)g*=-1,Math.abs(d)<Math.abs(g)&&(g/=4);if(b<a.top&&0>f||b>a.top&&0<f)f*=-1,Math.abs(e)<Math.abs(f)&&(f/=4);this.moveBug(c+g,b+f)}},flyRand:function(){this.stop();
var a={};a.top=this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);a.left=this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.startFlying(a)},startFlying:function(a){var b=this.bug.top,c=this.bug.left,d=a.left-c,e=a.top-b;this.bug.left=a.left;this.bug.top=a.top;this.angle_rad=Math.atan(e/d);this.angle_deg=this.rad2deg(this.angle_rad);this.angle_deg=0<d?90+this.angle_deg:270+this.angle_deg;
this.moveBug(c,b,this.angle_deg);var f=this;this.flyperiodical=setInterval(function(){f.fly(a)},10)},flyIn:function(){this.bug||this.makeBug();if(this.bug){this.stop();var a=Math.round(4*Math.random()-.5),b=document,c=b.documentElement,d=b.getElementsByTagName("body")[0];b=window.innerWidth||c.clientWidth||d.clientWidth;c=window.innerHeight||c.clientHeight||d.clientHeight;3<a&&(a=3);0>a&&(a=0);0===a?(a=-2*this.options.bugHeight,b*=Math.random()):1===a?(a=Math.random()*c,b+=2*this.options.bugWidth):
2===a?(a=c+2*this.options.bugHeight,b*=Math.random()):(a=Math.random()*c,b=-3*this.options.bugWidth);this.bug.style.backgroundPosition=-3*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.bug.top=a;this.bug.left=b;this.drawBug();a={};a.top=this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);a.left=this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.startFlying(a)}},
walkIn:function(){this.bug||this.makeBug();if(this.bug){this.stop();var a=Math.round(4*Math.random()-.5),b=document,c=b.documentElement,d=b.getElementsByTagName("body")[0];b=window.innerWidth||c.clientWidth||d.clientWidth;c=window.innerHeight||c.clientHeight||d.clientHeight;3<a&&(a=3);0>a&&(a=0);0===a?(a=-1.3*this.options.bugHeight,b*=Math.random()):1===a?(a=Math.random()*c,b+=.3*this.options.bugWidth):2===a?(a=c+.3*this.options.bugHeight,b*=Math.random()):(a=Math.random()*c,b=-1.3*this.options.bugWidth);
this.bug.style.backgroundPosition=-3*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.bug.top=a;this.bug.left=b;this.drawBug();this.go()}},flyOff:function(){this.stop();var a=this.random(0,3),b={},c=document,d=c.documentElement,e=c.getElementsByTagName("body")[0];c=window.innerWidth||d.clientWidth||e.clientWidth;d=window.innerHeight||d.clientHeight||e.clientHeight;0===a?(b.top=-200,b.left=Math.random()*c):1===a?(b.top=Math.random()*d,b.left=c+200):2===a?(b.top=
d+200,b.left=Math.random()*c):(b.top=Math.random()*d,b.left=-200);this.startFlying(b)},die:function(){this.stop();var a=this.random(0,this.options.numDeathTypes-1);this.alive=!1;this.drop(a)},drop:function(a){var b=this.bug.top,c=document,d=c.documentElement;c=c.getElementsByTagName("body")[0];var e=window.innerHeight||d.clientHeight||c.clientHeight;e-=this.options.bugHeight;var f=this.random(0,20,!0);Date.now();var g=this;this.bug.classList.add("bug-dead");this.dropTimer=requestAnimFrame(function(c){g._lastTimestamp=
c;g.dropping(c,b,e,f,a)})},dropping:function(a,b,c,d,e){a-=this._lastTimestamp;var f=b+.002*a*a,g=this;f>=c?(f=c,clearTimeout(this.dropTimer),this.angle_deg=0,this.angle_rad=this.deg2rad(this.angle_deg),this.transform("rotate("+(90-this.angle_deg)+"deg) scale("+this.zoom+")"),this.bug.style.top=null,this.bug.style.bottom=Math.ceil((this.options.bugWidth*this.zoom-this.options.bugHeight*this.zoom)/2-this.options.bugHeight/2*(1-this.zoom))+"px",this.bug.style.left=this.bug.left+"px",this.bug.style.backgroundPosition=
"-"+2*e*this.options.bugWidth+"px 100%",this.twitch(e)):(this.dropTimer=requestAnimFrame(function(a){g.dropping(a,b,c,d,e)}),20>a||(this.angle_deg=(this.angle_deg+d)%360,this.angle_rad=this.deg2rad(this.angle_deg),this.moveBug(this.bug.left,f,this.angle_deg)))},twitch:function(a,b){b||(b=0);var c=this;if(0===a||1===a)c.twitchTimer=setTimeout(function(){c.bug.style.backgroundPosition="-"+(2*a+b%2)*c.options.bugWidth+"px 100%";c.twitchTimer=setTimeout(function(){b++;c.bug.style.backgroundPosition="-"+
(2*a+b%2)*c.options.bugWidth+"px 100%";c.twitch(a,++b)},c.random(300,800))},this.random(1E3,1E4))},rad2deg:function(a){return a*this.rad2deg_k},deg2rad:function(a){return a*this.deg2rad_k},random:function(a,b,c){if(a==b)return a;a=Math.round(a-.5+Math.random()*(b-a+1));return c?.5<Math.random()?a:-a:a},next_small_turn:function(){this.small_turn_counter=Math.round(10*Math.random())},next_large_turn:function(){this.large_turn_counter=Math.round(40*Math.random())},next_stationary:function(){this.toggle_stationary_counter=
this.random(50,300)},bug_near_window_edge:function(){this.near_edge=0;this.bug.top<this.options.edge_resistance?this.near_edge|=this.NEAR_TOP_EDGE:this.bug.top>document.documentElement.clientHeight-this.options.edge_resistance&&(this.near_edge|=this.NEAR_BOTTOM_EDGE);this.bug.left<this.options.edge_resistance?this.near_edge|=this.NEAR_LEFT_EDGE:this.bug.left>document.documentElement.clientWidth-this.options.edge_resistance&&(this.near_edge|=this.NEAR_RIGHT_EDGE);return this.near_edge},getPos:function(){return this.inserted&&
this.bug&&this.bug.style?{top:parseInt(this.bug.top,10),left:parseInt(this.bug.left,10)}:null}},SpawnBug=function(){var a={},b;for(b in Bug)Bug.hasOwnProperty(b)&&(a[b]=Bug[b]);return a},mergeOptions=function(a,b,c){"undefined"==typeof c&&(c=!0);a=c?cloneOf(a):a;for(var d in b)b.hasOwnProperty(d)&&(a[d]=b[d]);return a},cloneOf=function(a){if(null==a||"object"!=typeof a)return a;var b=a.constructor(),c;for(c in a)a.hasOwnProperty(c)&&(b[c]=cloneOf(a[c]));return b};
window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a,b){window.setTimeout(a,1E3/60)}}();
new BugController({
imageSprite: "/assets/images/fly-sprite.webp",
canDie: false,
minBugs: 5,
maxBugs: 30,
mouseOver: "fly"
});
new SpiderController({
imageSprite: "/assets/images/spider-sprite.webp",
canDie: false,
minBugs: 2,
maxBugs: 20,
mouseOver: "fly"
});
</script>
<noscript>
<style>
body {
-moz-transform: scale(-1, -1);
-o-transform: scale(-1, -1);
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1);
}
</style>
</noscript>
{% endif %}
{% if v %}
<script>function formkey() {return '{{v.formkey}}';}</script>
<script src="/assets/js/default.js?v=50"></script>
{% endif %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="thumbnail" content="/assets/images/{{'SITE_NAME' | app_config}}/preview.webp">
<link rel="icon" type="image/png" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp">
{% block title %}
<title>{{'SITE_NAME' | app_config}}</title>
<meta property="og:type" content="article" />
<meta property="og:title" content="{{'SITE_NAME' | app_config}}" />
<meta property="og:site_name" content="{{request.host}}" />
<meta property="og:image" content="{{'SITE_NAME' | app_config}}/assets/images/{{'SITE_NAME' | app_config}}/preview.webp" />
<meta property="og:url" content="{{request.path | full_link}}">
<meta property="og:description" name="description" content="{{'SITE_NAME' | app_config}} - {{'SLOGAN' | app_config}}">
<meta property="og:author" name="author" content="@{{request.host_url}}" />
<meta property="og:site_name" content="{{request.host}}" />
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:site" content="@{{request.host_url}}">
<meta name="twitter:title" content="{{'SITE_NAME' | app_config}}" />
<meta name="twitter:creator" content="@{{request.host_url}}">
<meta name="twitter:description" content="{{'SITE_NAME' | app_config}} - {{'SLOGAN' | app_config}}" />
<meta name="twitter:image" content="/assets/images/{{'SITE_NAME' | app_config}}/preview.webp" />
<meta name="twitter:url" content="{{request.path | full_link}}" />
{% endblock %}
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp">
<!---<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp">--->
<link rel="manifest" href="/assets/manifest.json">
<link rel="mask-icon" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp" color="#{{'DEFAULT_COLOR' | app_config}}">
<link rel="shortcut icon" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp">
<meta name="apple-mobile-web-app-title" content="{{'SITE_NAME' | app_config}}">
<meta name="application-name" content="{{'SITE_NAME' | app_config}}">
<meta name="msapplication-TileColor" content="#{{'DEFAULT_COLOR' | app_config}}">
<meta name="msapplication-config" content="/assets/images/browserconfig.xml">
<meta name="theme-color" content="#{{'DEFAULT_COLOR' | app_config}}">
<link
rel="apple-touch-startup-image"
sizes="320x480"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="640x960"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-icon"
sizes="640x1136"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-icon"
sizes="750x1334"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="768x1004"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="768x1024"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="828x1792"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1024x748"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1024x768"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1125x2436"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1242x2208"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1242x2688"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1334x750"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1536x2008"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1536x2048"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1668x2224"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="1792x828"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="2048x1496"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="2048x1536"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="2048x2732"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="2208x1242"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="2224x1668"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="2436x1125"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="2668x1242"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
<link
rel="apple-touch-startup-image"
sizes="2737x2048"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.webp"
/>
{% block stylesheets %}
{% if v %}
<link rel="stylesheet" href="/assets/css/{{v.theme}}_{{v.themecolor}}.css?v=61">
{% if v.agendaposter %}<link rel="stylesheet" href="/assets/css/agendaposter.css?v=61">{% endif %}
{% else %}
<link rel="stylesheet" href="/assets/css/{{'DEFAULT_THEME' | app_config}}.css?v=61">
{% endif %}
{% endblock %}
<link href="/assets/css/fa.css" rel="stylesheet">
{% block fixedMobileBarJS %}
{% endblock %}
</head>
<body id="{% if request.path != '/comments' %}{% block pagetype %}frontpage{% endblock %}{% endif %}" style="overflow-x: hidden; {% if v and v.background %} background:url(/assets/images/backgrounds/{{v.background}}) no-repeat center center fixed !important; background-size: cover!important; background-color: #000!important;{% endif %} {% if 'rdrama' in request.host %}margin-top: 29px!important;{% endif %}">
{% if "marsey.tech" not in request.host and '@' not in request.path %}
<a rel="nofollow noopener noreferrer" href="{% if 'rdrama' in request.host %}https://secure.transequality.org/site/Donation2?df_id=1480{% else %}/{% endif %}">
<img loading="lazy" src="/assets/images/{{'SITE_NAME' | app_config}}/{% if v %}banner.webp{% else %}cached.webp{% endif %}" width="100%">
</a>
{% endif %}
{% include "header.html" %}
{% block mobileUserBanner %}
{% endblock %}
{% block mobileBanner %}
{% endblock %}
{% block postNav %}
{% endblock %}
<div class="container {% if request.path=='/' or '/post/' in request.path or '/comment/' in request.path %} transparent {% endif %}">
<div class="row justify-content-around" id="main-content-row">
<div class="col h-100 {% block customPadding %}{% if not '/message' in request.path %}custom-gutters{% endif %}{% endblock %}" id="main-content-col">
{% block desktopUserBanner %}
{% endblock %}
{% block desktopBanner %}
{% endblock %}
{% block PseudoSubmitForm %}
{% endblock %}
{% block searchText %}
{% endblock %}
{% block content %}
{% endblock %}
{% block pagenav %}
{% endblock %}
</div>
</div>
</div>
{% block mobilenavbar %}
{% include "mobile_navigation_bar.html" %}
{% endblock %}
{% block actionsModal %}
{% endblock %}
{% block reportCommentModal %}
{% endblock %}
{% block GIFtoast %}
{% endblock %}
{% block GIFpicker %}
{% endblock %}
<div class="toast clipboard" id="toast-success" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body text-center">
<i class="fas fa-check-circle text-success mr-2"></i>Link copied to clipboard
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text"></span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text"></span>
</div>
</div>
<style>
.mirrored {
transform: scaleX(-1);-webkit-transform: scaleX(-1);
}
</style>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/assets/js/lozad.js?v=53"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"></script>
{% if v and v.agendaposter %}
<script>
var BugDispatch={options:{minDelay:500,maxDelay:1E4,minBugs:2,maxBugs:20,minSpeed:5,maxSpeed:10,maxLargeTurnDeg:150,maxSmallTurnDeg:10,maxWiggleDeg:5,imageSprite:"fly-sprite.gif",bugWidth:13,bugHeight:14,num_frames:5,zoom:10,canFly:!0,canDie:!0,numDeathTypes:3,monitorMouseMovement:!1,eventDistanceToBug:40,minTimeBetweenMultipy:1E3,mouseOver:"random"},initialize:function(a){this.options=mergeOptions(this.options,a);this.options.minBugs>this.options.maxBugs&&(this.options.minBugs=this.options.maxBugs);
this.modes=["multiply","nothing"];this.options.canFly&&this.modes.push("fly","flyoff");this.options.canDie&&this.modes.push("die");-1==this.modes.indexOf(this.options.mouseOver)&&(this.options.mouseOver="random");this.transform=null;this.transforms={Moz:function(a){this.bug.style.MozTransform=a},webkit:function(a){this.bug.style.webkitTransform=a},O:function(a){this.bug.style.OTransform=a},ms:function(a){this.bug.style.msTransform=a},Khtml:function(a){this.bug.style.KhtmlTransform=a},w3c:function(a){this.bug.style.transform=
a}};if("transform"in document.documentElement.style)this.transform=this.transforms.w3c;else{var b=["Moz","webkit","O","ms","Khtml"],c=0;for(c=0;c<b.length;c++)if(b[c]+"Transform"in document.documentElement.style){this.transform=this.transforms[b[c]];break}}if(this.transform){this.bugs=[];b="multiply"===this.options.mouseOver?this.options.minBugs:this.random(this.options.minBugs,this.options.maxBugs,!0);c=0;var d=this;for(c=0;c<b;c++){a=JSON.parse(JSON.stringify(this.options));var e=SpawnBug();a.wingsOpen=
this.options.canFly?.5<Math.random()?!0:!1:!0;a.walkSpeed=this.random(this.options.minSpeed,this.options.maxSpeed);e.initialize(this.transform,a);this.bugs.push(e)}this.spawnDelay=[];for(c=0;c<b;c++)a=this.random(this.options.minDelay,this.options.maxDelay,!0),e=this.bugs[c],this.spawnDelay[c]=setTimeout(function(a){return function(){d.options.canFly?a.flyIn():a.walkIn()}}(e),a),d.add_events_to_bug(e);this.options.monitorMouseMovement&&(window.onmousemove=function(){d.check_if_mouse_close_to_bug()})}},
stop:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].stop()},end:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].stop(),this.bugs[a].remove()},reset:function(){this.stop();for(var a=0;a<this.bugs.length;a++)this.bugs[a].reset(),this.bugs[a].walkIn()},killAll:function(){for(var a=0;a<this.bugs.length;a++)this.spawnDelay[a]&&clearTimeout(this.spawnDelay[a]),this.bugs[a].die()},
add_events_to_bug:function(a){var b=this;a.bug&&(a.bug.addEventListener?a.bug.addEventListener("mouseover",function(c){b.on_bug(a)}):a.bug.attachEvent&&a.bug.attachEvent("onmouseover",function(c){b.on_bug(a)}))},check_if_mouse_close_to_bug:function(a){if(a=a||window.event){var b=0,c=0;a.client&&a.client.x?(b=a.client.x,c=a.client.y):a.clientX?(b=a.clientX,c=a.clientY):a.page&&a.page.x?(b=a.page.x-(document.body.scrollLeft+document.documentElement.scrollLeft),c=a.page.y-(document.body.scrollTop+document.documentElement.scrollTop)):
a.pageX&&(b=a.pageX-(document.body.scrollLeft+document.documentElement.scrollLeft),c=a.pageY-(document.body.scrollTop+document.documentElement.scrollTop));a=this.bugs.length;var d;for(d=0;d<a;d++){var e=this.bugs[d].getPos();e&&Math.abs(e.top-c)+Math.abs(e.left-b)<this.options.eventDistanceToBug&&!this.bugs[d].flyperiodical&&this.near_bug(this.bugs[d])}}},near_bug:function(a){this.on_bug(a)},on_bug:function(a){if(a.alive){var b=this.options.mouseOver;"random"===b&&(b=this.modes[this.random(0,this.modes.length-
1,!0)]);if("fly"===b)a.stop(),a.flyRand();else if("nothing"!==b)if("flyoff"===b)a.stop(),a.flyOff();else if("die"===b)a.die();else if("multiply"===b&&!this.multiplyDelay&&this.bugs.length<this.options.maxBugs){var c=SpawnBug();b=JSON.parse(JSON.stringify(this.options));var d=a.getPos(),e=this;b.wingsOpen=this.options.canFly?.5<Math.random()?!0:!1:!0;b.walkSpeed=this.random(this.options.minSpeed,this.options.maxSpeed);c.initialize(this.transform,b);c.drawBug(d.top,d.left);b.canFly?(c.flyRand(),a.flyRand()):
(c.go(),a.go());this.bugs.push(c);this.multiplyDelay=!0;setTimeout(function(){e.add_events_to_bug(c);e.multiplyDelay=!1},this.options.minTimeBetweenMultipy)}}},random:function(a,b,c){if(a==b)return c?Math.round(a):a;var d=a-.5+Math.random()*(b-a+1);d>b?d=b:d<a&&(d=a);return c?Math.round(d):d}},BugController=function(){this.initialize.apply(this,arguments)};BugController.prototype=BugDispatch;
var SpiderController=function(){this.options=mergeOptions(this.options,{imageSprite:"spider-sprite.gif",bugWidth:69,bugHeight:90,num_frames:7,canFly:!1,canDie:!0,numDeathTypes:2,zoom:6,minDelay:200,maxDelay:3E3,minSpeed:6,maxSpeed:13,minBugs:3,maxBugs:10});this.initialize.apply(this,arguments)};SpiderController.prototype=BugDispatch;
var Bug={options:{wingsOpen:!1,walkSpeed:2,flySpeed:40,edge_resistance:50,zoom:10},initialize:function(a,b){this.options=mergeOptions(this.options,b);this.NEAR_TOP_EDGE=1;this.NEAR_BOTTOM_EDGE=2;this.NEAR_LEFT_EDGE=4;this.NEAR_RIGHT_EDGE=8;this.directions={};this.directions[this.NEAR_TOP_EDGE]=270;this.directions[this.NEAR_BOTTOM_EDGE]=90;this.directions[this.NEAR_LEFT_EDGE]=0;this.directions[this.NEAR_RIGHT_EDGE]=180;this.directions[this.NEAR_TOP_EDGE+this.NEAR_LEFT_EDGE]=315;this.directions[this.NEAR_TOP_EDGE+
this.NEAR_RIGHT_EDGE]=225;this.directions[this.NEAR_BOTTOM_EDGE+this.NEAR_LEFT_EDGE]=45;this.directions[this.NEAR_BOTTOM_EDGE+this.NEAR_RIGHT_EDGE]=135;this.large_turn_angle_deg=this.angle_rad=this.angle_deg=0;this.near_edge=!1;this.edge_test_counter=10;this.fly_counter=this.large_turn_counter=this.small_turn_counter=0;this.toggle_stationary_counter=50*Math.random();this.zoom=this.random(this.options.zoom,10)/10;this.stationary=!1;this.bug=null;this.active=!0;this.wingsOpen=this.options.wingsOpen;
this.transform=a;this.flyIndex=this.walkIndex=0;this.alive=!0;this.twitchTimer=null;this.rad2deg_k=180/Math.PI;this.deg2rad_k=Math.PI/180;this.makeBug();this.angle_rad=this.deg2rad(this.angle_deg);this.angle_deg=this.random(0,360,!0)},go:function(){if(this.transform){this.drawBug();var a=this;this.animating=!0;this.going=requestAnimFrame(function(b){a.animate(b)})}},stop:function(){this.animating=!1;this.going&&(clearTimeout(this.going),this.going=null);this.flyperiodical&&(clearTimeout(this.flyperiodical),
this.flyperiodical=null);this.twitchTimer&&(clearTimeout(this.twitchTimer),this.twitchTimer=null)},remove:function(){this.active=!1;this.inserted&&this.bug.parentNode&&(this.bug.parentNode.removeChild(this.bug),this.inserted=!1)},reset:function(){this.active=this.alive=!0;this.bug.style.bottom="";this.bug.style.top=0;this.bug.style.left=0;this.bug.classList.remove("bug-dead")},animate:function(a){if(this.animating&&this.alive&&this.active){var b=this;this.going=requestAnimFrame(function(a){b.animate(a)});
"_lastTimestamp"in this||(this._lastTimestamp=a);var c=a-this._lastTimestamp;if(!(40>c||(200<c&&(c=200),this._lastTimestamp=a,0>=--this.toggle_stationary_counter&&this.toggleStationary(),this.stationary))){if(0>=--this.edge_test_counter&&this.bug_near_window_edge()&&(this.angle_deg%=360,0>this.angle_deg&&(this.angle_deg+=360),15<Math.abs(this.directions[this.near_edge]-this.angle_deg))){a=this.directions[this.near_edge]-this.angle_deg;var d=360-this.angle_deg+this.directions[this.near_edge];this.large_turn_angle_deg=
Math.abs(a)<Math.abs(d)?a:d;this.edge_test_counter=10;this.large_turn_counter=100;this.small_turn_counter=30}0>=--this.large_turn_counter&&(this.large_turn_angle_deg=this.random(1,this.options.maxLargeTurnDeg,!0),this.next_large_turn());if(0>=--this.small_turn_counter)this.angle_deg+=this.random(1,this.options.maxSmallTurnDeg),this.next_small_turn();else{a=this.random(1,this.options.maxWiggleDeg,!0);if(0<this.large_turn_angle_deg&&0>a||0>this.large_turn_angle_deg&&0<a)a=-a;this.large_turn_angle_deg-=
a;this.angle_deg+=a}this.angle_rad=this.deg2rad(this.angle_deg);this.moveBug(this.bug.left+c/100*this.options.walkSpeed*Math.cos(this.angle_rad),this.bug.top+c/100*this.options.walkSpeed*-Math.sin(this.angle_rad),90-this.angle_deg);this.walkFrame()}}},makeBug:function(){if(!this.bug&&this.active){var a=this.wingsOpen?"0":"-"+this.options.bugHeight+"px",b=document.createElement("div");b.className="bug";b.style.background="transparent url("+this.options.imageSprite+") no-repeat 0 "+a;b.style.width=
this.options.bugWidth+"px";b.style.height=this.options.bugHeight+"px";b.style.position="fixed";b.style.top=0;b.style.left=0;b.style.zIndex="9999999";this.bug=b;this.setPos()}},setPos:function(a,b){this.bug.top=a||this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);this.bug.left=b||this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.moveBug(this.bug.left,this.bug.top,90-this.angle_deg)},
moveBug:function(a,b,c){this.bug.left=a;this.bug.top=b;a="translate("+parseInt(a)+"px,"+parseInt(b)+"px)";c&&(a+=" rotate("+c+"deg)");a+=" scale("+this.zoom+")";this.transform(a)},drawBug:function(a,b){this.bug||this.makeBug();this.bug&&(a&&b?this.setPos(a,b):this.setPos(this.bug.top,this.bug.left),this.inserted||(this.inserted=!0,document.body.appendChild(this.bug)))},toggleStationary:function(){this.stationary=!this.stationary;this.next_stationary();var a=this.wingsOpen?"0":"-"+this.options.bugHeight+
"px";this.bug.style.backgroundPosition=this.stationary?"0 "+a:"-"+this.options.bugWidth+"px "+a},walkFrame:function(){this.bug.style.backgroundPosition=-1*this.walkIndex*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.walkIndex++;this.walkIndex>=this.options.num_frames&&(this.walkIndex=0)},fly:function(a){var b=this.bug.top,c=this.bug.left,d=c-a.left,e=b-a.top,f=Math.atan(e/d);50>Math.abs(d)+Math.abs(e)&&(this.bug.style.backgroundPosition=-2*this.options.bugWidth+
"px -"+2*this.options.bugHeight+"px");30>Math.abs(d)+Math.abs(e)&&(this.bug.style.backgroundPosition=-1*this.options.bugWidth+"px -"+2*this.options.bugHeight+"px");if(10>Math.abs(d)+Math.abs(e))this.bug.style.backgroundPosition="0 0",this.stop(),this.go();else{var g=Math.cos(f)*this.options.flySpeed;f=Math.sin(f)*this.options.flySpeed;if(c>a.left&&0<g||c>a.left&&0>g)g*=-1,Math.abs(d)<Math.abs(g)&&(g/=4);if(b<a.top&&0>f||b>a.top&&0<f)f*=-1,Math.abs(e)<Math.abs(f)&&(f/=4);this.moveBug(c+g,b+f)}},flyRand:function(){this.stop();
var a={};a.top=this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);a.left=this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.startFlying(a)},startFlying:function(a){var b=this.bug.top,c=this.bug.left,d=a.left-c,e=a.top-b;this.bug.left=a.left;this.bug.top=a.top;this.angle_rad=Math.atan(e/d);this.angle_deg=this.rad2deg(this.angle_rad);this.angle_deg=0<d?90+this.angle_deg:270+this.angle_deg;
this.moveBug(c,b,this.angle_deg);var f=this;this.flyperiodical=setInterval(function(){f.fly(a)},10)},flyIn:function(){this.bug||this.makeBug();if(this.bug){this.stop();var a=Math.round(4*Math.random()-.5),b=document,c=b.documentElement,d=b.getElementsByTagName("body")[0];b=window.innerWidth||c.clientWidth||d.clientWidth;c=window.innerHeight||c.clientHeight||d.clientHeight;3<a&&(a=3);0>a&&(a=0);0===a?(a=-2*this.options.bugHeight,b*=Math.random()):1===a?(a=Math.random()*c,b+=2*this.options.bugWidth):
2===a?(a=c+2*this.options.bugHeight,b*=Math.random()):(a=Math.random()*c,b=-3*this.options.bugWidth);this.bug.style.backgroundPosition=-3*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.bug.top=a;this.bug.left=b;this.drawBug();a={};a.top=this.random(this.options.edge_resistance,document.documentElement.clientHeight-this.options.edge_resistance);a.left=this.random(this.options.edge_resistance,document.documentElement.clientWidth-this.options.edge_resistance);this.startFlying(a)}},
walkIn:function(){this.bug||this.makeBug();if(this.bug){this.stop();var a=Math.round(4*Math.random()-.5),b=document,c=b.documentElement,d=b.getElementsByTagName("body")[0];b=window.innerWidth||c.clientWidth||d.clientWidth;c=window.innerHeight||c.clientHeight||d.clientHeight;3<a&&(a=3);0>a&&(a=0);0===a?(a=-1.3*this.options.bugHeight,b*=Math.random()):1===a?(a=Math.random()*c,b+=.3*this.options.bugWidth):2===a?(a=c+.3*this.options.bugHeight,b*=Math.random()):(a=Math.random()*c,b=-1.3*this.options.bugWidth);
this.bug.style.backgroundPosition=-3*this.options.bugWidth+"px "+(this.wingsOpen?"0":"-"+this.options.bugHeight+"px");this.bug.top=a;this.bug.left=b;this.drawBug();this.go()}},flyOff:function(){this.stop();var a=this.random(0,3),b={},c=document,d=c.documentElement,e=c.getElementsByTagName("body")[0];c=window.innerWidth||d.clientWidth||e.clientWidth;d=window.innerHeight||d.clientHeight||e.clientHeight;0===a?(b.top=-200,b.left=Math.random()*c):1===a?(b.top=Math.random()*d,b.left=c+200):2===a?(b.top=
d+200,b.left=Math.random()*c):(b.top=Math.random()*d,b.left=-200);this.startFlying(b)},die:function(){this.stop();var a=this.random(0,this.options.numDeathTypes-1);this.alive=!1;this.drop(a)},drop:function(a){var b=this.bug.top,c=document,d=c.documentElement;c=c.getElementsByTagName("body")[0];var e=window.innerHeight||d.clientHeight||c.clientHeight;e-=this.options.bugHeight;var f=this.random(0,20,!0);Date.now();var g=this;this.bug.classList.add("bug-dead");this.dropTimer=requestAnimFrame(function(c){g._lastTimestamp=
c;g.dropping(c,b,e,f,a)})},dropping:function(a,b,c,d,e){a-=this._lastTimestamp;var f=b+.002*a*a,g=this;f>=c?(f=c,clearTimeout(this.dropTimer),this.angle_deg=0,this.angle_rad=this.deg2rad(this.angle_deg),this.transform("rotate("+(90-this.angle_deg)+"deg) scale("+this.zoom+")"),this.bug.style.top=null,this.bug.style.bottom=Math.ceil((this.options.bugWidth*this.zoom-this.options.bugHeight*this.zoom)/2-this.options.bugHeight/2*(1-this.zoom))+"px",this.bug.style.left=this.bug.left+"px",this.bug.style.backgroundPosition=
"-"+2*e*this.options.bugWidth+"px 100%",this.twitch(e)):(this.dropTimer=requestAnimFrame(function(a){g.dropping(a,b,c,d,e)}),20>a||(this.angle_deg=(this.angle_deg+d)%360,this.angle_rad=this.deg2rad(this.angle_deg),this.moveBug(this.bug.left,f,this.angle_deg)))},twitch:function(a,b){b||(b=0);var c=this;if(0===a||1===a)c.twitchTimer=setTimeout(function(){c.bug.style.backgroundPosition="-"+(2*a+b%2)*c.options.bugWidth+"px 100%";c.twitchTimer=setTimeout(function(){b++;c.bug.style.backgroundPosition="-"+
(2*a+b%2)*c.options.bugWidth+"px 100%";c.twitch(a,++b)},c.random(300,800))},this.random(1E3,1E4))},rad2deg:function(a){return a*this.rad2deg_k},deg2rad:function(a){return a*this.deg2rad_k},random:function(a,b,c){if(a==b)return a;a=Math.round(a-.5+Math.random()*(b-a+1));return c?.5<Math.random()?a:-a:a},next_small_turn:function(){this.small_turn_counter=Math.round(10*Math.random())},next_large_turn:function(){this.large_turn_counter=Math.round(40*Math.random())},next_stationary:function(){this.toggle_stationary_counter=
this.random(50,300)},bug_near_window_edge:function(){this.near_edge=0;this.bug.top<this.options.edge_resistance?this.near_edge|=this.NEAR_TOP_EDGE:this.bug.top>document.documentElement.clientHeight-this.options.edge_resistance&&(this.near_edge|=this.NEAR_BOTTOM_EDGE);this.bug.left<this.options.edge_resistance?this.near_edge|=this.NEAR_LEFT_EDGE:this.bug.left>document.documentElement.clientWidth-this.options.edge_resistance&&(this.near_edge|=this.NEAR_RIGHT_EDGE);return this.near_edge},getPos:function(){return this.inserted&&
this.bug&&this.bug.style?{top:parseInt(this.bug.top,10),left:parseInt(this.bug.left,10)}:null}},SpawnBug=function(){var a={},b;for(b in Bug)Bug.hasOwnProperty(b)&&(a[b]=Bug[b]);return a},mergeOptions=function(a,b,c){"undefined"==typeof c&&(c=!0);a=c?cloneOf(a):a;for(var d in b)b.hasOwnProperty(d)&&(a[d]=b[d]);return a},cloneOf=function(a){if(null==a||"object"!=typeof a)return a;var b=a.constructor(),c;for(c in a)a.hasOwnProperty(c)&&(b[c]=cloneOf(a[c]));return b};
window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a,b){window.setTimeout(a,1E3/60)}}();
new BugController({
imageSprite: "/assets/images/fly-sprite.gif",
canDie: false,
minBugs: 5,
maxBugs: 30,
mouseOver: "fly"
});
new SpiderController({
imageSprite: "/assets/images/spider-sprite.gif",
canDie: false,
minBugs: 2,
maxBugs: 20,
mouseOver: "fly"
});
</script>
<noscript>
<style>
body {
-moz-transform: scale(-1, -1);
-o-transform: scale(-1, -1);
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1);
}
</style>
</noscript>
{% endif %}
{% if v %}
<script>function formkey() {return '{{v.formkey}}';}</script>
<script src="/assets/js/default.js?v=54"></script>
{% endif %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="thumbnail" content="/assets/images/{{'SITE_NAME' | app_config}}/preview.gif">
<link rel="icon" type="image/png" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif">
{% block title %}
<title>{{'SITE_NAME' | app_config}}</title>
<meta property="og:type" content="article" />
<meta property="og:title" content="{{'SITE_NAME' | app_config}}" />
<meta property="og:site_name" content="{{request.host}}" />
<meta property="og:image" content="{{'SITE_NAME' | app_config}}/assets/images/{{'SITE_NAME' | app_config}}/preview.gif" />
<meta property="og:url" content="{{request.path | full_link}}">
<meta property="og:description" name="description" content="{{'SITE_NAME' | app_config}} - {{'SLOGAN' | app_config}}">
<meta property="og:author" name="author" content="@{{request.host_url}}" />
<meta property="og:site_name" content="{{request.host}}" />
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:site" content="@{{request.host_url}}">
<meta name="twitter:title" content="{{'SITE_NAME' | app_config}}" />
<meta name="twitter:creator" content="@{{request.host_url}}">
<meta name="twitter:description" content="{{'SITE_NAME' | app_config}} - {{'SLOGAN' | app_config}}" />
<meta name="twitter:image" content="/assets/images/{{'SITE_NAME' | app_config}}/preview.gif" />
<meta name="twitter:url" content="{{request.path | full_link}}" />
{% endblock %}
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif">
<!---<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif">--->
<link rel="manifest" href="/assets/manifest.json">
<link rel="mask-icon" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif" color="#{{'DEFAULT_COLOR' | app_config}}">
<link rel="shortcut icon" href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif">
<meta name="apple-mobile-web-app-title" content="{{'SITE_NAME' | app_config}}">
<meta name="application-name" content="{{'SITE_NAME' | app_config}}">
<meta name="msapplication-TileColor" content="#{{'DEFAULT_COLOR' | app_config}}">
<meta name="msapplication-config" content="/assets/images/browserconfig.xml">
<meta name="theme-color" content="#{{'DEFAULT_COLOR' | app_config}}">
<link
rel="apple-touch-startup-image"
sizes="320x480"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="640x960"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-icon"
sizes="640x1136"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-icon"
sizes="750x1334"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="768x1004"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="768x1024"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="828x1792"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1024x748"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1024x768"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1125x2436"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1242x2208"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1242x2688"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1334x750"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1536x2008"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1536x2048"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1668x2224"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="1792x828"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="2048x1496"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="2048x1536"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="2048x2732"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="2208x1242"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="2224x1668"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="2436x1125"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="2668x1242"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
<link
rel="apple-touch-startup-image"
sizes="2737x2048"
href="/assets/images/{{'SITE_NAME' | app_config}}/icon.gif"
/>
{% block stylesheets %}
{% if v %}
<style>:root{--primary:#{{v.themecolor}}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=80">
<link rel="stylesheet" href="/assets/css/{{v.theme}}.css?v=80">
{% if v.agendaposter %}<link rel="stylesheet" href="/assets/css/agendaposter.css?v=80">{% elif v.css %}<link rel="stylesheet" href="/@{{v.username}}/css">{% endif %}
{% else %}
<style>:root{--primary:#{{'DEFAULT_COLOR' | app_config}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=80"><link rel="stylesheet" href="/assets/css/{{'DEFAULT_THEME' | app_config}}.css?v=80">
{% endif %}
{% endblock %}
<link href="/assets/css/fa.css?v=52" rel="stylesheet">
{% block fixedMobileBarJS %}
{% endblock %}
</head>
<body id="{% if request.path != '/comments' %}{% block pagetype %}frontpage{% endblock %}{% endif %}" style="overflow-x: hidden; {% if v and v.background %} background:url(/assets/images/backgrounds/{{v.background}}) no-repeat center center fixed !important; background-size: cover!important; background-color: #000!important;{% endif %} {% if 'rama' in request.host %}margin-top: 29px!important;{% endif %}">
{% if '@' not in request.path %}
<a rel="nofollow noopener noreferrer" href="{% if 'rama' in request.host %}https://secure.transequality.org/site/Donation2?df_id=1480{% else %}/{% endif %}">
<img loading="lazy" style="margin-top: -3px;" src="/assets/images/{{'SITE_NAME' | app_config}}/{% if v %}banner.gif{% else %}cached.gif{% endif %}" width="100%">
</a>
{% endif %}
{% include "header.html" %}
{% block mobileUserBanner %}
{% endblock %}
{% block mobileBanner %}
{% endblock %}
{% block postNav %}
{% endblock %}
<div class="container {% if request.path=='/' or '/post/' in request.path or '/comment/' in request.path %} transparent {% endif %}">
<div class="row justify-content-around" id="main-content-row">
<div class="col h-100 {% block customPadding %}{% if not '/message' in request.path %}custom-gutters{% endif %}{% endblock %}" id="main-content-col">
{% block desktopUserBanner %}
{% endblock %}
{% block desktopBanner %}
{% endblock %}
{% block PseudoSubmitForm %}
{% endblock %}
{% block searchText %}
{% endblock %}
{% block content %}
{% endblock %}
{% block pagenav %}
{% endblock %}
</div>
</div>
</div>
{% block mobilenavbar %}
{% include "mobile_navigation_bar.html" %}
{% endblock %}
{% block actionsModal %}
{% endblock %}
{% block reportCommentModal %}
{% endblock %}
{% block GIFtoast %}
{% endblock %}
{% block GIFpicker %}
{% endblock %}
<div class="toast clipboard" id="toast-success" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body text-center">
<i class="fas fa-check-circle text-success mr-2"></i>Link copied to clipboard
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text"></span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
<style>
.mirrored {
transform: scaleX(-1);-webkit-transform: scaleX(-1);
}
</style>
</body>
</html>

View File

@ -1,66 +1,62 @@
<script>
function delete_postModal(id) {
function delete_post(){
this.innerHTML='<span class="spinner-border spinner-border-sm mr-2" role="status" aria-hidden="true"></span>Deleting post';
this.disabled = true;
var url = '/delete_post/' + id
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
var form = new FormData()
form.append("formkey", formkey());
xhr.withCredentials=true;
xhr.send(form);
location.reload();
}
document.getElementById("deletePostButton-mobile").onclick = delete_post;
document.getElementById("deletePostButton").onclick = delete_post;
};
</script>
<div class="modal fade modal-sm-bottom" id="deletePostModal" tabindex="-1" role="dialog" aria-labelledby="deletePostModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header d-none d-md-flex">
<h5 class="modal-title">Delete post?</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div class="modal-body text-center">
<div class="py-4">
<i class="fad fa-trash-alt text-muted d-none d-md-block" style="font-size: 3.5rem;"></i>
<span class="fa-stack fa-2x text-muted d-md-none">
<i class="fas fa-circle text-danger opacity-25 fa-stack-2x"></i>
<i class="fas text-danger fa-trash-alt fa-stack-1x"></i>
</span>
</div>
<div class="h4 d-md-none">Delete post?</div>
<p class="d-none d-md-block">Your post will be removed everywhere on {{'SITE_NAME' | app_config}}. This action can be undone.</p>
<p class="text-muted d-md-none">Your post will be removed everywhere on {{'SITE_NAME' | app_config}}. This action can be undone.</p>
<div class="d-md-none">
<button type="button" id="deletePostButton-mobile" class="btn btn-danger btn-block btn-lg">Delete post</button>
<button type="button" class="btn btn-secondary btn-block btn-lg" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
<div class="modal-footer d-none d-md-flex">
<button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="deletePostButton" class="btn btn-danger">Delete post</button>
</div>
</div>
</div>
</div>
<script>
function delete_postModal(id) {
function delete_post(){
this.innerHTML='<span class="spinner-border spinner-border-sm mr-2" role="status" aria-hidden="true"></span>Deleting post';
this.disabled = true;
var url = '/delete_post/' + id
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
var form = new FormData()
form.append("formkey", formkey());
xhr.withCredentials=true;
xhr.onload = function() {location.reload(true);};
xhr.send(form);
}
document.getElementById("deletePostButton-mobile").onclick = delete_post;
document.getElementById("deletePostButton").onclick = delete_post;
};
</script>
<div class="modal fade modal-sm-bottom" id="deletePostModal" tabindex="-1" role="dialog" aria-labelledby="deletePostModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header d-none d-md-flex">
<h5 class="modal-title">Delete post?</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div class="modal-body text-center">
<div class="py-4">
<i class="fad fa-trash-alt text-muted d-none d-md-block" style="font-size: 3.5rem;"></i>
</div>
<div class="h4 d-md-none">Delete post?</div>
<p class="d-none d-md-block">Your post will be removed everywhere on {{'SITE_NAME' | app_config}}. This action can be undone.</p>
<p class="text-muted d-md-none">Your post will be removed everywhere on {{'SITE_NAME' | app_config}}. This action can be undone.</p>
<div class="d-md-none">
<button type="button" id="deletePostButton-mobile" class="btn btn-danger btn-block btn-lg">Delete post</button>
<button type="button" class="btn btn-secondary btn-block btn-lg" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
<div class="modal-footer d-none d-md-flex">
<button type="button" class="btn btn-link text-muted" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="deletePostButton" class="btn btn-danger">Delete post</button>
</div>
</div>
</div>
</div>

View File

@ -1,36 +1,34 @@
{% extends "email/default.html" %}
{% block image %}verify.webp{% endblock %}
{% block title %}Remove Two-Factor Authentication{% endblock %}</h1>
{% block preheader %}Remove Two-Factor Authentication.{% endblock %}
{% block content %}
<p>We received a request to remove two-factor authentication from your account. In 72 hours, click the link below.</p>
<p>If you didn't make this request, change your password and use the Log Out Everywhere feature in your <a href="/settings/security">Security Settings</a> to permanently invalidate the link.</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Remove 2FA</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{'SITE_NAME' | app_config}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}
{% extends "email/default.html" %}
{% block title %}Remove Two-Factor Authentication{% endblock %}</h1>
{% block preheader %}Remove Two-Factor Authentication.{% endblock %}
{% block content %}
<p>We received a request to remove two-factor authentication from your account. In 72 hours, click the link below.</p>
<p>If you didn't make this request, change your password and use the Log Out Everywhere feature in your <a href="/settings/security">Security Settings</a> to permanently invalidate the link.</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Remove 2FA</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{'SITE_NAME' | app_config}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}

465
files/templates/email/default.html 100644 → 100755
View File

@ -1,416 +1,49 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
@import url('https://fonts.googleapis.com/css?family=Roboto:400,500&display=swap');
html {
font-size: 14px;
}
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #FF66AC!important;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
body,
td,
th {
font-family: "Roboto", Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #121213;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #121213;
font-size: 1rem;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #121213;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 1rem;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 1rem;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.button {
background-color: #FF66AC;
border-top: 10px solid #FF66AC;
border-right: 18px solid #FF66AC;
border-bottom: 10px solid #FF66AC;
border-left: 18px solid #FF66AC;
display: inline-block;
color: #FFF!important;
text-decoration: none;
border-radius: .25rem;
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #23CE6B;
border-top: 10px solid #23CE6B;
border-right: 18px solid #23CE6B;
border-bottom: 10px solid #23CE6B;
border-left: 18px solid #23CE6B;
}
.button--red {
background-color: #F05D5E;
border-top: 10px solid #F05D5E;
border-right: 18px solid #F05D5E;
border-bottom: 10px solid #F05D5E;
border-left: 18px solid #F05D5E;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #EDF2F7;
padding: 1rem;
border-radius: 0.35rem;
}
.attributes_item {
padding: 0;
}
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #EDF2F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #121213;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #E6E6E6;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #E6E6E6;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #121213;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #EDF2F7;
color: #121213;
}
p {
color: #121213;
}
p.sub {
color: #6B6E76;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #EDF2F7;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-masthead {
display: none;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 1.25rem;
font-weight: bold;
color: #121213;
text-decoration: none;
}
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #cfcfcf;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #cfcfcf;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #6B6E76;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #E6E6E6;
}
.content-cell {
padding: 35px;
}
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #121213 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3 {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
</head>
<body>
<span class="preheader">{% block preheader %}Thanks for joining {{'SITE_NAME' | app_config}}! Please take a sec to verify the email you used to sign up.{% endblock %}</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="/" class="f-fallback email-masthead_name">
{{'SITE_NAME' | app_config}}
</a>
</td>
</tr>
<tr>
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>{% block title %}Title Goes Here{% endblock %}</h1>
{% block content %}
{% for entry in data %}
<h3>{{entry[0]}}</h3>
<p>{{entry[1]}}</p>
{% endfor %}
{% endblock %}
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
</head>
<body>
<span class="preheader">{% block preheader %}Thanks for joining {{'SITE_NAME' | app_config}}! Please take a sec to verify the email you used to sign up.{% endblock %}</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="/" class="f-fallback email-masthead_name">
{{'SITE_NAME' | app_config}}
</a>
</td>
</tr>
<tr>
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>{% block title %}Title Goes Here{% endblock %}</h1>
{% block content %}
{% for entry in data %}
<h3>{{entry[0]}}</h3>
<p>{{entry[1]}}</p>
{% endfor %}
{% endblock %}
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -1,56 +1,56 @@
{% extends "email/default.html" %}
{% block title %}Verify Your Email{% endblock %}</h1>
{% block preheader %}Verify your new {{'SITE_NAME' | app_config}} email.{% endblock %}
{% block content %}
<p>You told us you wanted to change your {{'SITE_NAME' | app_config}} account email. To finish this process, please verify your new email address:</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your current information:</p>
<table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{'SITE_NAME' | app_config}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}
{% extends "email/default.html" %}
{% block title %}Verify Your Email{% endblock %}</h1>
{% block preheader %}Verify your new {{'SITE_NAME' | app_config}} email.{% endblock %}
{% block content %}
<p>You told us you wanted to change your {{'SITE_NAME' | app_config}} account email. To finish this process, please verify your new email address:</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your current information:</p>
<table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{'SITE_NAME' | app_config}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}

View File

@ -1,54 +1,54 @@
{% extends "email/default.html" %}
{% block title %}Welcome to {{'SITE_NAME' | app_config}}!{% endblock %}</h1>
{% block content %}
<p>Thanks for joining {{'SITE_NAME' | app_config}}. Were happy to have you on board. To get the most out of {{'SITE_NAME' | app_config}}, please verify your account email:</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your username.</p>
<table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{'SITE_NAME' | app_config}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}
{% extends "email/default.html" %}
{% block title %}Welcome to {{'SITE_NAME' | app_config}}!{% endblock %}</h1>
{% block content %}
<p>Thanks for joining {{'SITE_NAME' | app_config}}. Were happy to have you on board. To get the most out of {{'SITE_NAME' | app_config}}, please verify your account email:</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your username.</p>
<table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{'SITE_NAME' | app_config}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}

View File

@ -1,54 +1,54 @@
{% extends "email/default.html" %}
{% block title %}Reset Your Password{% endblock %}
{% block preheader %}Reset your {{'SITE_NAME' | app_config}} password.{% endblock %}
{% block content %}
<p>To reset your password, click the button below:</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Reset password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your login information:</p>
<table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}
{% extends "email/default.html" %}
{% block title %}Reset Your Password{% endblock %}
{% block preheader %}Reset your {{'SITE_NAME' | app_config}} password.{% endblock %}
{% block content %}
<p>To reset your password, click the button below:</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Reset password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your login information:</p>
<table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}

View File

@ -1,6 +1,6 @@
{{p.embed_url | safe}}
<script src="/assets/js/twitter.js" charset="utf-8">
</script>
<script>
document.getElementById('twitter-widget-0').setAttribute('sandbox','')
{{p.embed_url | safe}}
<script src="/assets/js/twitter.js" charset="utf-8">
</script>
<script>
document.getElementById('twitter-widget-0').setAttribute('sandbox','')
</script>

View File

@ -1,6 +1,6 @@
{{p.embed_url | safe}}
<script src="https://platform.twitter.com/widgets.js" charset="utf-8">
</script>
<script>
document.getElementById('twitter-widget-0').setAttribute('sandbox','')
{{p.embed_url | safe}}
<script src="https://platform.twitter.com/widgets.js" charset="utf-8">
</script>
<script>
document.getElementById('twitter-widget-0').setAttribute('sandbox','')
</script>

View File

@ -1,3 +1,3 @@
<div class="embed-responsive embed-responsive-16by9 mb-3">
<iframe loading="lazy" src="{{p.embed_url}}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<div class="embed-responsive embed-responsive-16by9 mb-3">
<iframe loading="lazy" src="{{p.embed_url}}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

196
files/templates/emoji_modal.html 100644 → 100755
View File

@ -1,99 +1,99 @@
<script src="/assets/js/emoji_modal.js?v=50"></script>
<style>
a.emojitab {
padding: 0.5rem 0.7rem !important;
font-size: 13px !important;
}
@media (min-width: 576px)
{
.modal-dialog {
max-width: 65% !important;
margin: 1.75rem auto !important;
}
}
.emoji2:focus {
border: 1px solid var(--primary) !important;
}
</style>
<div id="form" class="d-none"></div>
<div class="modal fade" id="emojiModal" tabindex="-1" role="dialog" aria-labelledby="emojiModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered p-2 py-5" role="document">
<div class="modal-content" id="emojiTabs">
<div class="modal-header">
<div>
<ul class="nav nav-pills py-2">
<li class="nav-item">
<a class="nav-link active emojitab" data-bs-toggle="tab" href="#emoji-tab-favorite">Favorite</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-marsey">Marsey</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-platy">Platy</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-tay">Tay</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-classic">Classic</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-rage">Rage</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-wojak">Wojak</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-flags">Flags</a>
</li>
</ul>
</div>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<i class="fal fa-times text-muted"></i>
</button>
</div>
<div class="px-3"><input class="form-control px-2" type="text" id="emoji_search" placeholder="Search.."></div>
<div style="overflow-y: scroll;">
<div class="modal-body p-0" id="emoji-modal-body">
<div id="emoji-tab-search"></div>
<div id="no-emojis-found"></div>
<div class="tab-content">
<div class="tab-pane fade show active" id="emoji-tab-favorite">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_favorite">
{% if session.get("favorite_emojis") %}
{{session.get("favorite_emojis") | favorite_emojis | safe}}
{% endif %}
</div>
</div>
<div class="tab-pane fade" id="emoji-tab-marsey">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_marsey"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-platy">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_platy"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-tay">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_tay"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-classic">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_classic"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-rage">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_rage"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-wojak">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_wojak"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-flags">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_flags"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/assets/js/emoji_modal.js?v=56"></script>
<style>
a.emojitab {
padding: 0.5rem 0.7rem !important;
font-size: 13px !important;
}
@media (min-width: 576px)
{
.modal-dialog {
max-width: 65% !important;
margin: 1.75rem auto !important;
}
}
.emoji2:focus {
border: 1px solid var(--primary) !important;
}
</style>
<div id="form" class="d-none"></div>
<div class="modal fade" id="emojiModal" tabindex="-1" role="dialog" aria-labelledby="emojiModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered p-2 py-5" role="document">
<div class="modal-content" id="emojiTabs">
<div class="modal-header">
<div>
<ul class="nav nav-pills py-2">
<li class="nav-item">
<a class="nav-link active emojitab" data-bs-toggle="tab" href="#emoji-tab-favorite">Favorite</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-marsey">Marsey</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-platy">Platy</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-tay">Tay</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-classic">Classic</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-rage">Rage</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-wojak">Wojak</a>
</li>
<li class="nav-item">
<a class="nav-link emojitab" data-bs-toggle="tab" href="#emoji-tab-flags">Flags</a>
</li>
</ul>
</div>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<i class="fal fa-times text-muted"></i>
</button>
</div>
<div class="px-3"><input class="form-control px-2" type="text" id="emoji_search" placeholder="Search.."></div>
<div style="overflow-y: scroll;">
<div class="modal-body p-0" id="emoji-modal-body">
<div id="emoji-tab-search"></div>
<div id="no-emojis-found"></div>
<div class="tab-content">
<div class="tab-pane fade show active" id="emoji-tab-favorite">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_favorite">
{% if session.get("favorite_emojis") %}
{{session.get("favorite_emojis") | favorite_emojis | safe}}
{% endif %}
</div>
</div>
<div class="tab-pane fade" id="emoji-tab-marsey">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_marsey"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-platy">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_platy"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-tay">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_tay"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-classic">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_classic"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-rage">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_rage"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-wojak">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_wojak"></div>
</div>
<div class="tab-pane fade" id="emoji-tab-flags">
<div class="d-flex flex-wrap py-3 pl-2" id="EMOJIS_flags"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

40
files/templates/errors/400.html 100644 → 100755
View File

@ -1,20 +1,20 @@
{% extends "default.html" %}
{% block title %}
<title>400 Bad Request</title>
{% endblock %}
{% block pagetype %}error-400{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-10 col-md-5">
<div class="text-center px-3 my-8">
<img loading="lazy" src="/assets/images/emojis/marseybrainlet.webp">
<pre></pre>
<h1 class="h5">400 Bad Request</h1>
<p class="text-muted mb-5">That request was bad and you should feel bad.</p>
</div>
</div>
</div>
{% endblock %}
{% extends "default.html" %}
{% block title %}
<title>400 Bad Request</title>
{% endblock %}
{% block pagetype %}error-400{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-10 col-md-5">
<div class="text-center px-3 my-8">
<img loading="lazy" src="/assets/images/emojis/marseybrainlet.webp">
<pre></pre>
<h1 class="h5">400 Bad Request</h1>
<p class="text-muted mb-5">That request was bad and you should feel bad.</p>
</div>
</div>
</div>
{% endblock %}

46
files/templates/errors/401.html 100644 → 100755
View File

@ -1,24 +1,24 @@
{% extends "default.html" %}
{% block title %}
<title>401 Not Authorized</title>
{% endblock %}
{% block pagetype %}error-401{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-10 col-md-5">
<div class="text-center px-3 my-8">
<img loading="lazy" src="/assets/images/emojis/marseydead.webp">
<pre></pre>
<h1 class="h5">401 Not Authorized</h1>
<p class="text-muted mb-5">What you're trying to do requires an account. I think. The original error message said something about a castle and I hated that.</p>
<div><a href="/signup" class="btn btn-primary mb-2">Create an account</a></div>
<div><a href="/login" class="text-muted text-small">Or sign in</a></div>
</div>
</div>
</div>
{% extends "default.html" %}
{% block title %}
<title>401 Not Authorized</title>
{% endblock %}
{% block pagetype %}error-401{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-10 col-md-5">
<div class="text-center px-3 my-8">
<img loading="lazy" src="/assets/images/emojis/marseydead.webp">
<pre></pre>
<h1 class="h5">401 Not Authorized</h1>
<p class="text-muted mb-5">What you're trying to do requires an account. I think. The original error message said something about a castle and I hated that.</p>
<div><a href="/signup" class="btn btn-primary mb-2">Create an account</a></div>
<div><a href="/login" class="text-muted text-small">Or sign in</a></div>
</div>
</div>
</div>
{% endblock %}

42
files/templates/errors/403.html 100644 → 100755
View File

@ -1,21 +1,21 @@
{% extends "default.html" %}
{% block title %}
<title>403 Forbidden</title>
{% endblock %}
{% block pagetype %}error-403{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-10 col-md-5">
<div class="text-center px-3 my-8">
<img loading="lazy" src="/assets/images/emojis/marseytroll.webp">
<pre></pre>
<h1 class="h5">403 Forbidden</h1>
<p class="text-muted mb-5">YOU AREN'T WELCOME HERE GO AWAY</p>
<div><a href="/" class="btn btn-primary">Go to frontpage</a></div>
</div>
</div>
</div>
{% endblock %}
{% extends "default.html" %}
{% block title %}
<title>403 Forbidden</title>
{% endblock %}
{% block pagetype %}error-403{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-10 col-md-5">
<div class="text-center px-3 my-8">
<img loading="lazy" src="/assets/images/emojis/marseytroll.webp">
<pre></pre>
<h1 class="h5">403 Forbidden</h1>
<p class="text-muted mb-5">YOU AREN'T WELCOME HERE GO AWAY</p>
<div><a href="/" class="btn btn-primary">Go to frontpage</a></div>
</div>
</div>
</div>
{% endblock %}

42
files/templates/errors/404.html 100644 → 100755
View File

@ -1,21 +1,21 @@
{% extends "default.html" %}
{% block title %}
<title>404 Page Not Found</title>
{% endblock %}
{% block pagetype %}error-404{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-10 col-md-5">
<div class="text-center px-3 my-8">
<img loading="lazy" src="/assets/images/emojis/marseyconfused.webp">
<pre></pre>
<h1 class="h5">404 Page Not Found</h1>
<p class="text-muted mb-5">Someone typed something wrong and it was probably you, please do better.</p>
<div><a href="/" class="btn btn-primary">Go to frontpage</a></div>
</div>
</div>
</div>
{% endblock %}
{% extends "default.html" %}
{% block title %}
<title>404 Page Not Found</title>
{% endblock %}
{% block pagetype %}error-404{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-10 col-md-5">
<div class="text-center px-3 my-8">
<img loading="lazy" src="/assets/images/emojis/marseyconfused.webp">
<pre></pre>
<h1 class="h5">404 Page Not Found</h1>
<p class="text-muted mb-5">Someone typed something wrong and it was probably you, please do better.</p>
<div><a href="/" class="btn btn-primary">Go to frontpage</a></div>
</div>
</div>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More