forked from rDrama/rDrama
sneed
commit
1fc69b9ce9
|
@ -0,0 +1,4 @@
|
|||
*.css linguist-detectable=false
|
||||
*.js linguist-detectable=true
|
||||
*.html linguist-detectable=false
|
||||
*.py linguist-detectable=true
|
|
@ -0,0 +1,3 @@
|
|||
github: Aevann1
|
||||
patreon: Aevann
|
||||
custom: ["https://rdrama.gumroad.com/l/tfcvri"]
|
|
@ -0,0 +1,115 @@
|
|||
ruqquscache/
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
#custom
|
||||
*.DS_Store
|
||||
#node_modules/
|
||||
package-lock.json
|
||||
*.css.map
|
||||
*.map
|
||||
local.txt
|
||||
*.scssc
|
||||
.idea/*
|
|
@ -0,0 +1,9 @@
|
|||
[{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "net.rdrama",
|
||||
"sha256_cert_fingerprints":
|
||||
["ED:D9:72:9B:CD:61:52:18:AB:95:D0:21:BD:7F:04:6F:89:04:AC:B9:73:A2:2E:90:A5:2B:0C:13:F8:4A:EC:18"]
|
||||
}
|
||||
}]
|
|
@ -0,0 +1,4 @@
|
|||
This is a Brave Rewards publisher verification file.
|
||||
|
||||
Domain: rdrama.net
|
||||
Token: 0774158a4aec1e891263f84cf37919c0aa19309b9fba4ad9c4a0aae8946f5d0d
|
|
@ -0,0 +1 @@
|
|||
importScripts("https://js.pusher.com/beams/service-worker.js");
|
|
@ -0,0 +1,17 @@
|
|||
FROM ubuntu:20.04
|
||||
|
||||
COPY supervisord.conf /etc/supervisord.conf
|
||||
|
||||
RUN apt update \
|
||||
&& apt install -y python3.8 python3-pip supervisor
|
||||
|
||||
RUN mkdir -p /opt/Drama/service
|
||||
|
||||
COPY requirements.txt /opt/Drama/service/requirements.txt
|
||||
|
||||
RUN cd /opt/Drama/service \
|
||||
&& pip3 install -r requirements.txt
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
||||
CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]
|
|
@ -0,0 +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.
|
|
@ -0,0 +1,13 @@
|
|||
version: 0.0
|
||||
os: linux
|
||||
files:
|
||||
- source: /
|
||||
destination: ruqqus
|
||||
permissions:
|
||||
- object: ruqqus/*
|
||||
mode: 4755
|
||||
hooks:
|
||||
AfterInstall:
|
||||
- location: scripts/install_pip
|
||||
ApplicationStart:
|
||||
- location: scripts/start_ruqqus
|
|
@ -0,0 +1,8 @@
|
|||
version: 0.2
|
||||
phases:
|
||||
install:
|
||||
runtime-versions:
|
||||
python: 3.7
|
||||
artifacts:
|
||||
files:
|
||||
- '**/*'
|
|
@ -0,0 +1,7 @@
|
|||
for theme in ['dark', 'light', 'coffee', 'tron', '4chan']:
|
||||
with open(f"D:/#D/ruqqus/assets/style/{theme}_ff66ac.css", encoding='utf-8') as t:
|
||||
text = t.read()
|
||||
for color in ['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)
|
||||
with open(f"D:/#D/ruqqus/assets/style/{theme}_{color}.css", encoding='utf-8', mode='w') as nt:
|
||||
nt.write(newtext)
|
|
@ -0,0 +1,6 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
|
@ -0,0 +1,62 @@
|
|||
version: '2.3'
|
||||
|
||||
services:
|
||||
ruqqus:
|
||||
build:
|
||||
context: .
|
||||
volumes:
|
||||
- "./:/opt/Drama/service"
|
||||
environment:
|
||||
- PYTHONPATH="opt/Drama/service"
|
||||
- REDIS_URL=redis://127.0.0.1
|
||||
- DATABASE_URL=postgresql://postgres@127.0.0.1:5432/postgres
|
||||
- DATABASE_CONNECTION_POOL_URL=postgresql://postgres@127.0.0.1:5432/postgres
|
||||
- MASTER_KEY=${MASTER_KEY:-KTVciAUQFpFh2WdJ/oiHJlxl6FvzRZp8kYzAAv3l2OA=}
|
||||
- SESSION_COOKIE_SECURE=false
|
||||
- FLASK_DEBUG=1
|
||||
- FLASK_ENV=development
|
||||
- domain=localhost
|
||||
- SITE_NAME=Drama
|
||||
- CLOUDFLARE_ZONE=vcxvdfgfc6r554etrgd
|
||||
- CLOUDFLARE_KEY=vcxvdfgfc6r554etrgd
|
||||
- TENOR_KEY=vcxvdfgfc6r554etrgd
|
||||
- S3_BUCKET_NAME=i.ruqqus.ga
|
||||
- AWS_ACCESS_KEY_ID=vcxvdfgfc6r554etrgd
|
||||
- AWS_SECRET_ACCESS_KEY=vcxvdfgfc6r554etrgd
|
||||
- MAILGUN_KEY=vcxvdfgfc6r554etrgd
|
||||
- admin_email=drama@rdrama.net
|
||||
- FORCE_HTTPS=1
|
||||
- DISCORD_SERVER_ID=vcxvdfgfc6r554etrgd
|
||||
- DISCORD_CLIENT_ID=vcxvdfgfc6r554etrgd
|
||||
- DISCORD_CLIENT_SECRET=vcxvdfgfc6r554etrgd
|
||||
- DISCORD_BOT_TOKEN=vcxvdfgfc6r554etrgd
|
||||
- imgurkey=vcxvdfgfc6r554etrgd
|
||||
- FACEBOOK_TOKEN=vcxvdfgfc6r554etrgd
|
||||
- HCAPTCHA_SITEKEY=vcxvdfgfc6r554etrgd
|
||||
- HCAPTCHA_SECRET=vcxvdfgfc6r554etrgd
|
||||
- youtubekey=vcxvdfgfc6r554etrgd
|
||||
- PUSHER_KEY=vcxvdfgfc6r554etrgd
|
||||
links:
|
||||
- "redis"
|
||||
- "postgres"
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
volumes:
|
||||
- ./redis.conf:/opt/Drama/redis.conf
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
postgres:
|
||||
image: postgres:12.3
|
||||
volumes:
|
||||
- "./schema.sql:/docker-entrypoint-initdb.d/00-schema.sql"
|
||||
environment:
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
ports:
|
||||
- "5432:5432"
|
|
@ -0,0 +1,17 @@
|
|||
git pull
|
||||
git add .
|
||||
git commit -m "sneed"
|
||||
git push
|
||||
|
||||
sass ./ruqqus/assets/style/dark.scss D:/#D/ruqqus/assets/style/dark_ff66ac.css
|
||||
sass ./ruqqus/assets/style/light.scss D:/#D/ruqqus/assets/style/light_ff66ac.css
|
||||
sass ./ruqqus/assets/style/coffee.scss D:/#D/ruqqus/assets/style/coffee_ff66ac.css
|
||||
sass ./ruqqus/assets/style/tron.scss D:/#D/ruqqus/assets/style/tron_ff66ac.css
|
||||
sass ./ruqqus/assets/style/4chan.scss D:/#D/ruqqus/assets/style/4chan_ff66ac.css
|
||||
python ./compilecss.py
|
||||
git add .
|
||||
git commit -m "css"
|
||||
git push
|
||||
|
||||
cd D:\1
|
||||
git pull
|
|
@ -0,0 +1,3 @@
|
|||
git add .
|
||||
git commit -m "force push"
|
||||
git push --force
|
|
@ -0,0 +1,78 @@
|
|||
# How to Install Drama Locally
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
Installing Drama locally is the fastest way to get the software up and running and start tinkering under the hood.
|
||||
|
||||
---
|
||||
|
||||
## Windows
|
||||
|
||||
### Install Docker
|
||||
|
||||
Install Docker on your machine.
|
||||
|
||||
[Docker installation for Windows](https://docs.docker.com/docker-for-windows/install/)
|
||||
|
||||
### Download Drama
|
||||
|
||||
Download the latest release of Drama from GitHub.
|
||||
|
||||
[Drama Latest Release - GitHub](https://github.com/Drama/Drama/releases)
|
||||
|
||||
### PowerShell
|
||||
|
||||
Press shift+right click inside the code folder and run PowerShell. Then in PowerShell, run the following command:
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
That's it! Visit `localhost` in your browser.
|
||||
|
||||
---
|
||||
|
||||
## Linux
|
||||
|
||||
### Install Docker
|
||||
|
||||
Install Docker on your machine.
|
||||
|
||||
[Docker installation for Ubuntu](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-18-04)
|
||||
|
||||
### Install Docker-compose
|
||||
|
||||
Install Docker-compose on your machine.
|
||||
|
||||
[Docker-compose installation for Ubuntu](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04)
|
||||
|
||||
### Download Drama
|
||||
|
||||
Navigate to `/opt`
|
||||
|
||||
```
|
||||
cd /opt
|
||||
```
|
||||
|
||||
then clone Drama into your machine.
|
||||
|
||||
```
|
||||
git clone https://github.com/Drama/Drama/
|
||||
```
|
||||
|
||||
### Run Drama
|
||||
|
||||
Navigate to `/opt/Drama`
|
||||
|
||||
```
|
||||
cd /opt/Drama
|
||||
```
|
||||
|
||||
then run this command
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
That's it! Visit `localhost` in your browser.
|
|
@ -0,0 +1,69 @@
|
|||
async-timeout
|
||||
attrs
|
||||
beautifulsoup4
|
||||
bleach
|
||||
boto3
|
||||
botocore
|
||||
certifi
|
||||
chardet
|
||||
click
|
||||
cycler
|
||||
eventlet
|
||||
Flask
|
||||
Flask-Caching==1.9.0
|
||||
Flask-Compress==1.9.0
|
||||
Flask-Limiter==1.1.0
|
||||
Flask-Markdown==0.3
|
||||
flask-socketio==5.1.0
|
||||
gevent
|
||||
gevent-websocket
|
||||
greenlet
|
||||
gunicorn
|
||||
idna
|
||||
idna-ssl
|
||||
ImageHash
|
||||
importlib-metadata
|
||||
itsdangerous
|
||||
Jinja2
|
||||
jmespath
|
||||
kiwisolver
|
||||
libsass
|
||||
limits
|
||||
lockfile
|
||||
Markdown
|
||||
MarkupSafe
|
||||
matplotlib
|
||||
mistletoe
|
||||
multidict
|
||||
newrelic
|
||||
numpy
|
||||
piexif
|
||||
Pillow
|
||||
psutil
|
||||
psycogreen
|
||||
psycopg2-binary
|
||||
py3socket
|
||||
pyotp
|
||||
pyparsing
|
||||
python-daemon
|
||||
python-dateutil
|
||||
PyWavelets
|
||||
qrcode
|
||||
redis
|
||||
requests
|
||||
s3transfer
|
||||
scipy
|
||||
six
|
||||
soupsieve
|
||||
SQLAlchemy==1.3.19
|
||||
typing-extensions
|
||||
urllib3
|
||||
webencodings
|
||||
websockets
|
||||
Werkzeug
|
||||
yattag
|
||||
yarl
|
||||
zipp
|
||||
pusher_push_notifications
|
||||
youtube-dl
|
||||
testresources
|
|
@ -0,0 +1,352 @@
|
|||
import gevent.monkey
|
||||
gevent.monkey.patch_all()
|
||||
|
||||
import os
|
||||
from os import environ
|
||||
import secrets
|
||||
from flask import *
|
||||
from flask_caching import Cache
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
from flask_compress import Compress
|
||||
from flask_socketio import SocketIO
|
||||
from time import sleep
|
||||
from collections import deque
|
||||
import psycopg2
|
||||
|
||||
from flaskext.markdown import Markdown
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.exc import OperationalError, StatementError, InternalError
|
||||
from sqlalchemy.orm import Session, sessionmaker, scoped_session, Query as _Query
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.pool import QueuePool
|
||||
import threading
|
||||
import requests
|
||||
import random
|
||||
import redis
|
||||
import gevent
|
||||
import sys
|
||||
|
||||
from redis import BlockingConnectionPool, ConnectionPool
|
||||
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
|
||||
_version = "2.37.4"
|
||||
|
||||
app = Flask(__name__,
|
||||
template_folder='./templates',
|
||||
static_folder='./static'
|
||||
)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=3)
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
app.config["SITE_NAME"]=environ.get("SITE_NAME", "Ruqqus").strip()
|
||||
|
||||
app.config["SITE_COLOR"]=environ.get("SITE_COLOR", "805ad5").strip()
|
||||
|
||||
app.config["RUQQUSPATH"]=environ.get("RUQQUSPATH", os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['DATABASE_URL'] = environ.get(
|
||||
"DATABASE_CONNECTION_POOL_URL",
|
||||
environ.get("DATABASE_URL"))
|
||||
|
||||
app.config['SQLALCHEMY_READ_URIS'] = [
|
||||
environ.get("DATABASE_CONNECTION_READ_01_URL"),
|
||||
environ.get("DATABASE_CONNECTION_READ_02_URL"),
|
||||
environ.get("DATABASE_CONNECTION_READ_03_URL")
|
||||
]
|
||||
|
||||
app.config['SECRET_KEY'] = environ.get('MASTER_KEY')
|
||||
app.config["SERVER_NAME"] = environ.get(
|
||||
"domain", environ.get(
|
||||
"SERVER_NAME", "")).strip()
|
||||
|
||||
app.config["SHORT_DOMAIN"]=environ.get("SHORT_DOMAIN","").strip()
|
||||
app.config["SESSION_COOKIE_NAME"] = "session_ruqqus"
|
||||
app.config["VERSION"] = _version
|
||||
app.config['MAX_CONTENT_LENGTH'] = 64 * 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["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["DISABLE_SIGNUPS"]=int(environ.get("DISABLE_SIGNUPS",0))
|
||||
|
||||
app.jinja_env.cache = {}
|
||||
|
||||
app.config["UserAgent"] = f"Content Aquisition for Pink message board v{_version}."
|
||||
|
||||
if "localhost" in app.config["SERVER_NAME"]:
|
||||
app.config["CACHE_TYPE"] = "null"
|
||||
else:
|
||||
app.config["CACHE_TYPE"] = environ.get("CACHE_TYPE", 'filesystem').strip()
|
||||
|
||||
app.config["CACHE_DIR"] = environ.get("CACHE_DIR", "ruqquscache")
|
||||
|
||||
# captcha configs
|
||||
app.config["HCAPTCHA_SITEKEY"] = environ.get("HCAPTCHA_SITEKEY","").strip()
|
||||
app.config["HCAPTCHA_SECRET"] = environ.get(
|
||||
"HCAPTCHA_SECRET","").strip()
|
||||
app.config["SIGNUP_HOURLY_LIMIT"]=int(environ.get("SIGNUP_HOURLY_LIMIT",0))
|
||||
|
||||
# antispam configs
|
||||
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.1))
|
||||
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", 5))
|
||||
|
||||
app.config["CACHE_REDIS_URL"] = environ.get(
|
||||
"REDIS_URL").strip().lstrip() if environ.get("REDIS_URL") else None
|
||||
app.config["CACHE_DEFAULT_TIMEOUT"] = 60
|
||||
app.config["CACHE_KEY_PREFIX"] = "flask_caching_"
|
||||
|
||||
app.config["S3_BUCKET"]=environ.get("S3_BUCKET_NAME","i.rdrama.net").strip()
|
||||
|
||||
app.config["REDIS_POOL_SIZE"]=int(environ.get("REDIS_POOL_SIZE", 10))
|
||||
|
||||
redispool=ConnectionPool(
|
||||
max_connections=app.config["REDIS_POOL_SIZE"],
|
||||
host=app.config["CACHE_REDIS_URL"][8:]
|
||||
) if app.config["CACHE_TYPE"]=="redis" else None
|
||||
app.config["CACHE_OPTIONS"]={'connection_pool':redispool} if app.config["CACHE_TYPE"]=="redis" else {}
|
||||
|
||||
app.config["READ_ONLY"]=bool(int(environ.get("READ_ONLY", False)))
|
||||
app.config["BOT_DISABLE"]=bool(int(environ.get("BOT_DISABLE", False)))
|
||||
|
||||
app.config["TENOR_KEY"]=environ.get("TENOR_KEY",'').strip()
|
||||
|
||||
|
||||
Markdown(app)
|
||||
cache = Cache(app)
|
||||
Compress(app)
|
||||
|
||||
class CorsMatch(str):
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
if other in ['https://rdrama.net', f'https://{app.config["SERVER_NAME"]}']:
|
||||
return True
|
||||
|
||||
elif other.endswith(".rdrama.net"):
|
||||
return True
|
||||
|
||||
elif isinstance(other, list):
|
||||
if f'https://{app.config["SERVER_NAME"]}' in other:
|
||||
return True
|
||||
elif any([x.endswith(".rdrama.net") for x in other]):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
app.config["RATELIMIT_STORAGE_URL"] = environ.get("REDIS_URL").strip() if environ.get("REDIS_URL") else 'memory://'
|
||||
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
|
||||
|
||||
|
||||
def limiter_key_func():
|
||||
return request.remote_addr
|
||||
|
||||
|
||||
limiter = Limiter(
|
||||
app,
|
||||
key_func=limiter_key_func,
|
||||
default_limits=["100/minute"],
|
||||
headers_enabled=True,
|
||||
strategy="fixed-window"
|
||||
)
|
||||
|
||||
_engine=create_engine(
|
||||
app.config['DATABASE_URL'],
|
||||
poolclass=QueuePool,
|
||||
pool_size=int(environ.get("PG_POOL_SIZE",10)),
|
||||
pool_use_lifo=True
|
||||
)
|
||||
|
||||
def retry(f):
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return f(self, *args, **kwargs)
|
||||
except OperationalError as e:
|
||||
#self.session.rollback()
|
||||
raise(DatabaseOverload)
|
||||
except:
|
||||
self.session.rollback()
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
wrapper.__name__=f.__name__
|
||||
return wrapper
|
||||
|
||||
|
||||
class RetryingQuery(_Query):
|
||||
|
||||
@retry
|
||||
def all(self):
|
||||
return super().all()
|
||||
|
||||
@retry
|
||||
def count(self):
|
||||
return super().count()
|
||||
|
||||
@retry
|
||||
def first(self):
|
||||
return super().first()
|
||||
|
||||
db_session=scoped_session(sessionmaker(bind=_engine, query_cls=RetryingQuery))
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
#set the shared redis cache for misc stuff
|
||||
|
||||
r=redis.Redis(
|
||||
host=app.config["CACHE_REDIS_URL"][8:],
|
||||
decode_responses=True,
|
||||
ssl_cert_reqs=None,
|
||||
connection_pool=redispool
|
||||
) if app.config["CACHE_REDIS_URL"] else None
|
||||
|
||||
local_ban_cache={}
|
||||
|
||||
UA_BAN_CACHE_TTL = int(environ.get("UA_BAN_CACHE_TTL", 3600))
|
||||
|
||||
|
||||
|
||||
# import and bind all routing functions
|
||||
import ruqqus.classes
|
||||
from ruqqus.routes import *
|
||||
import ruqqus.helpers.jinja2
|
||||
|
||||
@cache.memoize(UA_BAN_CACHE_TTL)
|
||||
def get_useragent_ban_response(user_agent_str):
|
||||
"""
|
||||
Given a user agent string, returns a tuple in the form of:
|
||||
(is_user_agent_banned, (insult, status_code))
|
||||
"""
|
||||
#if request.path.startswith("/socket.io/"):
|
||||
# return False, (None, None)
|
||||
|
||||
result = g.db.query(
|
||||
ruqqus.classes.Agent).filter(
|
||||
ruqqus.classes.Agent.kwd.in_(
|
||||
user_agent_str.split())).first()
|
||||
if result:
|
||||
return True, (result.mock or "Follow the robots.txt, dumbass",
|
||||
result.status_code or 418)
|
||||
return False, (None, None)
|
||||
|
||||
def drop_connection():
|
||||
|
||||
g.db.close()
|
||||
gevent.getcurrent().kill()
|
||||
|
||||
|
||||
# enforce https
|
||||
@app.before_request
|
||||
def before_request():
|
||||
|
||||
if request.method.lower() != "get" and app.config["READ_ONLY"]:
|
||||
return jsonify({"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()
|
||||
|
||||
if g.db.query(IP).filter_by(addr=request.remote_addr).first():
|
||||
abort(503)
|
||||
|
||||
g.timestamp = int(time.time())
|
||||
|
||||
session.permanent = True
|
||||
|
||||
ua_banned, response_tuple = get_useragent_ban_response(
|
||||
request.headers.get("User-Agent", "NoAgent"))
|
||||
if ua_banned and request.path != "/robots.txt":
|
||||
return response_tuple
|
||||
|
||||
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)
|
||||
|
||||
if not session.get("session_id"):
|
||||
session["session_id"] = secrets.token_hex(16)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def log_event(name, link):
|
||||
|
||||
x = requests.get(link)
|
||||
|
||||
if x.status_code != 200:
|
||||
return
|
||||
|
||||
text = f'> **{name}**\r> {link}'
|
||||
|
||||
url = os.environ.get("DISCORD_WEBHOOK")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
data = {"username": "ruqqus",
|
||||
"content": text
|
||||
}
|
||||
|
||||
x = requests.post(url, headers=headers, json=data)
|
||||
print(x.status_code)
|
||||
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
|
||||
try: g.db.commit()
|
||||
except: pass
|
||||
g.db.close()
|
||||
|
||||
response.headers.add('Access-Control-Allow-Headers',
|
||||
"Origin, X-Requested-With, Content-Type, Accept, x-auth"
|
||||
)
|
||||
response.headers.add("Cache-Control",
|
||||
"maxage=600")
|
||||
response.headers.add("Strict-Transport-Security", "max-age=31536000")
|
||||
response.headers.add("Referrer-Policy", "same-origin")
|
||||
# response.headers.add("X-Content-Type-Options","nosniff")
|
||||
response.headers.add("Feature-Policy",
|
||||
"geolocation 'none'; midi 'none'; notifications 'none'; push 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; vibrate 'none'; fullscreen 'none'; payment 'none';")
|
||||
if not request.path.startswith("/embed/"):
|
||||
response.headers.add("X-Frame-Options",
|
||||
"deny")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/<path:path>", subdomain="www")
|
||||
def www_redirect(path):
|
||||
|
||||
return redirect(f"https://{app.config['SERVER_NAME']}/{path}")
|
|
@ -0,0 +1,20 @@
|
|||
from .alts import *
|
||||
from .badges import *
|
||||
from .boards import *
|
||||
from .board_relationships import *
|
||||
from .clients import *
|
||||
from .comment import *
|
||||
from .custom_errors import *
|
||||
from .domains import Domain
|
||||
from .flags import *
|
||||
from .user import *
|
||||
from .userblock import *
|
||||
from .submission import *
|
||||
from .votes import *
|
||||
from .images import *
|
||||
from .domains import *
|
||||
from .subscriptions import *
|
||||
from .ips import *
|
||||
from .titles import *
|
||||
from .paypal import *
|
||||
from .mod_logs import *
|
|
@ -0,0 +1,15 @@
|
|||
from sqlalchemy import *
|
||||
from ruqqus.__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})>"
|
|
@ -0,0 +1,92 @@
|
|||
from flask import render_template
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from ruqqus.__main__ import Base, app
|
||||
|
||||
|
||||
class BadgeDef(Base):
|
||||
|
||||
__tablename__ = "badge_defs"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
name = Column(String(64))
|
||||
description = Column(String(64))
|
||||
icon = Column(String(64))
|
||||
kind = Column(Integer, default=1)
|
||||
rank = Column(Integer, default=1)
|
||||
qualification_expr = Column(String(128), default=None)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"<BadgeDef(badge_id={self.id})>"
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
|
||||
return f"/assets/images/badges/{self.icon}"
|
||||
|
||||
@property
|
||||
def json_core(self):
|
||||
data={
|
||||
"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(64))
|
||||
url = Column(String(256))
|
||||
created_utc = Column(Integer)
|
||||
badge = relationship("BadgeDef", lazy="joined", innerjoin=True)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"<Badge(user_id={self.user_id}, badge_id={self.badge_id})>"
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
if self.description:
|
||||
return self.description
|
||||
else:
|
||||
return self.badge.description
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self.badge.id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.badge.name
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self.badge.path
|
||||
|
||||
@property
|
||||
def rendered(self):
|
||||
|
||||
return render_template("badge.html", b=self)
|
||||
|
||||
@property
|
||||
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
|
||||
def json(self):
|
||||
return self.json_core
|
|
@ -0,0 +1,198 @@
|
|||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship
|
||||
from ruqqus.__main__ import Base, cache
|
||||
from .mix_ins import *
|
||||
import time
|
||||
|
||||
|
||||
class ModRelationship(Base, Age_times):
|
||||
__tablename__ = "mods"
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
board_id = Column(Integer, ForeignKey("boards.id"))
|
||||
created_utc = Column(Integer, default=0)
|
||||
accepted = Column(Boolean, default=False)
|
||||
invite_rescinded = Column(Boolean, default=False)
|
||||
|
||||
perm_content = Column(Boolean, default=False)
|
||||
perm_appearance = Column(Boolean, default=False)
|
||||
perm_config = Column(Boolean, default=False)
|
||||
perm_access = Column(Boolean, default=False)
|
||||
perm_full = Column(Boolean, default=False)
|
||||
#permRules = Column(Boolean, default=False)
|
||||
#permTitles = Column(Boolean, default=False)
|
||||
#permLodges = Column(Boolean, default=False)
|
||||
|
||||
user = relationship("User", lazy="joined")
|
||||
board = relationship("Board", lazy="joined")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time.time())
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Mod(id={self.id}, uid={self.user_id}, board_id={self.board_id})>"
|
||||
|
||||
@property
|
||||
def permlist(self):
|
||||
if self.perm_full:
|
||||
return "full"
|
||||
|
||||
output=[]
|
||||
for p in ["access","appearance", "config","content"]:
|
||||
if self.__dict__[f"perm_{p}"]:
|
||||
output.append(p)
|
||||
|
||||
|
||||
return ", ".join(output) if output else "none"
|
||||
|
||||
@property
|
||||
def permchangelist(self):
|
||||
output=[]
|
||||
for p in ["full", "access","appearance", "config","content"]:
|
||||
if self.__dict__.get(f"perm_{p}"):
|
||||
output.append(f"+{p}")
|
||||
else:
|
||||
output.append(f"-{p}")
|
||||
|
||||
return ", ".join(output)
|
||||
|
||||
|
||||
@property
|
||||
def json_core(self):
|
||||
return {
|
||||
'user_id':self.user_id,
|
||||
'board_id':self.board_id,
|
||||
'created_utc':self.created_utc,
|
||||
'accepted':self.accepted,
|
||||
'invite_rescinded':self.invite_rescinded,
|
||||
'perm_content':self.perm_full or self.perm_content,
|
||||
'perm_config':self.perm_full or self.perm_config,
|
||||
'perm_access':self.perm_full or self.perm_access,
|
||||
'perm_appearance':self.perm_full or self.perm_appearance,
|
||||
'perm_full':self.perm_full,
|
||||
}
|
||||
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
data=self.json_core
|
||||
|
||||
data["user"]=self.user.json_core
|
||||
#data["guild"]=self.board.json_core
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
|
||||
class BanRelationship(Base, Stndrd, Age_times):
|
||||
|
||||
__tablename__ = "bans"
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
board_id = Column(Integer, ForeignKey("boards.id"))
|
||||
created_utc = Column(BigInteger, default=0)
|
||||
banning_mod_id = Column(Integer, ForeignKey("users.id"))
|
||||
is_active = Column(Boolean, default=False)
|
||||
mod_note = Column(String(128), default="")
|
||||
|
||||
user = relationship(
|
||||
"User",
|
||||
lazy="joined",
|
||||
primaryjoin="User.id==BanRelationship.user_id")
|
||||
banning_mod = relationship(
|
||||
"User",
|
||||
lazy="joined",
|
||||
primaryjoin="User.id==BanRelationship.banning_mod_id")
|
||||
board = relationship("Board")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time.time())
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Ban(id={self.id}, uid={self.uid}, board_id={self.board_id})>"
|
||||
|
||||
@property
|
||||
def json_core(self):
|
||||
return {
|
||||
'user_id':self.user_id,
|
||||
'board_id':self.board_id,
|
||||
'created_utc':self.created_utc,
|
||||
'mod_id':self.banning_mod_id
|
||||
}
|
||||
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
data=self.json_core
|
||||
|
||||
data["user"]=self.user.json_core
|
||||
data["mod"]=self.banning_mod.json_core
|
||||
data["guild"]=self.board.json_core
|
||||
|
||||
return data
|
||||
|
||||
class ContributorRelationship(Base, Stndrd, Age_times):
|
||||
|
||||
__tablename__ = "contributors"
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
board_id = Column(Integer, ForeignKey("boards.id"))
|
||||
created_utc = Column(BigInteger, default=0)
|
||||
is_active = Column(Boolean, default=True)
|
||||
approving_mod_id = Column(Integer, ForeignKey("users.id"))
|
||||
|
||||
user = relationship(
|
||||
"User",
|
||||
lazy="joined",
|
||||
primaryjoin="User.id==ContributorRelationship.user_id")
|
||||
approving_mod = relationship(
|
||||
"User",
|
||||
lazy='joined',
|
||||
primaryjoin="User.id==ContributorRelationship.approving_mod_id")
|
||||
board = relationship("Board", lazy="subquery")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time.time())
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Contributor(id={self.id}, uid={self.uid}, board_id={self.board_id})>"
|
||||
|
||||
|
||||
class PostRelationship(Base):
|
||||
|
||||
__tablename__ = "postrels"
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
post_id = Column(Integer, ForeignKey("submissions.id"))
|
||||
board_id = Column(Integer, ForeignKey("boards.id"))
|
||||
|
||||
post = relationship("Submission", lazy="subquery")
|
||||
board = relationship("Board", lazy="subquery")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PostRel(id={self.id}, pid={self.post_id}, board_id={self.board_id})>"
|
||||
|
||||
|
||||
class BoardBlock(Base, Stndrd, Age_times):
|
||||
|
||||
__tablename__ = "boardblocks"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
board_id = Column(Integer, ForeignKey("boards.id"))
|
||||
created_utc = Column(Integer)
|
||||
|
||||
user = relationship("User")
|
||||
board = relationship("Board")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<BoardBlock(id={self.id}, uid={self.user_id}, board_id={self.board_id})>"
|
|
@ -0,0 +1,414 @@
|
|||
from sqlalchemy.orm import lazyload
|
||||
from .userblock import *
|
||||
from .submission import *
|
||||
from .board_relationships import *
|
||||
from .comment import Comment
|
||||
from .mix_ins import *
|
||||
from ruqqus.__main__ import Base, cache
|
||||
|
||||
|
||||
class Board(Base, Stndrd, Age_times):
|
||||
|
||||
__tablename__ = "boards"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
created_utc = Column(Integer)
|
||||
description = Column(String)
|
||||
|
||||
description_html=Column(String)
|
||||
over_18=Column(Boolean, default=False)
|
||||
is_nsfl=Column(Boolean, default=False)
|
||||
is_banned=Column(Boolean, default=False)
|
||||
disablesignups=Column(Boolean, default=False)
|
||||
has_banner=Column(Boolean, default=False)
|
||||
has_profile=Column(Boolean, default=False)
|
||||
creator_id=Column(Integer, ForeignKey("users.id"))
|
||||
ban_reason=Column(String(256), default=None)
|
||||
color=Column(String(8), default="FF66AC")
|
||||
restricted_posting=Column(Boolean, default=False)
|
||||
hide_banner_data=Column(Boolean, default=False)
|
||||
profile_nonce=Column(Integer, default=0)
|
||||
banner_nonce=Column(Integer, default=0)
|
||||
is_private=Column(Boolean, default=False)
|
||||
color_nonce=Column(Integer, default=0)
|
||||
rank_trending=Column(Float, default=0)
|
||||
stored_subscriber_count=Column(Integer, default=1)
|
||||
all_opt_out=Column(Boolean, default=False)
|
||||
is_siegable=Column(Boolean, default=True)
|
||||
secondary_color=Column(String(6), default="cfcfcf")
|
||||
motd = Column(String(1000), default='')
|
||||
|
||||
moderators=relationship("ModRelationship")
|
||||
submissions=relationship("Submission", primaryjoin="Board.id==Submission.board_id")
|
||||
contributors=relationship("ContributorRelationship", lazy="dynamic")
|
||||
bans=relationship("BanRelationship", lazy="dynamic")
|
||||
postrels=relationship("PostRelationship", lazy="dynamic")
|
||||
trending_rank=deferred(Column(Float, server_default=FetchedValue()))
|
||||
|
||||
# db side functions
|
||||
subscriber_count = deferred(Column(Integer, server_default=FetchedValue()))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
kwargs["created_utc"] = int(time.time())
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Board(name={self.name})>"
|
||||
|
||||
@property
|
||||
def fullname(self):
|
||||
return f"t4_{self.base36id}"
|
||||
|
||||
@property
|
||||
def mods_list(self):
|
||||
|
||||
z = [x for x in self.moderators if x.accepted and not (
|
||||
x.user.is_deleted or (x.user.is_banned and not x.user.unban_utc))]
|
||||
|
||||
z = sorted(z, key=lambda x: x.created_utc)
|
||||
return z
|
||||
|
||||
@property
|
||||
def mods(self):
|
||||
|
||||
z = [x for x in self.moderators if x.accepted]
|
||||
|
||||
z = sorted(z, key=lambda x: x.created_utc)
|
||||
|
||||
z = [x.user for x in z]
|
||||
|
||||
return z
|
||||
|
||||
@property
|
||||
def invited_mods(self):
|
||||
|
||||
z = [x.user for x in self.moderators if x.accepted ==
|
||||
False and x.invite_rescinded == False]
|
||||
z = sorted(z, key=lambda x: x.created_utc)
|
||||
return z
|
||||
|
||||
@property
|
||||
def mod_invites(self):
|
||||
z = [x for x in self.moderators if x.accepted ==
|
||||
False and x.invite_rescinded == False]
|
||||
z = sorted(z, key=lambda x: x.created_utc)
|
||||
return z
|
||||
|
||||
@property
|
||||
def mods_count(self):
|
||||
|
||||
return len(self.mods_list)
|
||||
|
||||
@property
|
||||
def permalink(self):
|
||||
|
||||
return f"/+{self.name}"
|
||||
|
||||
def can_take(self, post):
|
||||
if self.is_banned:
|
||||
return False
|
||||
return not self.postrels.filter_by(post_id=post.id).first()
|
||||
|
||||
def has_mod(self, user, perm=None):
|
||||
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
if self.is_banned:
|
||||
return False
|
||||
|
||||
m=self.__dict__.get("_mod")
|
||||
if not m:
|
||||
for x in user.moderates:
|
||||
if x.board_id == self.id and x.accepted and not x.invite_rescinded:
|
||||
self.__dict__["mod"]=x
|
||||
m=x
|
||||
|
||||
if not m:
|
||||
return False
|
||||
|
||||
if perm:
|
||||
|
||||
return m if (m.perm_full or m.__dict__[f"perm_{perm}"]) else False
|
||||
|
||||
else:
|
||||
return m
|
||||
|
||||
|
||||
return False
|
||||
|
||||
def has_mod_record(self, user, perm=None):
|
||||
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
if self.is_banned:
|
||||
return False
|
||||
|
||||
for x in user.moderates:
|
||||
if x.board_id == self.id and not x.invite_rescinded:
|
||||
|
||||
if perm:
|
||||
return x if x.__dict__[f"perm_{perm}"] else False
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
return False
|
||||
def can_invite_mod(self, user):
|
||||
|
||||
return user.id not in [
|
||||
x.user_id for x in self.moderators if not x.invite_rescinded]
|
||||
|
||||
def has_rescinded_invite(self, user):
|
||||
|
||||
return user.id in [
|
||||
x.user_id for x in self.moderators if x.invite_rescinded == True]
|
||||
|
||||
def has_invite(self, user):
|
||||
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
for x in [
|
||||
i for i in self.moderators if not i.invite_rescinded and not i.accepted]:
|
||||
|
||||
if x.user_id == user.id:
|
||||
return x
|
||||
|
||||
return None
|
||||
|
||||
def has_ban(self, user):
|
||||
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
if user.admin_level >=2:
|
||||
return None
|
||||
|
||||
return g.db.query(BanRelationship).filter_by(
|
||||
board_id=self.id, user_id=user.id, is_active=True).first()
|
||||
|
||||
def has_contributor(self, user):
|
||||
|
||||
if user is None:
|
||||
return False
|
||||
|
||||
return g.db.query(ContributorRelationship).filter_by(
|
||||
user_id=user.id, board_id=self.id, is_active=True).first()
|
||||
|
||||
def can_submit(self, user):
|
||||
|
||||
if user is None:
|
||||
return False
|
||||
|
||||
if user.admin_level >= 4:
|
||||
return True
|
||||
|
||||
if self.has_ban(user):
|
||||
return False
|
||||
|
||||
if self.has_contributor(user) or self.has_mod(user):
|
||||
return True
|
||||
|
||||
if self.is_private or self.restricted_posting:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def can_comment(self, user):
|
||||
|
||||
if user is None:
|
||||
return False
|
||||
|
||||
if user.admin_level >= 4:
|
||||
return True
|
||||
|
||||
if self.has_ban(user):
|
||||
return False
|
||||
|
||||
if self.has_contributor(user) or self.has_mod(user):
|
||||
return True
|
||||
|
||||
if self.is_private:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def can_view(self, user):
|
||||
|
||||
if user is None:
|
||||
return False
|
||||
|
||||
if user.admin_level >= 4:
|
||||
return True
|
||||
|
||||
if self.has_contributor(user) or self.has_mod(
|
||||
user) or self.has_invite(user):
|
||||
return True
|
||||
|
||||
if self.is_private:
|
||||
return False
|
||||
|
||||
@property
|
||||
def banner_url(self):
|
||||
return "/assets/images/preview.png"
|
||||
|
||||
@property
|
||||
def profile_url(self):
|
||||
return "/assets/images/favicon.png"
|
||||
|
||||
@property
|
||||
def css_url(self):
|
||||
return f"/assets/{self.fullname}/main/{self.color_nonce}.css"
|
||||
|
||||
@property
|
||||
def css_dark_url(self):
|
||||
return f"/assets/{self.fullname}/dark/{self.color_nonce}.css"
|
||||
|
||||
def has_participant(self, user):
|
||||
return (g.db.query(Submission).filter_by(original_board_id=self.id, author_id=user.id).first() or
|
||||
g.db.query(Comment).filter_by(
|
||||
author_id=user.id, original_board_id=self.id).first()
|
||||
)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def n_pins(self):
|
||||
return g.db.query(Submission).filter_by(
|
||||
board_id=self.id, is_pinned=True).count()
|
||||
|
||||
@property
|
||||
def can_pin_another(self):
|
||||
|
||||
return self.n_pins < 4
|
||||
|
||||
@property
|
||||
def json_core(self):
|
||||
|
||||
if self.is_banned:
|
||||
return {'name': self.name,
|
||||
'permalink': self.permalink,
|
||||
'is_banned': True,
|
||||
'ban_reason': self.ban_reason,
|
||||
'id': self.base36id
|
||||
}
|
||||
return {'name': self.name,
|
||||
'profile_url': self.profile_url,
|
||||
'banner_url': self.banner_url,
|
||||
'created_utc': self.created_utc,
|
||||
'permalink': self.permalink,
|
||||
'description': self.description,
|
||||
'description_html': self.description_html,
|
||||
'over_18': self.over_18,
|
||||
'is_banned': False,
|
||||
'is_private': self.is_private,
|
||||
'is_restricted': self.restricted_posting,
|
||||
'id': self.base36id,
|
||||
'fullname': self.fullname,
|
||||
'banner_url': self.banner_url,
|
||||
'profile_url': self.profile_url,
|
||||
'color': "#" + self.color,
|
||||
'is_siege_protected': not self.is_siegable
|
||||
}
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
data=self.json_core
|
||||
|
||||
if self.is_banned:
|
||||
return data
|
||||
|
||||
|
||||
data['guildmasters']=[x.json_core for x in self.mods]
|
||||
data['subscriber_count']= self.subscriber_count
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@property
|
||||
def show_settings_icons(self):
|
||||
return self.is_private or self.restricted_posting or self.over_18 or self.all_opt_out
|
||||
|
||||
@cache.memoize(600)
|
||||
def comment_idlist(self, page=1, v=None, nsfw=False, **kwargs):
|
||||
|
||||
posts = g.db.query(Submission).options(
|
||||
lazyload('*')).filter_by(board_id=self.id)
|
||||
|
||||
if not nsfw:
|
||||
posts = posts.filter_by(over_18=False)
|
||||
|
||||
if v and not v.show_nsfl:
|
||||
posts = posts.filter_by(is_nsfl=False)
|
||||
|
||||
if self.is_private:
|
||||
if v and (self.can_view(v) or v.admin_level >= 4):
|
||||
pass
|
||||
elif v:
|
||||
posts = posts.filter(or_(Submission.post_public == True,
|
||||
Submission.author_id == v.id
|
||||
)
|
||||
)
|
||||
else:
|
||||
posts = posts.filter_by(post_public=True)
|
||||
|
||||
posts = posts.subquery()
|
||||
|
||||
comments = g.db.query(Comment).options(lazyload('*'))
|
||||
|
||||
if v and v.hide_offensive:
|
||||
comments = comments.filter_by(is_offensive=False)
|
||||
|
||||
if v and v.hide_bot:
|
||||
comments = comments.filter_by(is_bot=False)
|
||||
|
||||
if v and not self.has_mod(v) and v.admin_level <= 3:
|
||||
# blocks
|
||||
blocking = g.db.query(
|
||||
UserBlock.target_id).filter_by(
|
||||
user_id=v.id).subquery()
|
||||
blocked = g.db.query(
|
||||
UserBlock.user_id).filter_by(
|
||||
target_id=v.id).subquery()
|
||||
|
||||
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)
|
||||
|
||||
comments = comments.join(
|
||||
posts, Comment.parent_submission == posts.c.id)
|
||||
|
||||
comments = comments.order_by(Comment.created_utc.desc()).offset(
|
||||
25 * (page - 1)).limit(26).all()
|
||||
|
||||
return [x.id for x in comments]
|
||||
|
||||
|
||||
def user_guild_rep(self, user):
|
||||
|
||||
return user.guild_rep(self)
|
||||
|
||||
def is_guildmaster(self, perm=None):
|
||||
mod=self.__dict__.get('_is_guildmaster', False)
|
||||
if not mod:
|
||||
return False
|
||||
if not perm:
|
||||
return True
|
||||
|
||||
return mod.__dict__[f"perm_{perm}"]
|
||||
|
||||
|
||||
@property
|
||||
def siege_rep_requirement(self):
|
||||
|
||||
now=int(time.time())
|
||||
|
||||
return self.stored_subscriber_count//10 + min(180, (now-self.created_utc)//(60*60*24))
|
|
@ -0,0 +1,93 @@
|
|||
from flask import *
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship, lazyload
|
||||
|
||||
from .mix_ins import Stndrd
|
||||
from .submission import Submission
|
||||
from .comment import Comment
|
||||
from ruqqus.__main__ import Base
|
||||
|
||||
|
||||
class OauthApp(Base, Stndrd):
|
||||
|
||||
__tablename__ = "oauth_apps"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
client_id = Column(String(64))
|
||||
client_secret = Column(String(128))
|
||||
app_name = Column(String(50))
|
||||
redirect_uri = Column(String(4096))
|
||||
author_id = Column(Integer, ForeignKey("users.id"))
|
||||
is_banned = Column(Boolean, default=False)
|
||||
description = Column(String(256), default=None)
|
||||
|
||||
author = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<OauthApp(id={self.id})>"
|
||||
|
||||
@property
|
||||
def permalink(self):
|
||||
|
||||
return f"/admin/app/{self.base36id}"
|
||||
|
||||
def idlist(self, page=1, **kwargs):
|
||||
|
||||
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()]
|
||||
|
||||
def comments_idlist(self, page=1, **kwargs):
|
||||
|
||||
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, Stndrd):
|
||||
|
||||
__tablename__ = "client_auths"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
oauth_client = Column(Integer, ForeignKey("oauth_apps.id"))
|
||||
oauth_code = Column(String(128))
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
scope_identity = Column(Boolean, default=False)
|
||||
scope_create = Column(Boolean, default=False)
|
||||
scope_read = Column(Boolean, default=False)
|
||||
scope_update = Column(Boolean, default=False)
|
||||
scope_delete = Column(Boolean, default=False)
|
||||
scope_vote = Column(Boolean, default=False)
|
||||
scope_guildmaster = Column(Boolean, default=False)
|
||||
access_token = Column(String(128))
|
||||
refresh_token = Column(String(128))
|
||||
access_token_expire_utc = Column(Integer)
|
||||
|
||||
user = relationship("User", lazy="joined")
|
||||
application = relationship("OauthApp", lazy="joined")
|
||||
|
||||
@property
|
||||
def scopelist(self):
|
||||
|
||||
output = ""
|
||||
output += "identity," if self.scope_identity else ""
|
||||
output += "create," if self.scope_create else ""
|
||||
output += "read," if self.scope_read else ""
|
||||
output += "update," if self.scope_update else ""
|
||||
output += "delete," if self.scope_delete else ""
|
||||
output += "vote," if self.scope_vote else ""
|
||||
output += "guildmaster," if self.scope_guildmaster else ""
|
||||
|
||||
output = output.rstrip(',')
|
||||
|
||||
return output
|
|
@ -0,0 +1,508 @@
|
|||
from flask import *
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship, deferred
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from .mix_ins import *
|
||||
from ruqqus.helpers.base36 import *
|
||||
from ruqqus.helpers.lazy import lazy
|
||||
from ruqqus.__main__ import Base
|
||||
from .votes import CommentVote
|
||||
|
||||
class CommentAux(Base):
|
||||
|
||||
__tablename__ = "comments_aux"
|
||||
|
||||
key_id = Column(Integer, primary_key=True)
|
||||
id = Column(Integer, ForeignKey("comments.id"))
|
||||
body = Column(String(10000), default=None)
|
||||
body_html = Column(String(20000))
|
||||
ban_reason = Column(String(256), default='')
|
||||
|
||||
|
||||
class Comment(Base, Age_times, Scores, Stndrd, Fuzzing):
|
||||
|
||||
__tablename__ = "comments"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
comment_aux = relationship(
|
||||
"CommentAux",
|
||||
lazy="joined",
|
||||
uselist=False,
|
||||
innerjoin=True,
|
||||
primaryjoin="Comment.id==CommentAux.id")
|
||||
author_id = Column(Integer, ForeignKey("users.id"))
|
||||
parent_submission = Column(Integer, ForeignKey("submissions.id"))
|
||||
# this column is foreignkeyed to comment(id) but we can't do that yet as
|
||||
# "comment" class isn't yet defined
|
||||
parent_fullname = Column(Integer)
|
||||
created_utc = Column(Integer, default=0)
|
||||
edited_utc = Column(Integer, default=0)
|
||||
is_banned = Column(Boolean, default=False)
|
||||
distinguish_level = Column(Integer, default=0)
|
||||
gm_distinguish = Column(Integer, ForeignKey("boards.id"), default=0)
|
||||
distinguished_board = relationship("Board", lazy="joined", primaryjoin="Comment.gm_distinguish==Board.id")
|
||||
deleted_utc = Column(Integer, default=0)
|
||||
purged_utc = Column(Integer, default=0)
|
||||
is_approved = Column(Integer, default=0)
|
||||
approved_utc = Column(Integer, default=0)
|
||||
creation_ip = Column(String(64), default='')
|
||||
score_hot = Column(Float, default=0)
|
||||
score_top = Column(Integer, default=1)
|
||||
level = Column(Integer, default=0)
|
||||
parent_comment_id = Column(Integer, ForeignKey("comments.id"))
|
||||
original_board_id = Column(Integer, ForeignKey("boards.id"))
|
||||
|
||||
over_18 = Column(Boolean, default=False)
|
||||
is_offensive = Column(Boolean, default=False)
|
||||
is_nsfl = Column(Boolean, default=False)
|
||||
is_bot = Column(Boolean, default=False)
|
||||
is_pinned = Column(Boolean, default=False)
|
||||
creation_region=Column(String(2), default=None)
|
||||
sentto=Column(Integer, default=None)
|
||||
|
||||
app_id = Column(Integer, ForeignKey("oauth_apps.id"), default=None)
|
||||
oauth_app=relationship("OauthApp")
|
||||
|
||||
post = relationship("Submission")
|
||||
flags = relationship("CommentFlag", backref="comment")
|
||||
author = relationship(
|
||||
"User",
|
||||
lazy="joined",
|
||||
innerjoin=True,
|
||||
primaryjoin="User.id==Comment.author_id")
|
||||
board = association_proxy("post", "board")
|
||||
original_board = relationship(
|
||||
"Board", primaryjoin="Board.id==Comment.original_board_id")
|
||||
|
||||
upvotes = Column(Integer, default=1)
|
||||
downvotes = Column(Integer, default=0)
|
||||
|
||||
parent_comment = relationship("Comment", remote_side=[id])
|
||||
child_comments = relationship("Comment", remote_side=[parent_comment_id])
|
||||
|
||||
awards = relationship("AwardRelationship", lazy="joined")
|
||||
|
||||
# These are virtual properties handled as postgres functions server-side
|
||||
# There is no difference to SQLAlchemy, but they cannot be written to
|
||||
ups = deferred(Column(Integer, server_default=FetchedValue()))
|
||||
downs = deferred(Column(Integer, server_default=FetchedValue()))
|
||||
is_public = deferred(Column(Boolean, server_default=FetchedValue()))
|
||||
|
||||
score = deferred(Column(Integer, server_default=FetchedValue()))
|
||||
|
||||
rank_fiery = deferred(Column(Float, server_default=FetchedValue()))
|
||||
rank_hot = deferred(Column(Float, server_default=FetchedValue()))
|
||||
|
||||
board_id = deferred(Column(Integer, server_default=FetchedValue()))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time.time())
|
||||
|
||||
kwargs["creation_ip"] = request.remote_addr
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"<Comment(id={self.id})>"
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def score_disputed(self):
|
||||
return (self.upvotes+1) * (self.downvotes+1)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def fullname(self):
|
||||
return f"t3_{self.base36id}"
|
||||
|
||||
def children(self, v):
|
||||
return sorted([x for x in self.child_comments if not x.author.shadowbanned or (v and v.id == x.author_id)], key=lambda x: x.score, reverse=True)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def is_deleted(self):
|
||||
return bool(self.deleted_utc)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def is_top_level(self):
|
||||
return self.parent_fullname and self.parent_fullname.startswith("t2_")
|
||||
|
||||
@property
|
||||
def is_archived(self):
|
||||
return self.post.is_archived
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def parent(self):
|
||||
|
||||
if not self.parent_submission:
|
||||
return None
|
||||
|
||||
if self.is_top_level:
|
||||
return self.post
|
||||
|
||||
else:
|
||||
return g.db.query(Comment).get(self.parent_comment_id)
|
||||
|
||||
@property
|
||||
def replies(self):
|
||||
|
||||
r = self.__dict__.get("replies", None)
|
||||
if r is None:
|
||||
r = self.child_comments
|
||||
return r
|
||||
|
||||
@replies.setter
|
||||
def replies(self, value):
|
||||
self.__dict__["replies"] = value
|
||||
|
||||
@property
|
||||
def replies2(self):
|
||||
return self.__dict__.get("replies2", [])
|
||||
|
||||
@replies2.setter
|
||||
def replies2(self, value):
|
||||
self.__dict__["replies2"] = value
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def permalink(self):
|
||||
if self.post: return f"{self.post.permalink}/{self.id}/"
|
||||
else: return f"/comment/{self.id}/"
|
||||
|
||||
@property
|
||||
def any_descendants_live(self):
|
||||
|
||||
if self.replies == []:
|
||||
return False
|
||||
|
||||
if any([not x.is_banned and x.deleted_utc == 0 for x in self.replies]):
|
||||
return True
|
||||
|
||||
else:
|
||||
return any([x.any_descendants_live for x in self.replies])
|
||||
|
||||
def rendered_comment(self, v=None, render_replies=True,
|
||||
standalone=False, level=1, **kwargs):
|
||||
|
||||
kwargs["post_base36id"] = kwargs.get(
|
||||
"post_base36id", self.post.base36id if self.post else None)
|
||||
|
||||
if self.is_banned or self.deleted_utc > 0:
|
||||
if v and v.admin_level > 1:
|
||||
return render_template("single_comment.html",
|
||||
v=v,
|
||||
c=self,
|
||||
render_replies=render_replies,
|
||||
standalone=standalone,
|
||||
level=level,
|
||||
**kwargs)
|
||||
|
||||
elif self.any_descendants_live:
|
||||
return render_template("single_comment_removed.html",
|
||||
c=self,
|
||||
render_replies=render_replies,
|
||||
standalone=standalone,
|
||||
level=level,
|
||||
**kwargs)
|
||||
else:
|
||||
return ""
|
||||
|
||||
return render_template("single_comment.html",
|
||||
v=v,
|
||||
c=self,
|
||||
render_replies=render_replies,
|
||||
standalone=standalone,
|
||||
level=level,
|
||||
**kwargs)
|
||||
|
||||
@property
|
||||
def active_flags(self):
|
||||
if self.is_approved:
|
||||
return 0
|
||||
else:
|
||||
return self.flag_count
|
||||
|
||||
def visibility_reason(self, v):
|
||||
if not v or self.author_id == v.id:
|
||||
return "this is your content."
|
||||
elif not self.board:
|
||||
return None
|
||||
elif self.board.has_mod(v):
|
||||
return f"you are a guildmaster of +{self.board.name}."
|
||||
elif self.board.has_contributor(v):
|
||||
return f"you are an approved contributor in +{self.board.name}."
|
||||
elif self.parent.author_id == v.id:
|
||||
return "this is a reply to your content."
|
||||
elif v.admin_level >= 4:
|
||||
return "you are a Drama admin."
|
||||
|
||||
@property
|
||||
def json_raw(self):
|
||||
data= {
|
||||
'id': self.base36id,
|
||||
'fullname': self.fullname,
|
||||
'level': self.level,
|
||||
'author_name': self.author.username if not self.author.is_deleted else None,
|
||||
'body': self.body,
|
||||
'body_html': self.body_html,
|
||||
'is_archived': self.is_archived,
|
||||
'is_bot': self.is_bot,
|
||||
'created_utc': self.created_utc,
|
||||
'edited_utc': self.edited_utc or 0,
|
||||
'is_banned': bool(self.is_banned),
|
||||
'is_deleted': self.is_deleted,
|
||||
'is_nsfw': self.over_18,
|
||||
'is_offensive': self.is_offensive,
|
||||
'is_nsfl': self.is_nsfl,
|
||||
'permalink': self.permalink,
|
||||
'post_id': self.post.base36id,
|
||||
'score': self.score_fuzzed,
|
||||
'upvotes': self.upvotes_fuzzed,
|
||||
'downvotes': self.downvotes_fuzzed,
|
||||
'award_count': self.award_count,
|
||||
'is_bot': self.is_bot
|
||||
}
|
||||
|
||||
if self.ban_reason:
|
||||
data["ban_reason"]=self.ban_reason
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@property
|
||||
def json_core(self):
|
||||
if self.is_banned:
|
||||
data= {'is_banned': True,
|
||||
'ban_reason': self.ban_reason,
|
||||
'id': self.base36id,
|
||||
'post': self.post.base36id,
|
||||
'level': self.level,
|
||||
'parent': self.parent_fullname
|
||||
}
|
||||
elif self.deleted_utc > 0:
|
||||
data= {'deleted_utc': self.deleted_utc,
|
||||
'id': self.base36id,
|
||||
'post': self.post.base36id,
|
||||
'level': self.level,
|
||||
'parent': self.parent_fullname
|
||||
}
|
||||
else:
|
||||
|
||||
data=self.json_raw
|
||||
|
||||
if self.level>=2:
|
||||
data['parent_comment_id']= base36encode(self.parent_comment_id),
|
||||
|
||||
if "replies" in self.__dict__:
|
||||
data['replies']=[x.json_core for x in self.replies]
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
|
||||
data=self.json_core
|
||||
|
||||
if self.deleted_utc > 0 or self.is_banned:
|
||||
return data
|
||||
|
||||
data["author"]=self.author.json_core
|
||||
data["post"]=self.post.json_core
|
||||
data["guild"]=self.post.board.json_core
|
||||
|
||||
if self.level >= 2:
|
||||
data["parent"]=self.parent.json_core
|
||||
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@property
|
||||
def voted(self):
|
||||
|
||||
x = self.__dict__.get("_voted")
|
||||
if x is not None:
|
||||
return x
|
||||
|
||||
if g.v:
|
||||
x = g.db.query(CommentVote).filter_by(
|
||||
comment_id=self.id,
|
||||
user_id=g.v.id
|
||||
).first()
|
||||
|
||||
if x:
|
||||
x = x.vote_type
|
||||
else:
|
||||
x = 0
|
||||
else:
|
||||
x = 0
|
||||
return x
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.__dict__.get("_title", self.author.title)
|
||||
|
||||
@property
|
||||
def is_blocking(self):
|
||||
return self.__dict__.get('_is_blocking', 0)
|
||||
|
||||
@property
|
||||
def is_blocked(self):
|
||||
return self.__dict__.get('_is_blocked', 0)
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
if self.comment_aux: return self.comment_aux.body
|
||||
else: return ""
|
||||
|
||||
@body.setter
|
||||
def body(self, x):
|
||||
self.comment_aux.body = x
|
||||
g.db.add(self.comment_aux)
|
||||
|
||||
@property
|
||||
def body_html(self):
|
||||
return self.comment_aux.body_html
|
||||
|
||||
@body_html.setter
|
||||
def body_html(self, x):
|
||||
self.comment_aux.body_html = x
|
||||
g.db.add(self.comment_aux)
|
||||
|
||||
def realbody(self, v):
|
||||
body = self.comment_aux.body_html
|
||||
if not v or v.slurreplacer: body = body.replace(" nigger"," 🏀").replace(" Nigger"," 🏀").replace(" NIGGER"," 🏀").replace(" pedo"," libertarian").replace(" Pedo"," Libertarian ").replace(" PEDO"," LIBERTARIAN ").replace(" tranny"," 🚄").replace(" Tranny"," 🚄").replace(" TRANNY"," 🚄").replace(" fag"," cute twink").replace(" Fag"," Cute twink").replace(" FAG"," CUTE TWINK").replace(" faggot"," cute twink").replace(" Faggot"," Cute twink").replace(" FAGGOT"," CUTE TWINK").replace(" trump"," DDR").replace(" Trump"," DDR").replace(" TRUMP"," DDR").replace(" biden"," DDD").replace(" Biden"," DDD").replace(" BIDEN"," DDD").replace(" steve akins"," penny verity oaken").replace(" Steve Akins"," Penny Verity Oaken").replace(" STEVE AKINS"," PENNY VERITY OAKEN").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" kill yourself"," keep yourself safe").replace(" KILL YOURSELF"," KEEP YOURSELF SAFE")
|
||||
if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com")
|
||||
return body
|
||||
|
||||
@property
|
||||
def ban_reason(self):
|
||||
return self.comment_aux.ban_reason
|
||||
|
||||
@ban_reason.setter
|
||||
def ban_reason(self, x):
|
||||
self.comment_aux.ban_reason = x
|
||||
g.db.add(self.comment_aux)
|
||||
|
||||
@property
|
||||
def flag_count(self):
|
||||
return len(self.flags)
|
||||
|
||||
@property
|
||||
def award_count(self):
|
||||
return len(self.awards)
|
||||
|
||||
def collapse_for_user(self, v):
|
||||
|
||||
if self.over_18 and not (v and v.over_18) and not self.post.over_18:
|
||||
return True
|
||||
|
||||
if not v:
|
||||
return False
|
||||
|
||||
if self.is_offensive and v.hide_offensive:
|
||||
return True
|
||||
|
||||
if self.is_bot and v.hide_bot:
|
||||
return True
|
||||
|
||||
if any([x in self.body for x in v.filter_words]):
|
||||
return True
|
||||
|
||||
if self.is_banned: return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def flagged_by(self):
|
||||
return [x.user for x in self.flags]
|
||||
|
||||
@property
|
||||
def self_download_json(self):
|
||||
|
||||
#This property should never be served to anyone but author and admin
|
||||
if not self.is_banned and not self.is_banned:
|
||||
return self.json_core
|
||||
|
||||
data= {
|
||||
"author": self.author.name,
|
||||
"body": self.body,
|
||||
"body_html": self.body_html,
|
||||
"is_banned": bool(self.is_banned),
|
||||
"deleted_utc": self.deleted_utc,
|
||||
'created_utc': self.created_utc,
|
||||
'id': self.base36id,
|
||||
'fullname': self.fullname,
|
||||
'permalink': self.permalink,
|
||||
'post_id': self.post.base36id,
|
||||
'level': self.level
|
||||
}
|
||||
if self.level>=2:
|
||||
data['parent_comment_id']= base36encode(self.parent_comment_id)
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def json_admin(self):
|
||||
data= self.json_raw
|
||||
|
||||
data["creation_ip"] = self.creation_ip
|
||||
data["creation_region"] = self.creation_region
|
||||
|
||||
return data
|
||||
|
||||
def is_guildmaster(self, perm=None):
|
||||
mod=self.__dict__.get('_is_guildmaster', False)
|
||||
|
||||
if not mod:
|
||||
return False
|
||||
elif not perm:
|
||||
return True
|
||||
else:
|
||||
return mod.perm_full or mod.__dict__[f"perm_{perm}"]
|
||||
|
||||
return output
|
||||
|
||||
@property
|
||||
def is_exiled_for(self):
|
||||
return self.__dict__.get('_is_exiled_for', None)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def is_op(self):
|
||||
return self.author_id==self.post.author_id and not self.author.is_deleted and not self.post.author.is_deleted and not self.post.is_deleted
|
||||
|
||||
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
comment_id = Column(Integer, ForeignKey("comments.id"))
|
||||
read = Column(Boolean, default=False)
|
||||
followsender = Column(Integer, default=None)
|
||||
unfollowsender = Column(Integer, default=None)
|
||||
blocksender = Column(Integer, default=None)
|
||||
unblocksender = Column(Integer, default=None)
|
||||
|
||||
comment = relationship("Comment", lazy="joined", innerjoin=True)
|
||||
user=relationship("User", innerjoin=True)
|
||||
|
||||
# Server side computed values (copied from corresponding comment)
|
||||
created_utc = Column(Integer, server_default=FetchedValue())
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"<Notification(id={self.id})>"
|
||||
|
||||
@property
|
||||
def voted(self):
|
||||
return 0
|
|
@ -0,0 +1,5 @@
|
|||
class PaymentRequired(Exception):
|
||||
status_code=402
|
||||
def __init__(self):
|
||||
Exception.__init__(self)
|
||||
self.status_code=402
|
|
@ -0,0 +1,46 @@
|
|||
from sqlalchemy import *
|
||||
from ruqqus.__main__ import Base
|
||||
|
||||
reasons = {
|
||||
1: "URL shorteners are not allowed.",
|
||||
3: "Piracy is not allowed.",
|
||||
4: "Sites hosting digitally malicious content are not allowed.",
|
||||
5: "Spam",
|
||||
6: "Doxxing is not allowed.",
|
||||
7: "Sexualizing minors is strictly prohibited."
|
||||
}
|
||||
|
||||
|
||||
class Domain(Base):
|
||||
|
||||
__tablename__ = "domains"
|
||||
id = Column(Integer, primary_key=True)
|
||||
domain = Column(String)
|
||||
can_submit = Column(Boolean, default=True)
|
||||
can_comment = Column(Boolean, default=True)
|
||||
reason = Column(Integer, default=0)
|
||||
show_thumbnail = Column(Boolean, default=False)
|
||||
embed_function = Column(String(64), default=None)
|
||||
embed_template = Column(String(32), default=None)
|
||||
|
||||
@property
|
||||
def reason_text(self):
|
||||
return reasons.get(self.reason)
|
||||
|
||||
@property
|
||||
def permalink(self):
|
||||
return f"/admin/domain/{self.domain}"
|
||||
|
||||
|
||||
|
||||
class BadLink(Base):
|
||||
|
||||
__tablename__ = "badlinks"
|
||||
id = Column(Integer, primary_key=True)
|
||||
reason = Column(Integer)
|
||||
link = Column(String(512))
|
||||
autoban = Column(Boolean, default=False)
|
||||
|
||||
@property
|
||||
def reason_text(self):
|
||||
return reasons.get(self.reason)
|
|
@ -0,0 +1,53 @@
|
|||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship
|
||||
from ruqqus.__main__ import Base
|
||||
from .mix_ins import *
|
||||
|
||||
class Flag(Base, Stndrd):
|
||||
|
||||
__tablename__ = "flags"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
post_id = Column(Integer, ForeignKey("submissions.id"))
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
created_utc = Column(Integer)
|
||||
|
||||
user = relationship("User", lazy = "joined", primaryjoin = "Flag.user_id == User.id", uselist = False)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"<Flag(id={self.id})>"
|
||||
|
||||
|
||||
class CommentFlag(Base, Stndrd):
|
||||
|
||||
__tablename__ = "commentflags"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
comment_id = Column(Integer, ForeignKey("comments.id"))
|
||||
created_utc = Column(Integer)
|
||||
|
||||
user = relationship("User", lazy = "joined", primaryjoin = "CommentFlag.user_id == User.id", uselist = False)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"<CommentFlag(id={self.id})>"
|
||||
|
||||
|
||||
class Report(Base):
|
||||
|
||||
__tablename__ = "reports"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
post_id = Column(Integer, ForeignKey("submissions.id"))
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
created_utc = Column(Integer)
|
||||
|
||||
board_id = Column(Integer, server_default=FetchedValue())
|
||||
|
||||
user = relationship("User", lazy = "joined", primaryjoin = "Report.user_id == User.id", uselist = False)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"<Report(id={self.id})>"
|
|
@ -0,0 +1,37 @@
|
|||
from sqlalchemy import *
|
||||
from flask import g
|
||||
from ruqqus.__main__ import Base
|
||||
|
||||
|
||||
class Image(Base):
|
||||
__tablename__ = "images"
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
state = Column(String(8))
|
||||
number = Column(Integer)
|
||||
text = Column(String(64))
|
||||
deletehash = Column(String(64))
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return f"/assets/images/platy.jpg"
|
||||
|
||||
|
||||
|
||||
def random_image():
|
||||
n=g.db.query(Image).count()
|
||||
return g.db.query(Image).order_by(Image.id.asc()).first()
|
||||
|
||||
|
||||
|
||||
class BadPic(Base):
|
||||
|
||||
#Class for tracking fuzzy hashes of banned csam images
|
||||
|
||||
__tablename__="badpics"
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
description=Column(String(255), default=None)
|
||||
phash=Column(String(64))
|
||||
ban_reason=Column(String(64))
|
||||
ban_time=Column(Integer)
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from sqlalchemy import *
|
||||
from ruqqus.__main__ import Base
|
||||
|
||||
|
||||
class IP(Base):
|
||||
|
||||
__tablename__ = "ips"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
addr = Column(String(64))
|
||||
reason = Column(String(256), default="")
|
||||
banned_by = Column(Integer, ForeignKey("users.id"), default=True)
|
||||
until_utc=Column(Integer, default=None)
|
||||
|
||||
|
||||
class Agent(Base):
|
||||
|
||||
__tablename__ = "useragents"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
kwd = Column(String(64))
|
||||
reason = Column(String(256), default="")
|
||||
banned_by = Column(Boolean, ForeignKey("users.id"), default=True)
|
||||
mock = Column(String(256), default="")
|
||||
status_code = Column(Integer, default=418)
|
|
@ -0,0 +1,174 @@
|
|||
from ruqqus.helpers.base36 import *
|
||||
from ruqqus.helpers.lazy import lazy
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
|
||||
class Stndrd:
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def base36id(self):
|
||||
return base36encode(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 created_iso(self):
|
||||
|
||||
t = time.gmtime(self.created_utc)
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%S+00:00", t)
|
||||
|
||||
|
||||
class Age_times:
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
return now - self.created_utc
|
||||
|
||||
@property
|
||||
def created_date(self):
|
||||
|
||||
return time.strftime("%d %b %Y", time.gmtime(self.created_utc))
|
||||
|
||||
@property
|
||||
def created_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
|
||||
|
||||
@property
|
||||
def age_string(self):
|
||||
|
||||
age = self.age
|
||||
|
||||
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)
|
||||
|
||||
# compute number of months
|
||||
months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year)
|
||||
# remove a month count if current day of month < creation day of month
|
||||
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 edited_string(self):
|
||||
|
||||
if not self.edited_utc:
|
||||
return "never"
|
||||
|
||||
age = int(time.time()) - self.edited_utc
|
||||
|
||||
if age < 60:
|
||||
return "just now"
|
||||
elif age < 3600:
|
||||
minutes = int(age / 60)
|
||||
return f"{minutes}m ago"
|
||||
elif age < 86400:
|
||||
hours = int(age / 3600)
|
||||
return f"{hours}hr ago"
|
||||
elif age < 2678400:
|
||||
days = int(age / 86400)
|
||||
return f"{days}d ago"
|
||||
|
||||
now = time.gmtime()
|
||||
ctd = time.gmtime(self.edited_utc)
|
||||
months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year)
|
||||
|
||||
if months < 12:
|
||||
return f"{months}mo ago"
|
||||
else:
|
||||
years = now.tm_year - ctd.tm_year
|
||||
return f"{years}yr ago"
|
||||
|
||||
@property
|
||||
def edited_date(self):
|
||||
return time.strftime("%d %B %Y", time.gmtime(self.edited_utc))
|
||||
|
||||
@property
|
||||
def edited_datetime(self):
|
||||
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.edited_utc)))
|
||||
|
||||
class Scores:
|
||||
|
||||
@property
|
||||
#@cache.memoize(timeout=60)
|
||||
def score_percent(self):
|
||||
# try:
|
||||
# return int((self.ups/(self.ups+self.downs))*100)
|
||||
# except ZeroDivisionError:
|
||||
# return 0
|
||||
|
||||
return 101
|
||||
|
||||
@property
|
||||
#@cache.memoize(timeout=60)
|
||||
def score(self):
|
||||
return int(self.score_top) or 0
|
||||
|
||||
|
||||
class Fuzzing:
|
||||
|
||||
@property
|
||||
#@cache.memoize(timeout=60)
|
||||
def score_fuzzed(self):
|
||||
|
||||
real = self.score_top if self.score_top else self.score
|
||||
real = int(real)
|
||||
if real <= 10:
|
||||
return real
|
||||
|
||||
k = 0.01
|
||||
|
||||
a = math.floor(real * (1 - k))
|
||||
b = math.ceil(real * (1 + k))
|
||||
return random.randint(a, b)
|
||||
|
||||
@property
|
||||
def upvotes_fuzzed(self):
|
||||
|
||||
if self.upvotes <= 10 or self.is_archived:
|
||||
return self.upvotes
|
||||
|
||||
lower = int(self.upvotes * 0.99)
|
||||
upper = int(self.upvotes * 1.01) + 1
|
||||
|
||||
return random.randint(lower, upper)
|
||||
|
||||
@property
|
||||
def downvotes_fuzzed(self):
|
||||
if self.downvotes <= 10 or self.is_archived:
|
||||
return self.downvotes
|
||||
|
||||
lower = int(self.downvotes * 0.99)
|
||||
upper = int(self.downvotes * 1.01) + 1
|
||||
|
||||
return random.randint(lower, upper)
|
|
@ -0,0 +1,387 @@
|
|||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship
|
||||
from ruqqus.__main__ import Base
|
||||
from .mix_ins import *
|
||||
import time
|
||||
|
||||
class ModAction(Base, Stndrd, Age_times):
|
||||
__tablename__ = "modactions"
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
board_id = Column(Integer, ForeignKey("boards.id"))
|
||||
kind = Column(String(32))
|
||||
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(256), default=None)
|
||||
created_utc = Column(Integer, default=0)
|
||||
|
||||
|
||||
user = relationship("User", lazy="joined", primaryjoin="User.id==ModAction.user_id")
|
||||
target_user = relationship("User", lazy="joined", primaryjoin="User.id==ModAction.target_user_id")
|
||||
board = relationship("Board", lazy="joined")
|
||||
target_post = relationship("Submission", lazy="joined")
|
||||
target_comment = relationship("Comment", lazy="joined")
|
||||
|
||||
|
||||
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.base36id})>"
|
||||
|
||||
@property
|
||||
def actiontype(self):
|
||||
return ACTIONTYPES[self.kind]
|
||||
|
||||
@property
|
||||
def note(self):
|
||||
|
||||
if self.kind=="exile_user":
|
||||
if self.target_post:
|
||||
return f'for <a href="{self.target_post.permalink}">post</a>'
|
||||
elif self.target_comment:
|
||||
return f'for <a href="{self.target_comment.permalink}">comment</a>'
|
||||
else: return self._note
|
||||
else:
|
||||
return self._note or ""
|
||||
|
||||
@note.setter
|
||||
def note(self, x):
|
||||
self._note=x
|
||||
|
||||
@property
|
||||
def string(self):
|
||||
|
||||
output = self.actiontype["str"].format(self=self)
|
||||
|
||||
if self.note:
|
||||
output += f" <i>({self.note})</i>"
|
||||
|
||||
return output
|
||||
|
||||
@property
|
||||
def target_link(self):
|
||||
if self.target_user:
|
||||
if self.target_user.is_deleted:
|
||||
return "[deleted user]"
|
||||
else:
|
||||
return f'<a href="{self.target_user.permalink}">{self.target_user.username}</a>'
|
||||
elif self.target_post:
|
||||
return f'<a href="{self.target_post.permalink}">{self.target_post.title}</a>'
|
||||
elif self.target_comment:
|
||||
return f'<a href="{self.target_comment.permalink}">comment</a>'
|
||||
|
||||
else:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
data={
|
||||
"id":self.base36id,
|
||||
"guild": self.board.name,
|
||||
"kind": self.kind,
|
||||
"created_utc": self.created_utc,
|
||||
"mod": self.user.username,
|
||||
}
|
||||
|
||||
if self.target_user_id:
|
||||
data["target_user_id"]=self.target_user.base36id
|
||||
data["target_user"]=self.target_user.username
|
||||
|
||||
if self.target_comment_id:
|
||||
data["target_comment_id"]=self.target_comment.base36id
|
||||
|
||||
if self.target_submission_id:
|
||||
data["target_submission_id"]=self.target_submission.base36id
|
||||
|
||||
if self._note:
|
||||
data["note"]=self._note
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return self.actiontype['icon']
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
return self.actiontype['color']
|
||||
|
||||
@property
|
||||
def permalink(self):
|
||||
return f"/log/{self.base36id}"
|
||||
@property
|
||||
def title_text(self):
|
||||
if self.user.is_deleted:
|
||||
return f"[deleted user] {self.actiontype['title'].format(self=self)}"
|
||||
else:
|
||||
return f"@{self.user.username} {self.actiontype['title'].format(self=self)}"
|
||||
|
||||
|
||||
|
||||
|
||||
ACTIONTYPES={
|
||||
"kick_post":{
|
||||
"str":'kicked post {self.target_link}',
|
||||
"icon":"fa-sign-out fa-flip-horizontal",
|
||||
"color": "bg-danger",
|
||||
"title": 'kicked post {self.target_post.title}'
|
||||
},
|
||||
"approve_post":{
|
||||
"str":'approved post {self.target_link}',
|
||||
"icon":"fa-check",
|
||||
"color": "bg-success",
|
||||
"title": 'approved post {self.target_post.title}'
|
||||
},
|
||||
"yank_post":{
|
||||
"str":'yanked post {self.target_link}',
|
||||
"icon":"fa-hand-lizard",
|
||||
"color": "bg-muted",
|
||||
"title": 'yanked post {self.target_post.title}'
|
||||
},
|
||||
"exile_user":{
|
||||
"str":'banned user {self.target_link}',
|
||||
"icon":"fa-user-slash",
|
||||
"color": "bg-danger",
|
||||
"title": 'banned user {self.target_user.username}'
|
||||
},
|
||||
"unexile_user":{
|
||||
"str":'unbanned user {self.target_link}',
|
||||
"icon": "fa-user-slash",
|
||||
"color": "bg-muted",
|
||||
"title": 'unbanned user {self.target_user.username}'
|
||||
},
|
||||
"nuke_user":{
|
||||
"str":'removed all content of {self.target_link}',
|
||||
"icon":"fa-user-slash",
|
||||
"color": "bg-danger",
|
||||
"title": 'removed all content of {self.target_user.username}'
|
||||
},
|
||||
"unnuke_user":{
|
||||
"str":'approved all content of {self.target_link}',
|
||||
"icon": "fa-user-slash",
|
||||
"color": "bg-muted",
|
||||
"title": 'approved all content of {self.target_user.username}'
|
||||
},
|
||||
"shadowban": {
|
||||
"str": 'shadowbanned {self.target_link}',
|
||||
"icon": "fa-user-slash",
|
||||
"color": "bg-danger",
|
||||
"title": 'shadowbanned {self.target_user.username}'
|
||||
},
|
||||
"unshadowban": {
|
||||
"str": 'unshadowbanned {self.target_link}',
|
||||
"icon": "fa-user-slash",
|
||||
"color": "bg-muted",
|
||||
"title": 'unshadowbanned {self.target_user.username}'
|
||||
},
|
||||
"agendaposter": {
|
||||
"str": "set agendaposter theme on {self.target_link}",
|
||||
"icon": "fa-user-slash",
|
||||
"color": "bg-muted",
|
||||
"title": "set agendaposter theme on {self.target_link}"
|
||||
},
|
||||
"unagendaposter": {
|
||||
"str": "removed agendaposter theme from {self.target_link}",
|
||||
"icon": "fa-user-slash",
|
||||
"color": "bg-muted",
|
||||
"title": "removed agendaposter theme from {self.target_link}"
|
||||
},
|
||||
"set_flair_locked":{
|
||||
"str":"set {self.target_link}'s flair (locked)",
|
||||
"icon": "fa-user-slash",
|
||||
"color": "bg-muted",
|
||||
"title": "set {self.target_link}'s flair (locked)"
|
||||
},
|
||||
"set_flair_notlocked":{
|
||||
"str":"set {self.target_link}'s flair (not locked)",
|
||||
"icon": "fa-user-slash",
|
||||
"color": "bg-muted",
|
||||
"title": "set {self.target_link}'s flair (not locked)"
|
||||
},
|
||||
"contrib_user":{
|
||||
"str":'added contributor {self.target_link}',
|
||||
"icon": "fa-user-check",
|
||||
"color": "bg-info",
|
||||
"title": 'added contributor {self.target_user.username}'
|
||||
},
|
||||
"uncontrib_user":{
|
||||
"str":'removed contributor {self.target_link}',
|
||||
"icon": "fa-user-check",
|
||||
"color": "bg-muted",
|
||||
"title": 'removed user {self.target_user.username}'
|
||||
},
|
||||
"herald_comment":{
|
||||
"str":'heralded their {self.target_link}',
|
||||
"icon": "fa-crown",
|
||||
"color": "bg-warning",
|
||||
"title": 'heralded their comment'
|
||||
},
|
||||
"herald_post":{
|
||||
"str":'heralded their post {self.target_link}',
|
||||
"icon": "fa-crown",
|
||||
"color": "bg-warning",
|
||||
"title": 'heralded their post {self.target_post.title}'
|
||||
},
|
||||
"unherald_comment":{
|
||||
"str":'un-heralded their {self.target_link}',
|
||||
"icon": "fa-crown",
|
||||
"color": "bg-muted",
|
||||
"title": 'un-heralded their comment'
|
||||
},
|
||||
"unherald_post":{
|
||||
"str":'un-heralded their post {self.target_link}',
|
||||
"icon": "fa-crown",
|
||||
"color": "bg-muted",
|
||||
"title": 'un-heralded their post {self.target_post.title}'
|
||||
},
|
||||
"pin_comment":{
|
||||
"str":'pinned a {self.target_link}',
|
||||
"icon":"fa-thumbtack fa-rotate--45",
|
||||
"color": "bg-info",
|
||||
"title": 'pinned a comment'
|
||||
},
|
||||
"unpin_comment":{
|
||||
"str":'un-pinned a {self.target_link}',
|
||||
"icon":"fa-thumbtack fa-rotate--45",
|
||||
"color": "bg-muted",
|
||||
"title": 'un-pinned a comment'
|
||||
},
|
||||
"pin_post":{
|
||||
"str":'pinned post {self.target_link}',
|
||||
"icon":"fa-thumbtack fa-rotate--45",
|
||||
"color": "bg-success",
|
||||
"title": 'pinned post {self.target_post.title}'
|
||||
},
|
||||
"unpin_post":{
|
||||
"str":'un-pinned post {self.target_link}',
|
||||
"icon":"fa-thumbtack fa-rotate--45",
|
||||
"color": "bg-muted",
|
||||
"title": 'un-pinned post {self.target_post.title}'
|
||||
},
|
||||
"invite_mod":{
|
||||
"str":'invited badmin {self.target_link}',
|
||||
"icon":"fa-user-crown",
|
||||
"color": "bg-info",
|
||||
"title": 'invited badmin @{self.target_user.username}'
|
||||
},
|
||||
"uninvite_mod":{
|
||||
"str":'rescinded badmin invitation to {self.target_link}',
|
||||
"icon":"fa-user-crown",
|
||||
"color": "bg-muted",
|
||||
"title": 'rescinded badmin invitation to @{self.target_user.username}'
|
||||
},
|
||||
"accept_mod_invite":{
|
||||
"str":'accepted badmin invitation',
|
||||
"icon":"fa-user-crown",
|
||||
"color": "bg-warning",
|
||||
"title": 'accepted badmin invitation'
|
||||
},
|
||||
"remove_mod":{
|
||||
"str":'removed badmin {self.target_link}',
|
||||
"icon":"fa-user-crown",
|
||||
"color": "bg-danger",
|
||||
"title": 'removed badmin @{self.target_user.username}'
|
||||
},
|
||||
"dethrone_self":{
|
||||
"str":'stepped down as guildmaster',
|
||||
"icon":"fa-user-crown",
|
||||
"color": "bg-danger",
|
||||
"title": 'stepped down as guildmaster'
|
||||
},
|
||||
"add_mod":{
|
||||
"str":'added badmin {self.target_link}',
|
||||
"icon":"fa-user-crown",
|
||||
"color": "bg-success",
|
||||
"title": 'added badmin @{self.target_user.username}'
|
||||
},
|
||||
"update_settings":{
|
||||
"str":'updated setting',
|
||||
"icon":"fa-cog",
|
||||
"color": "bg-info",
|
||||
"title": 'updated settings'
|
||||
},
|
||||
"update_appearance":{
|
||||
"str":'updated appearance',
|
||||
"icon":"fa-palette",
|
||||
"color": "bg-info",
|
||||
"title": 'updated appearance'
|
||||
},
|
||||
"set_nsfw":{
|
||||
"str":'set nsfw on post {self.target_link}',
|
||||
"icon":"fa-eye-evil",
|
||||
"color": "bg-danger",
|
||||
"title": 'set nsfw on post {self.target_post.title}'
|
||||
},
|
||||
"unset_nsfw":{
|
||||
"str":'un-set nsfw on post {self.target_link}',
|
||||
"icon":"fa-eye-evil",
|
||||
"color": "bg-muted",
|
||||
"title": 'un-set nsfw on post {self.target_post.title}'
|
||||
},
|
||||
"set_nsfl":{
|
||||
"str":'set nsfl on post {self.target_link}',
|
||||
"icon":"fa-skull",
|
||||
"color": "bg-black",
|
||||
"title": 'set nsfl on post {self.target_post.title}'
|
||||
},
|
||||
"unset_nsfl":{
|
||||
"str":'un-set nsfl on post {self.target_link}',
|
||||
"icon":"fa-skull",
|
||||
"color": "bg-muted",
|
||||
"title": 'un-set nsfw on post {self.target_post.title}'
|
||||
},
|
||||
"ban_post":{
|
||||
"str": 'removed post {self.target_link}',
|
||||
"icon":"fa-feather-alt",
|
||||
"color": "bg-danger",
|
||||
"title": "removed post {self.target_post.title}"
|
||||
},
|
||||
"unban_post":{
|
||||
"str": 'reinstated post {self.target_link}',
|
||||
"icon":"fa-feather-alt",
|
||||
"color": "bg-muted",
|
||||
"title": "reinstated post {self.target_post.title}"
|
||||
},
|
||||
"ban_comment":{
|
||||
"str": 'removed {self.target_link}',
|
||||
"icon":"fa-comment",
|
||||
"color": "bg-danger",
|
||||
"title": "removed comment"
|
||||
},
|
||||
"unban_comment":{
|
||||
"str": 'reinstated {self.target_link}',
|
||||
"icon":"fa-comment",
|
||||
"color": "bg-muted",
|
||||
"title": "reinstated comment"
|
||||
},
|
||||
"change_perms":{
|
||||
"str": 'changed permissions on badmin {self.target_link}',
|
||||
"icon":"fa-user-cog",
|
||||
"color": "bg-info",
|
||||
"title": "changed permissions on {self.target_user.username}"
|
||||
},
|
||||
"change_invite":{
|
||||
"str": 'changed permissions on badmin invitation to {self.target_link}',
|
||||
"icon":"fa-user-cog",
|
||||
"color": "bg-muted",
|
||||
"title": "changed permissions on invitation to {self.target_user.username}"
|
||||
},
|
||||
"create_guild":{
|
||||
"str": 'created +{self.board.name}',
|
||||
"icon": "fa-chess-rook",
|
||||
"color": "bg-primary",
|
||||
"title": "created +{self.board.name}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
import requests
|
||||
from os import environ
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship
|
||||
from .mix_ins import *
|
||||
from ruqqus.__main__ import Base, app
|
||||
|
||||
PAYPAL_ID=environ.get("PAYPAL_CLIENT_ID", "").strip()
|
||||
PAYPAL_SECRET=environ.get("PAYPAL_CLIENT_SECRET", "").strip()
|
||||
PAYPAL_WEBHOOK_ID=environ.get("PAYPAL_WEBHOOK_ID", "").strip()
|
||||
|
||||
PAYPAL_URL="https://api.paypal.com"
|
||||
|
||||
STATUSES={
|
||||
1:"CREATED",
|
||||
2:"AUTHORIZED",
|
||||
3:"CAPTURED",
|
||||
-2:"REVERSED"
|
||||
}
|
||||
|
||||
class PayPalClient():
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.paypal_token=None
|
||||
self.token_expires=0
|
||||
self.webhook_id=PAYPAL_WEBHOOK_ID
|
||||
|
||||
def print(self, x):
|
||||
|
||||
try:
|
||||
print(x)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def new_token(self):
|
||||
|
||||
url=f"{PAYPAL_URL}/v1/oauth2/token"
|
||||
|
||||
headers={
|
||||
"Accept":"application/json"
|
||||
}
|
||||
|
||||
data={
|
||||
"grant_type":"client_credentials"
|
||||
}
|
||||
|
||||
x=requests.post(url, headers=headers, data=data, auth=(PAYPAL_ID,PAYPAL_SECRET))
|
||||
|
||||
x=x.json()
|
||||
|
||||
self.paypal_token=x["access_token"]
|
||||
self.token_expires=int(time.time())+int(x["expires_in"])
|
||||
|
||||
def _get(self, url):
|
||||
|
||||
if time.time()>self.token_expires:
|
||||
self.new_token()
|
||||
|
||||
url=PAYPAL_URL+url
|
||||
|
||||
headers={
|
||||
"Content-Type":"application/json",
|
||||
# "Accept":"application/json",
|
||||
"Authorization":f"Bearer {self.paypal_token}"
|
||||
}
|
||||
|
||||
return requests.get(url, headers=headers)
|
||||
|
||||
|
||||
def _post(self, url, data=None):
|
||||
|
||||
if time.time()>self.token_expires:
|
||||
self.new_token()
|
||||
|
||||
url=PAYPAL_URL+url
|
||||
|
||||
headers={
|
||||
"Content-Type":"application/json",
|
||||
# "Accept":"application/json",
|
||||
"Authorization":f"Bearer {self.paypal_token}"
|
||||
}
|
||||
|
||||
return requests.post(url, headers=headers, json=data)
|
||||
|
||||
def create(self, txn):
|
||||
|
||||
if not txn.id:
|
||||
raise ValueError("txn must be flushed first")
|
||||
|
||||
url="/v2/checkout/orders"
|
||||
|
||||
data={
|
||||
"intent":"CAPTURE",
|
||||
"purchase_units":
|
||||
[
|
||||
{
|
||||
"amount": {
|
||||
"currency_code":"USD",
|
||||
"value": str(txn.usd_cents/100)
|
||||
}
|
||||
}
|
||||
],
|
||||
"application_context":{
|
||||
"return_url":f"https://{app.config['SERVER_NAME']}/shop/buy_coins_completed?txid={txn.base36id}"
|
||||
}
|
||||
}
|
||||
|
||||
r=self._post(url, data=data)
|
||||
|
||||
x=r.json()
|
||||
|
||||
if x["status"]=="CREATED":
|
||||
txn.paypal_id=x["id"]
|
||||
txn.status=1
|
||||
|
||||
def authorize(self, txn):
|
||||
|
||||
url=f"{txn.paypal_url}/authorize"
|
||||
|
||||
x= self._post(url)
|
||||
x=x.json()
|
||||
|
||||
status=x["status"]
|
||||
if status in ["SAVED", "COMPLETED"]:
|
||||
txn.status=2
|
||||
|
||||
return x["status"] in ["SAVED", "COMPLETED"]
|
||||
|
||||
|
||||
def capture(self, txn):
|
||||
|
||||
url=f"{txn.paypal_url}/capture"
|
||||
|
||||
x=self._post(url)
|
||||
x=x.json()
|
||||
|
||||
try:
|
||||
status=x["status"]
|
||||
if status=="COMPLETED":
|
||||
txn.status=3
|
||||
except KeyError:
|
||||
abort(403)
|
||||
|
||||
return status=="COMPLETED"
|
||||
|
||||
|
||||
class PayPalTxn(Base, Stndrd, Age_times):
|
||||
|
||||
__tablename__="paypal_txns"
|
||||
|
||||
id=Column(Integer, primary_key=True)
|
||||
user_id=Column(Integer, ForeignKey("users.id"))
|
||||
created_utc=Column(Integer)
|
||||
paypal_id=Column(String)
|
||||
usd_cents=Column(Integer)
|
||||
coin_count=Column(Integer)
|
||||
promo_id=Column(Integer, ForeignKey("promocodes.id"))
|
||||
|
||||
status=Column(Integer, default=0) #0=initialized 1=created, 2=authorized, 3=captured, -1=failed, -2=reversed
|
||||
|
||||
user=relationship("User", lazy="joined")
|
||||
promo=relationship("PromoCode", lazy="joined")
|
||||
|
||||
@property
|
||||
def approve_url(self):
|
||||
|
||||
return f"https://www.paypal.com/checkoutnow?token={self.paypal_id}"
|
||||
|
||||
@property
|
||||
def paypal_url(self):
|
||||
|
||||
return f"/v2/checkout/orders/{self.paypal_id}"
|
||||
|
||||
@property
|
||||
def permalink(self):
|
||||
return f"/paypaltxn/{self.base36id}"
|
||||
|
||||
@property
|
||||
def display_usd(self):
|
||||
s=str(self.usd_cents)
|
||||
d=s[0:-2] or '0'
|
||||
c=s[-2:]
|
||||
return f"${d}.{c}"
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
return STATUSES[self.status]
|
||||
|
||||
class PromoCode(Base):
|
||||
|
||||
__tablename__="promocodes"
|
||||
|
||||
id=Column(Integer, primary_key=True)
|
||||
code=Column(String(64))
|
||||
is_active=Column(Boolean)
|
||||
percent_off=Column(Integer, default=None)
|
||||
flat_cents_off=Column(Integer, default=None)
|
||||
flat_cents_min=Column(Integer, default=None)
|
||||
promo_start_utc=Column(Integer, default=None)
|
||||
promo_end_utc=Column(Integer, default=None)
|
||||
promo_info=Column(String(64), default=None)
|
||||
|
||||
def adjust_price(self, cents):
|
||||
|
||||
now=int(time.time())
|
||||
|
||||
if self.promo_start_utc and now < self.promo_start_utc:
|
||||
return cents
|
||||
|
||||
elif self.promo_end_utc and now > self.promo_end_utc:
|
||||
return cents
|
||||
|
||||
if not self.is_active:
|
||||
return cents
|
||||
|
||||
if self.percent_off:
|
||||
x = (100-self.percent_off)/100
|
||||
return int(cents * x)
|
||||
|
||||
if self.flat_cents_off:
|
||||
if cents >= self.flat_cents_min:
|
||||
cents -= self.flat_cents_off
|
||||
return cents
|
||||
|
||||
else:
|
||||
return cents
|
||||
|
||||
@property
|
||||
def display_flat_off(self):
|
||||
s=str(self.flat_cents_off)
|
||||
d=s[0:-2] or '0'
|
||||
c=s[-2:]
|
||||
return f"${d}.{c}"
|
||||
|
||||
@property
|
||||
def display_flat_min(self):
|
||||
s=str(self.flat_cents_min)
|
||||
d=s[0:-2] or '0'
|
||||
c=s[-2:]
|
||||
return f"${d}.{c}"
|
||||
|
||||
@property
|
||||
def promo_text(self):
|
||||
|
||||
now=int(time.time())
|
||||
|
||||
if self.promo_start_utc and now < self.promo_start_utc:
|
||||
return f"This promotion hasn't started yet. Try again later."
|
||||
|
||||
elif self.promo_end_utc and now > self.promo_end_utc:
|
||||
return f"This promotion has already ended. Sorry about that."
|
||||
|
||||
elif self.percent_off:
|
||||
text= f"Save {self.percent_off}% on all purchases with code {self.code}."
|
||||
|
||||
elif self.flat_cents_off and self.flat_cents_min:
|
||||
text= f"Save {self.display_flat_off} on any purchase over {self.display_flat_min} with code {self.code}."
|
||||
|
||||
if self.promo_info:
|
||||
text += f" Your purchase will also support {self.promo_info}."
|
||||
|
||||
return text
|
||||
|
||||
|
||||
|
||||
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"), default=None)
|
||||
comment_id=Column(Integer, ForeignKey("comments.id"), default=None)
|
|
@ -0,0 +1,610 @@
|
|||
from flask import render_template, request, g
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship, deferred
|
||||
import re, random
|
||||
from urllib.parse import urlparse
|
||||
from .mix_ins import *
|
||||
from ruqqus.helpers.base36 import *
|
||||
from ruqqus.helpers.lazy import lazy
|
||||
from ruqqus.__main__ import Base
|
||||
|
||||
class SubmissionAux(Base):
|
||||
|
||||
__tablename__ = "submissions_aux"
|
||||
|
||||
# we don't care about this ID
|
||||
key_id = Column(BigInteger, primary_key=True)
|
||||
id = Column(BigInteger, ForeignKey("submissions.id"))
|
||||
title = Column(String(500), default=None)
|
||||
title_html = Column(String(500), default=None)
|
||||
url = Column(String(500), default=None)
|
||||
body = Column(String(10000), default="")
|
||||
body_html = Column(String(20000), default="")
|
||||
ban_reason = Column(String(128), default="")
|
||||
embed_url = Column(String(256), default="")
|
||||
meta_title=Column(String(512), default="")
|
||||
meta_description=Column(String(1024), default="")
|
||||
|
||||
|
||||
class Submission(Base, Stndrd, Age_times, Scores, Fuzzing):
|
||||
|
||||
__tablename__ = "submissions"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
submission_aux = relationship(
|
||||
"SubmissionAux",
|
||||
lazy="joined",
|
||||
uselist=False,
|
||||
innerjoin=True,
|
||||
primaryjoin="Submission.id==SubmissionAux.id")
|
||||
author_id = Column(BigInteger, ForeignKey("users.id"))
|
||||
repost_id = Column(BigInteger, ForeignKey("submissions.id"), default=0)
|
||||
edited_utc = Column(BigInteger, default=0)
|
||||
created_utc = Column(BigInteger, default=0)
|
||||
thumburl = Column(String, default=None)
|
||||
is_banned = Column(Boolean, default=False)
|
||||
views = Column(Integer, default=0)
|
||||
deleted_utc = Column(Integer, default=0)
|
||||
purged_utc = Column(Integer, default=0)
|
||||
distinguish_level = Column(Integer, default=0)
|
||||
gm_distinguish = Column(Integer, ForeignKey("boards.id"), default=0)
|
||||
distinguished_board = relationship("Board", lazy="joined", primaryjoin="Board.id==Submission.gm_distinguish")
|
||||
created_str = Column(String(255), default=None)
|
||||
stickied = Column(Boolean, default=False)
|
||||
is_pinned = Column(Boolean, default=False)
|
||||
private = Column(Boolean, default=False)
|
||||
_comments = relationship(
|
||||
"Comment",
|
||||
lazy="dynamic",
|
||||
primaryjoin="Comment.parent_submission==Submission.id",
|
||||
backref="submissions")
|
||||
domain_ref = Column(Integer, ForeignKey("domains.id"))
|
||||
domain_obj = relationship("Domain")
|
||||
flags = relationship("Flag", backref="submission")
|
||||
is_approved = Column(Integer, ForeignKey("users.id"), default=0)
|
||||
approved_utc = Column(Integer, default=0)
|
||||
board_id = Column(Integer, ForeignKey("boards.id"), default=None)
|
||||
original_board_id = Column(Integer, ForeignKey("boards.id"), default=None)
|
||||
over_18 = Column(Boolean, default=False)
|
||||
original_board = relationship(
|
||||
"Board", primaryjoin="Board.id==Submission.original_board_id")
|
||||
creation_ip = Column(String(64), default="")
|
||||
mod_approved = Column(Integer, default=None)
|
||||
accepted_utc = Column(Integer, default=0)
|
||||
has_thumb = Column(Boolean, default=False)
|
||||
post_public = Column(Boolean, default=True)
|
||||
score_hot = Column(Float, default=0)
|
||||
score_top = Column(Float, default=1)
|
||||
score_activity = Column(Float, default=0)
|
||||
is_offensive = Column(Boolean, default=False)
|
||||
is_nsfl = Column(Boolean, default=False)
|
||||
board = relationship(
|
||||
"Board",
|
||||
lazy="joined",
|
||||
innerjoin=True,
|
||||
primaryjoin="Submission.board_id==Board.id")
|
||||
author = relationship(
|
||||
"User",
|
||||
lazy="joined",
|
||||
innerjoin=True,
|
||||
primaryjoin="Submission.author_id==User.id")
|
||||
is_pinned = Column(Boolean, default=False)
|
||||
score_best = Column(Float, default=0)
|
||||
reports = relationship("Report", backref="submission")
|
||||
is_bot = Column(Boolean, default=False)
|
||||
|
||||
upvotes = Column(Integer, default=1)
|
||||
downvotes = Column(Integer, default=0)
|
||||
creation_region=Column(String(2), default=None)
|
||||
|
||||
app_id=Column(Integer, ForeignKey("oauth_apps.id"), default=None)
|
||||
oauth_app=relationship("OauthApp")
|
||||
|
||||
approved_by = relationship(
|
||||
"User",
|
||||
uselist=False,
|
||||
primaryjoin="Submission.is_approved==User.id")
|
||||
|
||||
# not sure if we need this
|
||||
reposts = relationship("Submission", lazy="joined", remote_side=[id])
|
||||
|
||||
# These are virtual properties handled as postgres functions server-side
|
||||
# There is no difference to SQLAlchemy, but they cannot be written to
|
||||
|
||||
ups = deferred(Column(Integer, server_default=FetchedValue()))
|
||||
downs = deferred(Column(Integer, server_default=FetchedValue()))
|
||||
comment_count = Column(Integer, server_default=FetchedValue())
|
||||
score = deferred(Column(Float, server_default=FetchedValue()))
|
||||
|
||||
awards = relationship("AwardRelationship", lazy="joined")
|
||||
|
||||
rank_hot = deferred(Column(Float, server_default=FetchedValue()))
|
||||
rank_fiery = deferred(Column(Float, server_default=FetchedValue()))
|
||||
rank_activity = deferred(Column(Float, server_default=FetchedValue()))
|
||||
rank_best = deferred(Column(Float, server_default=FetchedValue()))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time.time())
|
||||
kwargs["created_str"] = time.strftime(
|
||||
"%I:%M %p on %d %b %Y", time.gmtime(
|
||||
kwargs["created_utc"]))
|
||||
|
||||
kwargs["creation_ip"] = request.remote_addr
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Submission(id={self.id})>"
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def board_base36id(self):
|
||||
return base36encode(self.board_id)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def is_deleted(self):
|
||||
return bool(self.deleted_utc)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def hotscore(self):
|
||||
return 10000000*(self.upvotes - self.downvotes + 1)/(((self.age+3600)/1000)**(1.35))
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def score_disputed(self):
|
||||
return (self.upvotes+1) * (self.downvotes+1)
|
||||
|
||||
@property
|
||||
def is_repost(self):
|
||||
return bool(self.repost_id)
|
||||
|
||||
@property
|
||||
def is_archived(self):
|
||||
return false
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def fullname(self):
|
||||
return f"t2_{self.base36id}"
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def permalink(self):
|
||||
|
||||
output = self.title.lower()
|
||||
|
||||
output = re.sub('&\w{2,3};', '', output)
|
||||
|
||||
output = [re.sub('\W', '', word) for word in output.split()]
|
||||
output = [x for x in output if x][0:6]
|
||||
|
||||
output = '-'.join(output)
|
||||
|
||||
if not output:
|
||||
output = '-'
|
||||
|
||||
return f"/post/{self.id}/{output}"
|
||||
|
||||
@property
|
||||
def is_archived(self):
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
cutoff = now - (60 * 60 * 24 * 180)
|
||||
|
||||
return self.created_utc < cutoff
|
||||
|
||||
def rendered_page(self, sort=None, comment=None, comment_info=None, v=None):
|
||||
|
||||
# check for banned
|
||||
if v and v.admin_level >= 3:
|
||||
template = "submission.html"
|
||||
elif self.is_banned:
|
||||
template = "submission_banned.html"
|
||||
else:
|
||||
template = "submission.html"
|
||||
|
||||
# load and tree comments
|
||||
# calling this function with a comment object will do a comment
|
||||
# permalink thing
|
||||
if "replies" not in self.__dict__ and "_preloaded_comments" in self.__dict__:
|
||||
self.tree_comments(comment=comment)
|
||||
|
||||
# return template
|
||||
is_allowed_to_comment = self.board.can_comment(
|
||||
v) and not self.is_archived
|
||||
|
||||
return render_template(template,
|
||||
v=v,
|
||||
p=self,
|
||||
sort=sort,
|
||||
linked_comment=comment,
|
||||
comment_info=comment_info,
|
||||
is_allowed_to_comment=is_allowed_to_comment,
|
||||
render_replies=True,
|
||||
)
|
||||
|
||||
@property
|
||||
@lazy
|
||||
def domain(self):
|
||||
|
||||
if not self.url:
|
||||
return "text post"
|
||||
domain = urlparse(self.url).netloc
|
||||
if domain.startswith("www."):
|
||||
domain = domain.split("www.")[1]
|
||||
return domain.replace("old.reddit.com", "reddit.com")
|
||||
|
||||
def tree_comments(self, comment=None, v=None):
|
||||
|
||||
comments = self.__dict__.get('_preloaded_comments',[])
|
||||
if not comments:
|
||||
return
|
||||
|
||||
pinned_comment=[]
|
||||
|
||||
index = {}
|
||||
for c in comments:
|
||||
|
||||
if c.is_pinned and c.parent_fullname==self.fullname:
|
||||
pinned_comment+=[c]
|
||||
continue
|
||||
|
||||
if c.parent_fullname in index:
|
||||
index[c.parent_fullname].append(c)
|
||||
else:
|
||||
index[c.parent_fullname] = [c]
|
||||
|
||||
for c in comments:
|
||||
c.__dict__["replies"] = index.get(c.fullname, [])
|
||||
|
||||
if comment:
|
||||
self.__dict__["replies"] = [comment]
|
||||
else:
|
||||
self.__dict__["replies"] = pinned_comment + index.get(self.fullname, [])
|
||||
|
||||
@property
|
||||
def active_flags(self):
|
||||
if self.is_approved:
|
||||
return 0
|
||||
else:
|
||||
return len(self.flags)
|
||||
|
||||
@property
|
||||
#@lazy
|
||||
def thumb_url(self):
|
||||
|
||||
if self.over_18: return f"/assets/images/nsfw.png"
|
||||
elif self.has_thumb:
|
||||
if self.thumburl: return self.thumburl
|
||||
else: return f"https://s3.eu-central-1.amazonaws.com/i.ruqqus.ga/posts/{self.base36id}/thumb.png"
|
||||
elif self.is_image:
|
||||
return self.url
|
||||
else:
|
||||
return None
|
||||
|
||||
def visibility_reason(self, v):
|
||||
|
||||
|
||||
if not v or self.author_id == v.id:
|
||||
return "this is your content."
|
||||
elif self.is_pinned:
|
||||
return "a guildmaster has pinned it."
|
||||
elif self.board.has_mod(v):
|
||||
return f"you are a guildmaster of +{self.board.name}."
|
||||
elif self.board.has_contributor(v):
|
||||
return f"you are an approved contributor in +{self.board.name}."
|
||||
elif v.admin_level >= 4:
|
||||
return "you are a Drama admin."
|
||||
|
||||
@property
|
||||
|
||||
def json_raw(self):
|
||||
data = {'author_name': self.author.username if not self.author.is_deleted else None,
|
||||
'permalink': self.permalink,
|
||||
'is_banned': bool(self.is_banned),
|
||||
'is_deleted': self.is_deleted,
|
||||
'created_utc': self.created_utc,
|
||||
'id': self.base36id,
|
||||
'fullname': self.fullname,
|
||||
'title': self.title,
|
||||
'is_nsfw': self.over_18,
|
||||
'is_nsfl': self.is_nsfl,
|
||||
'is_bot': self.is_bot,
|
||||
'thumb_url': self.thumb_url,
|
||||
'domain': self.domain,
|
||||
'is_archived': self.is_archived,
|
||||
'url': self.url,
|
||||
'body': self.body,
|
||||
'body_html': self.body_html,
|
||||
'created_utc': self.created_utc,
|
||||
'edited_utc': self.edited_utc or 0,
|
||||
'guild_name': self.board.name,
|
||||
'guild_id': base36encode(self.board_id),
|
||||
'comment_count': self.comment_count,
|
||||
'score': self.score_fuzzed,
|
||||
'upvotes': self.upvotes_fuzzed,
|
||||
'downvotes': self.downvotes_fuzzed,
|
||||
'award_count': self.award_count,
|
||||
'is_offensive': self.is_offensive,
|
||||
'meta_title': self.meta_title,
|
||||
'meta_description': self.meta_description,
|
||||
'voted': self.voted
|
||||
}
|
||||
if self.ban_reason:
|
||||
data["ban_reason"]=self.ban_reason
|
||||
|
||||
if self.board_id != self.original_board_id and self.original_board:
|
||||
data['original_guild_name'] = self.original_board.name
|
||||
data['original_guild_id'] = base36encode(self.original_board_id)
|
||||
return data
|
||||
|
||||
@property
|
||||
def json_core(self):
|
||||
|
||||
if self.is_banned:
|
||||
return {'is_banned': True,
|
||||
'is_deleted': self.is_deleted,
|
||||
'ban_reason': self.ban_reason,
|
||||
'id': self.base36id,
|
||||
'title': self.title,
|
||||
'permalink': self.permalink,
|
||||
'guild_name': self.board.name
|
||||
}
|
||||
elif self.is_deleted:
|
||||
return {'is_banned': bool(self.is_banned),
|
||||
'is_deleted': True,
|
||||
'id': self.base36id,
|
||||
'title': self.title,
|
||||
'permalink': self.permalink,
|
||||
'guild_name': self.board.name
|
||||
}
|
||||
|
||||
return self.json_raw
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
|
||||
data=self.json_core
|
||||
|
||||
if self.deleted_utc > 0 or self.is_banned:
|
||||
return data
|
||||
|
||||
data["author"]=self.author.json_core
|
||||
data["guild"]=self.board.json_core
|
||||
data["original_guild"]=self.original_board.json_core if not self.board_id==self.original_board_id else None
|
||||
data["comment_count"]=self.comment_count
|
||||
|
||||
|
||||
if "replies" in self.__dict__:
|
||||
data["replies"]=[x.json_core for x in self.replies]
|
||||
|
||||
if "_voted" in self.__dict__:
|
||||
data["voted"] = self._voted
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def voted(self):
|
||||
return self._voted if "_voted" in self.__dict__ else 0
|
||||
|
||||
@property
|
||||
def user_title(self):
|
||||
return self._title if "_title" in self.__dict__ else self.author.title
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.submission_aux.title
|
||||
|
||||
@title.setter
|
||||
def title(self, x):
|
||||
self.submission_aux.title = x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.submission_aux.url
|
||||
|
||||
@url.setter
|
||||
def url(self, x):
|
||||
self.submission_aux.url = x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
def realurl(self, v):
|
||||
if v and v.agendaposter and random.randint(1, 10) < 4:
|
||||
return 'https://secure.actblue.com/donate/ms_blm_homepage_2019'
|
||||
elif self.url:
|
||||
if v and not v.oldreddit: return self.url.replace("old.reddit.com", "reddit.com")
|
||||
if self.url: return self.url
|
||||
return ""
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self.submission_aux.body
|
||||
|
||||
@body.setter
|
||||
def body(self, x):
|
||||
self.submission_aux.body = x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
@property
|
||||
def body_html(self):
|
||||
return self.submission_aux.body_html
|
||||
|
||||
@body_html.setter
|
||||
def body_html(self, x):
|
||||
self.submission_aux.body_html = x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
def realbody(self, v):
|
||||
body = self.submission_aux.body_html
|
||||
if not v or v.slurreplacer: body = body.replace(" nigger"," 🏀").replace(" Nigger"," 🏀").replace(" NIGGER"," 🏀").replace(" pedo"," libertarian").replace(" Pedo"," Libertarian ").replace(" PEDO"," LIBERTARIAN ").replace(" tranny"," 🚄").replace(" Tranny"," 🚄").replace(" TRANNY"," 🚄").replace(" fag"," cute twink").replace(" Fag"," Cute twink").replace(" FAG"," CUTE TWINK").replace(" faggot"," cute twink").replace(" Faggot"," Cute twink").replace(" FAGGOT"," CUTE TWINK").replace(" trump"," DDR").replace(" Trump"," DDR").replace(" TRUMP"," DDR").replace(" biden"," DDD").replace(" Biden"," DDD").replace(" BIDEN"," DDD").replace(" steve akins"," penny verity oaken").replace(" Steve Akins"," Penny Verity Oaken").replace(" STEVE AKINS"," PENNY VERITY OAKEN").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" kill yourself"," keep yourself safe").replace(" KILL YOURSELF"," KEEP YOURSELF SAFE")
|
||||
if v and not v.oldreddit: body = body.replace("old.reddit.com", "reddit.com")
|
||||
return body
|
||||
|
||||
@property
|
||||
def title_html(self):
|
||||
return self.submission_aux.title_html
|
||||
|
||||
@title_html.setter
|
||||
def title_html(self, x):
|
||||
self.submission_aux.title_html = x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
def realtitle(self, v):
|
||||
if self.title_html: title = self.title_html
|
||||
else: title = self.title
|
||||
if not v or v.slurreplacer: title = title.replace(" nigger"," 🏀").replace(" Nigger"," 🏀").replace(" NIGGER"," 🏀").replace(" pedo"," libertarian").replace(" Pedo"," Libertarian ").replace(" PEDO"," LIBERTARIAN ").replace(" tranny"," 🚄").replace(" Tranny"," 🚄").replace(" TRANNY"," 🚄").replace(" fag"," cute twink").replace(" Fag"," Cute twink").replace(" FAG"," CUTE TWINK").replace(" faggot"," cute twink").replace(" Faggot"," Cute twink").replace(" FAGGOT"," CUTE TWINK").replace(" trump"," DDR").replace(" Trump"," DDR").replace(" TRUMP"," DDR").replace(" biden"," DDD").replace(" Biden"," DDD").replace(" BIDEN"," DDD").replace(" steve akins"," penny verity oaken").replace(" Steve Akins"," Penny Verity Oaken").replace(" STEVE AKINS"," PENNY VERITY OAKEN").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" RETARD"," RSLUR").replace(" rapist"," male feminist").replace(" Rapist"," Male feminist").replace(" RAPIST"," MALE FEMINIST").replace(" kill yourself"," keep yourself safe").replace(" KILL YOURSELF"," KEEP YOURSELF SAFE")
|
||||
return title
|
||||
|
||||
@property
|
||||
def ban_reason(self):
|
||||
return self.submission_aux.ban_reason
|
||||
|
||||
@ban_reason.setter
|
||||
def ban_reason(self, x):
|
||||
self.submission_aux.ban_reason = x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
@property
|
||||
def embed_url(self):
|
||||
return self.submission_aux.embed_url
|
||||
|
||||
@embed_url.setter
|
||||
def embed_url(self, x):
|
||||
self.submission_aux.embed_url = x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
@property
|
||||
def meta_title(self):
|
||||
return self.submission_aux.meta_title
|
||||
|
||||
@meta_title.setter
|
||||
def meta_title(self, x):
|
||||
self.submission_aux.meta_title=x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
@property
|
||||
def meta_description(self):
|
||||
return self.submission_aux.meta_description
|
||||
|
||||
@meta_description.setter
|
||||
def meta_description(self, x):
|
||||
self.submission_aux.meta_description=x
|
||||
g.db.add(self.submission_aux)
|
||||
|
||||
|
||||
def is_guildmaster(self, perm=None):
|
||||
mod=self.__dict__.get('_is_guildmaster', False)
|
||||
|
||||
if not mod:
|
||||
return False
|
||||
elif not perm:
|
||||
return True
|
||||
else:
|
||||
return mod.perm_full or mod.__dict__[f"perm_{perm}"]
|
||||
|
||||
return output
|
||||
|
||||
@property
|
||||
def is_blocking_guild(self):
|
||||
return self.__dict__.get('_is_blocking_guild', False)
|
||||
|
||||
@property
|
||||
def is_blocked(self):
|
||||
return self.__dict__.get('_is_blocked', False)
|
||||
|
||||
@property
|
||||
def is_blocking(self):
|
||||
return self.__dict__.get('_is_blocking', False)
|
||||
|
||||
@property
|
||||
def is_subscribed(self):
|
||||
return self.__dict__.get('_is_subscribed', False)
|
||||
|
||||
@property
|
||||
def is_public(self):
|
||||
return self.post_public or not self.board.is_private
|
||||
|
||||
@property
|
||||
def flag_count(self):
|
||||
return len(self.flags)
|
||||
|
||||
@property
|
||||
def report_count(self):
|
||||
return len(self.reports)
|
||||
|
||||
@property
|
||||
def award_count(self):
|
||||
return len(self.awards)
|
||||
|
||||
@property
|
||||
def embed_template(self):
|
||||
return f"site_embeds/{self.domain_obj.embed_template}.html"
|
||||
|
||||
@property
|
||||
def flagged_by(self):
|
||||
return [x.user for x in self.flags]
|
||||
|
||||
@property
|
||||
def is_image(self):
|
||||
if self.url: return self.url.endswith('jpg') or self.url.endswith('png') or self.url.endswith('.gif') or self.url.endswith('jpeg') or self.url.endswith('?maxwidth=9999') or self.url.endswith('?maxwidth=8888')
|
||||
else: return False
|
||||
|
||||
@property
|
||||
def self_download_json(self):
|
||||
|
||||
#This property should never be served to anyone but author and admin
|
||||
if not self.is_banned and self.deleted_utc == 0:
|
||||
return self.json_core
|
||||
|
||||
data= {
|
||||
"title":self.title,
|
||||
"author": self.author.username,
|
||||
"url": self.url,
|
||||
"body": self.body,
|
||||
"body_html": self.body_html,
|
||||
"is_banned": bool(self.is_banned),
|
||||
"deleted_utc": self.deleted_utc,
|
||||
'created_utc': self.created_utc,
|
||||
'id': self.base36id,
|
||||
'fullname': self.fullname,
|
||||
'guild_name': self.board.name,
|
||||
'comment_count': self.comment_count,
|
||||
'permalink': self.permalink
|
||||
}
|
||||
|
||||
if self.original_board_id and (self.original_board_id!= self.board_id):
|
||||
|
||||
data['original_guild_name'] = self.original_board.name
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def json_admin(self):
|
||||
|
||||
data=self.json_raw
|
||||
|
||||
data["creation_ip"]=self.creation_ip
|
||||
data["creation_region"]=self.creation_region
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def is_exiled_for(self):
|
||||
return self.__dict__.get('_is_exiled_for', None)
|
||||
|
||||
|
||||
|
||||
class SaveRelationship(Base, Stndrd):
|
||||
|
||||
__tablename__="save_relationship"
|
||||
|
||||
id=Column(Integer, primary_key=true)
|
||||
user_id=Column(Integer, ForeignKey("users.id"))
|
||||
submission_id=Column(Integer, ForeignKey("submissions.id"))
|
||||
type=Column(Integer)
|
|
@ -0,0 +1,52 @@
|
|||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship
|
||||
from ruqqus.__main__ import Base
|
||||
import time
|
||||
|
||||
|
||||
class Subscription(Base):
|
||||
__tablename__ = "subscriptions"
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(BigInteger, ForeignKey("users.id"))
|
||||
board_id = Column(BigInteger, ForeignKey("boards.id"))
|
||||
created_utc = Column(BigInteger, default=0)
|
||||
is_active = Column(Boolean, default=True)
|
||||
submission_id = Column(BigInteger, default=0)
|
||||
|
||||
user = relationship("User", uselist=False)
|
||||
board = relationship("Board", uselist=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time.time())
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<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"))
|
||||
created_utc = Column(BigInteger, default=0)
|
||||
|
||||
user = relationship(
|
||||
"User",
|
||||
uselist=False,
|
||||
primaryjoin="User.id==Follow.user_id")
|
||||
target = relationship(
|
||||
"User",
|
||||
lazy="joined",
|
||||
primaryjoin="User.id==Follow.target_id")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time.time())
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Follow(id={self.id})>"
|
|
@ -0,0 +1,37 @@
|
|||
from sqlalchemy import *
|
||||
from flask import render_template
|
||||
from ruqqus.__main__ import Base
|
||||
|
||||
class Title(Base):
|
||||
|
||||
__tablename__ = "titles"
|
||||
id = Column(Integer, primary_key=True)
|
||||
is_before = Column(Boolean, default=True)
|
||||
text = Column(String(64))
|
||||
qualification_expr = Column(String(256))
|
||||
requirement_string = Column(String(512))
|
||||
color = Column(String(6), default="888888")
|
||||
kind = Column(Integer, default=1)
|
||||
|
||||
background_color_1 = Column(String(6), default=None)
|
||||
background_color_2 = Column(String(6), default=None)
|
||||
gradient_angle = Column(Integer, default=0)
|
||||
box_shadow_color = Column(String(32), default=None)
|
||||
text_shadow_color = Column(String(32), default=None)
|
||||
|
||||
def check_eligibility(self, v):
|
||||
|
||||
return bool(eval(self.qualification_expr, {}, {"v": v}))
|
||||
|
||||
@property
|
||||
def rendered(self):
|
||||
return render_template('title.html', t=self)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
|
||||
return {'id': self.id,
|
||||
'text': self.text,
|
||||
'color': f'#{self.color}',
|
||||
'kind': self.kind
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
|||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship
|
||||
from .mix_ins import *
|
||||
from ruqqus.__main__ import Base
|
||||
|
||||
class UserBlock(Base, Stndrd, Age_times):
|
||||
|
||||
__tablename__ = "userblocks"
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
target_id = Column(Integer, ForeignKey("users.id"))
|
||||
created_utc = Column(Integer)
|
||||
|
||||
user = relationship(
|
||||
"User",
|
||||
innerjoin=True,
|
||||
primaryjoin="User.id==UserBlock.user_id")
|
||||
target = relationship(
|
||||
"User",
|
||||
innerjoin=True,
|
||||
primaryjoin="User.id==UserBlock.target_id")
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return f"<UserBlock(user={user.username}, target={target.username})>"
|
|
@ -0,0 +1,128 @@
|
|||
from flask import *
|
||||
from time import time
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import relationship
|
||||
from ruqqus.helpers.base36 import *
|
||||
from ruqqus.__main__ import Base
|
||||
|
||||
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"))
|
||||
created_utc = Column(Integer, default=0)
|
||||
creation_ip = Column(String, default=None)
|
||||
app_id = Column(Integer, ForeignKey("oauth_apps.id"), default=None)
|
||||
|
||||
user = relationship("User", lazy="subquery")
|
||||
post = relationship("Submission", lazy="subquery")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time())
|
||||
|
||||
kwargs["creation_ip"]=request.remote_addr
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Vote(id={self.id})>"
|
||||
|
||||
def change_to(self, x):
|
||||
"""
|
||||
1 - upvote
|
||||
0 - novote
|
||||
-1 - downvote
|
||||
"""
|
||||
if x in ["-1", "0", "1"]:
|
||||
x = int(x)
|
||||
elif x not in [-1, 0, 1]:
|
||||
abort(400)
|
||||
|
||||
self.vote_type = x
|
||||
self.created_utc = int(time())
|
||||
|
||||
g.db.add(self)
|
||||
|
||||
@property
|
||||
def json_core(self):
|
||||
data={
|
||||
"user_id": self.user_id,
|
||||
"submission_id":self.submission_id,
|
||||
"created_utc": self.created_utc,
|
||||
"vote_type":self.vote_type
|
||||
}
|
||||
return data
|
||||
|
||||
@property
|
||||
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"))
|
||||
created_utc = Column(Integer, default=0)
|
||||
creation_ip = Column(String, default=None)
|
||||
app_id = Column(Integer, ForeignKey("oauth_apps.id"), default=None)
|
||||
|
||||
user = relationship("User", lazy="subquery")
|
||||
comment = relationship("Comment", lazy="subquery")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "created_utc" not in kwargs:
|
||||
kwargs["created_utc"] = int(time())
|
||||
|
||||
kwargs["creation_ip"]=request.remote_addr
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CommentVote(id={self.id})>"
|
||||
|
||||
def change_to(self, x):
|
||||
"""
|
||||
1 - upvote
|
||||
0 - novote
|
||||
-1 - downvote
|
||||
"""
|
||||
if x in ["-1", "0", "1"]:
|
||||
x = int(x)
|
||||
elif x not in [-1, 0, 1]:
|
||||
abort(400)
|
||||
|
||||
self.vote_type = x
|
||||
self.created_utc = int(time())
|
||||
|
||||
g.db.add(self)
|
||||
|
||||
@property
|
||||
def json_core(self):
|
||||
data={
|
||||
"user_id": self.user_id,
|
||||
"comment_id":self.comment_id,
|
||||
"created_utc": self.created_utc,
|
||||
"vote_type":self.vote_type
|
||||
}
|
||||
return data
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
data=self.json_core
|
||||
data["user"]=self.user.json_core
|
||||
data["comment"]=self.comment.json_core
|
||||
|
||||
return data
|
|
@ -0,0 +1,180 @@
|
|||
import mistletoe
|
||||
|
||||
from ruqqus.classes import *
|
||||
from flask import g
|
||||
from .markdown import *
|
||||
from .sanitize import *
|
||||
|
||||
|
||||
def send_notification(vid, user, text):
|
||||
|
||||
text = text.replace('r/', 'r\/').replace('u/', 'u\/')
|
||||
with CustomRenderer() as renderer:
|
||||
text_html = renderer.render(mistletoe.Document(text))
|
||||
|
||||
text_html = sanitize(text_html, linkgen=True)
|
||||
|
||||
new_comment = Comment(author_id=vid,
|
||||
parent_submission=None,
|
||||
distinguish_level=6,
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
|
||||
g.db.flush()
|
||||
|
||||
new_aux = CommentAux(id=new_comment.id,
|
||||
body=text,
|
||||
body_html=text_html,
|
||||
)
|
||||
g.db.add(new_aux)
|
||||
|
||||
notif = Notification(comment_id=new_comment.id,
|
||||
user_id=user.id)
|
||||
g.db.add(notif)
|
||||
g.db.commit()
|
||||
|
||||
|
||||
def send_pm(vid, user, text):
|
||||
|
||||
with CustomRenderer() as renderer: text_html = renderer.render(mistletoe.Document(text))
|
||||
|
||||
text_html = sanitize(text_html, linkgen=True)
|
||||
|
||||
new_comment = Comment(author_id=vid,
|
||||
parent_submission=None,
|
||||
level=1,
|
||||
sentto=user.id
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
|
||||
g.db.flush()
|
||||
|
||||
new_aux = CommentAux(id=new_comment.id, body=text, body_html=text_html)
|
||||
g.db.add(new_aux)
|
||||
|
||||
notif = Notification(comment_id=new_comment.id, user_id=user.id)
|
||||
g.db.add(notif)
|
||||
g.db.commit()
|
||||
|
||||
|
||||
def send_follow_notif(vid, user, text):
|
||||
|
||||
with CustomRenderer() as renderer:
|
||||
text_html = renderer.render(mistletoe.Document(text))
|
||||
text_html = sanitize(text_html, linkgen=True)
|
||||
|
||||
new_comment = Comment(author_id=1046,
|
||||
parent_submission=None,
|
||||
distinguish_level=6,
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
g.db.flush()
|
||||
|
||||
new_aux = CommentAux(id=new_comment.id,
|
||||
body=text,
|
||||
body_html=text_html,
|
||||
)
|
||||
g.db.add(new_aux)
|
||||
|
||||
notif = Notification(comment_id=new_comment.id,
|
||||
user_id=user,
|
||||
followsender=vid)
|
||||
g.db.add(notif)
|
||||
g.db.commit()
|
||||
|
||||
def send_unfollow_notif(vid, user, text):
|
||||
|
||||
with CustomRenderer() as renderer:
|
||||
text_html = renderer.render(mistletoe.Document(text))
|
||||
text_html = sanitize(text_html, linkgen=True)
|
||||
|
||||
new_comment = Comment(author_id=1046,
|
||||
parent_submission=None,
|
||||
distinguish_level=6,
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
g.db.flush()
|
||||
|
||||
new_aux = CommentAux(id=new_comment.id,
|
||||
body=text,
|
||||
body_html=text_html,
|
||||
)
|
||||
g.db.add(new_aux)
|
||||
|
||||
notif = Notification(comment_id=new_comment.id,
|
||||
user_id=user,
|
||||
unfollowsender=vid)
|
||||
g.db.add(notif)
|
||||
g.db.commit()
|
||||
|
||||
def send_block_notif(vid, user, text):
|
||||
|
||||
with CustomRenderer() as renderer:
|
||||
text_html = renderer.render(mistletoe.Document(text))
|
||||
text_html = sanitize(text_html, linkgen=True)
|
||||
|
||||
new_comment = Comment(author_id=1046,
|
||||
parent_submission=None,
|
||||
distinguish_level=6,
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
g.db.flush()
|
||||
|
||||
new_aux = CommentAux(id=new_comment.id,
|
||||
body=text,
|
||||
body_html=text_html,
|
||||
)
|
||||
g.db.add(new_aux)
|
||||
|
||||
notif = Notification(comment_id=new_comment.id,
|
||||
user_id=user,
|
||||
blocksender=vid)
|
||||
g.db.add(notif)
|
||||
g.db.commit()
|
||||
|
||||
def send_unblock_notif(vid, user, text):
|
||||
|
||||
with CustomRenderer() as renderer:
|
||||
text_html = renderer.render(mistletoe.Document(text))
|
||||
text_html = sanitize(text_html, linkgen=True)
|
||||
|
||||
new_comment = Comment(author_id=1046,
|
||||
parent_submission=None,
|
||||
distinguish_level=6,
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
g.db.flush()
|
||||
|
||||
new_aux = CommentAux(id=new_comment.id,
|
||||
body=text,
|
||||
body_html=text_html,
|
||||
)
|
||||
g.db.add(new_aux)
|
||||
|
||||
notif = Notification(comment_id=new_comment.id,
|
||||
user_id=user,
|
||||
unblocksender=vid)
|
||||
g.db.add(notif)
|
||||
g.db.commit()
|
||||
|
||||
def send_admin(vid, text):
|
||||
|
||||
with CustomRenderer() as renderer: text_html = renderer.render(mistletoe.Document(text))
|
||||
|
||||
text_html = sanitize(text_html, linkgen=True)
|
||||
|
||||
new_comment = Comment(author_id=vid,
|
||||
parent_submission=None,
|
||||
level=1,
|
||||
sentto=0
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
g.db.flush()
|
||||
new_aux = CommentAux(id=new_comment.id, body=text, body_html=text_html)
|
||||
g.db.add(new_aux)
|
||||
|
||||
admins = g.db.query(User).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)
|
||||
g.db.commit()
|
|
@ -0,0 +1,309 @@
|
|||
import requests
|
||||
from os import environ
|
||||
import piexif
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
from PIL import Image as IImage
|
||||
import imagehash
|
||||
from os import remove
|
||||
import base64
|
||||
import io
|
||||
from ruqqus.classes.images import *
|
||||
from ruqqus.__main__ import db_session
|
||||
from .base36 import hex2bin
|
||||
|
||||
CF_KEY = environ.get("CLOUDFLARE_KEY").strip()
|
||||
CF_ZONE = environ.get("CLOUDFLARE_ZONE").strip()
|
||||
imgurkey = environ.get("imgurkey").strip()
|
||||
|
||||
def check_phash(db, name):
|
||||
|
||||
return db.query(BadPic).filter(
|
||||
func.levenshtein(
|
||||
BadPic.phash,
|
||||
hex2bin(str(imagehash.phash(IImage.open(name))))
|
||||
) < 10
|
||||
).first()
|
||||
|
||||
|
||||
def upload_from_url(name, url):
|
||||
|
||||
print('upload from url')
|
||||
|
||||
x = requests.get(url)
|
||||
|
||||
print('got content')
|
||||
|
||||
tempname = name.replace("/", "_")
|
||||
|
||||
with open(tempname, "wb") as file:
|
||||
for chunk in x.iter_content(1024):
|
||||
file.write(chunk)
|
||||
|
||||
if tempname.split('.')[-1] in ['jpg', 'jpeg']:
|
||||
piexif.remove(tempname)
|
||||
|
||||
upload_file(tempname,
|
||||
Key=name,
|
||||
ExtraArgs={'ACL': 'public-read',
|
||||
"ContentType": "image/png",
|
||||
"StorageClass": "INTELLIGENT_TIERING"
|
||||
}
|
||||
)
|
||||
|
||||
remove(tempname)
|
||||
|
||||
|
||||
def crop_and_resize(img, resize):
|
||||
|
||||
i = img
|
||||
|
||||
# get constraining dimension
|
||||
org_ratio = i.width / i.height
|
||||
new_ratio = resize[0] / resize[1]
|
||||
|
||||
if new_ratio > org_ratio:
|
||||
crop_height = int(i.width / new_ratio)
|
||||
box = (0, (i.height // 2) - (crop_height // 2),
|
||||
i.width, (i.height // 2) + (crop_height // 2))
|
||||
else:
|
||||
crop_width = int(new_ratio * i.height)
|
||||
box = ((i.width // 2) - (crop_width // 2), 0,
|
||||
(i.width // 2) + (crop_width // 2), i.height)
|
||||
|
||||
return i.resize(resize, box=box)
|
||||
|
||||
|
||||
def upload_file(name, file, resize=None):
|
||||
|
||||
if resize:
|
||||
tempname = name.replace("/", "_")
|
||||
|
||||
print(type(file))
|
||||
file.save(tempname)
|
||||
|
||||
if tempname.split('.')[-1] in ['jpg', 'jpeg']:
|
||||
piexif.remove(tempname)
|
||||
|
||||
i = IImage.open(tempname)
|
||||
i = crop_and_resize(i, resize)
|
||||
img = io.BytesIO()
|
||||
i.save(img, format='PNG')
|
||||
req = requests.post('https://api.imgur.com/3/upload.json', headers = {"Authorization": f"Client-ID {imgurkey}"}, data = {'image': base64.b64encode(img.getvalue())})
|
||||
try: resp = req.json()['data']
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(req)
|
||||
print(req.text)
|
||||
return
|
||||
remove(tempname)
|
||||
else:
|
||||
req = requests.post('https://api.imgur.com/3/upload.json', headers = {"Authorization": f"Client-ID {imgurkey}"}, data = {'image': base64.b64encode(file.read())})
|
||||
try: resp = req.json()['data']
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(req)
|
||||
print(req.text)
|
||||
return
|
||||
try: url = resp['link'].replace(".png", "_d.png").replace(".jpg", "_d.jpg").replace(".jpeg", "_d.jpeg") + "?maxwidth=9999"
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(req)
|
||||
print(req.text)
|
||||
return
|
||||
|
||||
new_image = Image(
|
||||
text=url,
|
||||
deletehash=resp["deletehash"],
|
||||
)
|
||||
|
||||
g.db.add(new_image)
|
||||
return(url)
|
||||
|
||||
|
||||
def upload_from_file(name, filename, resize=None):
|
||||
|
||||
tempname = name.replace("/", "_")
|
||||
|
||||
if filename.split('.')[-1] in ['jpg', 'jpeg']:
|
||||
piexif.remove(tempname)
|
||||
|
||||
i = IImage.open(tempname)
|
||||
if resize: i = crop_and_resize(i, resize)
|
||||
img = io.BytesIO()
|
||||
i.save(img, format='PNG')
|
||||
try:
|
||||
req = requests.post('https://api.imgur.com/3/upload.json', headers = {"Authorization": f"Client-ID {imgurkey}"}, data = {'image': base64.b64encode(img.getvalue())})
|
||||
resp = req.json()['data']
|
||||
remove(filename)
|
||||
url = resp['link'].replace(".png", "_d.png").replace(".jpg", "_d.jpg").replace(".jpeg", "_d.jpeg") + "?maxwidth=9999"
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(req)
|
||||
print(req.text)
|
||||
return
|
||||
|
||||
new_image = Image(
|
||||
text=url,
|
||||
deletehash=resp["deletehash"],
|
||||
)
|
||||
|
||||
g.db.add(new_image)
|
||||
return(url)
|
||||
|
||||
def delete_file(name):
|
||||
|
||||
image = g.db.query(Image).filter(Image.text == name).first()
|
||||
if image:
|
||||
requests.delete(f'https://api.imgur.com/3/image/{image.deletehash}', headers = {"Authorization": f"Client-ID {imgurkey}"})
|
||||
headers = {"Authorization": f"Bearer {CF_KEY}", "Content-Type": "application/json"}
|
||||
data = {'files': [name]}
|
||||
url = f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE}/purge_cache"
|
||||
requests.post(url, headers=headers, json=data)
|
||||
|
||||
|
||||
def check_csam(post):
|
||||
|
||||
# Relies on Cloudflare's photodna implementation
|
||||
# 451 returned by CF = positive match
|
||||
|
||||
# ignore non-link posts
|
||||
if not post.url:
|
||||
return
|
||||
|
||||
parsed_url = urlparse(post.url)
|
||||
|
||||
headers = {"User-Agent": "Drama webserver"}
|
||||
for i in range(10):
|
||||
x = requests.get(post.url, headers=headers)
|
||||
|
||||
if x.status_code in [200, 451]:
|
||||
break
|
||||
else:
|
||||
time.sleep(20)
|
||||
|
||||
db=db_session()
|
||||
|
||||
if x.status_code == 451:
|
||||
|
||||
# ban user and alts
|
||||
post.author.ban_reason="Sexualizing Minors"
|
||||
post.author.is_banned=1
|
||||
db.add(v)
|
||||
for alt in post.author.alts_threaded(db):
|
||||
alt.ban_reason="Sexualizing Minors"
|
||||
alt.is_banned=1
|
||||
db.add(alt)
|
||||
|
||||
# remove content
|
||||
post.is_banned = True
|
||||
db.add(post)
|
||||
|
||||
db.commit()
|
||||
|
||||
# nuke aws
|
||||
delete_file(parsed_url.path.lstrip('/'))
|
||||
db.close()
|
||||
return
|
||||
|
||||
#check phash
|
||||
tempname = f"test_post_{post.base36id}"
|
||||
|
||||
with open(tempname, "wb") as file:
|
||||
for chunk in x.iter_content(1024):
|
||||
file.write(chunk)
|
||||
|
||||
h=check_phash(db, tempname)
|
||||
if h:
|
||||
|
||||
now=int(time.time())
|
||||
unban=now+60*60*24*h.ban_time if h.ban_time else 0
|
||||
# ban user and alts
|
||||
post.author.ban_reason=h.ban_reason
|
||||
post.author.is_banned=1
|
||||
post.author.unban_utc = unban
|
||||
db.add(v)
|
||||
for alt in post.author.alts_threaded(db):
|
||||
alt.ban_reason=h.ban_reason
|
||||
alt.is_banned=1
|
||||
alt.unban_utc = unban
|
||||
db.add(alt)
|
||||
|
||||
# remove content
|
||||
post.is_banned = True
|
||||
db.add(post)
|
||||
|
||||
db.commit()
|
||||
|
||||
# nuke aws
|
||||
delete_file(parsed_url.path.lstrip('/'))
|
||||
|
||||
remove(tempname)
|
||||
db.close()
|
||||
|
||||
|
||||
|
||||
|
||||
def check_csam_url(url, v, delete_content_function):
|
||||
|
||||
parsed_url = urlparse(url)
|
||||
|
||||
headers = {"User-Agent": "Drama webserver"}
|
||||
for i in range(10):
|
||||
x = requests.get(url, headers=headers)
|
||||
|
||||
if x.status_code in [200, 451]:
|
||||
break
|
||||
else:
|
||||
time.sleep(20)
|
||||
|
||||
db=db_session()
|
||||
|
||||
if x.status_code == 451:
|
||||
v.ban_reason="Sexualizing Minors"
|
||||
v.is_banned=1
|
||||
db.add(v)
|
||||
for alt in v.alts_threaded(db):
|
||||
alt.ban_reason="Sexualizing Minors"
|
||||
alt.is_banned=1
|
||||
db.add(alt)
|
||||
|
||||
delete_content_function()
|
||||
|
||||
db.commit()
|
||||
db.close()
|
||||
delete_file(parsed_url.path.lstrip('/'))
|
||||
return
|
||||
|
||||
tempname=f"test_from_url_{parsed_url.path}"
|
||||
tempname=tempname.replace('/','_')
|
||||
|
||||
with open(tempname, "wb") as file:
|
||||
for chunk in x.iter_content(1024):
|
||||
file.write(chunk)
|
||||
|
||||
h=check_phash(db, tempname)
|
||||
if h:
|
||||
|
||||
now=int(time.time())
|
||||
unban=now+60*60*24*h.ban_time if h.ban_time else 0
|
||||
# ban user and alts
|
||||
v.ban_reason=h.ban_reason
|
||||
v.is_banned=1
|
||||
v.unban_utc = unban
|
||||
db.add(v)
|
||||
for alt in v.alts_threaded(db):
|
||||
alt.ban_reason=h.ban_reason
|
||||
alt.is_banned=1
|
||||
alt.unban_utc = unban
|
||||
db.add(alt)
|
||||
|
||||
delete_content_function()
|
||||
|
||||
db.commit()
|
||||
|
||||
# nuke aws
|
||||
delete_file(parsed_url.path.lstrip('/'))
|
||||
|
||||
remove(tempname)
|
||||
db.close()
|
|
@ -0,0 +1,68 @@
|
|||
from flask import abort
|
||||
|
||||
def base36encode(number, alphabet='0123456789abcdefghijklmnopqrstuvwxyz'):
|
||||
"""Converts an integer to a base36 string."""
|
||||
if not isinstance(number, int):
|
||||
raise TypeError('number must be an integer')
|
||||
|
||||
base36 = ''
|
||||
sign = ''
|
||||
|
||||
if number < 0:
|
||||
sign = '-'
|
||||
number = -number
|
||||
|
||||
if 0 <= number < len(alphabet):
|
||||
return sign + alphabet[number]
|
||||
|
||||
while number != 0:
|
||||
number, i = divmod(number, len(alphabet))
|
||||
base36 = alphabet[i] + base36
|
||||
|
||||
return sign + base36
|
||||
|
||||
|
||||
def base36decode(number):
|
||||
try:
|
||||
return int(str(number), 36)
|
||||
except ValueError:
|
||||
abort(400)
|
||||
|
||||
|
||||
def base_encode(number, base):
|
||||
|
||||
alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'[0:base]
|
||||
|
||||
output = ''
|
||||
sign = ''
|
||||
|
||||
if number < 0:
|
||||
sign = '-'
|
||||
number = -number
|
||||
|
||||
if 0 <= number < len(alphabet):
|
||||
return sign + alphabet[number]
|
||||
|
||||
while number != 0:
|
||||
number, i = divmod(number, len(alphabet))
|
||||
output = alphabet[i] + output
|
||||
|
||||
return sign + output
|
||||
|
||||
#got this one from stackoverflow
|
||||
def hex2bin(hexstr):
|
||||
value = int(hexstr, 16)
|
||||
bindigits = []
|
||||
|
||||
# Seed digit: 2**0
|
||||
digit = (value % 2)
|
||||
value //= 2
|
||||
bindigits.append(digit)
|
||||
|
||||
while value > 0:
|
||||
# Next power of 2**n
|
||||
digit = (value % 2)
|
||||
value //= 2
|
||||
bindigits.append(digit)
|
||||
|
||||
return ''.join([str(d) for d in bindigits])
|
|
@ -0,0 +1,67 @@
|
|||
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={
|
||||
"linked": "849621030926286920",
|
||||
"admin": "846509661288267776",
|
||||
"feedback": "850716291714383883",
|
||||
"newuser": "854783259229421589",
|
||||
"norep": "850971811918512208",
|
||||
}
|
||||
|
||||
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 delete_role(user, role_name):
|
||||
role_id = ROLES[role_name]
|
||||
url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}"
|
||||
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
|
||||
requests.delete(url, headers=headers)
|
||||
|
||||
@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/850266802449678366/messages"
|
||||
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
|
||||
data={"content": message}
|
||||
requests.post(url, headers=headers, data=data)
|
|
@ -0,0 +1,80 @@
|
|||
import re
|
||||
from urllib.parse import *
|
||||
import requests
|
||||
from os import environ
|
||||
from ruqqus.__main__ import app
|
||||
|
||||
youtube_regex = re.compile("^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|shorts\/|\&v=)([^#\&\?]*).*")
|
||||
|
||||
ruqqus_regex = re.compile("^.*rdrama.net/post/+\w+/(\w+)(/\w+/(\w+))?")
|
||||
|
||||
twitter_regex=re.compile("/status/(\d+)")
|
||||
|
||||
FACEBOOK_TOKEN=environ.get("FACEBOOK_TOKEN","").strip()
|
||||
|
||||
|
||||
|
||||
def youtube_embed(url):
|
||||
|
||||
try:
|
||||
yt_id = re.match(youtube_regex, url).group(2)
|
||||
except AttributeError:
|
||||
return "error"
|
||||
|
||||
if not yt_id or len(yt_id) != 11:
|
||||
return "error"
|
||||
|
||||
x = urlparse(url)
|
||||
params = parse_qs(x.query)
|
||||
t = params.get('t', params.get('start', [0]))[0]
|
||||
if t:
|
||||
return f"https://youtube.com/embed/{yt_id}?start={t}"
|
||||
else:
|
||||
return f"https://youtube.com/embed/{yt_id}"
|
||||
|
||||
|
||||
def ruqqus_embed(url):
|
||||
|
||||
matches = re.match(ruqqus_regex, url)
|
||||
|
||||
post_id = matches.group(1)
|
||||
comment_id = matches.group(3)
|
||||
|
||||
if comment_id:
|
||||
return f"https://{app.config['SERVER_NAME']}/embed/comment/{comment_id}"
|
||||
else:
|
||||
return f"https://{app.config['SERVER_NAME']}/embed/post/{post_id}"
|
||||
|
||||
|
||||
def bitchute_embed(url):
|
||||
|
||||
return url.replace("/video/", "/embed/")
|
||||
|
||||
def twitter_embed(url):
|
||||
|
||||
|
||||
oembed_url=f"https://publish.twitter.com/oembed"
|
||||
params={
|
||||
"url":url,
|
||||
"omit_script":"t"
|
||||
}
|
||||
x=requests.get(oembed_url, params=params)
|
||||
|
||||
return x.json()["html"]
|
||||
|
||||
def instagram_embed(url):
|
||||
|
||||
oembed_url=f"https://graph.facebook.com/v9.0/instagram_oembed"
|
||||
params={
|
||||
"url":url,
|
||||
"access_token":FACEBOOK_TOKEN,
|
||||
"omitscript":'true'
|
||||
}
|
||||
|
||||
headers={
|
||||
"User-Agent":"Instagram embedder for Drama"
|
||||
}
|
||||
|
||||
x=requests.get(oembed_url, params=params, headers=headers)
|
||||
|
||||
return x.json()["html"]
|
|
@ -0,0 +1,41 @@
|
|||
from bs4 import BeautifulSoup
|
||||
from flask import *
|
||||
from urllib.parse import urlparse
|
||||
from ruqqus.classes import Domain
|
||||
|
||||
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
|
||||
|
||||
# parse domain into all possible subdomains
|
||||
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)
|
||||
|
||||
# search db for domain rules that prohibit commenting
|
||||
bans = [
|
||||
x for x in g.db.query(Domain).filter_by(
|
||||
can_comment=False).filter(
|
||||
Domain.domain.in_(
|
||||
list(domain_list))).all()]
|
||||
|
||||
if bans:
|
||||
return bans
|
||||
else:
|
||||
return []
|
|
@ -0,0 +1,758 @@
|
|||
from ruqqus.classes import *
|
||||
from flask import g
|
||||
from sqlalchemy.orm import joinedload, aliased
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def get_user(username, v=None, nSession=None, graceful=False):
|
||||
|
||||
username = username.replace('\\', '')
|
||||
username = username.replace('_', '\_')
|
||||
username = username.replace('%', '')
|
||||
|
||||
if not nSession:
|
||||
nSession = g.db
|
||||
|
||||
user = nSession.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 = nSession.query(UserBlock).filter(
|
||||
or_(
|
||||
and_(
|
||||
UserBlock.user_id == v.id,
|
||||
UserBlock.target_id == user.id
|
||||
),
|
||||
and_(UserBlock.user_id == user.id,
|
||||
UserBlock.target_id == v.id
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
user._is_blocking = block and block.user_id == v.id
|
||||
user._is_blocked = block and block.target_id == v.id
|
||||
|
||||
return user
|
||||
|
||||
def get_account(base36id, v=None, nSession=None, graceful=False):
|
||||
|
||||
if not nSession:
|
||||
nSession = g.db
|
||||
|
||||
if isinstance(base36id, str): id = base36decode(base36id)
|
||||
else: id = base36id
|
||||
|
||||
user = nSession.query(User
|
||||
).filter(
|
||||
User.id == id
|
||||
).first()
|
||||
|
||||
if not user:
|
||||
if not graceful:
|
||||
abort(404)
|
||||
else:
|
||||
return None
|
||||
|
||||
if v:
|
||||
block = nSession.query(UserBlock).filter(
|
||||
or_(
|
||||
and_(
|
||||
UserBlock.user_id == v.id,
|
||||
UserBlock.target_id == user.id
|
||||
),
|
||||
and_(UserBlock.user_id == user.id,
|
||||
UserBlock.target_id == v.id
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
user._is_blocking = block and block.user_id == v.id
|
||||
user._is_blocked = block and block.target_id == v.id
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_post(pid, v=None, graceful=False, nSession=None, **kwargs):
|
||||
|
||||
if isinstance(pid, str):
|
||||
i = base36decode(pid)
|
||||
else:
|
||||
i = pid
|
||||
|
||||
nSession = nSession or kwargs.get("session")or g.db
|
||||
|
||||
if v:
|
||||
vt = nSession.query(Vote).filter_by(
|
||||
user_id=v.id, submission_id=i).subquery()
|
||||
mod = nSession.query(ModRelationship).filter_by(
|
||||
user_id=v.id, accepted=True, invite_rescinded=False).subquery()
|
||||
boardblocks = nSession.query(
|
||||
BoardBlock).filter_by(user_id=v.id).subquery()
|
||||
blocking = v.blocking.subquery()
|
||||
|
||||
items = nSession.query(
|
||||
Submission,
|
||||
vt.c.vote_type,
|
||||
aliased(ModRelationship, alias=mod),
|
||||
boardblocks.c.id,
|
||||
blocking.c.id,
|
||||
# aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Submission.author).joinedload(User.title)
|
||||
)
|
||||
|
||||
if v.admin_level>=4:
|
||||
items=items.options(joinedload(Submission.oauth_app))
|
||||
|
||||
items=items.filter(Submission.id == i
|
||||
).join(
|
||||
vt,
|
||||
vt.c.submission_id == Submission.id,
|
||||
isouter=True
|
||||
).join(
|
||||
mod,
|
||||
mod.c.board_id == Submission.board_id,
|
||||
isouter=True
|
||||
).join(
|
||||
boardblocks,
|
||||
boardblocks.c.board_id == Submission.board_id,
|
||||
isouter=True
|
||||
).join(
|
||||
blocking,
|
||||
blocking.c.target_id == Submission.author_id,
|
||||
isouter=True
|
||||
# ).join(
|
||||
# exile,
|
||||
# and_(exile.c.target_submission_id==Submission.id, exile.c.board_id==Submission.original_board_id),
|
||||
# isouter=True
|
||||
).first()
|
||||
|
||||
if not items and not graceful:
|
||||
abort(404)
|
||||
|
||||
x = items[0]
|
||||
x._voted = items[1] or 0
|
||||
x._is_guildmaster = items[2] or 0
|
||||
x._is_blocking_guild = items[3] or 0
|
||||
x._is_blocking = items[4] or 0
|
||||
# x._is_exiled_for=items[5] or 0
|
||||
|
||||
else:
|
||||
items = nSession.query(
|
||||
Submission,
|
||||
# aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Submission.author).joinedload(User.title)
|
||||
# ).join(
|
||||
# exile,
|
||||
# and_(exile.c.target_submission_id==Submission.id, exile.c.board_id==Submission.original_board_id),
|
||||
# isouter=True
|
||||
).filter(Submission.id == i).first()
|
||||
|
||||
if not items and not graceful:
|
||||
abort(404)
|
||||
|
||||
x=items
|
||||
# x._is_exiled_for=items[1] or 0
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def get_posts(pids, sort="hot", v=None):
|
||||
|
||||
if not pids:
|
||||
return []
|
||||
|
||||
pids=tuple(pids)
|
||||
|
||||
# exile=g.db.query(ModAction).options(
|
||||
# lazyload('*')
|
||||
# ).filter(
|
||||
# ModAction.kind=="exile_user",
|
||||
# ModAction.target_submission_id.in_(pids)
|
||||
# ).subquery()
|
||||
|
||||
if v:
|
||||
vt = g.db.query(Vote).filter(
|
||||
Vote.submission_id.in_(pids),
|
||||
Vote.user_id==v.id
|
||||
).subquery()
|
||||
|
||||
mod = g.db.query(ModRelationship).filter_by(
|
||||
user_id=v.id, accepted=True, invite_rescinded=False).subquery()
|
||||
|
||||
boardblocks = g.db.query(BoardBlock).filter_by(
|
||||
user_id=v.id).subquery()
|
||||
blocking = v.blocking.subquery()
|
||||
blocked = v.blocked.subquery()
|
||||
subs = g.db.query(Subscription).filter_by(user_id=v.id, is_active=True).subquery()
|
||||
|
||||
query = g.db.query(
|
||||
Submission,
|
||||
vt.c.vote_type,
|
||||
aliased(ModRelationship, alias=mod),
|
||||
boardblocks.c.id,
|
||||
blocking.c.id,
|
||||
blocked.c.id,
|
||||
subs.c.id,
|
||||
# aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Submission.author).joinedload(User.title)
|
||||
).filter(
|
||||
Submission.id.in_(pids)
|
||||
).join(
|
||||
vt, vt.c.submission_id==Submission.id, isouter=True
|
||||
).join(
|
||||
mod,
|
||||
mod.c.board_id == Submission.board_id,
|
||||
isouter=True
|
||||
).join(
|
||||
boardblocks,
|
||||
boardblocks.c.board_id == Submission.board_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
|
||||
).join(
|
||||
subs,
|
||||
subs.c.board_id == Submission.board_id,
|
||||
isouter=True
|
||||
# ).join(
|
||||
# exile,
|
||||
# and_(exile.c.target_submission_id==Submission.id, exile.c.board_id==Submission.original_board_id),
|
||||
# isouter=True
|
||||
).order_by(None).all()
|
||||
|
||||
posts=[x for x in query]
|
||||
|
||||
output = [p[0] for p in query]
|
||||
for i in range(len(output)):
|
||||
output[i]._voted = posts[i][1] or 0
|
||||
output[i]._is_guildmaster = posts[i][2] or 0
|
||||
output[i]._is_blocking_guild = posts[i][3] or 0
|
||||
output[i]._is_blocking = posts[i][4] or 0
|
||||
output[i]._is_blocked = posts[i][5] or 0
|
||||
output[i]._is_subscribed = posts[i][6] or 0
|
||||
# output[i]._is_exiled_for=posts[i][7] or 0
|
||||
else:
|
||||
query = g.db.query(
|
||||
Submission,
|
||||
# aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Submission.author).joinedload(User.title)
|
||||
).filter(Submission.id.in_(pids)
|
||||
# ).join(
|
||||
# exile,
|
||||
# and_(exile.c.target_submission_id==Submission.id, exile.c.board_id==Submission.original_board_id),
|
||||
# isouter=True
|
||||
).order_by(None).all()
|
||||
|
||||
output=[x for x in query]
|
||||
|
||||
# output=[]
|
||||
# for post in posts:
|
||||
# p=post[0]
|
||||
# p._is_exiled_for=post[1] or 0
|
||||
# output.append(p)
|
||||
|
||||
return sorted(output, key=lambda x: pids.index(x.id))
|
||||
|
||||
|
||||
def get_post_with_comments(pid, sort="top", v=None):
|
||||
|
||||
post = get_post(pid, v=v)
|
||||
|
||||
if v:
|
||||
votes = g.db.query(CommentVote).filter_by(user_id=v.id).subquery()
|
||||
|
||||
blocking = v.blocking.subquery()
|
||||
|
||||
blocked = v.blocked.subquery()
|
||||
|
||||
comms = g.db.query(
|
||||
Comment,
|
||||
votes.c.vote_type,
|
||||
blocking.c.id,
|
||||
blocked.c.id,
|
||||
).options(
|
||||
joinedload(Comment.author)
|
||||
)
|
||||
if v.admin_level >=4:
|
||||
comms=comms.options(joinedload(Comment.oauth_app))
|
||||
|
||||
comms=comms.filter(
|
||||
Comment.parent_submission == post.id
|
||||
).join(
|
||||
votes,
|
||||
votes.c.comment_id == Comment.id,
|
||||
isouter=True
|
||||
).join(
|
||||
blocking,
|
||||
blocking.c.target_id == Comment.author_id,
|
||||
isouter=True
|
||||
).join(
|
||||
blocked,
|
||||
blocked.c.user_id == Comment.author_id,
|
||||
isouter=True
|
||||
)
|
||||
|
||||
if sort == "top":
|
||||
comments = comms.order_by(Comment.score.desc()).all()
|
||||
elif sort == "bottom":
|
||||
comments = comms.order_by(Comment.score.asc()).all()
|
||||
elif sort == "new":
|
||||
comments = comms.order_by(Comment.created_utc.desc()).all()
|
||||
elif sort == "old":
|
||||
comments = comms.order_by(Comment.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
comments = sorted(comms.all(), key=lambda x: x[0].score_disputed, reverse=True)
|
||||
elif sort == "random":
|
||||
c = comms.all()
|
||||
comments = random.sample(c, k=len(c))
|
||||
else:
|
||||
abort(422)
|
||||
|
||||
output = []
|
||||
for c in comments:
|
||||
comment = c[0]
|
||||
if comment.author and comment.author.shadowbanned and not (v and v.id == comment.author_id): continue
|
||||
comment._voted = c[1] or 0
|
||||
comment._is_blocking = c[2] or 0
|
||||
comment._is_blocked = c[3] or 0
|
||||
output.append(comment)
|
||||
|
||||
post._preloaded_comments = output
|
||||
|
||||
else:
|
||||
comms = g.db.query(
|
||||
Comment
|
||||
).options(
|
||||
joinedload(Comment.author).joinedload(User.title)
|
||||
).filter(
|
||||
Comment.parent_submission == post.id
|
||||
)
|
||||
|
||||
if sort == "top":
|
||||
comments = comms.order_by(Comment.score.desc()).all()
|
||||
elif sort == "bottom":
|
||||
comments = comms.order_by(Comment.score.asc()).all()
|
||||
elif sort == "new":
|
||||
comments = comms.order_by(Comment.created_utc.desc()).all()
|
||||
elif sort == "old":
|
||||
comments = comms.order_by(Comment.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
comments = sorted(comms.all(), key=lambda x: x.score_disputed, reverse=True)
|
||||
elif sort == "random":
|
||||
c = comms.all()
|
||||
comments = random.sample(c, k=len(c))
|
||||
else:
|
||||
abort(422)
|
||||
|
||||
if random.random() < 0.1:
|
||||
for comment in comments:
|
||||
if comment.author and comment.author.shadowbanned:
|
||||
rand = random.randint(500,1400)
|
||||
vote = CommentVote(user_id=rand,
|
||||
vote_type=random.choice([-1, 1]),
|
||||
comment_id=comment.id)
|
||||
g.db.add(vote)
|
||||
try: g.db.flush()
|
||||
except: g.db.rollback()
|
||||
comment.upvotes = comment.ups
|
||||
comment.downvotes = comment.downs
|
||||
g.db.add(comment)
|
||||
|
||||
post._preloaded_comments = [x for x in comments if not (x.author and x.author.shadowbanned) or (v and v.id == x.author_id)]
|
||||
|
||||
return post
|
||||
|
||||
|
||||
def get_comment(cid, nSession=None, v=None, graceful=False, **kwargs):
|
||||
|
||||
if isinstance(cid, str):
|
||||
i = base36decode(cid)
|
||||
else:
|
||||
i = cid
|
||||
|
||||
nSession = nSession or kwargs.get('session') or g.db
|
||||
|
||||
exile = nSession.query(ModAction
|
||||
).options(
|
||||
lazyload('*')
|
||||
).filter_by(
|
||||
kind="exile_user"
|
||||
).subquery()
|
||||
|
||||
if v:
|
||||
blocking = v.blocking.subquery()
|
||||
blocked = v.blocked.subquery()
|
||||
vt = nSession.query(CommentVote).filter(
|
||||
CommentVote.user_id == v.id,
|
||||
CommentVote.comment_id == i).subquery()
|
||||
|
||||
mod=nSession.query(ModRelationship
|
||||
).filter_by(
|
||||
user_id=v.id,
|
||||
accepted=True
|
||||
).subquery()
|
||||
|
||||
|
||||
items = nSession.query(
|
||||
Comment,
|
||||
vt.c.vote_type,
|
||||
aliased(ModRelationship, alias=mod),
|
||||
aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Comment.author).joinedload(User.title)
|
||||
)
|
||||
|
||||
if v.admin_level >=4:
|
||||
items=items.options(joinedload(Comment.oauth_app))
|
||||
|
||||
items=items.filter(
|
||||
Comment.id == i
|
||||
).join(
|
||||
vt,
|
||||
vt.c.comment_id == Comment.id,
|
||||
isouter=True
|
||||
).join(
|
||||
Comment.post,
|
||||
isouter=True
|
||||
).join(
|
||||
mod,
|
||||
mod.c.board_id==Submission.board_id,
|
||||
isouter=True
|
||||
).join(
|
||||
exile,
|
||||
and_(exile.c.target_comment_id==Comment.id, exile.c.board_id==Comment.original_board_id),
|
||||
isouter=True
|
||||
).first()
|
||||
|
||||
if not items and not graceful:
|
||||
abort(404)
|
||||
|
||||
x = items[0]
|
||||
x._voted = items[1] or 0
|
||||
x._is_guildmaster=items[2] or 0
|
||||
x._is_exiled_for=items[3] or 0
|
||||
|
||||
block = nSession.query(UserBlock).filter(
|
||||
or_(
|
||||
and_(
|
||||
UserBlock.user_id == v.id,
|
||||
UserBlock.target_id == x.author_id
|
||||
),
|
||||
and_(UserBlock.user_id == x.author_id,
|
||||
UserBlock.target_id == v.id
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
x._is_blocking = block and block.user_id == v.id
|
||||
x._is_blocked = block and block.target_id == v.id
|
||||
|
||||
else:
|
||||
q = nSession.query(
|
||||
Comment,
|
||||
aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Comment.author).joinedload(User.title)
|
||||
).join(
|
||||
exile,
|
||||
and_(exile.c.target_comment_id==Comment.id, exile.c.board_id==Comment.original_board_id),
|
||||
isouter=True
|
||||
).filter(Comment.id == i).first()
|
||||
|
||||
if not q and not graceful:
|
||||
abort(404)
|
||||
|
||||
x=q[0]
|
||||
x._is_exiled_for=q[1]
|
||||
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def get_comments(cids, v=None, nSession=None, sort="new",
|
||||
load_parent=False, **kwargs):
|
||||
|
||||
if not cids:
|
||||
return []
|
||||
|
||||
cids=tuple(cids)
|
||||
|
||||
nSession = nSession or kwargs.get('session') or g.db
|
||||
|
||||
exile=nSession.query(ModAction
|
||||
).options(
|
||||
lazyload('*')
|
||||
).filter(
|
||||
ModAction.kind=="exile_user",
|
||||
ModAction.target_comment_id.in_(cids)
|
||||
).distinct(ModAction.target_comment_id).subquery()
|
||||
|
||||
if v:
|
||||
vt = nSession.query(CommentVote).filter(
|
||||
CommentVote.comment_id.in_(cids),
|
||||
CommentVote.user_id==v.id
|
||||
).subquery()
|
||||
|
||||
mod=nSession.query(ModRelationship
|
||||
).filter_by(
|
||||
user_id=v.id,
|
||||
accepted=True
|
||||
).subquery()
|
||||
|
||||
|
||||
|
||||
query = nSession.query(
|
||||
Comment,
|
||||
aliased(CommentVote, alias=vt),
|
||||
aliased(ModRelationship, alias=mod),
|
||||
aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Comment.author).joinedload(User.title)
|
||||
)
|
||||
|
||||
if v.admin_level >=4:
|
||||
query=query.options(joinedload(Comment.oauth_app))
|
||||
|
||||
if load_parent:
|
||||
query = query.options(
|
||||
joinedload(
|
||||
Comment.parent_comment
|
||||
).joinedload(
|
||||
Comment.author
|
||||
).joinedload(
|
||||
User.title
|
||||
)
|
||||
)
|
||||
|
||||
query = query.join(
|
||||
vt,
|
||||
vt.c.comment_id == Comment.id,
|
||||
isouter=True
|
||||
).join(
|
||||
Comment.post,
|
||||
isouter=True
|
||||
).join(
|
||||
mod,
|
||||
mod.c.board_id==Submission.board_id,
|
||||
isouter=True
|
||||
).join(
|
||||
exile,
|
||||
and_(exile.c.target_comment_id==Comment.id, exile.c.board_id==Comment.original_board_id),
|
||||
isouter=True
|
||||
).filter(
|
||||
Comment.id.in_(cids)
|
||||
)
|
||||
|
||||
|
||||
|
||||
query=query.options(
|
||||
# contains_eager(Comment.post).contains_eager(Submission.board)
|
||||
).order_by(None).all()
|
||||
|
||||
comments=[x for x in query]
|
||||
|
||||
output = [x[0] for x in comments]
|
||||
for i in range(len(output)):
|
||||
output[i]._voted = comments[i][1].vote_type if comments[i][1] else 0
|
||||
output[i]._is_guildmaster = comments[i][2]
|
||||
output[i]._is_exiled_for = comments[i][3]
|
||||
|
||||
|
||||
|
||||
else:
|
||||
query = nSession.query(
|
||||
Comment,
|
||||
aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Comment.author).joinedload(User.title),
|
||||
joinedload(Comment.post).joinedload(Submission.board)
|
||||
).filter(
|
||||
Comment.id.in_(cids)
|
||||
).join(
|
||||
exile,
|
||||
and_(exile.c.target_comment_id==Comment.id, exile.c.board_id==Comment.original_board_id),
|
||||
isouter=True
|
||||
).order_by(None).all()
|
||||
|
||||
comments=[x for x in query]
|
||||
|
||||
output=[x[0] for x in comments]
|
||||
for i in range(len(output)):
|
||||
output[i]._is_exiled_for=comments[i][1]
|
||||
|
||||
|
||||
output = sorted(output, key=lambda x: cids.index(x.id))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_board(bid, graceful=False):
|
||||
|
||||
return g.db.query(Board).first()
|
||||
|
||||
|
||||
def get_guild(name, graceful=False):
|
||||
|
||||
return g.db.query(Board).first()
|
||||
|
||||
|
||||
def get_domain(s):
|
||||
|
||||
# parse domain into all possible subdomains
|
||||
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(Domain).filter(
|
||||
Domain.domain.in_(domain_list)).all()]
|
||||
|
||||
if not doms:
|
||||
return None
|
||||
|
||||
# return the most specific domain - the one with the longest domain
|
||||
# property
|
||||
doms = sorted(doms, key=lambda x: len(x.domain), reverse=True)
|
||||
|
||||
return doms[0]
|
||||
|
||||
|
||||
def get_title(x):
|
||||
|
||||
title = g.db.query(Title).filter_by(id=x).first()
|
||||
|
||||
if not title:
|
||||
abort(400)
|
||||
|
||||
else:
|
||||
return title
|
||||
|
||||
|
||||
def get_mod(uid, bid):
|
||||
|
||||
mod = g.db.query(ModRelationship).filter_by(board_id=bid,
|
||||
user_id=uid,
|
||||
accepted=True,
|
||||
invite_rescinded=False).first()
|
||||
|
||||
return mod
|
||||
|
||||
|
||||
def get_application(client_id, graceful=False):
|
||||
|
||||
application = g.db.query(OauthApp).filter_by(client_id=client_id).first()
|
||||
if not application and not graceful:
|
||||
abort(404)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
def get_from_permalink(link, v=None):
|
||||
|
||||
if "@" in link:
|
||||
|
||||
name = re.search("/@(\w+)", link)
|
||||
if name:
|
||||
name=name.match(1)
|
||||
return get_user(name)
|
||||
|
||||
if "+" in link:
|
||||
|
||||
x = re.search("/\+(\w+)$", link)
|
||||
if x:
|
||||
name=x.match(1)
|
||||
return get_guild(name)
|
||||
|
||||
ids = re.search("://[^/]+\w+/post/(\w+)/[^/]+(/(\w+))?", link)
|
||||
|
||||
try:
|
||||
post_id = ids.group(1)
|
||||
comment_id = ids.group(3)
|
||||
except: abort(400)
|
||||
|
||||
if comment_id:
|
||||
return get_comment(int(comment_id), v=v)
|
||||
|
||||
else:
|
||||
return get_post(int(post_id), v=v)
|
||||
|
||||
|
||||
def get_from_fullname(fullname, v=None, graceful=False):
|
||||
|
||||
parts = fullname.split('_')
|
||||
|
||||
if len(parts) != 2:
|
||||
if graceful:
|
||||
return None
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
kind = parts[0]
|
||||
b36 = parts[1]
|
||||
|
||||
if kind == 't1':
|
||||
return get_account(b36, v=v, graceful=graceful)
|
||||
elif kind == 't2':
|
||||
return get_post(b36, v=v, graceful=graceful)
|
||||
elif kind == 't3':
|
||||
return get_comment(b36, v=v, graceful=graceful)
|
||||
elif kind == 't4':
|
||||
return get_board(b36, graceful=graceful)
|
||||
|
||||
def get_txn(paypal_id):
|
||||
|
||||
txn= g.db.query(PayPalTxn).filter_by(paypal_id=paypal_id).first()
|
||||
|
||||
if not txn:
|
||||
abort(404)
|
||||
|
||||
return txn
|
||||
|
||||
def get_txid(txid):
|
||||
|
||||
txn= g.db.query(PayPalTxn).filter_by(id=base36decode(txid)).first()
|
||||
|
||||
if not txn:
|
||||
abort(404)
|
||||
elif txn.status==1:
|
||||
abort(404)
|
||||
|
||||
return txn
|
||||
|
||||
|
||||
def get_promocode(code):
|
||||
|
||||
code = code.replace('\\', '')
|
||||
code = code.replace("_", "\_")
|
||||
|
||||
code = g.db.query(PromoCode).filter(PromoCode.code.ilike(code)).first()
|
||||
|
||||
return code
|
|
@ -0,0 +1,87 @@
|
|||
from os import environ, path
|
||||
import calendar
|
||||
from .get import *
|
||||
from ruqqus.__main__ import app, cache
|
||||
|
||||
|
||||
@app.template_filter("total_users")
|
||||
@cache.memoize(timeout=60)
|
||||
def total_users(x):
|
||||
|
||||
return db.query(User).filter_by(is_banned=0).count()
|
||||
|
||||
|
||||
@app.template_filter("source_code")
|
||||
@cache.memoize(timeout=60 * 60 * 24)
|
||||
def source_code(file_name):
|
||||
|
||||
return open(path.expanduser('~') + '/ruqqus/' +
|
||||
file_name, mode="r+").read()
|
||||
|
||||
|
||||
@app.template_filter("full_link")
|
||||
def full_link(url):
|
||||
|
||||
return f"https://{app.config['SERVER_NAME']}{url}"
|
||||
|
||||
|
||||
@app.template_filter("env")
|
||||
def env_var_filter(x):
|
||||
|
||||
x = environ.get(x, 1)
|
||||
|
||||
try:
|
||||
return int(x)
|
||||
except BaseException:
|
||||
try:
|
||||
return float(x)
|
||||
except BaseException:
|
||||
return x
|
||||
|
||||
|
||||
@app.template_filter("js_str_escape")
|
||||
def js_str_escape(s):
|
||||
|
||||
s = s.replace("'", r"\'")
|
||||
|
||||
return s
|
||||
|
||||
|
||||
@app.template_filter("is_mod")
|
||||
@cache.memoize(60)
|
||||
def jinja_is_mod(uid, bid):
|
||||
|
||||
return bool(get_mod(uid, bid))
|
||||
|
||||
@app.template_filter("coin_goal")
|
||||
@cache.cached(timeout=600, key_prefix="premium_coin_goal")
|
||||
def coin_goal(x):
|
||||
|
||||
now = time.gmtime()
|
||||
midnight_month_start = time.struct_time((now.tm_year,
|
||||
now.tm_mon,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
now.tm_wday,
|
||||
now.tm_yday,
|
||||
0)
|
||||
)
|
||||
cutoff = calendar.timegm(midnight_month_start)
|
||||
|
||||
coins=g.db.query(func.sum(PayPalTxn.coin_count)).filter(
|
||||
PayPalTxn.created_utc>cutoff,
|
||||
PayPalTxn.status==3).all()[0][0] or 0
|
||||
|
||||
|
||||
return int(100*coins/1000)
|
||||
|
||||
|
||||
@app.template_filter("app_config")
|
||||
def app_config(x):
|
||||
return app.config.get(x)
|
||||
|
||||
# @app.template_filter("general_chat_count")
|
||||
# def general_chat_count(x):
|
||||
# return get_guild("general").chat_count
|
|
@ -0,0 +1,20 @@
|
|||
# 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
|
|
@ -0,0 +1,109 @@
|
|||
from .get import *
|
||||
|
||||
from mistletoe.span_token import SpanToken
|
||||
from mistletoe.html_renderer import HTMLRenderer
|
||||
import re
|
||||
|
||||
from flask import g
|
||||
|
||||
#preprocess re
|
||||
|
||||
enter_re=re.compile("(\n\r?\w+){3,}")
|
||||
|
||||
|
||||
|
||||
# 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 OpMention(SpanToken):
|
||||
|
||||
# pattern = re.compile("(^|\W|\s)@([Oo][Pp])\b")
|
||||
# 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.permalink}" class="d-inline-block mention-user" data-original-name="{user.original_username}"><img src="/uid/{user.base36id}/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}" 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}" class="d-inline-block">u/{target}</a>'
|
|
@ -0,0 +1,118 @@
|
|||
import flask_caching
|
||||
from flask_caching import backends
|
||||
import hashlib
|
||||
|
||||
|
||||
class CustomCache(backends.rediscache.RedisCache):
|
||||
|
||||
def __init__(self, app, config, *args):
|
||||
|
||||
self.caches = [
|
||||
flask_caching.Cache(
|
||||
app,
|
||||
config={"CACHE_TYPE": 'redis',
|
||||
"CACHE_REDIS_URL": url}
|
||||
) for url in app.config['redis_urls']
|
||||
]
|
||||
|
||||
def key_to_cache(self, key):
|
||||
|
||||
return self.caches[self.key_to_cache_number(key)]
|
||||
|
||||
def key_to_cache_number(self, key):
|
||||
|
||||
return int(hashlib.md5(bytes(key, 'utf-8')).hexdigest()
|
||||
[-5:], 16) % len(self.caches)
|
||||
|
||||
def sharded_keys(self, keys, return_index=False):
|
||||
|
||||
sharded_keys = {i: [] for i in range(len(self.caches))}
|
||||
idx = {}
|
||||
for key in keys:
|
||||
cache = self.key_to_cache_number(key)
|
||||
sharded_keys[cache].append(key)
|
||||
idx[key] = [cache, len(sharded_keys[cache]) - 1]
|
||||
|
||||
if not return_index:
|
||||
return sharded_keys
|
||||
else:
|
||||
return sharded_keys, idx
|
||||
|
||||
def get(self, key):
|
||||
|
||||
cache = self.key_to_cache(key)
|
||||
|
||||
return cache.get(key)
|
||||
|
||||
def get_many(self, *keys):
|
||||
|
||||
sharded_keys, idx = self.sharded_keys(keys, return_index=True)
|
||||
|
||||
objects = {i: self.caches[i].get_many(
|
||||
*sharded_keys[i]) for i in range(len(self.caches))}
|
||||
|
||||
output = [objects[idx[key][0]][idx[key][1]] for key in keys]
|
||||
|
||||
return output
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
cache = self.key_to_cache(key)
|
||||
return cache.set(key, value, timeout=timeout)
|
||||
|
||||
def add(self, key, value, timeout=None):
|
||||
cache = self.key_to_cache(key)
|
||||
return cache.add(key, value, timeout=timeout)
|
||||
|
||||
def set_many(self, mapping, timeout=None):
|
||||
|
||||
caches = {i: {} for i in range(len(self.caches))}
|
||||
for key in mapping:
|
||||
caches[self.key_to_cache_number(key)][key] = mapping[key]
|
||||
|
||||
for i in caches:
|
||||
self.caches[i].set_many(caches[i], timeout=timeout)
|
||||
|
||||
def delete(self, key):
|
||||
|
||||
cache = self.key_to_cache(key)
|
||||
return cache.delete(key)
|
||||
|
||||
def delete_many(self, *keys):
|
||||
|
||||
if not keys:
|
||||
return
|
||||
|
||||
sharded_keys = self.sharded_keys(keys)
|
||||
|
||||
for i in sharded_keys:
|
||||
self.caches[i].delete_many(*sharded_keys[i])
|
||||
|
||||
return True
|
||||
|
||||
def has(self, key):
|
||||
cache = self.key_to_cache(key)
|
||||
return cache.has(key)
|
||||
|
||||
def clear(self):
|
||||
|
||||
return any([i.clear() for i in self.caches])
|
||||
|
||||
def inc(self, key, delta=1):
|
||||
cache = self.key_to_cache(key)
|
||||
cache.inc(key, delta=delta)
|
||||
|
||||
def dec(self, key, delta=1):
|
||||
cache = self.key_to_cache(key)
|
||||
cache.dec(key, delta=delta)
|
||||
|
||||
def unlink(self, *keys):
|
||||
|
||||
if not keys:
|
||||
return
|
||||
|
||||
sharded_keys = self.sharded_keys(keys)
|
||||
|
||||
for i in sharded_keys:
|
||||
self.caches[i].unlink(*sharded_keys[i])
|
||||
|
||||
return True
|
|
@ -0,0 +1,220 @@
|
|||
import bleach
|
||||
from bs4 import BeautifulSoup
|
||||
from bleach.linkifier import LinkifyFilter
|
||||
from urllib.parse import ParseResult, urlunparse
|
||||
from functools import partial
|
||||
from .get import *
|
||||
import os.path
|
||||
|
||||
_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',
|
||||
]
|
||||
|
||||
_allowed_attributes = {
|
||||
'a': ['href', 'title', "rel", "data-original-name"],
|
||||
'i': [],
|
||||
'img': ['src', 'class'],
|
||||
'span': ['style']
|
||||
}
|
||||
|
||||
_allowed_protocols = [
|
||||
'http',
|
||||
'https'
|
||||
]
|
||||
|
||||
_allowed_styles =[
|
||||
'color',
|
||||
'font-weight'
|
||||
]
|
||||
|
||||
# filter to make all links show domain on hover
|
||||
|
||||
|
||||
def a_modify(attrs, new=False):
|
||||
|
||||
raw_url=attrs.get((None, "href"), None)
|
||||
if raw_url:
|
||||
parsed_url = urlparse(raw_url)
|
||||
|
||||
domain = parsed_url.netloc
|
||||
attrs[(None, "target")] = "_blank"
|
||||
if domain and not domain.endswith("rdrama.net"):
|
||||
attrs[(None, "rel")] = "nofollow noopener"
|
||||
|
||||
# Force https for all external links in comments
|
||||
# (Ruqqus already forces its own https)
|
||||
new_url = ParseResult(scheme="https",
|
||||
netloc=parsed_url.netloc,
|
||||
path=parsed_url.path,
|
||||
params=parsed_url.params,
|
||||
query=parsed_url.query,
|
||||
fragment=parsed_url.fragment)
|
||||
|
||||
attrs[(None, "href")] = urlunparse(new_url)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
_clean_wo_links = bleach.Cleaner(tags=_allowed_tags,
|
||||
attributes=_allowed_attributes,
|
||||
protocols=_allowed_protocols,
|
||||
)
|
||||
|
||||
_clean_w_links = bleach.Cleaner(tags=_allowed_tags,
|
||||
attributes=_allowed_attributes,
|
||||
protocols=_allowed_protocols,
|
||||
styles=_allowed_styles,
|
||||
filters=[partial(LinkifyFilter,
|
||||
skip_tags=["pre"],
|
||||
parse_email=False,
|
||||
callbacks=[a_modify]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def sanitize(text, linkgen=False, flair=False):
|
||||
|
||||
text = text.replace("\ufeff", "").replace("m.youtube.com", "youtube.com")
|
||||
|
||||
if linkgen:
|
||||
sanitized = _clean_w_links.clean(text)
|
||||
|
||||
#soupify
|
||||
soup = BeautifulSoup(sanitized, features="html.parser")
|
||||
|
||||
#img elements - embed
|
||||
for tag in soup.find_all("img"):
|
||||
|
||||
url = tag.get("src", "")
|
||||
if not url: continue
|
||||
netloc = urlparse(url).netloc
|
||||
|
||||
domain = get_domain(netloc)
|
||||
if not(netloc) or (domain and domain.show_thumbnail):
|
||||
|
||||
if "profile-pic-20" not in tag.get("class", ""):
|
||||
#print(tag.get('class'))
|
||||
# set classes and wrap in link
|
||||
|
||||
tag["rel"] = "nofollow"
|
||||
tag["style"] = "max-height: 100px; max-width: 100%;"
|
||||
tag["class"] = "in-comment-image rounded-sm my-2"
|
||||
|
||||
link = soup.new_tag("a")
|
||||
link["href"] = tag["src"]
|
||||
link["rel"] = "nofollow noopener"
|
||||
link["target"] = "_blank"
|
||||
|
||||
link["onclick"] = f"expandDesktopImage('{tag['src']}');"
|
||||
link["data-toggle"] = "modal"
|
||||
link["data-target"] = "#expandImageModal"
|
||||
|
||||
tag.wrap(link)
|
||||
else:
|
||||
# non-whitelisted images get replaced with links
|
||||
new_tag = soup.new_tag("a")
|
||||
new_tag.string = tag["src"]
|
||||
new_tag["href"] = tag["src"]
|
||||
new_tag["rel"] = "nofollow noopener"
|
||||
tag.replace_with(new_tag)
|
||||
|
||||
#disguised link preventer
|
||||
for tag in soup.find_all("a"):
|
||||
|
||||
if re.match("https?://\S+", str(tag.string)):
|
||||
try:
|
||||
tag.string = tag["href"]
|
||||
except:
|
||||
tag.string = ""
|
||||
|
||||
#clean up tags in code
|
||||
for tag in soup.find_all("code"):
|
||||
tag.contents=[x.string for x in tag.contents if x.string]
|
||||
|
||||
#whatever else happens with images, there are only two sets of classes allowed
|
||||
for tag in soup.find_all("img"):
|
||||
if 'profile-pic-20' not in tag.attrs.get("class",""):
|
||||
tag.attrs['class']="in-comment-image rounded-sm my-2"
|
||||
|
||||
#table format
|
||||
for tag in soup.find_all("table"):
|
||||
tag.attrs['class']="table table-striped"
|
||||
|
||||
for tag in soup.find_all("thead"):
|
||||
tag.attrs['class']="bg-primary text-white"
|
||||
|
||||
|
||||
sanitized = str(soup)
|
||||
|
||||
else:
|
||||
sanitized = _clean_wo_links.clean(text)
|
||||
|
||||
start = '<s>'
|
||||
end = '</s>'
|
||||
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>')
|
||||
|
||||
if flair: emojisize = 20
|
||||
else: emojisize = 30
|
||||
for i in re.finditer(':(.{1,30}?):', sanitized):
|
||||
if os.path.isfile(f'/d/ruqqus/assets/images/emojis/{i.group(1)}.gif'):
|
||||
sanitized = sanitized.replace(f':{i.group(1)}:', f'<img data-toggle="tooltip" title="{i.group(1)}" delay="0" height={emojisize} src="/assets/images/emojis/{i.group(1)}.gif"<span>')
|
||||
|
||||
sanitized = sanitized.replace("https://www.", "https://").replace("https://youtu.be/", "https://youtube.com/embed/").replace("https://music.youtube.com/watch?v=", "https://youtube.com/embed/").replace("/watch?v=", "/embed/").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/embed/")
|
||||
|
||||
for i in re.finditer('<a href="(https://(streamable|youtube).com/(e|embed)/.*?)"', sanitized):
|
||||
url = i.group(1)
|
||||
replacing = f'<a href="{url}" rel="nofollow noopener" target="_blank">{url}</a>'
|
||||
htmlsource = f'<div style="padding-top:5px; padding-bottom: 10px;"><iframe frameborder="0" src="{url}?controls=0"></iframe></div>'
|
||||
sanitized = sanitized.replace(replacing, htmlsource)
|
||||
|
||||
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" target="_blank">{url}</a>'
|
||||
htmlsource = f'<iframe src="{url}" width="100%" height="80" frameBorder="0" allowtransparency="true" allow="encrypted-media"></iframe>'
|
||||
sanitized = sanitized.replace(replacing, htmlsource)
|
||||
|
||||
sanitized = sanitized.replace("https://mobile.twitter.com", "https://twitter.com")
|
||||
|
||||
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/")
|
||||
|
||||
for i in re.finditer('(/comments/.*?)"', sanitized):
|
||||
url = i.group(1)
|
||||
if not "sort=" in url: sanitized = sanitized.replace(url, f"{url}?sort=controversial")
|
||||
|
||||
return sanitized
|
|
@ -0,0 +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)
|
|
@ -0,0 +1,35 @@
|
|||
from flask import *
|
||||
import time
|
||||
from .security import *
|
||||
|
||||
|
||||
def session_over18(board):
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
return session.get('over_18', {}).get(board.base36id, 0) >= now
|
||||
|
||||
|
||||
def session_isnsfl(board):
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
return session.get('show_nsfl', {}).get(board.base36id, 0) >= now
|
||||
|
||||
|
||||
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)
|
|
@ -0,0 +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, **kw):
|
||||
self._column_args = columns
|
||||
self.list = args
|
||||
self.alias_name = self.name = kw.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, **kw):
|
||||
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
|
|
@ -0,0 +1,20 @@
|
|||
from .get import *
|
||||
|
||||
def expand_url(post_url, fragment_url):
|
||||
|
||||
# convert src into full url
|
||||
if fragment_url.startswith("https://"):
|
||||
return fragment_url
|
||||
elif fragment_url.startswith("http://"):
|
||||
return f"https://{fragment_url.split('http://')[1]}"
|
||||
elif fragment_url.startswith('//'):
|
||||
return f"https:{fragment_url}"
|
||||
elif fragment_url.startswith('/'):
|
||||
parsed_url = urlparse(post_url)
|
||||
return f"https://{parsed_url.netloc}{fragment_url}"
|
||||
else:
|
||||
return f"{post_url}{'/' if not post_url.endswith('/') else ''}{fragment_url}"
|
||||
|
||||
def thumbnail_thread(pid, debug=False):
|
||||
|
||||
return True, "Success"
|
|
@ -0,0 +1,557 @@
|
|||
from werkzeug.wrappers.response import Response as RespObj
|
||||
from .get import *
|
||||
from .alerts import send_notification
|
||||
from ruqqus.__main__ import Base, app, db_session
|
||||
|
||||
|
||||
def get_logged_in_user(db=None):
|
||||
|
||||
if not db:
|
||||
db=g.db
|
||||
|
||||
if request.path.startswith("/api/v1"):
|
||||
|
||||
token = request.headers.get("Authorization")
|
||||
if not token:
|
||||
|
||||
#let admins hit api/v1 from browser
|
||||
# x=request.session.get('user_id')
|
||||
# nonce=request.session.get('login_nonce')
|
||||
# if not x or not nonce:
|
||||
# return None, None
|
||||
# user=g.db.query(User).filter_by(id=x).first()
|
||||
# if not user:
|
||||
# return None, None
|
||||
# if user.admin_level >=3 and nonce>=user.login_nonce:
|
||||
# return user, None
|
||||
return None, None
|
||||
|
||||
token = token.split()
|
||||
if len(token) < 2:
|
||||
return None, None
|
||||
|
||||
token = token[1]
|
||||
if not token:
|
||||
return None, None
|
||||
|
||||
client = db.query(ClientAuth).filter(
|
||||
ClientAuth.access_token == token).first()
|
||||
#ClientAuth.access_token_expire_utc > int(time.time()
|
||||
|
||||
x = (client.user, client) if client else (None, None)
|
||||
|
||||
|
||||
elif "user_id" in session:
|
||||
|
||||
uid = session.get("user_id")
|
||||
nonce = session.get("login_nonce", 0)
|
||||
if not uid:
|
||||
x= (None, None)
|
||||
v = db.query(User).options(
|
||||
joinedload(User.moderates).joinedload(ModRelationship.board), #joinedload(Board.reports),
|
||||
).filter_by(
|
||||
id=uid,
|
||||
is_deleted=False
|
||||
).first()
|
||||
|
||||
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)
|
||||
|
||||
else:
|
||||
x=(None, None)
|
||||
|
||||
if x[0]:
|
||||
x[0].client=x[1]
|
||||
|
||||
return x
|
||||
|
||||
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="ban evasion")
|
||||
send_notification(1046, v, "Your Drama account has been permanently suspended for the following reason:\n\n> ban evasion")
|
||||
|
||||
for post in g.db.query(Submission).filter_by(author_id=v.id).all():
|
||||
if post.is_banned:
|
||||
continue
|
||||
|
||||
post.is_banned=True
|
||||
post.ban_reason="ban evasion"
|
||||
g.db.add(post)
|
||||
|
||||
ma=ModAction(
|
||||
kind="ban_post",
|
||||
user_id=2317,
|
||||
target_submission_id=post.id,
|
||||
board_id=post.board_id,
|
||||
note="ban evasion"
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
g.db.commit()
|
||||
|
||||
for comment in g.db.query(Comment).filter_by(author_id=v.id).all():
|
||||
if comment.is_banned:
|
||||
continue
|
||||
|
||||
comment.is_banned=True
|
||||
comment.ban_reason="ban evasion"
|
||||
g.db.add(comment)
|
||||
|
||||
ma=ModAction(
|
||||
kind="ban_comment",
|
||||
user_id=2317,
|
||||
target_comment_id=comment.id,
|
||||
board_id=comment.post.board_id,
|
||||
note="ban evasion"
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
g.db.commit()
|
||||
try: abort(403)
|
||||
except Exception as e: print(e)
|
||||
|
||||
else:
|
||||
v.ban_evade +=1
|
||||
g.db.add(v)
|
||||
g.db.commit()
|
||||
|
||||
|
||||
|
||||
|
||||
# Wrappers
|
||||
def auth_desired(f):
|
||||
# decorator for any view that changes if user is logged in (most pages)
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
v, c = get_logged_in_user()
|
||||
|
||||
if c:
|
||||
kwargs["c"] = c
|
||||
|
||||
check_ban_evade(v)
|
||||
|
||||
resp = make_response(f(*args, v=v, **kwargs))
|
||||
if v:
|
||||
resp.headers.add("Cache-Control", "private")
|
||||
resp.headers.add(
|
||||
"Access-Control-Allow-Origin",
|
||||
app.config["SERVER_NAME"])
|
||||
else:
|
||||
resp.headers.add("Cache-Control", "public")
|
||||
return resp
|
||||
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
|
||||
def auth_required(f):
|
||||
# decorator for any view that requires login (ex. settings)
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
v, c = get_logged_in_user()
|
||||
|
||||
#print(v, c)
|
||||
|
||||
if not v:
|
||||
abort(401)
|
||||
|
||||
check_ban_evade(v)
|
||||
|
||||
if c:
|
||||
kwargs["c"] = c
|
||||
|
||||
g.v = v
|
||||
|
||||
# an ugly hack to make api work
|
||||
resp = make_response(f(*args, v=v, **kwargs))
|
||||
|
||||
resp.headers.add("Cache-Control", "private")
|
||||
resp.headers.add(
|
||||
"Access-Control-Allow-Origin",
|
||||
app.config["SERVER_NAME"])
|
||||
return resp
|
||||
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
|
||||
def is_not_banned(f):
|
||||
# decorator that enforces lack of ban
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
v, c = get_logged_in_user()
|
||||
|
||||
#print(v, c)
|
||||
|
||||
if not v:
|
||||
abort(401)
|
||||
|
||||
check_ban_evade(v)
|
||||
|
||||
if v.is_suspended:
|
||||
abort(403)
|
||||
|
||||
if c:
|
||||
kwargs["c"] = c
|
||||
|
||||
g.v = v
|
||||
|
||||
resp = make_response(f(*args, v=v, **kwargs))
|
||||
resp.headers.add("Cache-Control", "private")
|
||||
resp.headers.add(
|
||||
"Access-Control-Allow-Origin",
|
||||
app.config["SERVER_NAME"])
|
||||
return resp
|
||||
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
# Require tos agreement
|
||||
|
||||
|
||||
def tos_agreed(f):
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
v = kwargs['v']
|
||||
|
||||
cutoff = int(environ.get("tos_cutoff", 0))
|
||||
|
||||
if v.tos_agreed_utc > cutoff:
|
||||
return f(*args, **kwargs)
|
||||
else:
|
||||
return redirect("/terms#agreebox")
|
||||
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
def premium_required(f):
|
||||
|
||||
#decorator that enforces valid premium status
|
||||
#use under auth_required or is_not_banned
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
v=kwargs["v"]
|
||||
|
||||
if not v.has_premium:
|
||||
abort(403)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
wrapper.__name__=f.__name__
|
||||
return wrapper
|
||||
|
||||
|
||||
def no_negative_balance(s):
|
||||
|
||||
def wrapper_maker(f):
|
||||
|
||||
#decorator that enforces valid premium status
|
||||
#use under auth_required or is_not_banned
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
v=kwargs["v"]
|
||||
|
||||
if v.negative_balance_cents:
|
||||
if s=="toast":
|
||||
return jsonify({"error":"You can't do that while your account balance is negative. Visit your account settings to bring your balance up to zero."}), 402
|
||||
elif s=="html":
|
||||
raise(PaymentRequired)
|
||||
else:
|
||||
raise(PaymentRequired)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
wrapper.__name__=f.__name__
|
||||
return wrapper
|
||||
|
||||
return wrapper_maker
|
||||
|
||||
def is_guildmaster(*perms):
|
||||
# decorator that enforces guildmaster status and verifies permissions
|
||||
# use under auth_required
|
||||
def wrapper_maker(f):
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
v = kwargs["v"]
|
||||
boardname = kwargs.get("boardname")
|
||||
board_id = kwargs.get("bid")
|
||||
bid=request.values.get("bid", request.values.get("board_id"))
|
||||
|
||||
if boardname:
|
||||
board = get_guild(boardname)
|
||||
elif board_id:
|
||||
board = get_board(board_id)
|
||||
elif bid:
|
||||
board = get_board(bid)
|
||||
else:
|
||||
return jsonify({"error": f"no guild specified"}), 400
|
||||
|
||||
m=board.has_mod(v)
|
||||
if not m:
|
||||
return jsonify({"error":f"You aren't a guildmaster of +{board.name}"}), 403
|
||||
|
||||
if perms:
|
||||
for perm in perms:
|
||||
if not m.__dict__.get(f"perm_{perm}") and not m.perm_full:
|
||||
return jsonify({"error":f"Permission `{perm}` required"}), 403
|
||||
|
||||
|
||||
if v.is_banned and not v.unban_utc:
|
||||
abort(403)
|
||||
|
||||
return f(*args, board=board, **kwargs)
|
||||
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
return wrapper_maker
|
||||
|
||||
|
||||
# this wrapper takes args and is a bit more complicated
|
||||
def admin_level_required(x):
|
||||
|
||||
def wrapper_maker(f):
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
v, c = get_logged_in_user()
|
||||
|
||||
if c:
|
||||
return jsonify({"error": "No admin api access"}), 403
|
||||
|
||||
if not v:
|
||||
abort(401)
|
||||
|
||||
if v.is_banned:
|
||||
abort(403)
|
||||
|
||||
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)
|
||||
|
||||
resp.headers.add("Cache-Control", "private")
|
||||
resp.headers.add(
|
||||
"Access-Control-Allow-Origin",
|
||||
app.config["SERVER_NAME"])
|
||||
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.path.startswith("/api/v1"):
|
||||
|
||||
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
|
||||
|
||||
|
||||
def no_cors(f):
|
||||
"""
|
||||
Decorator prevents content being iframe'd
|
||||
"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
origin = request.headers.get("Origin", None)
|
||||
|
||||
if origin and origin != "https://" + app.config["SERVER_NAME"] and app.config["FORCE_HTTPS"]==1:
|
||||
|
||||
return "This page may not be embedded in other webpages.", 403
|
||||
|
||||
resp = make_response(f(*args, **kwargs))
|
||||
resp.headers.add("Access-Control-Allow-Origin",
|
||||
app.config["SERVER_NAME"]
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
# wrapper for api-related things that discriminates between an api url
|
||||
# and an html url for the same content
|
||||
# f should return {'api':lambda:some_func(), 'html':lambda:other_func()}
|
||||
|
||||
|
||||
def public(*scopes, no_ban=False):
|
||||
|
||||
def wrapper_maker(f):
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
if request.path.startswith(('/api/v1','/api/v2')):
|
||||
|
||||
v = kwargs.get('v')
|
||||
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
if isinstance(result, dict):
|
||||
resp = result['api']()
|
||||
else:
|
||||
resp = result
|
||||
|
||||
if not isinstance(resp, RespObj):
|
||||
resp = make_response(resp)
|
||||
|
||||
resp.headers.add("Cache-Control", "private")
|
||||
resp.headers.add(
|
||||
"Access-Control-Allow-Origin",
|
||||
app.config["SERVER_NAME"])
|
||||
return resp
|
||||
|
||||
else:
|
||||
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
|
||||
try:
|
||||
if request.path.startswith('/inpage/'):
|
||||
return result['inpage']()
|
||||
elif request.path.startswith(('/api/vue/','/test/')):
|
||||
return result['api']()
|
||||
else:
|
||||
return result['html']()
|
||||
except KeyError:
|
||||
return result
|
||||
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
return wrapper_maker
|
||||
|
||||
|
||||
def api(*scopes, no_ban=False):
|
||||
|
||||
def wrapper_maker(f):
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
if request.path.startswith(('/api/v1','/api/v2')):
|
||||
|
||||
v = kwargs.get('v')
|
||||
client = kwargs.get('c')
|
||||
|
||||
if not v or not client:
|
||||
return jsonify(
|
||||
{"error": "401 Not Authorized. Invalid or Expired Token"}), 401
|
||||
|
||||
kwargs.pop('c')
|
||||
|
||||
# validate app associated with token
|
||||
if client.application.is_banned:
|
||||
return jsonify({"error": f"403 Forbidden. The application `{client.application.app_name}` is suspended."}), 403
|
||||
|
||||
# validate correct scopes for request
|
||||
for scope in scopes:
|
||||
if not client.__dict__.get(f"scope_{scope}"):
|
||||
return jsonify({"error": f"401 Not Authorized. Scope `{scope}` is required."}), 403
|
||||
|
||||
if (request.method == "POST" or no_ban) and client.user.is_suspended:
|
||||
return jsonify({"error": f"403 Forbidden. The user account is suspended."}), 403
|
||||
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
if isinstance(result, dict):
|
||||
resp = result['api']()
|
||||
else:
|
||||
resp = result
|
||||
|
||||
if not isinstance(resp, RespObj):
|
||||
resp = make_response(resp)
|
||||
|
||||
resp.headers.add("Cache-Control", "private")
|
||||
resp.headers.add(
|
||||
"Access-Control-Allow-Origin",
|
||||
app.config["SERVER_NAME"])
|
||||
return resp
|
||||
|
||||
else:
|
||||
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
|
||||
try:
|
||||
if request.path.startswith('/inpage/'):
|
||||
return result['inpage']()
|
||||
elif request.path.startswith(('/api/vue/','/test/')):
|
||||
return result['api']()
|
||||
else:
|
||||
return result['html']()
|
||||
except KeyError:
|
||||
return result
|
||||
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
return wrapper_maker
|
||||
|
||||
|
||||
SANCTIONS=[
|
||||
"CU", #Cuba
|
||||
"IR", #Iran
|
||||
"KP", #North Korea
|
||||
"SY", #Syria
|
||||
"TR", #Turkey
|
||||
"VE", #Venezuela
|
||||
]
|
||||
|
||||
def no_sanctions(f):
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
if request.headers.get("cf-ipcountry","") in SANCTIONS:
|
||||
abort(451)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
wrapper.__name__=f.__name__
|
||||
return wrapper
|
|
@ -0,0 +1 @@
|
|||
from .mail import *
|
|
@ -0,0 +1,98 @@
|
|||
from os import environ
|
||||
import requests
|
||||
import time
|
||||
from flask import *
|
||||
from urllib.parse import quote
|
||||
|
||||
from ruqqus.helpers.security import *
|
||||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.classes import *
|
||||
from ruqqus.__main__ import app
|
||||
|
||||
|
||||
def send_mail(to_address, subject, html, plaintext=None, files={},
|
||||
from_address="Drama <noreply@mail.rdrama.net>"):
|
||||
|
||||
url = "https://api.mailgun.net/v3/rdrama.net/messages"
|
||||
|
||||
data = {"from": from_address,
|
||||
"to": [to_address],
|
||||
"subject": subject,
|
||||
"text": plaintext,
|
||||
"html": html,
|
||||
}
|
||||
|
||||
return requests.post(url,
|
||||
auth=(
|
||||
"api", environ.get("MAILGUN_KEY").strip()),
|
||||
data=data,
|
||||
files=[("attachment", (k, files[k])) for k in files]
|
||||
)
|
||||
|
||||
|
||||
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="Validate your Drama account email."
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/verify_email", methods=["POST"])
|
||||
@is_not_banned
|
||||
def api_verify_email(v):
|
||||
|
||||
send_verification_email(v)
|
||||
|
||||
return "", 204
|
||||
|
||||
|
||||
@app.route("/activate", methods=["GET"])
|
||||
@auth_desired
|
||||
def activate(v):
|
||||
|
||||
email = request.args.get("email", "")
|
||||
id = request.args.get("id", "")
|
||||
timestamp = int(request.args.get("time", "0"))
|
||||
token = request.args.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).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,
|
||||
created_utc=time.time())
|
||||
g.db.add(mail_badge)
|
||||
|
||||
g.db.add(user)
|
||||
|
||||
return render_template("message_success.html", v=v, title="Email verified.", message=f"Your email {email} has been verified. Thank you.")
|
|
@ -0,0 +1,16 @@
|
|||
from .admin import *
|
||||
from .boards import *
|
||||
from .comments import *
|
||||
from .discord import *
|
||||
from .errors import *
|
||||
from .flagging 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 *
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,319 @@
|
|||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.helpers.alerts import *
|
||||
from ruqqus.classes import *
|
||||
from flask import *
|
||||
from ruqqus.__main__ import app, limiter, cache
|
||||
|
||||
valid_board_regex = re.compile("^[a-zA-Z0-9][a-zA-Z0-9_]{2,24}$")
|
||||
|
||||
@app.route("/mod/distinguish_post/<bid>/<pid>", methods=["POST"])
|
||||
@app.route("/api/v1/distinguish_post/<bid>/<pid>", methods=["POST"])
|
||||
@auth_required
|
||||
@is_guildmaster("content")
|
||||
@api("guildmaster")
|
||||
def mod_distinguish_post(bid, pid, board, v):
|
||||
|
||||
#print(pid, board, v)
|
||||
|
||||
post = get_post(pid, v=v)
|
||||
|
||||
if not post.board_id==board.id:
|
||||
abort(400)
|
||||
|
||||
if post.author_id != v.id:
|
||||
abort(403)
|
||||
|
||||
if post.gm_distinguish:
|
||||
post.gm_distinguish = 0
|
||||
else:
|
||||
post.gm_distinguish = board.id
|
||||
g.db.add(post)
|
||||
|
||||
ma=ModAction(
|
||||
kind="herald_post" if post.gm_distinguish else "unherald_post",
|
||||
user_id=v.id,
|
||||
target_submission_id=post.id,
|
||||
board_id=board.id
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
return "", 204
|
||||
|
||||
@app.route("/mod/invite_mod/<bid>", methods=["POST"])
|
||||
@auth_required
|
||||
@is_guildmaster("full")
|
||||
@validate_formkey
|
||||
def mod_invite_username(bid, board, v):
|
||||
|
||||
username = request.form.get("username", '').lstrip('@')
|
||||
user = get_user(username)
|
||||
if not user:
|
||||
return jsonify({"error": "That user doesn't exist."}), 404
|
||||
|
||||
if board.has_ban(user):
|
||||
return jsonify({"error": f"@{user.username} is exiled from +{board.name} and can't currently become a guildmaster."}), 409
|
||||
if not user.can_join_gms:
|
||||
return jsonify({"error": f"@{user.username} already leads enough guilds."}), 409
|
||||
|
||||
x = g.db.query(ModRelationship).filter_by(
|
||||
user_id=user.id, board_id=board.id).first()
|
||||
|
||||
if x and x.accepted:
|
||||
return jsonify({"error": f"@{user.username} is already a mod."}), 409
|
||||
|
||||
if x and not x.invite_rescinded:
|
||||
return jsonify({"error": f"@{user.username} has already been invited."}), 409
|
||||
|
||||
if x:
|
||||
|
||||
x.invite_rescinded = False
|
||||
g.db.add(x)
|
||||
|
||||
else:
|
||||
x = ModRelationship(
|
||||
user_id=user.id,
|
||||
board_id=board.id,
|
||||
accepted=False,
|
||||
perm_full=True,
|
||||
perm_content=True,
|
||||
perm_appearance=True,
|
||||
perm_access=True,
|
||||
perm_config=True
|
||||
)
|
||||
|
||||
text = f"You have been invited to become an admin. You can [click here](/badmins) and accept this invitation. Or, if you weren't expecting this, you can ignore it."
|
||||
send_notification(1046, user, text)
|
||||
|
||||
g.db.add(x)
|
||||
|
||||
ma=ModAction(
|
||||
kind="invite_mod",
|
||||
user_id=v.id,
|
||||
target_user_id=user.id,
|
||||
board_id=1
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
return "", 204
|
||||
|
||||
|
||||
@app.route("/mod/<bid>/rescind/<username>", methods=["POST"])
|
||||
@auth_required
|
||||
@is_guildmaster("full")
|
||||
@validate_formkey
|
||||
def mod_rescind_bid_username(bid, username, board, v):
|
||||
|
||||
user = get_user(username)
|
||||
|
||||
invitation = g.db.query(ModRelationship).filter_by(board_id=board.id,
|
||||
user_id=user.id,
|
||||
accepted=False).first()
|
||||
if not invitation:
|
||||
abort(404)
|
||||
|
||||
invitation.invite_rescinded = True
|
||||
|
||||
g.db.add(invitation)
|
||||
ma=ModAction(
|
||||
kind="uninvite_mod",
|
||||
user_id=v.id,
|
||||
target_user_id=user.id,
|
||||
board_id=1
|
||||
)
|
||||
g.db.add(ma)
|
||||
return "", 204
|
||||
|
||||
|
||||
@app.route("/mod/accept/<bid>", methods=["POST"])
|
||||
@app.route("/api/v1/accept_invite/<bid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
@api("guildmaster")
|
||||
def mod_accept_board(bid, v):
|
||||
|
||||
board = get_board(bid)
|
||||
|
||||
x = board.has_invite(v)
|
||||
if not x:
|
||||
abort(404)
|
||||
|
||||
if not v.can_join_gms:
|
||||
return jsonify({"error": f"You already lead enough guilds."}), 409
|
||||
if board.has_ban(v):
|
||||
return jsonify({"error": f"You are exiled from +{board.name} and can't currently become a guildmaster."}), 409
|
||||
x.accepted = True
|
||||
x.created_utc=int(time.time())
|
||||
g.db.add(x)
|
||||
|
||||
ma=ModAction(
|
||||
kind="accept_mod_invite",
|
||||
user_id=v.id,
|
||||
target_user_id=v.id,
|
||||
board_id=board.id
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
v.admin_level = 6
|
||||
return "", 204
|
||||
|
||||
@app.route("/mod/<bid>/step_down", methods=["POST"])
|
||||
@auth_required
|
||||
@is_guildmaster()
|
||||
@validate_formkey
|
||||
def mod_step_down(bid, board, v):
|
||||
|
||||
|
||||
v_mod = board.has_mod(v)
|
||||
|
||||
if not v_mod:
|
||||
abort(404)
|
||||
|
||||
g.db.delete(v_mod)
|
||||
|
||||
ma=ModAction(
|
||||
kind="dethrone_self",
|
||||
user_id=v.id,
|
||||
target_user_id=v.id,
|
||||
board_id=board.id
|
||||
)
|
||||
g.db.add(ma)
|
||||
v.admin_level = 0
|
||||
return "", 204
|
||||
|
||||
|
||||
|
||||
@app.route("/mod/<bid>/remove/<username>", methods=["POST"])
|
||||
@auth_required
|
||||
@is_guildmaster("full")
|
||||
@validate_formkey
|
||||
def mod_remove_username(bid, username, board, v):
|
||||
|
||||
user = get_user(username)
|
||||
|
||||
u_mod = board.has_mod(user)
|
||||
v_mod = board.has_mod(v)
|
||||
|
||||
if not u_mod:
|
||||
abort(400)
|
||||
elif not v_mod:
|
||||
abort(400)
|
||||
|
||||
if not u_mod.board_id==board.id:
|
||||
abort(400)
|
||||
|
||||
if not v_mod.board_id==board.id:
|
||||
abort(400)
|
||||
|
||||
if v_mod.id > u_mod.id:
|
||||
abort(403)
|
||||
|
||||
g.db.delete(u_mod)
|
||||
|
||||
ma=ModAction(
|
||||
kind="remove_mod",
|
||||
user_id=v.id,
|
||||
target_user_id=user.id,
|
||||
board_id=board.id
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
user.admin_level = 0
|
||||
return "", 204
|
||||
|
||||
@app.route("/badmins", methods=["GET"])
|
||||
@app.route("/api/vue/mod/mods", methods=["GET"])
|
||||
@app.route("/api/v1/mod/mods", methods=["GET"])
|
||||
@auth_desired
|
||||
@public("read")
|
||||
def board_about_mods(v):
|
||||
|
||||
board = get_guild("general")
|
||||
|
||||
me = board.has_mod(v)
|
||||
|
||||
return {
|
||||
"html":lambda:render_template("mods.html", v=v, b=board, me=me),
|
||||
"api":lambda:jsonify({"data":[x.json for x in board.mods_list]})
|
||||
}
|
||||
|
||||
@app.route("/log", methods=["GET"])
|
||||
@app.route("/api/v1/mod_log", methods=["GET"])
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def board_mod_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").order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all()
|
||||
actions=[i for i in actions]
|
||||
|
||||
next_exists=len(actions)==26
|
||||
actions=actions[0:25]
|
||||
|
||||
return {
|
||||
"html":lambda:render_template(
|
||||
"modlog.html",
|
||||
v=v,
|
||||
actions=actions,
|
||||
next_exists=next_exists,
|
||||
page=page
|
||||
),
|
||||
"api":lambda:jsonify({"data":[x.json for x in actions]})
|
||||
}
|
||||
|
||||
@app.route("/log/<aid>", methods=["GET"])
|
||||
@auth_desired
|
||||
def mod_log_item(aid, v):
|
||||
|
||||
action=g.db.query(ModAction).filter_by(id=base36decode(aid)).first()
|
||||
|
||||
if not action:
|
||||
abort(404)
|
||||
|
||||
if request.path != action.permalink:
|
||||
return redirect(action.permalink)
|
||||
|
||||
return render_template("modlog.html",
|
||||
v=v,
|
||||
actions=[action],
|
||||
next_exists=False,
|
||||
page=1,
|
||||
action=action
|
||||
)
|
||||
|
||||
@app.route("/mod/edit_perms", methods=["POST"])
|
||||
@auth_required
|
||||
@is_guildmaster("full")
|
||||
@validate_formkey
|
||||
def board_mod_perms_change(boardname, board, v):
|
||||
|
||||
user=get_user(request.form.get("username"))
|
||||
|
||||
v_mod=board.has_mod(v)
|
||||
u_mod=board.has_mod_record(user)
|
||||
|
||||
if v_mod.id > u_mod.id:
|
||||
return jsonify({"error":"You can't change perms on badmins above you."}), 403
|
||||
|
||||
#print({x:request.form.get(x) for x in request.form})
|
||||
|
||||
u_mod.perm_full = bool(request.form.get("perm_full" , False))
|
||||
u_mod.perm_access = bool(request.form.get("perm_access" , False))
|
||||
u_mod.perm_appearance = bool(request.form.get("perm_appearance" , False))
|
||||
u_mod.perm_config = bool(request.form.get("perm_config" , False))
|
||||
u_mod.perm_content = bool(request.form.get("perm_content" , False))
|
||||
|
||||
g.db.add(u_mod)
|
||||
g.db.commit()
|
||||
|
||||
ma=ModAction(
|
||||
kind="change_perms" if u_mod.accepted else "change_invite",
|
||||
user_id=v.id,
|
||||
board_id=board.id,
|
||||
target_user_id=user.id,
|
||||
note=u_mod.permchangelist
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
return redirect(f"{board.permalink}/mod/mods")
|
|
@ -0,0 +1,912 @@
|
|||
import threading
|
||||
|
||||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.helpers.filters import *
|
||||
from ruqqus.helpers.alerts import *
|
||||
from ruqqus.helpers.aws import *
|
||||
from ruqqus.helpers.session import *
|
||||
from ruqqus.classes import *
|
||||
from ruqqus.routes.front import comment_idlist
|
||||
from pusher_push_notifications import PushNotifications
|
||||
|
||||
from flask import *
|
||||
from ruqqus.__main__ import app, limiter
|
||||
|
||||
choices = ['Wow, you must be a JP fan.', 'This is one of the worst posts I have EVER seen. Delete it.', "No, don't reply like this, please do another wall of unhinged rant please.", '# 😴😴😴', "Ma'am we've been over this before. You need to stop.", "I've known more coherent downies.", "Your pulitzer's in the mail", "That's great and all, but I asked for my burger without cheese.", 'That degree finally paying off', "That's nice sweaty. Why don't you have a seat in the time out corner with Pizzashill until you calm down, then you can have your Capri Sun.", "All them words won't bring your pa back.", "You had a chance to not be completely worthless, but it looks like you threw it away. At least you're consistent.", 'Some people are able to display their intelligence by going on at length on a subject and never actually saying anything. This ability is most common in trades such as politics, public relations, and law. You have impressed me by being able to best them all, while still coming off as an absolute idiot.', "You can type 10,000 characters and you decided that these were the one's that you wanted.", 'Have you owned the libs yet?', "I don't know what you said, because I've seen another human naked.", 'Impressive. Normally people with such severe developmental disabilities struggle to write much more than a sentence or two. He really has exceded our expectations for the writing portion. Sadly the coherency of his writing, along with his abilities in the social skills and reading portions, are far behind his peers with similar disabilities.', "This is a really long way of saying you don't fuck.", "Sorry ma'am, looks like his delusions have gotten worse. We'll have to admit him,", 'https://i.kym-cdn.com/photos/images/newsfeed/001/038/094/0a1.jpg', 'If only you could put that energy into your relationships', 'Posts like this is why I do Heroine.', 'still unemployed then?', 'K', 'look im gunna have 2 ask u 2 keep ur giant dumps in the toilet not in my replys 😷😷😷', "Mommy is soooo proud of you, sweaty. Let's put this sperg out up on the fridge with all your other failures.", "Good job bobby, here's a star", "That was a mistake. You're about to find out the hard way why.", 'You sat down and wrote all this shit. You could have done so many other things with your life. What happened to your life that made you decide writing novels of bullshit on reddit was the best option?', "I don't have enough spoons to read this shit", "All those words won't bring daddy back.", 'OUT!', "Mommy is soooo proud of you, sweaty. Let's put this sperg out up on the fridge with all your other failures."]
|
||||
|
||||
PUSHER_KEY = environ.get("PUSHER_KEY", "").strip()
|
||||
|
||||
beams_client = PushNotifications(
|
||||
instance_id='02ddcc80-b8db-42be-9022-44c546b4dce6',
|
||||
secret_key=PUSHER_KEY,
|
||||
)
|
||||
|
||||
@app.route("/api/v1/post/<pid>/comment/<cid>", methods=["GET"])
|
||||
def comment_cid_api_redirect(cid=None, pid=None):
|
||||
redirect(f'/api/v1/comment/<cid>')
|
||||
|
||||
@app.route("/comment/<cid>", methods=["GET"])
|
||||
@app.route("/comment/<cid>", methods=["GET"])
|
||||
@app.route("/post_short/<pid>/<cid>", methods=["GET"])
|
||||
@app.route("/post_short/<pid>/<cid>/", methods=["GET"])
|
||||
@app.route("/api/v1/comment/<cid>", methods=["GET"])
|
||||
@app.route("/post/<pid>/<anything>/<cid>", methods=["GET"])
|
||||
@app.route("/api/vue/comment/<cid>")
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def post_pid_comment_cid(cid, pid=None, anything=None, v=None):
|
||||
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
try: cid = int(cid)
|
||||
except Exception as e: pass
|
||||
|
||||
comment = get_comment(cid, v=v)
|
||||
|
||||
if not comment.parent_submission and not (v and v.admin_level == 6): abort(403)
|
||||
|
||||
if not pid:
|
||||
if comment.parent_submission: pid = comment.parent_submission
|
||||
else: pid = 6489
|
||||
|
||||
try: pid = int(pid)
|
||||
except: abort(404)
|
||||
|
||||
post = get_post(pid, v=v)
|
||||
board = post.board
|
||||
|
||||
if post.over_18 and not (v and v.over_18) and not session_over18(comment.board):
|
||||
t = int(time.time())
|
||||
return {'html': lambda: render_template("errors/nsfw.html",
|
||||
v=v,
|
||||
t=t,
|
||||
lo_formkey=make_logged_out_formkey(
|
||||
t),
|
||||
board=comment.board
|
||||
),
|
||||
'api': lambda: {'error': f'This content is not suitable for some users and situations.'}
|
||||
|
||||
}
|
||||
|
||||
post._preloaded_comments = [comment]
|
||||
|
||||
# context improver
|
||||
context = int(request.args.get("context", 0))
|
||||
comment_info = comment
|
||||
c = comment
|
||||
while context > 0 and not c.is_top_level:
|
||||
|
||||
parent = get_comment(c.parent_comment_id, v=v)
|
||||
|
||||
post._preloaded_comments += [parent]
|
||||
|
||||
c = parent
|
||||
context -= 1
|
||||
top_comment = c
|
||||
|
||||
if v: defaultsortingcomments = v.defaultsortingcomments
|
||||
else: defaultsortingcomments = "top"
|
||||
sort=request.args.get("sort", defaultsortingcomments)
|
||||
|
||||
# children comments
|
||||
|
||||
current_ids = [comment.id]
|
||||
|
||||
exile=g.db.query(ModAction
|
||||
).filter_by(
|
||||
kind="exile_user"
|
||||
).distinct(ModAction.target_comment_id).subquery()
|
||||
|
||||
for i in range(6 - context):
|
||||
if v:
|
||||
|
||||
votes = g.db.query(CommentVote).filter(
|
||||
CommentVote.user_id == v.id).subquery()
|
||||
|
||||
blocking = v.blocking.subquery()
|
||||
blocked = v.blocked.subquery()
|
||||
|
||||
|
||||
comms = g.db.query(
|
||||
Comment,
|
||||
votes.c.vote_type,
|
||||
blocking.c.id,
|
||||
blocked.c.id,
|
||||
aliased(ModAction, alias=exile)
|
||||
).select_from(Comment).options(
|
||||
joinedload(Comment.author).joinedload(User.title)
|
||||
).filter(
|
||||
Comment.parent_comment_id.in_(current_ids)
|
||||
).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
|
||||
).join(
|
||||
exile,
|
||||
exile.c.target_comment_id==Comment.id,
|
||||
isouter=True
|
||||
)
|
||||
|
||||
if sort == "top":
|
||||
comments = comms.order_by(Comment.score.desc()).all()
|
||||
elif sort == "bottom":
|
||||
comments = comms.order_by(Comment.score.asc()).all()
|
||||
elif sort == "new":
|
||||
comments = comms.order_by(Comment.created_utc.desc()).all()
|
||||
elif sort == "old":
|
||||
comments = comms.order_by(Comment.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
comments = sorted(comms.all(), key=lambda x: x.score_disputed, reverse=True)
|
||||
elif sort == "random":
|
||||
c = comms.all()
|
||||
comments = random.sample(c, k=len(c))
|
||||
else:
|
||||
abort(422)
|
||||
|
||||
output = []
|
||||
for c in comms:
|
||||
comment = c[0]
|
||||
comment._voted = c[1] or 0
|
||||
comment._is_blocking = c[2] or 0
|
||||
comment._is_blocked = c[3] or 0
|
||||
comment._is_guildmaster=top_comment._is_guildmaster
|
||||
comment._is_exiled_for=c[4] or 0
|
||||
output.append(comment)
|
||||
else:
|
||||
|
||||
comms = g.db.query(
|
||||
Comment,
|
||||
aliased(ModAction, alias=exile)
|
||||
).options(
|
||||
joinedload(Comment.author).joinedload(User.title)
|
||||
).filter(
|
||||
Comment.parent_comment_id.in_(current_ids)
|
||||
).join(
|
||||
exile,
|
||||
exile.c.target_comment_id==Comment.id,
|
||||
isouter=True
|
||||
)
|
||||
|
||||
if sort == "top":
|
||||
comments = comms.order_by(Comment.score.asc()).all()
|
||||
elif sort == "bottom":
|
||||
comments = comms.order_by(Comment.score.desc()).all()
|
||||
elif sort == "new":
|
||||
comments = comms.order_by(Comment.created_utc.desc()).all()
|
||||
elif sort == "old":
|
||||
comments = comms.order_by(Comment.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
comments = sorted(comms.all(), key=lambda x: x[0].score_disputed, reverse=True)
|
||||
elif sort == "random":
|
||||
c = comms.all()
|
||||
comments = random.sample(c, k=len(c))
|
||||
else:
|
||||
abort(422)
|
||||
|
||||
output = []
|
||||
for c in comms:
|
||||
comment=c[0]
|
||||
comment._is_exiled_for=c[1] or 0
|
||||
output.append(comment)
|
||||
|
||||
post._preloaded_comments += output
|
||||
|
||||
current_ids = [x.id for x in output]
|
||||
|
||||
|
||||
post.tree_comments()
|
||||
|
||||
post.replies=[top_comment]
|
||||
|
||||
return {'html': lambda: post.rendered_page(v=v, sort=sort, comment=top_comment, comment_info=comment_info),
|
||||
'api': lambda: top_comment.json
|
||||
}
|
||||
|
||||
@app.route("/api/comment", methods=["POST"])
|
||||
@app.route("/api/v1/comment", methods=["POST"])
|
||||
@limiter.limit("6/minute")
|
||||
@is_not_banned
|
||||
@no_negative_balance('toast')
|
||||
@tos_agreed
|
||||
@validate_formkey
|
||||
@api("create")
|
||||
def api_comment(v):
|
||||
|
||||
parent_submission = base36decode(request.form.get("submission"))
|
||||
parent_fullname = request.form.get("parent_fullname")
|
||||
|
||||
# get parent item info
|
||||
parent_id = parent_fullname.split("_")[1]
|
||||
if parent_fullname.startswith("t2"):
|
||||
parent_post = get_post(parent_id, v=v)
|
||||
parent = parent_post
|
||||
parent_comment_id = None
|
||||
level = 1
|
||||
parent_submission = base36decode(parent_id)
|
||||
elif parent_fullname.startswith("t3"):
|
||||
parent = get_comment(parent_id, v=v)
|
||||
parent_comment_id = parent.id
|
||||
level = parent.level + 1
|
||||
parent_id = parent.parent_submission
|
||||
parent_submission = parent_id
|
||||
parent_post = get_post(base36encode(parent_id))
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
#process and sanitize
|
||||
body = request.form.get("body", "")[0:10000]
|
||||
body = body.strip()
|
||||
|
||||
if not body and not request.files.get('file'): return jsonify({"error":"You need to actually write something!"}), 400
|
||||
|
||||
for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif))', body, re.MULTILINE): body = body.replace(i.group(1), f'![]({i.group(1)})')
|
||||
with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body))
|
||||
body_html = sanitize(body_md, linkgen=True)
|
||||
|
||||
# Run safety filter
|
||||
bans = filter_comment_html(body_html)
|
||||
|
||||
if bans:
|
||||
ban = bans[0]
|
||||
reason = f"Remove the {ban.domain} link from your comment and try again."
|
||||
if ban.reason:
|
||||
reason += f" {ban.reason_text}"
|
||||
|
||||
#auto ban for digitally malicious content
|
||||
if any([x.reason==4 for x in bans]):
|
||||
v.ban(days=30, reason="Digitally malicious content")
|
||||
if any([x.reason==7 for x in bans]):
|
||||
v.ban( reason="Sexualizing minors")
|
||||
return jsonify({"error": reason}), 401
|
||||
|
||||
# check existing
|
||||
existing = g.db.query(Comment).join(CommentAux).filter(Comment.author_id == v.id,
|
||||
Comment.deleted_utc == 0,
|
||||
Comment.parent_comment_id == parent_comment_id,
|
||||
Comment.parent_submission == parent_submission,
|
||||
CommentAux.body == body
|
||||
).options(contains_eager(Comment.comment_aux)).first()
|
||||
if existing:
|
||||
return jsonify({"error": f"You already made that comment: {existing.permalink}"}), 409
|
||||
|
||||
if parent.author.any_block_exists(v) and not v.admin_level>=3:
|
||||
return jsonify(
|
||||
{"error": "You can't reply to users who have blocked you, or users you have blocked."}), 403
|
||||
|
||||
# check for archive and ban state
|
||||
post = get_post(parent_id)
|
||||
if post.is_archived or not post.board.can_comment(v):
|
||||
|
||||
return jsonify({"error": "You can't comment on this."}), 403
|
||||
|
||||
# get bot status
|
||||
is_bot = request.headers.get("X-User-Type","")=="Bot"
|
||||
|
||||
# check spam - this should hopefully be faster
|
||||
if not is_bot:
|
||||
now = int(time.time())
|
||||
cutoff = now - 60 * 60 * 24
|
||||
|
||||
similar_comments = g.db.query(Comment
|
||||
).options(
|
||||
lazyload('*')
|
||||
).join(Comment.comment_aux
|
||||
).filter(
|
||||
Comment.author_id == v.id,
|
||||
CommentAux.body.op(
|
||||
'<->')(body) < app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"],
|
||||
Comment.created_utc > cutoff
|
||||
).options(contains_eager(Comment.comment_aux)).all()
|
||||
|
||||
threshold = app.config["COMMENT_SPAM_COUNT_THRESHOLD"]
|
||||
if v.age >= (60 * 60 * 24 * 7):
|
||||
threshold *= 3
|
||||
elif v.age >= (60 * 60 * 24):
|
||||
threshold *= 2
|
||||
|
||||
if len(similar_comments) > threshold:
|
||||
text = "Your Drama account has been suspended for 1 day for the following reason:\n\n> Too much spam!"
|
||||
send_notification(1046, v, text)
|
||||
|
||||
v.ban(reason="Spamming.",
|
||||
days=1)
|
||||
|
||||
for alt in v.alts:
|
||||
if not alt.is_suspended:
|
||||
alt.ban(reason="Spamming.")
|
||||
|
||||
for comment in similar_comments:
|
||||
comment.is_banned = True
|
||||
comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly."
|
||||
g.db.add(comment)
|
||||
ma=ModAction(
|
||||
user_id=2317,
|
||||
target_comment_id=comment.id,
|
||||
kind="ban_comment",
|
||||
board_id=comment.post.board_id,
|
||||
note="spam"
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
g.db.commit()
|
||||
return jsonify({"error": "Too much spam!"}), 403
|
||||
|
||||
# check badlinks
|
||||
soup = BeautifulSoup(body_html, features="html.parser")
|
||||
links = [x['href'] for x in soup.find_all('a') if x.get('href')]
|
||||
|
||||
for link in links:
|
||||
parse_link = urlparse(link)
|
||||
check_url = ParseResult(scheme="https",
|
||||
netloc=parse_link.netloc,
|
||||
path=parse_link.path,
|
||||
params=parse_link.params,
|
||||
query=parse_link.query,
|
||||
fragment='')
|
||||
check_url = urlunparse(check_url)
|
||||
|
||||
badlink = g.db.query(BadLink).filter(
|
||||
literal(check_url).contains(
|
||||
BadLink.link)).first()
|
||||
|
||||
if badlink:
|
||||
return jsonify({"error": f"Remove the following link and try again: `{check_url}`. Reason: {badlink.reason_text}"}), 403
|
||||
|
||||
# create comment
|
||||
c = Comment(author_id=v.id,
|
||||
parent_submission=parent_submission,
|
||||
parent_fullname=parent.fullname,
|
||||
parent_comment_id=parent_comment_id,
|
||||
level=level,
|
||||
over_18=post.over_18 or request.form.get("over_18","")=="true",
|
||||
is_nsfl=post.is_nsfl or request.form.get("is_nsfl","")=="true",
|
||||
original_board_id=parent_post.board_id,
|
||||
is_bot=is_bot,
|
||||
app_id=v.client.application.id if v.client else None,
|
||||
creation_region=request.headers.get("cf-ipcountry")
|
||||
)
|
||||
|
||||
g.db.add(c)
|
||||
g.db.flush()
|
||||
|
||||
if v.dramacoins >= 0:
|
||||
if request.files.get("file"):
|
||||
file=request.files["file"]
|
||||
if not file.content_type.startswith('image/'):
|
||||
return jsonify({"error": "That wasn't an image!"}), 400
|
||||
|
||||
name = f'comment/{c.base36id}/{secrets.token_urlsafe(8)}'
|
||||
url = upload_file(name, file)
|
||||
|
||||
body = request.form.get("body") + f"\n\n![]({url})"
|
||||
with CustomRenderer(post_id=parent_id) as renderer:
|
||||
body_md = renderer.render(mistletoe.Document(body))
|
||||
body_html = sanitize(body_md, linkgen=True)
|
||||
|
||||
# #csam detection
|
||||
# def del_function():
|
||||
# delete_file(name)
|
||||
# c.is_banned=True
|
||||
# g.db.add(c)
|
||||
# g.db.commit()
|
||||
|
||||
# csam_thread=threading.Thread(target=check_csam_url,
|
||||
# args=(f"https://s3.eu-central-1.amazonaws.com/i.ruqqus.ga/{name}",
|
||||
# v,
|
||||
# del_function
|
||||
# )
|
||||
# )
|
||||
# csam_thread.start()
|
||||
|
||||
c_aux = CommentAux(
|
||||
id=c.id,
|
||||
body_html=body_html,
|
||||
body=body
|
||||
)
|
||||
|
||||
g.db.add(c_aux)
|
||||
g.db.flush()
|
||||
|
||||
if len(body) >= 1000 and v.username != "Snappy" and "</blockquote>" not in body_html:
|
||||
c2 = Comment(author_id=1832,
|
||||
parent_submission=parent_submission,
|
||||
parent_fullname=c.fullname,
|
||||
parent_comment_id=c.id,
|
||||
level=level+1,
|
||||
original_board_id=1,
|
||||
is_bot=True,
|
||||
)
|
||||
|
||||
g.db.add(c2)
|
||||
g.db.flush()
|
||||
|
||||
body = random.choice(choices)
|
||||
with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body))
|
||||
body_html2 = sanitize(body_md, linkgen=True)
|
||||
c_aux = CommentAux(
|
||||
id=c2.id,
|
||||
body_html=body_html2,
|
||||
body=body
|
||||
)
|
||||
g.db.add(c_aux)
|
||||
g.db.flush()
|
||||
n = Notification(comment_id=c2.id, user_id=v.id)
|
||||
g.db.add(n)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if random.random() < 0.001 and v.username != "Snappy" and v.username != "zozbot":
|
||||
c2 = Comment(author_id=1833,
|
||||
parent_submission=parent_submission,
|
||||
parent_fullname=c.fullname,
|
||||
parent_comment_id=c.id,
|
||||
level=level+1,
|
||||
original_board_id=1,
|
||||
is_bot=True,
|
||||
)
|
||||
|
||||
g.db.add(c2)
|
||||
g.db.flush()
|
||||
|
||||
body = "zoz"
|
||||
with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body))
|
||||
body_html2 = sanitize(body_md, linkgen=True)
|
||||
c_aux = CommentAux(
|
||||
id=c2.id,
|
||||
body_html=body_html2,
|
||||
body=body
|
||||
)
|
||||
g.db.add(c_aux)
|
||||
g.db.flush()
|
||||
n = Notification(comment_id=c2.id, user_id=v.id)
|
||||
g.db.add(n)
|
||||
|
||||
|
||||
|
||||
|
||||
c3 = Comment(author_id=1833,
|
||||
parent_submission=parent_submission,
|
||||
parent_fullname=c2.fullname,
|
||||
parent_comment_id=c2.id,
|
||||
level=level+2,
|
||||
original_board_id=1,
|
||||
is_bot=True,
|
||||
)
|
||||
|
||||
g.db.add(c3)
|
||||
g.db.flush()
|
||||
|
||||
body = "zle"
|
||||
with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body))
|
||||
body_html2 = sanitize(body_md, linkgen=True)
|
||||
c_aux = CommentAux(
|
||||
id=c3.id,
|
||||
body_html=body_html2,
|
||||
body=body
|
||||
)
|
||||
g.db.add(c_aux)
|
||||
g.db.flush()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
c4 = Comment(author_id=1833,
|
||||
parent_submission=parent_submission,
|
||||
parent_fullname=c3.fullname,
|
||||
parent_comment_id=c3.id,
|
||||
level=level+3,
|
||||
original_board_id=1,
|
||||
is_bot=True,
|
||||
)
|
||||
|
||||
g.db.add(c4)
|
||||
g.db.flush()
|
||||
|
||||
body = "zozzle"
|
||||
with CustomRenderer(post_id=parent_id) as renderer: body_md = renderer.render(mistletoe.Document(body))
|
||||
body_html2 = sanitize(body_md, linkgen=True)
|
||||
c_aux = CommentAux(
|
||||
id=c4.id,
|
||||
body_html=body_html2,
|
||||
body=body
|
||||
)
|
||||
g.db.add(c_aux)
|
||||
g.db.flush()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# queue up notification for parent author
|
||||
notify_users = set()
|
||||
|
||||
for x in g.db.query(Subscription.user_id).filter_by(submission_id=c.parent_submission).all():
|
||||
notify_users.add(x)
|
||||
|
||||
if parent.author.id != v.id: notify_users.add(parent.author.id)
|
||||
|
||||
soup = BeautifulSoup(body_html, features="html.parser")
|
||||
mentions = soup.find_all("a", href=re.compile("^/@(\w+)"))
|
||||
for mention in mentions:
|
||||
username = mention["href"].split("@")[1]
|
||||
|
||||
user = g.db.query(User).filter_by(username=username).first()
|
||||
|
||||
if user:
|
||||
if v.any_block_exists(user):
|
||||
continue
|
||||
if user.id != v.id:
|
||||
notify_users.add(user.id)
|
||||
|
||||
for x in notify_users:
|
||||
n = Notification(comment_id=c.id, user_id=x)
|
||||
g.db.add(n)
|
||||
try: g.db.flush()
|
||||
except: g.db.rollback()
|
||||
|
||||
if parent.author.id != v.id:
|
||||
beams_client.publish_to_interests(
|
||||
interests=[str(parent.author.id)],
|
||||
publish_body={
|
||||
'web': {
|
||||
'notification': {
|
||||
'title': f'New reply by @{v.username}',
|
||||
'body': c.body,
|
||||
'deep_link': f'https://rdrama.net{c.permalink}?context=5#context',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# create auto upvote
|
||||
vote = CommentVote(user_id=v.id,
|
||||
comment_id=c.id,
|
||||
vote_type=1
|
||||
)
|
||||
|
||||
g.db.add(vote)
|
||||
c=get_comment(c.id, v=v)
|
||||
|
||||
|
||||
# print(f"Content Event: @{v.username} comment {c.base36id}")
|
||||
|
||||
board = get_board(1)
|
||||
cache.delete_memoized(comment_idlist)
|
||||
cache.delete_memoized(User.commentlisting, v)
|
||||
|
||||
return {"html": lambda: jsonify({"html": render_template("comments.html",
|
||||
v=v,
|
||||
comments=[c],
|
||||
render_replies=False,
|
||||
is_allowed_to_comment=True
|
||||
)}),
|
||||
"api": lambda: c.json
|
||||
}
|
||||
|
||||
|
||||
|
||||
@app.route("/edit_comment/<cid>", methods=["POST"])
|
||||
@is_not_banned
|
||||
@validate_formkey
|
||||
@api("edit")
|
||||
def edit_comment(cid, v):
|
||||
|
||||
c = get_comment(cid, v=v)
|
||||
|
||||
if not c.author_id == v.id: abort(403)
|
||||
|
||||
if c.is_banned or c.deleted_utc > 0: abort(403)
|
||||
|
||||
body = request.form.get("body", "")[0:10000]
|
||||
for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif))', body, re.MULTILINE): body = body.replace(i.group(1), f'![]({i.group(1)})')
|
||||
with CustomRenderer(post_id=c.post.base36id) as renderer: body_md = renderer.render(mistletoe.Document(body))
|
||||
body_html = sanitize(body_md, linkgen=True)
|
||||
|
||||
bans = filter_comment_html(body_html)
|
||||
|
||||
if bans:
|
||||
|
||||
ban = bans[0]
|
||||
reason = f"Remove the {ban.domain} link from your comment and try again."
|
||||
|
||||
#auto ban for digitally malicious content
|
||||
if any([x.reason==4 for x in bans]):
|
||||
v.ban(days=30, reason="Digitally malicious content is not allowed.")
|
||||
return jsonify({"error":"Digitally malicious content is not allowed."})
|
||||
|
||||
if ban.reason:
|
||||
reason += f" {ban.reason_text}"
|
||||
|
||||
return jsonify({"error": reason}), 401
|
||||
|
||||
return {'html': lambda: render_template("comment_failed.html",
|
||||
action=f"/edit_comment/{c.base36id}",
|
||||
badlinks=[
|
||||
x.domain for x in bans],
|
||||
body=body,
|
||||
v=v
|
||||
),
|
||||
'api': lambda: ({'error': f'A blacklisted domain was used.'}, 400)
|
||||
}
|
||||
|
||||
# check badlinks
|
||||
soup = BeautifulSoup(body_html, features="html.parser")
|
||||
links = [x['href'] for x in soup.find_all('a') if x.get('href')]
|
||||
|
||||
for link in links:
|
||||
parse_link = urlparse(link)
|
||||
check_url = ParseResult(scheme="https",
|
||||
netloc=parse_link.netloc,
|
||||
path=parse_link.path,
|
||||
params=parse_link.params,
|
||||
query=parse_link.query,
|
||||
fragment='')
|
||||
check_url = urlunparse(check_url)
|
||||
|
||||
badlink = g.db.query(BadLink).filter(
|
||||
literal(check_url).contains(
|
||||
BadLink.link)).first()
|
||||
|
||||
if badlink:
|
||||
return jsonify({"error": f"Remove the following link and try again: `{check_url}`. Reason: {badlink.reason_text}"}), 403
|
||||
|
||||
# check spam - this should hopefully be faster
|
||||
now = int(time.time())
|
||||
cutoff = now - 60 * 60 * 24
|
||||
|
||||
similar_comments = g.db.query(Comment
|
||||
).options(
|
||||
lazyload('*')
|
||||
).join(Comment.comment_aux
|
||||
).filter(
|
||||
Comment.author_id == v.id,
|
||||
CommentAux.body.op(
|
||||
'<->')(body) < app.config["SPAM_SIMILARITY_THRESHOLD"],
|
||||
Comment.created_utc > cutoff
|
||||
).options(contains_eager(Comment.comment_aux)).all()
|
||||
|
||||
threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"]
|
||||
if v.age >= (60 * 60 * 24 * 30):
|
||||
threshold *= 4
|
||||
elif v.age >= (60 * 60 * 24 * 7):
|
||||
threshold *= 3
|
||||
elif v.age >= (60 * 60 * 24):
|
||||
threshold *= 2
|
||||
|
||||
if len(similar_comments) > threshold:
|
||||
text = "Your Drama account has been suspended for 1 day for the following reason:\n\n> Too much spam!"
|
||||
send_notification(1046, v, text)
|
||||
|
||||
v.ban(reason="Spamming.",
|
||||
days=1)
|
||||
|
||||
for comment in similar_comments:
|
||||
comment.is_banned = True
|
||||
comment.ban_reason = "Automatic spam removal. This happened because the post's creator submitted too much similar content too quickly."
|
||||
g.db.add(comment)
|
||||
|
||||
g.db.commit()
|
||||
return jsonify({"error": "Too much spam!"}), 403
|
||||
|
||||
if v.dramacoins >= 0:
|
||||
if request.files.get("file"):
|
||||
file=request.files["file"]
|
||||
if not file.content_type.startswith('image/'):
|
||||
return jsonify({"error": "That wasn't an image!"}), 400
|
||||
|
||||
name = f'comment/{c.base36id}/{secrets.token_urlsafe(8)}'
|
||||
url = upload_file(name, file)
|
||||
|
||||
body += f"\n\n![]({url})"
|
||||
with CustomRenderer(post_id=c.parent_submission) as renderer:
|
||||
body_md = renderer.render(mistletoe.Document(body))
|
||||
body_html = sanitize(body_md, linkgen=True)
|
||||
|
||||
# #csam detection
|
||||
# def del_function():
|
||||
# delete_file(name)
|
||||
# c.is_banned=True
|
||||
# g.db.add(c)
|
||||
# g.db.commit()
|
||||
|
||||
# csam_thread=threading.Thread(target=check_csam_url,
|
||||
# args=(f"https://s3.eu-central-1.amazonaws.com/i.ruqqus.ga/{name}",
|
||||
# v,
|
||||
# del_function
|
||||
# )
|
||||
# )
|
||||
# csam_thread.start()
|
||||
|
||||
c.body = body
|
||||
c.body_html = body_html
|
||||
|
||||
if int(time.time()) - c.created_utc > 60 * 3: c.edited_utc = int(time.time())
|
||||
|
||||
g.db.add(c)
|
||||
|
||||
g.db.commit()
|
||||
|
||||
path = request.form.get("current_page", "/")
|
||||
|
||||
# queue up notifications for username mentions
|
||||
notify_users = set()
|
||||
soup = BeautifulSoup(body_html, features="html.parser")
|
||||
mentions = soup.find_all("a", href=re.compile("^/@(\w+)"))
|
||||
|
||||
if len(mentions) > 0:
|
||||
notifs = g.db.query(Notification)
|
||||
for mention in mentions:
|
||||
username = mention["href"].split("@")[1]
|
||||
|
||||
user = g.db.query(User).filter_by(username=username).first()
|
||||
|
||||
if user:
|
||||
if v.any_block_exists(user):
|
||||
continue
|
||||
if user.id != v.id:
|
||||
notify_users.add(user.id)
|
||||
|
||||
for x in notify_users:
|
||||
notif = notifs.filter_by(comment_id=c.id, user_id=x).first()
|
||||
if not notif:
|
||||
n = Notification(comment_id=c.id, user_id=x)
|
||||
g.db.add(n)
|
||||
|
||||
return jsonify({"html": c.body_html})
|
||||
|
||||
@app.route("/delete/comment/<cid>", methods=["POST"])
|
||||
@app.route("/api/v1/delete/comment/<cid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
@api("delete")
|
||||
def delete_comment(cid, v):
|
||||
|
||||
c = g.db.query(Comment).filter_by(id=base36decode(cid)).first()
|
||||
|
||||
if not c:
|
||||
abort(404)
|
||||
|
||||
if not c.author_id == v.id:
|
||||
abort(403)
|
||||
|
||||
c.deleted_utc = int(time.time())
|
||||
|
||||
g.db.add(c)
|
||||
|
||||
cache.delete_memoized(User.commentlisting, v)
|
||||
|
||||
return {"html": lambda: ("", 204),
|
||||
"api": lambda: ("", 204)}
|
||||
|
||||
@app.route("/undelete/comment/<cid>", methods=["POST"])
|
||||
@app.route("/api/v1/undelete/comment/<cid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
@api("delete")
|
||||
def undelete_comment(cid, v):
|
||||
|
||||
c = g.db.query(Comment).filter_by(id=base36decode(cid)).first()
|
||||
|
||||
if not c:
|
||||
abort(404)
|
||||
|
||||
if not c.author_id == v.id:
|
||||
abort(403)
|
||||
|
||||
c.deleted_utc = 0
|
||||
|
||||
g.db.add(c)
|
||||
|
||||
cache.delete_memoized(User.commentlisting, v)
|
||||
|
||||
return {"html": lambda: ("", 204),
|
||||
"api": lambda: ("", 204)}
|
||||
|
||||
@app.route("/embed/comment/<cid>", methods=["GET"])
|
||||
@app.route("/embed/post/<pid>/comment/<cid>", methods=["GET"])
|
||||
@app.route("/api/v1/embed/comment/<cid>", methods=["GET"])
|
||||
@app.route("/api/v1/embed/post/<pid>/comment/<cid>", methods=["GET"])
|
||||
def embed_comment_cid(cid, pid=None):
|
||||
|
||||
comment = get_comment(cid)
|
||||
|
||||
if not comment.parent:
|
||||
abort(403)
|
||||
|
||||
if comment.is_banned or comment.deleted_utc > 0:
|
||||
return {'html': lambda: render_template("embeds/comment_removed.html", c=comment),
|
||||
'api': lambda: {'error': f'Comment {cid} has been removed'}
|
||||
}
|
||||
|
||||
if comment.board.is_banned:
|
||||
abort(410)
|
||||
|
||||
return render_template("embeds/comment.html", c=comment)
|
||||
|
||||
@app.route("/comment_pin/<cid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def mod_toggle_comment_pin(cid, v):
|
||||
|
||||
comment = get_comment(cid, v=v)
|
||||
|
||||
if v.admin_level != 6 and v.id != comment.post.author_id:
|
||||
abort(403)
|
||||
|
||||
comment.is_pinned = not comment.is_pinned
|
||||
|
||||
g.db.add(comment)
|
||||
g.db.commit()
|
||||
|
||||
if v.admin_level == 6:
|
||||
ma=ModAction(
|
||||
kind="pin_comment" if comment.is_pinned else "unpin_comment",
|
||||
user_id=v.id,
|
||||
board_id=1,
|
||||
target_comment_id=comment.id
|
||||
)
|
||||
g.db.add(ma)
|
||||
|
||||
html=render_template(
|
||||
"comments.html",
|
||||
v=v,
|
||||
comments=[comment],
|
||||
render_replies=False,
|
||||
is_allowed_to_comment=True
|
||||
)
|
||||
|
||||
html=str(BeautifulSoup(html, features="html.parser").find(id=f"comment-{comment.base36id}-only"))
|
||||
|
||||
return jsonify({"html":html})
|
||||
|
||||
|
||||
@app.route("/save_comment/<cid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def save_comment(cid, v):
|
||||
|
||||
comment=get_comment(cid)
|
||||
|
||||
new_save=SaveRelationship(user_id=v.id, submission_id=comment.id, type=2)
|
||||
|
||||
g.db.add(new_save)
|
||||
|
||||
try: g.db.flush()
|
||||
except: abort(422)
|
||||
|
||||
return "", 204
|
||||
|
||||
@app.route("/unsave_comment/<cid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def unsave_comment(cid, v):
|
||||
|
||||
comment=get_comment(cid)
|
||||
|
||||
save=g.db.query(SaveRelationship).filter_by(user_id=v.id, submission_id=comment.id, type=2).first()
|
||||
|
||||
g.db.delete(save)
|
||||
|
||||
return "", 204
|
|
@ -0,0 +1,156 @@
|
|||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.helpers.security import *
|
||||
from ruqqus.helpers.discord import add_role, delete_role
|
||||
from ruqqus.__main__ import app
|
||||
|
||||
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
|
||||
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
|
||||
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
|
||||
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN").strip()
|
||||
DISCORD_ENDPOINT = "https://discordapp.com/api/v6"
|
||||
|
||||
|
||||
WELCOME_CHANNEL="846509313941700618"
|
||||
|
||||
@app.route("/discord", methods=["GET"])
|
||||
@auth_required
|
||||
def join_discord(v):
|
||||
|
||||
if v.is_banned != 0: return "You're banned"
|
||||
if v.admin_level == 0 and v.dramacoins < 150: return "You must earn 150 dramacoins before entering the Discord server. You earn dramacoins by making posts/comments and getting upvoted."
|
||||
|
||||
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.route("/discord_redirect", methods=["GET"])
|
||||
@auth_required
|
||||
def discord_redirect(v):
|
||||
|
||||
|
||||
#validate state
|
||||
now=int(time.time())
|
||||
state=request.args.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)
|
||||
|
||||
#get discord token
|
||||
code = request.args.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)
|
||||
|
||||
|
||||
#get user ID
|
||||
url="https://discord.com/api/users/@me"
|
||||
headers={
|
||||
'Authorization': f"Bearer {token}"
|
||||
}
|
||||
x=requests.get(url, headers=headers)
|
||||
|
||||
x=x.json()
|
||||
|
||||
|
||||
|
||||
#add user to discord
|
||||
headers={
|
||||
'Authorization': f"Bot {BOT_TOKEN}",
|
||||
'Content-Type': "application/json"
|
||||
}
|
||||
|
||||
#remove existing user if applicable
|
||||
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).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)
|
||||
g.db.commit()
|
||||
|
||||
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{x['id']}"
|
||||
|
||||
name=v.username
|
||||
if v.real_id:
|
||||
name+= f" | {v.real_id}"
|
||||
|
||||
data={
|
||||
"access_token":token,
|
||||
"nick":name,
|
||||
}
|
||||
|
||||
x=requests.put(url, headers=headers, json=data)
|
||||
|
||||
if x.status_code in [201, 204]:
|
||||
|
||||
if v.admin_level > 0: add_role(v, "admin")
|
||||
else: add_role(v, "newuser")
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
add_role(v, "feedback")
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
if v.dramacoins > 100: add_role(v, "linked")
|
||||
else: add_role(v, "norep")
|
||||
|
||||
else:
|
||||
return jsonify(x.json())
|
||||
|
||||
#check on if they are already there
|
||||
#print(x.status_code)
|
||||
|
||||
if x.status_code==204:
|
||||
|
||||
##if user is already a member, remove old roles and update nick
|
||||
delete_role(v, "nick")
|
||||
|
||||
|
||||
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
|
||||
data={
|
||||
"nick": name
|
||||
}
|
||||
|
||||
req=requests.patch(url, headers=headers, json=data)
|
||||
|
||||
#print(req.status_code)
|
||||
#print(url)
|
||||
|
||||
return redirect(f"https://discord.com/channels/{SERVER_ID}/{WELCOME_CHANNEL}")
|
|
@ -0,0 +1,204 @@
|
|||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.helpers.session import *
|
||||
from ruqqus.classes.custom_errors import *
|
||||
from flask import *
|
||||
from urllib.parse import quote, urlencode
|
||||
import time
|
||||
from ruqqus.__main__ import app, r, cache, db_session
|
||||
import gevent
|
||||
|
||||
# Errors
|
||||
|
||||
|
||||
|
||||
|
||||
@app.errorhandler(401)
|
||||
def error_401(e):
|
||||
|
||||
path = request.path
|
||||
qs = urlencode(dict(request.args))
|
||||
argval = quote(f"{path}?{qs}", safe='')
|
||||
output = f"/login?redirect={argval}"
|
||||
|
||||
if request.path.startswith("/api/v1/"):
|
||||
return jsonify({"error": "401 Not Authorized"}), 401
|
||||
else:
|
||||
return redirect(output)
|
||||
|
||||
@app.errorhandler(PaymentRequired)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_402(e, v):
|
||||
return{"html": lambda: (render_template('errors/402.html', v=v), 402),
|
||||
"api": lambda: (jsonify({"error": "402 Payment Required"}), 402)
|
||||
}
|
||||
|
||||
@app.errorhandler(403)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_403(e, v):
|
||||
return{"html": lambda: (render_template('errors/403.html', v=v), 403),
|
||||
"api": lambda: (jsonify({"error": "403 Forbidden"}), 403)
|
||||
}
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_404(e, v):
|
||||
return{"html": lambda: (render_template('errors/404.html', v=v), 404),
|
||||
"api": lambda: (jsonify({"error": "404 Not Found"}), 404)
|
||||
}
|
||||
|
||||
|
||||
@app.errorhandler(405)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_405(e, v):
|
||||
return{"html": lambda: (render_template('errors/405.html', v=v), 405),
|
||||
"api": lambda: (jsonify({"error": "405 Method Not Allowed"}), 405)
|
||||
}
|
||||
|
||||
|
||||
@app.errorhandler(409)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_409(e, v):
|
||||
return{"html": lambda: (render_template('errors/409.html', v=v), 409),
|
||||
"api": lambda: (jsonify({"error": "409 Conflict"}), 409)
|
||||
}
|
||||
|
||||
|
||||
@app.errorhandler(413)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_413(e, v):
|
||||
return{"html": lambda: (render_template('errors/413.html', v=v), 413),
|
||||
"api": lambda: (jsonify({"error": "413 Request Payload Too Large"}), 413)
|
||||
}
|
||||
|
||||
|
||||
@app.errorhandler(422)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_422(e, v):
|
||||
return{"html": lambda: (render_template('errors/422.html', v=v), 422),
|
||||
"api": lambda: (jsonify({"error": "422 Unprocessable Entity"}), 422)
|
||||
}
|
||||
|
||||
|
||||
@app.errorhandler(429)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_429(e, v):
|
||||
return{"html": lambda: (render_template('errors/429.html', v=v), 429),
|
||||
"api": lambda: (jsonify({"error": "429 Too Many Requests"}), 429)
|
||||
}
|
||||
|
||||
|
||||
@app.errorhandler(451)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_451(e, v):
|
||||
return{"html": lambda: (render_template('errors/451.html', v=v), 451),
|
||||
"api": lambda: (jsonify({"error": "451 Unavailable For Legal Reasons"}), 451)
|
||||
}
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
@auth_desired
|
||||
@api()
|
||||
def error_500(e, v):
|
||||
try:
|
||||
g.db.rollback()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return{"html": lambda: (render_template('errors/500.html', v=v), 500),
|
||||
"api": lambda: (jsonify({"error": "500 Internal Server Error"}), 500)
|
||||
}
|
||||
|
||||
|
||||
@app.route("/allow_nsfw_logged_in/<bid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def allow_nsfw_logged_in(bid, v):
|
||||
|
||||
cutoff = int(time.time()) + 3600
|
||||
|
||||
if not session.get("over_18", None):
|
||||
session["over_18"] = {}
|
||||
|
||||
session["over_18"][bid] = cutoff
|
||||
|
||||
return redirect(request.form.get("redir"))
|
||||
|
||||
|
||||
@app.route("/allow_nsfw_logged_out/<bid>", methods=["POST"])
|
||||
@auth_desired
|
||||
def allow_nsfw_logged_out(bid, v):
|
||||
|
||||
if v:
|
||||
return redirect('/')
|
||||
|
||||
t = int(request.form.get('time'))
|
||||
|
||||
if not validate_logged_out_formkey(t,
|
||||
request.form.get("formkey")
|
||||
):
|
||||
abort(403)
|
||||
|
||||
if not session.get("over_18", None):
|
||||
session["over_18"] = {}
|
||||
|
||||
cutoff = int(time.time()) + 3600
|
||||
session["over_18"][bid] = cutoff
|
||||
|
||||
return redirect(request.form.get("redir"))
|
||||
|
||||
|
||||
@app.route("/allow_nsfl_logged_in/<bid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def allow_nsfl_logged_in(bid, v):
|
||||
|
||||
cutoff = int(time.time()) + 3600
|
||||
|
||||
if not session.get("show_nsfl", None):
|
||||
session["show_nsfl"] = {}
|
||||
|
||||
session["show_nsfl"][bid] = cutoff
|
||||
|
||||
return redirect(request.form.get("redir"))
|
||||
|
||||
|
||||
@app.route("/allow_nsfl_logged_out/<bid>", methods=["POST"])
|
||||
@auth_desired
|
||||
def allow_nsfl_logged_out(bid, v):
|
||||
|
||||
if v:
|
||||
return redirect('/')
|
||||
|
||||
t = int(request.form.get('time'))
|
||||
|
||||
if not validate_logged_out_formkey(t,
|
||||
request.form.get("formkey")
|
||||
):
|
||||
abort(403)
|
||||
|
||||
if not session.get("show_nsfl", None):
|
||||
session["show_nsfl"] = {}
|
||||
|
||||
cutoff = int(time.time()) + 3600
|
||||
session["show_nsfl"][bid] = cutoff
|
||||
|
||||
return redirect(request.form.get("redir"))
|
||||
|
||||
|
||||
@app.route("/error/<eid>", methods=["GET"])
|
||||
@auth_desired
|
||||
def error_all_preview(eid, v):
|
||||
|
||||
eid=int(eid)
|
||||
return render_template(f"errors/{eid}.html", v=v)
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import html
|
||||
from .front import frontlist
|
||||
from datetime import datetime
|
||||
from ruqqus.helpers.jinja2 import full_link
|
||||
from ruqqus.helpers.get import *
|
||||
from yattag import Doc
|
||||
|
||||
from ruqqus.__main__ import app
|
||||
|
||||
@app.route('/rss/<sort>/<t>', methods=["GET"])
|
||||
def feeds_user(sort='hot', t='all'):
|
||||
|
||||
page = int(request.args.get("page", 1))
|
||||
|
||||
posts = frontlist(
|
||||
sort=sort,
|
||||
page=page,
|
||||
t=t,
|
||||
v=None,
|
||||
hide_offensive=False,
|
||||
ids_only=False)
|
||||
|
||||
domain = environ.get(
|
||||
"domain", environ.get(
|
||||
"SERVER_NAME", None)).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:
|
||||
#print("POST IMAGE "+ str( post.is_image ))
|
||||
board_name = f"+{post.board.name}"
|
||||
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())
|
||||
|
||||
doc.stag("link", href=post.url)
|
||||
|
||||
with tag("author"):
|
||||
with tag("name"):
|
||||
text(post.author.username)
|
||||
with tag("uri"):
|
||||
text(f'https://{domain}/@{post.author.username}')
|
||||
|
||||
doc.stag("link", href=full_link(post.permalink))
|
||||
|
||||
doc.stag("category", term=board_name, label=board_name, schema=full_link("/" + board_name))
|
||||
|
||||
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"):
|
||||
text(html.escape(f"<img src={image_url}/><br/>{post.body_html}"))
|
||||
|
||||
return Response( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+ doc.getvalue(), mimetype="application/xml")
|
|
@ -0,0 +1,53 @@
|
|||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.helpers.get import *
|
||||
from flask import g
|
||||
from ruqqus.__main__ import app
|
||||
|
||||
|
||||
@app.route("/api/flag/post/<pid>", methods=["POST"])
|
||||
@auth_desired
|
||||
def api_flag_post(pid, v):
|
||||
|
||||
post = get_post(pid)
|
||||
|
||||
if v:
|
||||
existing = g.db.query(Flag).filter_by(
|
||||
user_id=v.id, post_id=post.id).filter(
|
||||
Flag.created_utc >= post.edited_utc).first()
|
||||
|
||||
if existing:
|
||||
return "", 409
|
||||
|
||||
flag = Flag(post_id=post.id,
|
||||
user_id=v.id,
|
||||
created_utc=int(time.time())
|
||||
)
|
||||
|
||||
|
||||
g.db.add(flag)
|
||||
|
||||
return "", 204
|
||||
|
||||
|
||||
@app.route("/api/flag/comment/<cid>", methods=["POST"])
|
||||
@auth_desired
|
||||
def api_flag_comment(cid, v):
|
||||
|
||||
comment = get_comment(cid)
|
||||
|
||||
if v:
|
||||
existing = g.db.query(CommentFlag).filter_by(
|
||||
user_id=v.id, comment_id=comment.id).filter(
|
||||
CommentFlag.created_utc >= comment.edited_utc).first()
|
||||
|
||||
if existing:
|
||||
return "", 409
|
||||
|
||||
flag = CommentFlag(comment_id=comment.id,
|
||||
user_id=v.id,
|
||||
created_utc=int(time.time())
|
||||
)
|
||||
|
||||
g.db.add(flag)
|
||||
|
||||
return "", 204
|
|
@ -0,0 +1,438 @@
|
|||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.helpers.get import *
|
||||
|
||||
from ruqqus.__main__ import app, cache
|
||||
from ruqqus.classes.submission import Submission
|
||||
|
||||
@app.route("/post/", methods=["GET"])
|
||||
def slash_post():
|
||||
return redirect("/")
|
||||
|
||||
# this is a test
|
||||
|
||||
@app.route("/notifications", methods=["GET"])
|
||||
@auth_required
|
||||
def notifications(v):
|
||||
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
page = int(request.args.get('page', 1))
|
||||
all_ = request.args.get('all', False)
|
||||
messages = request.args.get('messages', False)
|
||||
posts = request.args.get('posts', False)
|
||||
if messages:
|
||||
if v.admin_level == 6: comments = g.db.query(Comment).filter(or_(Comment.author_id==v.id, Comment.sentto==v.id, Comment.sentto==0)).filter(Comment.parent_submission == None).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all()
|
||||
else: comments = g.db.query(Comment).filter(or_(Comment.author_id==v.id, Comment.sentto==v.id)).filter(Comment.parent_submission == None).order_by(Comment.created_utc.desc()).offset(25 * (page - 1)).limit(26).all()
|
||||
next_exists = (len(comments) == 26)
|
||||
comments = comments[:25]
|
||||
elif posts:
|
||||
cids = v.notification_subscriptions(page=page, all_=all_)
|
||||
next_exists = (len(cids) == 26)
|
||||
cids = cids[:25]
|
||||
comments = get_comments(cids, v=v, sort="new", load_parent=True)
|
||||
else:
|
||||
cids = v.notification_commentlisting(page=page, all_=all_)
|
||||
next_exists = (len(cids) == 26)
|
||||
cids = cids[:25]
|
||||
comments = get_comments(cids, v=v, sort="new", load_parent=True)
|
||||
|
||||
listing = []
|
||||
for c in comments:
|
||||
c._is_blocked = False
|
||||
c._is_blocking = False
|
||||
if c.parent_submission:
|
||||
while c.parent_comment:
|
||||
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
|
||||
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,
|
||||
is_notification_page=True)
|
||||
|
||||
@cache.memoize(timeout=1500)
|
||||
def frontlist(v=None, sort="hot", page=1,t="all", ids_only=True, filter_words='', **kwargs):
|
||||
|
||||
posts = g.db.query(Submission).options(lazyload('*')).filter_by(is_banned=False,stickied=False,private=False,).filter(Submission.deleted_utc == 0)
|
||||
|
||||
if v and v.admin_level == 0:
|
||||
blocking = g.db.query(
|
||||
UserBlock.target_id).filter_by(
|
||||
user_id=v.id).subquery()
|
||||
blocked = g.db.query(
|
||||
UserBlock.user_id).filter_by(
|
||||
target_id=v.id).subquery()
|
||||
posts = posts.filter(
|
||||
Submission.author_id.notin_(blocking),
|
||||
Submission.author_id.notin_(blocked)
|
||||
)
|
||||
|
||||
if not (v and v.changelogsub):
|
||||
posts=posts.join(Submission.submission_aux)
|
||||
posts=posts.filter(not_(SubmissionAux.title.ilike(f'%[changelog]%')))
|
||||
|
||||
if v and filter_words:
|
||||
for word in filter_words:
|
||||
posts=posts.filter(not_(SubmissionAux.title.ilike(f'%{word}%')))
|
||||
|
||||
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)
|
||||
|
||||
gt = kwargs.get("gt")
|
||||
lt = kwargs.get("lt")
|
||||
|
||||
if gt:
|
||||
posts = posts.filter(Submission.created_utc > gt)
|
||||
|
||||
if lt:
|
||||
posts = posts.filter(Submission.created_utc < lt)
|
||||
|
||||
if sort == "hot":
|
||||
posts = sorted(posts.all(), key=lambda x: x.hotscore, reverse=True)
|
||||
elif sort == "new":
|
||||
posts = posts.order_by(Submission.created_utc.desc()).all()
|
||||
elif sort == "old":
|
||||
posts = posts.order_by(Submission.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
posts = sorted(posts.all(), key=lambda x: x.score_disputed, reverse=True)
|
||||
elif sort == "top":
|
||||
posts = posts.order_by(Submission.score.desc()).all()
|
||||
elif sort == "bottom":
|
||||
posts = posts.order_by(Submission.score.asc()).all()
|
||||
elif sort == "comments":
|
||||
posts = posts.order_by(Submission.comment_count.desc()).all()
|
||||
elif sort == "random":
|
||||
posts = posts.all()
|
||||
posts = random.sample(posts, k=len(posts))
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
firstrange = 25 * (page - 1)
|
||||
secondrange = firstrange+100
|
||||
posts = posts[firstrange:secondrange]
|
||||
|
||||
if v and v.hidevotedon: posts = [x for x in posts if x.voted == 0]
|
||||
|
||||
if page == 1: posts = g.db.query(Submission).filter_by(stickied=True).all() + posts
|
||||
|
||||
words = [' captainmeta4 ', ' cm4 ', ' dissident001 ', ' ladine ']
|
||||
|
||||
for post in posts:
|
||||
if post.author and post.author.admin_level == 0:
|
||||
for word in words:
|
||||
if word in post.title.lower():
|
||||
posts.remove(post)
|
||||
break
|
||||
|
||||
if random.random() < 0.02:
|
||||
for post in posts:
|
||||
if post.author and post.author.shadowbanned:
|
||||
rand = random.randint(500,1400)
|
||||
vote = Vote(user_id=rand,
|
||||
vote_type=random.choice([-1, 1]),
|
||||
submission_id=post.id)
|
||||
g.db.add(vote)
|
||||
try: g.db.flush()
|
||||
except: g.db.rollback()
|
||||
post.upvotes = post.ups
|
||||
post.downvotes = post.downs
|
||||
post.views = post.views + random.randint(7,10)
|
||||
g.db.add(post)
|
||||
|
||||
posts = [x for x in posts if not (x.author and x.author.shadowbanned) or (v and v.id == x.author_id)][:26]
|
||||
|
||||
if ids_only:
|
||||
posts = [x.id for x in posts]
|
||||
return posts
|
||||
return posts
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
@app.route("/api/v1/listing", methods=["GET"])
|
||||
@auth_desired
|
||||
def front_all(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
page = int(request.args.get("page") or 1)
|
||||
|
||||
# prevent invalid paging
|
||||
page = max(page, 1)
|
||||
|
||||
if v:
|
||||
defaultsorting = v.defaultsorting
|
||||
defaulttime = v.defaulttime
|
||||
else:
|
||||
defaultsorting = "hot"
|
||||
defaulttime = "all"
|
||||
|
||||
sort=request.args.get("sort", defaultsorting)
|
||||
t=request.args.get('t', defaulttime)
|
||||
|
||||
ids = frontlist(sort=sort,
|
||||
page=page,
|
||||
t=t,
|
||||
v=v,
|
||||
gt=int(request.args.get("utc_greater_than", 0)),
|
||||
lt=int(request.args.get("utc_less_than", 0)),
|
||||
filter_words=v.filter_words if v else [],
|
||||
)
|
||||
|
||||
# check existence of next page
|
||||
next_exists = (len(ids) == 26)
|
||||
ids = ids[0:25]
|
||||
|
||||
# check if ids exist
|
||||
posts = get_posts(ids, sort=sort, v=v)
|
||||
|
||||
if request.path == "/": return render_template("home.html", v=v, listing=posts, next_exists=next_exists, sort=sort, t=t, page=page)
|
||||
else: return jsonify({"data": [x.json for x in posts], "next_exists": next_exists})
|
||||
|
||||
@cache.memoize(timeout=1500)
|
||||
def changeloglist(v=None, sort="new", page=1 ,t="all", **kwargs):
|
||||
|
||||
posts = g.db.query(Submission).options(lazyload('*')).filter_by(is_banned=False,stickied=False,private=False,).filter(Submission.deleted_utc == 0)
|
||||
|
||||
if v and v.admin_level == 0:
|
||||
blocking = g.db.query(
|
||||
UserBlock.target_id).filter_by(
|
||||
user_id=v.id).subquery()
|
||||
blocked = g.db.query(
|
||||
UserBlock.user_id).filter_by(
|
||||
target_id=v.id).subquery()
|
||||
posts = posts.filter(
|
||||
Submission.author_id.notin_(blocking),
|
||||
Submission.author_id.notin_(blocked)
|
||||
)
|
||||
|
||||
posts=posts.join(Submission.submission_aux).join(Submission.author)
|
||||
posts=posts.filter(SubmissionAux.title.ilike(f'%[changelog]%', User.admin_level == 6))
|
||||
|
||||
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)
|
||||
|
||||
gt = kwargs.get("gt")
|
||||
lt = kwargs.get("lt")
|
||||
|
||||
if gt:
|
||||
posts = posts.filter(Submission.created_utc > gt)
|
||||
|
||||
if lt:
|
||||
posts = posts.filter(Submission.created_utc < lt)
|
||||
|
||||
if sort == "hot":
|
||||
posts = sorted(posts.all(), key=lambda x: x.hotscore, reverse=True)
|
||||
elif sort == "new":
|
||||
posts = posts.order_by(Submission.created_utc.desc()).all()
|
||||
elif sort == "old":
|
||||
posts = posts.order_by(Submission.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
posts = sorted(posts.all(), key=lambda x: x.score_disputed, reverse=True)
|
||||
elif sort == "top":
|
||||
posts = posts.order_by(Submission.score.desc()).all()
|
||||
elif sort == "bottom":
|
||||
posts = posts.order_by(Submission.score.asc()).all()
|
||||
elif sort == "comments":
|
||||
posts = posts.order_by(Submission.comment_count.desc()).all()
|
||||
elif sort == "random":
|
||||
posts = posts.all()
|
||||
posts = random.sample(posts, k=len(posts))
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
firstrange = 25 * (page - 1)
|
||||
secondrange = firstrange+26
|
||||
posts = posts[firstrange:secondrange]
|
||||
|
||||
posts = [x.id for x in posts]
|
||||
return posts
|
||||
|
||||
@app.route("/changelog", methods=["GET"])
|
||||
@app.route("/api/v1/changelog", methods=["GET"])
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def changelog(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
page = int(request.args.get("page") or 1)
|
||||
page = max(page, 1)
|
||||
|
||||
sort=request.args.get("sort", "new")
|
||||
t=request.args.get('t', "all")
|
||||
|
||||
ids = changeloglist(sort=sort,
|
||||
page=page,
|
||||
t=t,
|
||||
v=v,
|
||||
gt=int(request.args.get("utc_greater_than", 0)),
|
||||
lt=int(request.args.get("utc_less_than", 0)),
|
||||
)
|
||||
|
||||
# check existence of next page
|
||||
next_exists = (len(ids) == 26)
|
||||
ids = ids[0:25]
|
||||
|
||||
# check if ids exist
|
||||
posts = get_posts(ids, sort=sort, v=v)
|
||||
|
||||
return {'html': lambda: render_template("changelog.html",
|
||||
v=v,
|
||||
listing=posts,
|
||||
next_exists=next_exists,
|
||||
sort=sort,
|
||||
t=t,
|
||||
page=page,
|
||||
),
|
||||
'api': lambda: jsonify({"data": [x.json for x in posts],
|
||||
"next_exists": next_exists
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@app.route("/random", methods=["GET"])
|
||||
@auth_desired
|
||||
def random_post(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
x = g.db.query(Submission).filter(Submission.deleted_utc == 0, Submission.is_banned == False, Submission.score > 20)
|
||||
|
||||
total = x.count()
|
||||
n = random.randint(0, total - 1)
|
||||
|
||||
post = x.order_by(Submission.id.asc()).offset(n).limit(1).first()
|
||||
return redirect(post.permalink)
|
||||
|
||||
@cache.memoize(600)
|
||||
def comment_idlist(page=1, v=None, nsfw=False, sort="new", t="all", **kwargs):
|
||||
|
||||
posts = g.db.query(Submission).options(lazyload('*'))
|
||||
|
||||
posts = posts.subquery()
|
||||
|
||||
comments = g.db.query(Comment).options(lazyload('*'))
|
||||
|
||||
if v and v.admin_level <= 3:
|
||||
blocking = g.db.query(
|
||||
UserBlock.target_id).filter_by(
|
||||
user_id=v.id).subquery()
|
||||
blocked = g.db.query(
|
||||
UserBlock.user_id).filter_by(
|
||||
target_id=v.id).subquery()
|
||||
|
||||
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)
|
||||
|
||||
comments = comments.join(posts, Comment.parent_submission == posts.c.id)
|
||||
|
||||
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()).all()
|
||||
elif sort == "old":
|
||||
comments = comments.order_by(Comment.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
comments = sorted(comments.all(), key=lambda x: x.score_disputed, reverse=True)
|
||||
elif sort == "top":
|
||||
comments = comments.order_by(Comment.score.desc()).all()
|
||||
elif sort == "bottom":
|
||||
comments = comments.order_by(Comment.score.asc()).all()
|
||||
|
||||
firstrange = 25 * (page - 1)
|
||||
secondrange = firstrange+100
|
||||
comments = comments[firstrange:secondrange]
|
||||
|
||||
comments = [x.id for x in comments if not (x.author and x.author.shadowbanned) or (v and v.id == x.author_id)]
|
||||
|
||||
return comments[:26]
|
||||
|
||||
@app.route("/comments", methods=["GET"])
|
||||
@app.route("/api/v1/front/comments", methods=["GET"])
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def all_comments(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
page = int(request.args.get("page", 1))
|
||||
|
||||
sort=request.args.get("sort", "new")
|
||||
t=request.args.get("t", "all")
|
||||
|
||||
idlist = comment_idlist(v=v,
|
||||
page=page,
|
||||
sort=sort,
|
||||
t=t,
|
||||
)
|
||||
|
||||
comments = get_comments(idlist, v=v)
|
||||
|
||||
next_exists = len(idlist) == 26
|
||||
|
||||
idlist = idlist[0:25]
|
||||
|
||||
board = get_board(1)
|
||||
return {"html": lambda: render_template("home_comments.html",
|
||||
v=v,
|
||||
sort=sort,
|
||||
t=t,
|
||||
page=page,
|
||||
comments=comments,
|
||||
standalone=True,
|
||||
next_exists=next_exists),
|
||||
"api": lambda: jsonify({"data": [x.json for x in comments]})}
|
|
@ -0,0 +1,609 @@
|
|||
from urllib.parse import urlencode
|
||||
from ruqqus.mail import *
|
||||
from ruqqus.__main__ import app, limiter
|
||||
|
||||
valid_username_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$")
|
||||
valid_password_regex = re.compile("^.{8,100}$")
|
||||
|
||||
|
||||
@app.route("/login", methods=["GET"])
|
||||
@no_cors
|
||||
@auth_desired
|
||||
def login_get(v):
|
||||
|
||||
redir = request.args.get("redirect", "/")
|
||||
if v:
|
||||
return redirect(redir)
|
||||
|
||||
return render_template("login.html",
|
||||
failed=False,
|
||||
i=random_image(),
|
||||
redirect=redir)
|
||||
|
||||
|
||||
def check_for_alts(current_id):
|
||||
# account history
|
||||
past_accs = set(session.get("history", []))
|
||||
past_accs.add(current_id)
|
||||
session["history"] = list(past_accs)
|
||||
|
||||
# record alts
|
||||
for past_id in session["history"]:
|
||||
|
||||
if past_id == current_id:
|
||||
continue
|
||||
|
||||
check1 = g.db.query(Alt).filter_by(
|
||||
user1=current_id, user2=past_id).first()
|
||||
check2 = g.db.query(Alt).filter_by(
|
||||
user1=past_id, user2=current_id).first()
|
||||
|
||||
if not check1 and not check2:
|
||||
|
||||
try:
|
||||
new_alt = Alt(user1=past_id,
|
||||
user2=current_id)
|
||||
g.db.add(new_alt)
|
||||
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
# login post procedure
|
||||
|
||||
|
||||
@no_cors
|
||||
@app.route("/login", methods=["POST"])
|
||||
@limiter.limit("6/minute")
|
||||
def login_post():
|
||||
|
||||
username = request.form.get("username")
|
||||
|
||||
if "@" in username:
|
||||
account = g.db.query(User).filter(
|
||||
User.email.ilike(username),
|
||||
User.is_deleted == False).first()
|
||||
else:
|
||||
account = get_user(username, graceful=True)
|
||||
|
||||
if not account:
|
||||
time.sleep(random.uniform(0, 2))
|
||||
return render_template("login.html", failed=True, i=random_image())
|
||||
|
||||
if account.is_deleted:
|
||||
time.sleep(random.uniform(0, 2))
|
||||
return render_template("login.html", failed=True, i=random_image())
|
||||
|
||||
# test password
|
||||
|
||||
if request.form.get("password"):
|
||||
|
||||
if not account.verifyPass(request.form.get("password")):
|
||||
time.sleep(random.uniform(0, 2))
|
||||
return render_template("login.html", failed=True, i=random_image())
|
||||
|
||||
if account.mfa_secret:
|
||||
now = int(time.time())
|
||||
hash = generate_hash(f"{account.id}+{now}+2fachallenge")
|
||||
return render_template("login_2fa.html",
|
||||
v=account,
|
||||
time=now,
|
||||
hash=hash,
|
||||
i=random_image(),
|
||||
redirect=request.form.get("redirect", "/")
|
||||
)
|
||||
elif request.form.get("2fa_token", "x"):
|
||||
now = int(time.time())
|
||||
|
||||
if now - int(request.form.get("time")) > 600:
|
||||
return redirect('/login')
|
||||
|
||||
formhash = request.form.get("hash")
|
||||
if not validate_hash(f"{account.id}+{request.form.get('time')}+2fachallenge",
|
||||
formhash
|
||||
):
|
||||
return redirect("/login")
|
||||
|
||||
if not account.validate_2fa(request.form.get("2fa_token", "").strip()):
|
||||
hash = generate_hash(f"{account.id}+{time}+2fachallenge")
|
||||
return render_template("login_2fa.html",
|
||||
v=account,
|
||||
time=now,
|
||||
hash=hash,
|
||||
failed=True,
|
||||
i=random_image()
|
||||
)
|
||||
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
if account.is_banned and account.unban_utc > 0 and time.time() > account.unban_utc:
|
||||
account.unban()
|
||||
|
||||
# set session and user id
|
||||
session["user_id"] = account.id
|
||||
session["session_id"] = token_hex(16)
|
||||
session["login_nonce"] = account.login_nonce
|
||||
session.permanent = True
|
||||
|
||||
check_for_alts(account.id)
|
||||
|
||||
account.refresh_selfset_badges()
|
||||
|
||||
# check for previous page
|
||||
|
||||
redir = request.form.get("redirect", "/")
|
||||
if redir:
|
||||
return redirect(redir)
|
||||
else:
|
||||
return redirect(account.url)
|
||||
|
||||
|
||||
@app.route("/me", methods=["GET"])
|
||||
@auth_required
|
||||
def me(v):
|
||||
return redirect(v.url)
|
||||
|
||||
|
||||
@app.route("/logout", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def logout(v):
|
||||
|
||||
session.pop("user_id", None)
|
||||
session.pop("session_id", None)
|
||||
|
||||
return "", 204
|
||||
|
||||
# signing up
|
||||
|
||||
|
||||
@app.route("/signup", methods=["GET"])
|
||||
@no_cors
|
||||
@auth_desired
|
||||
def sign_up_get(v):
|
||||
board = g.db.query(Board).filter_by(id=1).first()
|
||||
if board.disablesignups: return "Signups are disable for the time being.", 403
|
||||
|
||||
if v:
|
||||
return redirect("/")
|
||||
|
||||
agent = request.headers.get("User-Agent", None)
|
||||
if not agent:
|
||||
abort(403)
|
||||
|
||||
# check for referral in link
|
||||
ref_id = None
|
||||
ref = request.args.get("ref", None)
|
||||
if ref:
|
||||
ref_user = g.db.query(User).filter(User.username.ilike(ref)).first()
|
||||
|
||||
else:
|
||||
ref_user = None
|
||||
|
||||
if ref_user and (ref_user.id in session.get("history", [])):
|
||||
return render_template("sign_up_failed_ref.html",
|
||||
i=random_image())
|
||||
|
||||
# check tor
|
||||
# if request.headers.get("CF-IPCountry")=="T1":
|
||||
# return render_template("sign_up_tor.html",
|
||||
# i=random_image(),
|
||||
# ref_user=ref_user)
|
||||
|
||||
# Make a unique form key valid for one account creation
|
||||
now = int(time.time())
|
||||
token = token_hex(16)
|
||||
session["signup_token"] = token
|
||||
ip = request.remote_addr
|
||||
|
||||
formkey_hashstr = str(now) + token + agent
|
||||
|
||||
# formkey is a hash of session token, timestamp, and IP address
|
||||
formkey = hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"),
|
||||
msg=bytes(formkey_hashstr, "utf-16"),
|
||||
digestmod='md5'
|
||||
).hexdigest()
|
||||
|
||||
redir = request.args.get("redirect", None)
|
||||
|
||||
error = request.args.get("error", None)
|
||||
|
||||
return render_template("sign_up.html",
|
||||
formkey=formkey,
|
||||
now=now,
|
||||
i=random_image(),
|
||||
redirect=redir,
|
||||
ref_user=ref_user,
|
||||
error=error,
|
||||
hcaptcha=app.config["HCAPTCHA_SITEKEY"]
|
||||
)
|
||||
|
||||
# signup api
|
||||
|
||||
|
||||
@app.route("/signup", methods=["POST"])
|
||||
@no_cors
|
||||
@auth_desired
|
||||
def sign_up_post(v):
|
||||
board = g.db.query(Board).filter_by(id=1).first()
|
||||
if board.disablesignups: return "Signups are disable for the time being.", 403
|
||||
|
||||
if v:
|
||||
abort(403)
|
||||
|
||||
agent = request.headers.get("User-Agent", None)
|
||||
if not agent:
|
||||
abort(403)
|
||||
|
||||
# check tor
|
||||
# if request.headers.get("CF-IPCountry")=="T1":
|
||||
# return render_template("sign_up_tor.html",
|
||||
# i=random_image()
|
||||
# )
|
||||
|
||||
form_timestamp = request.form.get("now", '0')
|
||||
form_formkey = request.form.get("formkey", "none")
|
||||
|
||||
submitted_token = session.get("signup_token", "")
|
||||
if not submitted_token:
|
||||
abort(400)
|
||||
|
||||
correct_formkey_hashstr = form_timestamp + submitted_token + agent
|
||||
|
||||
correct_formkey = hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"),
|
||||
msg=bytes(correct_formkey_hashstr, "utf-16"),
|
||||
digestmod='md5'
|
||||
).hexdigest()
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
username = request.form.get("username")
|
||||
|
||||
# define function that takes an error message and generates a new signup
|
||||
# form
|
||||
def new_signup(error):
|
||||
|
||||
args = {"error": error}
|
||||
if request.form.get("referred_by"):
|
||||
user = g.db.query(User).filter_by(
|
||||
id=request.form.get("referred_by")).first()
|
||||
if user:
|
||||
args["ref"] = user.username
|
||||
|
||||
return redirect(f"/signup?{urlencode(args)}")
|
||||
|
||||
if app.config["DISABLE_SIGNUPS"]:
|
||||
return new_signup("New account registration is currently closed. Please come back later.")
|
||||
|
||||
if now - int(form_timestamp) < 5:
|
||||
#print(f"signup fail - {username } - too fast")
|
||||
return new_signup("There was a problem. Please try again.")
|
||||
|
||||
if not hmac.compare_digest(correct_formkey, form_formkey):
|
||||
#print(f"signup fail - {username } - mismatched formkeys")
|
||||
return new_signup("There was a problem. Please try again.")
|
||||
|
||||
# check for matched passwords
|
||||
if not request.form.get(
|
||||
"password") == request.form.get("password_confirm"):
|
||||
return new_signup("Passwords did not match. Please try again.")
|
||||
|
||||
# check username/pass conditions
|
||||
if not re.match(valid_username_regex, request.form.get("username")):
|
||||
#print(f"signup fail - {username } - mismatched passwords")
|
||||
return new_signup("Invalid username")
|
||||
|
||||
if not re.match(valid_password_regex, request.form.get("password")):
|
||||
#print(f"signup fail - {username } - invalid password")
|
||||
return new_signup("Password must be between 8 and 100 characters.")
|
||||
|
||||
# if not re.match(valid_email_regex, request.form.get("email")):
|
||||
# return new_signup("That's not a valid email.")
|
||||
|
||||
# Check for existing acocunts
|
||||
email = request.form.get("email")
|
||||
email = email.strip()
|
||||
if not email:
|
||||
email = None
|
||||
|
||||
#counteract gmail username+2 and extra period tricks - convert submitted email to actual inbox
|
||||
if email and email.endswith("@gmail.com"):
|
||||
gmail_username=email.split('@')[0]
|
||||
gmail_username=gmail_username.split('+')[0]
|
||||
gmail_username=gmail_username.replace('.','')
|
||||
email=f"{gmail_username}@gmail.com"
|
||||
|
||||
|
||||
existing_account = get_user(request.form.get("username"), graceful=True)
|
||||
if existing_account and existing_account.reserved:
|
||||
return redirect(existing_account.permalink)
|
||||
|
||||
if existing_account or (email and g.db.query(
|
||||
User).filter(User.email.ilike(email)).first()):
|
||||
# #print(f"signup fail - {username } - email already exists")
|
||||
return new_signup(
|
||||
"An account with that username or email already exists.")
|
||||
|
||||
# check bot
|
||||
if app.config.get("HCAPTCHA_SITEKEY"):
|
||||
token = request.form.get("h-captcha-response")
|
||||
if not token:
|
||||
return new_signup("Unable to verify captcha [1].")
|
||||
|
||||
data = {"secret": app.config["HCAPTCHA_SECRET"],
|
||||
"response": token,
|
||||
"sitekey": app.config["HCAPTCHA_SITEKEY"]}
|
||||
url = "https://hcaptcha.com/siteverify"
|
||||
|
||||
x = requests.post(url, data=data)
|
||||
|
||||
if not x.json()["success"]:
|
||||
#print(x.json())
|
||||
return new_signup("Unable to verify captcha [2].")
|
||||
|
||||
# kill tokens
|
||||
session.pop("signup_token")
|
||||
|
||||
# get referral
|
||||
ref_id = int(request.form.get("referred_by", 0))
|
||||
|
||||
# upgrade user badge
|
||||
if ref_id:
|
||||
ref_user = g.db.query(User).options(
|
||||
lazyload('*')).filter_by(id=ref_id).first()
|
||||
if ref_user:
|
||||
ref_user.refresh_selfset_badges()
|
||||
g.db.add(ref_user)
|
||||
|
||||
# make new user
|
||||
try:
|
||||
new_user = User(
|
||||
username=username,
|
||||
original_username = username,
|
||||
password=request.form.get("password"),
|
||||
email=email,
|
||||
created_utc=int(time.time()),
|
||||
creation_ip=request.remote_addr,
|
||||
referred_by=ref_id or None,
|
||||
tos_agreed_utc=int(time.time()),
|
||||
creation_region=request.headers.get("cf-ipcountry"),
|
||||
ban_evade = int(any([x.is_banned for x in g.db.query(User).filter(User.id.in_(tuple(session.get("history", [])))).all() if x]))
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
#print(e)
|
||||
return new_signup("Please enter a valid email")
|
||||
|
||||
g.db.add(new_user)
|
||||
g.db.commit()
|
||||
|
||||
# give a beta badge
|
||||
beta_badge = Badge(user_id=new_user.id,
|
||||
badge_id=6)
|
||||
|
||||
g.db.add(beta_badge)
|
||||
|
||||
# check alts
|
||||
|
||||
check_for_alts(new_user.id)
|
||||
|
||||
# send welcome/verify email
|
||||
if email:
|
||||
send_verification_email(new_user)
|
||||
|
||||
# send welcome message
|
||||
send_notification(1046, new_user, "Dude bussy lmao")
|
||||
|
||||
session["user_id"] = new_user.id
|
||||
session["session_id"] = token_hex(16)
|
||||
|
||||
redir = request.form.get("redirect", None)
|
||||
|
||||
# #print(f"Signup event: @{new_user.username}")
|
||||
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@app.route("/forgot", methods=["GET"])
|
||||
def get_forgot():
|
||||
|
||||
return render_template("forgot_password.html",
|
||||
i=random_image()
|
||||
)
|
||||
|
||||
|
||||
@app.route("/forgot", methods=["POST"])
|
||||
def post_forgot():
|
||||
|
||||
username = request.form.get("username").lstrip('@')
|
||||
email = request.form.get("email",'').strip()
|
||||
|
||||
email=email.replace("_","\_")
|
||||
|
||||
user = g.db.query(User).filter(
|
||||
User.username.ilike(username),
|
||||
User.email.ilike(email),
|
||||
User.is_deleted == False).first()
|
||||
|
||||
if user:
|
||||
# generate url
|
||||
now = int(time.time())
|
||||
token = generate_hash(f"{user.id}+{now}+forgot+{user.login_nonce}")
|
||||
url = f"https://{app.config['SERVER_NAME']}/reset?id={user.id}&time={now}&token={token}"
|
||||
|
||||
send_mail(to_address=user.email,
|
||||
subject="Drama - Password Reset Request",
|
||||
html=render_template("email/password_reset.html",
|
||||
action_url=url,
|
||||
v=user)
|
||||
)
|
||||
|
||||
return render_template("forgot_password.html",
|
||||
msg="If the username and email matches an account, you will be sent a password reset email. You have ten minutes to complete the password reset process.",
|
||||
i=random_image())
|
||||
|
||||
|
||||
@app.route("/reset", methods=["GET"])
|
||||
def get_reset():
|
||||
|
||||
user_id = request.args.get("id")
|
||||
timestamp = int(request.args.get("time",0))
|
||||
token = request.args.get("token")
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
if now - timestamp > 600:
|
||||
return render_template("message.html",
|
||||
title="Password reset link expired",
|
||||
error="That password reset link has expired.")
|
||||
|
||||
user = g.db.query(User).filter_by(id=user_id).first()
|
||||
|
||||
if not validate_hash(f"{user_id}+{timestamp}+forgot+{user.login_nonce}", token):
|
||||
abort(400)
|
||||
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
reset_token = generate_hash(f"{user.id}+{timestamp}+reset+{user.login_nonce}")
|
||||
|
||||
return render_template("reset_password.html",
|
||||
v=user,
|
||||
token=reset_token,
|
||||
time=timestamp,
|
||||
i=random_image()
|
||||
)
|
||||
|
||||
|
||||
@app.route("/reset", methods=["POST"])
|
||||
@auth_desired
|
||||
def post_reset(v):
|
||||
if v:
|
||||
return redirect('/')
|
||||
|
||||
user_id = request.form.get("user_id")
|
||||
timestamp = int(request.form.get("time"))
|
||||
token = request.form.get("token")
|
||||
|
||||
password = request.form.get("password")
|
||||
confirm_password = request.form.get("confirm_password")
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
if now - timestamp > 600:
|
||||
return render_template("message.html",
|
||||
title="Password reset expired",
|
||||
error="That password reset form has expired.")
|
||||
|
||||
user = g.db.query(User).filter_by(id=user_id).first()
|
||||
|
||||
if not validate_hash(f"{user_id}+{timestamp}+reset+{user.login_nonce}", token):
|
||||
abort(400)
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if not password == confirm_password:
|
||||
return render_template("reset_password.html",
|
||||
v=user,
|
||||
token=token,
|
||||
time=timestamp,
|
||||
i=random_image(),
|
||||
error="Passwords didn't match.")
|
||||
|
||||
user.passhash = hash_password(password)
|
||||
g.db.add(user)
|
||||
|
||||
return render_template("message_success.html",
|
||||
title="Password reset successful!",
|
||||
message="Login normally to access your account.")
|
||||
|
||||
@app.route("/lost_2fa")
|
||||
@auth_desired
|
||||
def lost_2fa(v):
|
||||
|
||||
return render_template(
|
||||
"lost_2fa.html",
|
||||
i=random_image(),
|
||||
v=v
|
||||
)
|
||||
|
||||
@app.route("/request_2fa_disable", methods=["POST"])
|
||||
@limiter.limit("6/minute")
|
||||
def request_2fa_disable():
|
||||
|
||||
username=request.form.get("username")
|
||||
user=get_user(username, graceful=True)
|
||||
if not user or not user.email or not user.mfa_secret:
|
||||
return render_template("message.html",
|
||||
title="Removal request received",
|
||||
message="If username, password, and email match, we will send you an email.")
|
||||
|
||||
|
||||
email=request.form.get("email")
|
||||
if email and email.endswith("@gmail.com"):
|
||||
gmail_username=email.split('@')[0]
|
||||
gmail_username=gmail_username.split('+')[0]
|
||||
gmail_username=gmail_username.replace('.','')
|
||||
email=f"{gmail_username}@gmail.com"
|
||||
|
||||
if email != user.email:
|
||||
return render_template("message.html",
|
||||
title="Removal request received",
|
||||
message="If username, password, and email match, we will send you an email.")
|
||||
|
||||
|
||||
password =request.form.get("password")
|
||||
if not user.verifyPass(password):
|
||||
return render_template("message.html",
|
||||
title="Removal request received",
|
||||
message="If username, password, and email match, we will send you an email.")
|
||||
|
||||
#compute token
|
||||
valid=int(time.time())+60*60*24*3
|
||||
token=generate_hash(f"{user.id}+{user.username}+disable2fa+{valid}+{user.mfa_secret}+{user.login_nonce}")
|
||||
|
||||
action_url=f"https://{app.config['SERVER_NAME']}/reset_2fa?id={user.base36id}&t={valid}&token={token}"
|
||||
|
||||
send_mail(to_address=user.email,
|
||||
subject="Drama - 2FA Removal Request",
|
||||
html=render_template("email/2fa_remove.html",
|
||||
action_url=action_url,
|
||||
v=user)
|
||||
)
|
||||
|
||||
return render_template("message.html",
|
||||
title="Removal request received",
|
||||
message="If username, password, and email match, we will send you an email.")
|
||||
|
||||
@app.route("/reset_2fa", methods=["GET"])
|
||||
def reset_2fa():
|
||||
|
||||
now=int(time.time())
|
||||
t=int(request.args.get("t"))
|
||||
|
||||
if now<t:
|
||||
return render_template("message.html",
|
||||
title="Inactive Link",
|
||||
error="That link isn't active yet. Try again later.")
|
||||
elif now > t+3600*24:
|
||||
return render_template("message.html",
|
||||
title="Expired Link",
|
||||
error="That link has expired.")
|
||||
|
||||
token=request.args.get("token")
|
||||
uid=request.args.get("id")
|
||||
|
||||
user=get_account(uid)
|
||||
|
||||
if not validate_hash(f"{user.id}+{user.username}+disable2fa+{t}+{user.mfa_secret}+{user.login_nonce}", token):
|
||||
abort(403)
|
||||
|
||||
#validation successful, remove 2fa
|
||||
user.mfa_secret=None
|
||||
|
||||
g.db.add(user)
|
||||
g.db.commit()
|
||||
|
||||
return render_template("message_success.html",
|
||||
title="Two-factor authentication removed.",
|
||||
message="Login normally to access your account.")
|
|
@ -0,0 +1,480 @@
|
|||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.helpers.alerts import *
|
||||
from ruqqus.helpers.get import *
|
||||
from ruqqus.classes import *
|
||||
from flask import *
|
||||
from ruqqus.__main__ import app
|
||||
|
||||
SCOPES = {
|
||||
'identity': 'See your username',
|
||||
'create': 'Save posts and comments as you',
|
||||
'read': 'View Drama as you, including private or restricted content',
|
||||
'update': 'Edit your posts and comments',
|
||||
'delete': 'Delete your posts and comments',
|
||||
'vote': 'Cast votes as you',
|
||||
'guildmaster': 'Perform Badmin actions'
|
||||
}
|
||||
|
||||
|
||||
@app.route("/oauth/authorize", methods=["GET"])
|
||||
@auth_required
|
||||
def oauth_authorize_prompt(v):
|
||||
'''
|
||||
This page takes the following URL parameters:
|
||||
* client_id - Your application client ID
|
||||
* scope - Comma-separated list of scopes. Scopes are described above
|
||||
* redirect_uri - Your redirect link
|
||||
* state - Your anti-csrf token
|
||||
'''
|
||||
|
||||
client_id = request.args.get("client_id")
|
||||
|
||||
application = get_application(client_id)
|
||||
if not application:
|
||||
return jsonify({"oauth_error": "Invalid `client_id`"}), 401
|
||||
|
||||
if application.is_banned:
|
||||
return jsonify({"oauth_error": f"Application `{application.app_name}` is suspended."}), 403
|
||||
|
||||
scopes_txt = request.args.get('scope', "")
|
||||
|
||||
scopes = scopes_txt.split(',')
|
||||
if not scopes:
|
||||
return jsonify(
|
||||
{"oauth_error": "One or more scopes must be specified as a comma-separated list."}), 400
|
||||
|
||||
for scope in scopes:
|
||||
if scope not in SCOPES:
|
||||
return jsonify({"oauth_error": f"The provided scope `{scope}` is not valid."}), 400
|
||||
|
||||
if any(x in scopes for x in ["create", "update",
|
||||
"guildmaster"]) and "identity" not in scopes:
|
||||
return jsonify({"oauth_error": f"`identity` scope required when requesting `create`, `update`, or `guildmaster` scope."}), 400
|
||||
|
||||
redirect_uri = request.args.get("redirect_uri")
|
||||
if not redirect_uri:
|
||||
return jsonify({"oauth_error": f"`redirect_uri` must be provided."}), 400
|
||||
|
||||
valid_redirect_uris = [x.strip()
|
||||
for x in application.redirect_uri.split(",")]
|
||||
|
||||
if redirect_uri not in valid_redirect_uris:
|
||||
return jsonify({"oauth_error": "Invalid redirect_uri"}), 400
|
||||
|
||||
state = request.args.get("state")
|
||||
if not state:
|
||||
return jsonify({'oauth_error': 'state argument required'}), 400
|
||||
|
||||
permanent = bool(request.args.get("permanent"))
|
||||
|
||||
return render_template("oauth.html",
|
||||
v=v,
|
||||
application=application,
|
||||
SCOPES=SCOPES,
|
||||
state=state,
|
||||
scopes=scopes,
|
||||
scopes_txt=scopes_txt,
|
||||
redirect_uri=redirect_uri,
|
||||
permanent=int(permanent),
|
||||
i=random_image()
|
||||
)
|
||||
|
||||
|
||||
@app.route("/oauth/authorize", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def oauth_authorize_post(v):
|
||||
|
||||
client_id = request.form.get("client_id")
|
||||
scopes_txt = request.form.get("scopes")
|
||||
state = request.form.get("state")
|
||||
redirect_uri = request.form.get("redirect_uri")
|
||||
|
||||
application = get_application(client_id)
|
||||
if not application:
|
||||
return jsonify({"oauth_error": "Invalid `client_id`"}), 401
|
||||
if application.is_banned:
|
||||
return jsonify({"oauth_error": f"Application `{application.app_name}` is suspended."}), 403
|
||||
|
||||
valid_redirect_uris = [x.strip()
|
||||
for x in application.redirect_uri.split(",")]
|
||||
if redirect_uri not in valid_redirect_uris:
|
||||
return jsonify({"oauth_error": "Invalid redirect_uri"}), 400
|
||||
|
||||
scopes = scopes_txt.split(',')
|
||||
if not scopes:
|
||||
return jsonify(
|
||||
{"oauth_error": "One or more scopes must be specified as a comma-separated list"}), 400
|
||||
|
||||
for scope in scopes:
|
||||
if scope not in SCOPES:
|
||||
return jsonify({"oauth_error": f"The provided scope `{scope}` is not valid."}), 400
|
||||
|
||||
if any(x in scopes for x in ["create", "update",
|
||||
"guildmaster"]) and "identity" not in scopes:
|
||||
return jsonify({"oauth_error": f"`identity` scope required when requesting `create`, `update`, or `guildmaster` scope."}), 400
|
||||
|
||||
if not state:
|
||||
return jsonify({'oauth_error': 'state argument required'}), 400
|
||||
|
||||
permanent = bool(int(request.values.get("permanent", 0)))
|
||||
|
||||
new_auth = ClientAuth(
|
||||
oauth_client=application.id,
|
||||
oauth_code=secrets.token_urlsafe(128)[0:128],
|
||||
user_id=v.id,
|
||||
scope_identity="identity" in scopes,
|
||||
scope_create="create" in scopes,
|
||||
scope_read="read" in scopes,
|
||||
scope_update="update" in scopes,
|
||||
scope_delete="delete" in scopes,
|
||||
scope_vote="vote" in scopes,
|
||||
scope_guildmaster="guildmaster" in scopes,
|
||||
refresh_token=secrets.token_urlsafe(128)[0:128] if permanent else None
|
||||
)
|
||||
|
||||
g.db.add(new_auth)
|
||||
|
||||
return redirect(f"{redirect_uri}?code={new_auth.oauth_code}&scopes={scopes_txt}&state={state}")
|
||||
|
||||
|
||||
@app.route("/oauth/grant", methods=["POST"])
|
||||
def oauth_grant():
|
||||
'''
|
||||
This endpoint takes the following parameters:
|
||||
* code - The code parameter provided in the redirect
|
||||
* client_id - Your client ID
|
||||
* client_secret - your client secret
|
||||
'''
|
||||
|
||||
application = g.db.query(OauthApp).filter_by(
|
||||
client_id=request.values.get("client_id"),
|
||||
client_secret=request.values.get("client_secret")).first()
|
||||
if not application:
|
||||
return jsonify(
|
||||
{"oauth_error": "Invalid `client_id` or `client_secret`"}), 401
|
||||
if application.is_banned:
|
||||
return jsonify({"oauth_error": f"Application `{application.app_name}` is suspended."}), 403
|
||||
|
||||
if request.values.get("grant_type") == "code":
|
||||
|
||||
code = request.values.get("code")
|
||||
if not code:
|
||||
return jsonify({"oauth_error": "code required"}), 400
|
||||
|
||||
auth = g.db.query(ClientAuth).filter_by(
|
||||
oauth_code=code,
|
||||
access_token=None,
|
||||
oauth_client=application.id
|
||||
).first()
|
||||
|
||||
if not auth:
|
||||
return jsonify({"oauth_error": "Invalid code"}), 401
|
||||
|
||||
auth.oauth_code = None
|
||||
auth.access_token = secrets.token_urlsafe(128)[0:128]
|
||||
auth.access_token_expire_utc = int(time.time()) + 60 * 60
|
||||
|
||||
g.db.add(auth)
|
||||
|
||||
g.db.commit()
|
||||
|
||||
data = {
|
||||
"access_token": auth.access_token,
|
||||
"scopes": auth.scopelist,
|
||||
"expires_at": auth.access_token_expire_utc,
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
|
||||
if auth.refresh_token:
|
||||
data["refresh_token"] = auth.refresh_token
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
elif request.values.get("grant_type") == "refresh":
|
||||
|
||||
refresh_token = request.values.get('refresh_token')
|
||||
if not refresh_token:
|
||||
return jsonify({"oauth_error": "refresh_token required"}), 401
|
||||
|
||||
auth = g.db.query(ClientAuth).filter_by(
|
||||
refresh_token=refresh_token,
|
||||
oauth_code=None,
|
||||
oauth_client=application.id
|
||||
).first()
|
||||
|
||||
if not auth:
|
||||
return jsonify({"oauth_error": "Invalid refresh_token"}), 401
|
||||
|
||||
auth.access_token = secrets.token_urlsafe(128)[0:128]
|
||||
auth.access_token_expire_utc = int(time.time()) + 60 * 60
|
||||
|
||||
g.db.add(auth)
|
||||
|
||||
data = {
|
||||
"access_token": auth.access_token,
|
||||
"scopes": auth.scopelist,
|
||||
"expires_at": auth.access_token_expire_utc
|
||||
}
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
else:
|
||||
return jsonify({"oauth_error": f"Invalid grant_type `{request.values.get('grant_type','')}`. Expected `code` or `refresh`."}), 400
|
||||
|
||||
|
||||
@app.route("/api_keys", methods=["POST"])
|
||||
@is_not_banned
|
||||
def request_api_keys(v):
|
||||
|
||||
new_app = OauthApp(
|
||||
app_name=request.form.get('name'),
|
||||
redirect_uri=request.form.get('redirect_uri'),
|
||||
author_id=v.id,
|
||||
description=request.form.get("description")[0:256]
|
||||
)
|
||||
|
||||
g.db.add(new_app)
|
||||
|
||||
send_admin(1046, f"@{v.username} has requested API keys for `{request.form.get('name')}`. You can approve or deny the request [here](/admin/apps).")
|
||||
|
||||
return redirect('/settings/apps')
|
||||
|
||||
|
||||
@app.route("/delete_app/<aid>", methods=["POST"])
|
||||
@is_not_banned
|
||||
@validate_formkey
|
||||
def delete_oauth_app(v, aid):
|
||||
|
||||
aid = int(aid)
|
||||
app = g.db.query(OauthApp).filter_by(id=aid).first()
|
||||
|
||||
for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all():
|
||||
g.db.delete(auth)
|
||||
|
||||
g.db.commit()
|
||||
|
||||
g.db.delete(app)
|
||||
|
||||
return redirect('/apps')
|
||||
|
||||
|
||||
@app.route("/edit_app/<aid>", methods=["POST"])
|
||||
@is_not_banned
|
||||
@validate_formkey
|
||||
def edit_oauth_app(v, aid):
|
||||
|
||||
aid = int(aid)
|
||||
app = g.db.query(OauthApp).filter_by(id=aid).first()
|
||||
|
||||
app.redirect_uri = request.form.get('redirect_uri')
|
||||
app.app_name = request.form.get('name')
|
||||
app.description = request.form.get("description")[0:256]
|
||||
|
||||
g.db.add(app)
|
||||
|
||||
return redirect('/settings/apps')
|
||||
|
||||
|
||||
@app.route("/api/v1/identity")
|
||||
@auth_required
|
||||
@api("identity")
|
||||
def api_v1_identity(v):
|
||||
|
||||
return jsonify(v.json)
|
||||
|
||||
|
||||
@app.route("/admin/app/approve/<aid>", methods=["POST"])
|
||||
@admin_level_required(3)
|
||||
@validate_formkey
|
||||
def admin_app_approve(v, aid):
|
||||
|
||||
app = g.db.query(OauthApp).filter_by(id=base36decode(aid)).first()
|
||||
|
||||
app.client_id = secrets.token_urlsafe(64)[0:64]
|
||||
app.client_secret = secrets.token_urlsafe(128)[0:128]
|
||||
|
||||
g.db.add(app)
|
||||
|
||||
u = get_account(app.author_id, v=v)
|
||||
send_notification(1046, u, f"Your application `{app.app_name}` has been approved.")
|
||||
|
||||
return jsonify({"message": f"{app.app_name} approved"})
|
||||
|
||||
|
||||
@app.route("/admin/app/revoke/<aid>", methods=["POST"])
|
||||
@admin_level_required(3)
|
||||
@validate_formkey
|
||||
def admin_app_revoke(v, aid):
|
||||
|
||||
app = g.db.query(OauthApp).filter_by(id=base36decode(aid)).first()
|
||||
|
||||
app.client_id = None
|
||||
app.client_secret = None
|
||||
|
||||
g.db.add(app)
|
||||
|
||||
u = get_account(app.author_id, v=v)
|
||||
send_notification(1046, u, f"Your application `{app.app_name}` has been revoked.")
|
||||
|
||||
return jsonify({"message": f"{app.app_name} revoked"})
|
||||
|
||||
|
||||
@app.route("/admin/app/reject/<aid>", methods=["POST"])
|
||||
@admin_level_required(3)
|
||||
@validate_formkey
|
||||
def admin_app_reject(v, aid):
|
||||
|
||||
app = g.db.query(OauthApp).filter_by(id=base36decode(aid)).first()
|
||||
|
||||
for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all():
|
||||
g.db.delete(auth)
|
||||
|
||||
g.db.flush()
|
||||
u = get_account(app.author_id, v=v)
|
||||
send_notification(1046, u, f"Your application `{app.app_name}` has been rejected.")
|
||||
|
||||
g.db.delete(app)
|
||||
|
||||
return jsonify({"message": f"{app.app_name} rejected"})
|
||||
|
||||
|
||||
@app.route("/admin/app/<aid>", methods=["GET"])
|
||||
@admin_level_required(3)
|
||||
def admin_app_id(v, aid):
|
||||
|
||||
aid=base36decode(aid)
|
||||
|
||||
oauth = g.db.query(OauthApp).options(
|
||||
joinedload(
|
||||
OauthApp.author)).filter_by(
|
||||
id=aid).first()
|
||||
|
||||
pids=oauth.idlist(page=int(request.args.get("page",1)),
|
||||
)
|
||||
|
||||
next_exists=len(pids)==101
|
||||
pids=pids[0:100]
|
||||
|
||||
posts=get_posts(pids, v=v)
|
||||
|
||||
return render_template("admin/app.html",
|
||||
v=v,
|
||||
app=oauth,
|
||||
listing=posts,
|
||||
next_exists=next_exists
|
||||
)
|
||||
|
||||
@app.route("/admin/app/<aid>/comments", methods=["GET"])
|
||||
@admin_level_required(3)
|
||||
def admin_app_id_comments(v, aid):
|
||||
|
||||
aid=base36decode(aid)
|
||||
|
||||
oauth = g.db.query(OauthApp).options(
|
||||
joinedload(
|
||||
OauthApp.author)).filter_by(
|
||||
id=aid).first()
|
||||
|
||||
cids=oauth.comments_idlist(page=int(request.args.get("page",1)),
|
||||
)
|
||||
|
||||
next_exists=len(cids)==101
|
||||
cids=cids[0: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.route("/admin/apps", methods=["GET"])
|
||||
@admin_level_required(3)
|
||||
def admin_apps_list(v):
|
||||
|
||||
apps = g.db.query(OauthApp).options(
|
||||
joinedload(
|
||||
OauthApp.author)).filter(
|
||||
OauthApp.client_id==None).order_by(
|
||||
OauthApp.id.desc()).all()
|
||||
|
||||
return render_template("admin/apps.html", v=v, apps=apps)
|
||||
|
||||
|
||||
@app.route("/oauth/reroll/<aid>", methods=["POST"])
|
||||
@auth_required
|
||||
def reroll_oauth_tokens(aid, v):
|
||||
|
||||
aid = base36decode(aid)
|
||||
|
||||
a = g.db.query(OauthApp).filter_by(id=aid).first()
|
||||
|
||||
if a.author_id != v.id:
|
||||
abort(403)
|
||||
|
||||
a.client_id = secrets.token_urlsafe(64)[0:64]
|
||||
a.client_secret = secrets.token_urlsafe(128)[0:128]
|
||||
|
||||
g.db.add(a)
|
||||
|
||||
return jsonify({"message": "Tokens Rerolled",
|
||||
"id": a.client_id,
|
||||
"secret": a.client_secret
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/oauth/rescind/<aid>", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def oauth_rescind_app(aid, v):
|
||||
|
||||
aid = base36decode(aid)
|
||||
auth = g.db.query(ClientAuth).filter_by(id=aid).first()
|
||||
|
||||
if auth.user_id != v.id:
|
||||
abort(403)
|
||||
|
||||
g.db.delete(auth)
|
||||
|
||||
return jsonify({"message": f"{auth.application.app_name} Revoked"})
|
||||
|
||||
@app.route("/api/v1/release", methods=["POST"])
|
||||
@auth_required
|
||||
@api()
|
||||
def oauth_release_auth(v):
|
||||
|
||||
token=request.headers.get("Authorization").split()[1]
|
||||
|
||||
auth = g.db.query(ClientAuth).filter_by(user_id=v.id, access_token=token).first()
|
||||
if not auth:
|
||||
abort(404)
|
||||
|
||||
if not auth.refresh_token:
|
||||
abort(400)
|
||||
|
||||
auth.access_token_expire_utc=0
|
||||
g.db.add(auth)
|
||||
|
||||
return jsonify({"message":"Authorization released"})
|
||||
|
||||
@app.route("/api/v1/kill", methods=["POST"])
|
||||
@auth_required
|
||||
@api()
|
||||
def oauth_kill_auth(v):
|
||||
|
||||
token=request.headers.get("Authorization").split()[1]
|
||||
|
||||
auth = g.db.query(ClientAuth).filter_by(user_id=v.id, access_token=token).first()
|
||||
if not auth:
|
||||
abort(404)
|
||||
|
||||
g.db.delete(auth)
|
||||
|
||||
return jsonify({"message":"Authorization released"})
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,324 @@
|
|||
from ruqqus.helpers.wrappers import *
|
||||
import re
|
||||
from sqlalchemy import *
|
||||
from flask import *
|
||||
from ruqqus.classes.domains import reasons as REASONS
|
||||
from ruqqus.__main__ import app, cache
|
||||
import random
|
||||
|
||||
query_regex=re.compile("(\w+):(\S+)")
|
||||
valid_params=[
|
||||
'author',
|
||||
'domain',
|
||||
'over18'
|
||||
]
|
||||
|
||||
def searchparse(text):
|
||||
|
||||
#takes test in filter:term format and returns data
|
||||
|
||||
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
|
||||
|
||||
|
||||
@cache.memoize(300)
|
||||
def searchlisting(criteria, v=None, page=1, t="None", sort="top", b=None):
|
||||
|
||||
posts = g.db.query(Submission).options(
|
||||
lazyload('*')
|
||||
).join(
|
||||
Submission.submission_aux,
|
||||
).join(
|
||||
Submission.author
|
||||
)
|
||||
|
||||
if 'q' in criteria:
|
||||
words=criteria['q'].split()
|
||||
words=[SubmissionAux.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,
|
||||
User.is_private==False,
|
||||
User.is_deleted==False
|
||||
)
|
||||
|
||||
if 'domain' in criteria:
|
||||
domain=criteria['domain']
|
||||
posts=posts.filter(
|
||||
or_(
|
||||
SubmissionAux.url.ilike("https://"+domain+'/%'),
|
||||
SubmissionAux.url.ilike("https://"+domain+'/%'),
|
||||
SubmissionAux.url.ilike("https://"+domain),
|
||||
SubmissionAux.url.ilike("https://"+domain),
|
||||
SubmissionAux.url.ilike("https://www."+domain+'/%'),
|
||||
SubmissionAux.url.ilike("https://www."+domain+'/%'),
|
||||
SubmissionAux.url.ilike("https://www."+domain),
|
||||
SubmissionAux.url.ilike("https://www."+domain),
|
||||
SubmissionAux.url.ilike("https://old." + domain + '/%'),
|
||||
SubmissionAux.url.ilike("https://old." + domain + '/%'),
|
||||
SubmissionAux.url.ilike("https://old." + domain),
|
||||
SubmissionAux.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 = g.db.query(
|
||||
UserBlock.target_id).filter_by(
|
||||
user_id=v.id).subquery()
|
||||
blocked = g.db.query(
|
||||
UserBlock.user_id).filter_by(
|
||||
target_id=v.id).subquery()
|
||||
|
||||
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)
|
||||
|
||||
posts=posts.options(
|
||||
contains_eager(Submission.submission_aux),
|
||||
contains_eager(Submission.author),
|
||||
)
|
||||
|
||||
if sort == "new":
|
||||
posts = posts.order_by(Submission.created_utc.desc()).all()
|
||||
elif sort == "old":
|
||||
posts = posts.order_by(Submission.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
posts = sorted(posts.all(), key=lambda x: x.score_disputed, reverse=True)
|
||||
elif sort == "top":
|
||||
posts = posts.order_by(Submission.score.desc()).all()
|
||||
elif sort == "bottom":
|
||||
posts = posts.order_by(Submission.score.asc()).all()
|
||||
elif sort == "comments":
|
||||
posts = posts.order_by(Submission.comment_count.desc()).all()
|
||||
elif sort == "random":
|
||||
posts = posts.all()
|
||||
posts = random.sample(posts, k=len(posts))
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
total = len(posts)
|
||||
|
||||
firstrange = 25 * (page - 1)
|
||||
secondrange = firstrange+26
|
||||
posts = posts[firstrange:secondrange]
|
||||
|
||||
return total, [x.id for x in posts]
|
||||
|
||||
|
||||
@cache.memoize(300)
|
||||
def searchcommentlisting(criteria, v=None, page=1, t="None", sort="top"):
|
||||
|
||||
comments = g.db.query(Comment).options(lazyload('*')).filter(Comment.parent_submission != None).join(Comment.comment_aux)
|
||||
|
||||
if 'q' in criteria:
|
||||
words=criteria['q'].split()
|
||||
words=[CommentAux.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)
|
||||
|
||||
comments=comments.options(contains_eager(Comment.comment_aux))
|
||||
|
||||
if sort == "new":
|
||||
comments = comments.order_by(Comment.created_utc.desc()).all()
|
||||
elif sort == "old":
|
||||
comments = comments.order_by(Comment.created_utc.asc()).all()
|
||||
elif sort == "controversial":
|
||||
comments = sorted(comments.all(), key=lambda x: x.score_disputed, reverse=True)
|
||||
elif sort == "top":
|
||||
comments = comments.order_by(Comment.score.desc()).all()
|
||||
elif sort == "bottom":
|
||||
comments = comments.order_by(Comment.score.asc()).all()
|
||||
|
||||
total = len(list(comments))
|
||||
firstrange = 25 * (page - 1)
|
||||
secondrange = firstrange+26
|
||||
comments = comments[firstrange:secondrange]
|
||||
return total, [x.id for x in comments]
|
||||
|
||||
@app.route("/search/posts", methods=["GET"])
|
||||
@app.route("/api/v1/search", methods=["GET"])
|
||||
@app.route("/api/vue/search")
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def searchposts(v, search_type="posts"):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
query = request.args.get("q", '').strip()
|
||||
|
||||
page = max(1, int(request.args.get("page", 1)))
|
||||
|
||||
sort = request.args.get("sort", "top").lower()
|
||||
t = request.args.get('t', 'all').lower()
|
||||
|
||||
criteria=searchparse(query)
|
||||
total, ids = searchlisting(criteria, v=v, page=page, t=t, sort=sort)
|
||||
|
||||
next_exists = (len(ids) == 26)
|
||||
ids = ids[0: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
|
||||
|
||||
return {"html":lambda: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,
|
||||
reasons=REASONS
|
||||
),
|
||||
"api":lambda:jsonify({"data":[x.json for x in posts]})
|
||||
}
|
||||
|
||||
@app.route("/search/comments", methods=["GET"])
|
||||
@app.route("/api/v1/search/comments", methods=["GET"])
|
||||
@app.route("/api/vue/search/comments")
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def searchcomments(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
query = request.args.get("q", '').strip()
|
||||
|
||||
try: page = max(1, int(request.args.get("page", 1)))
|
||||
except: page = 1
|
||||
|
||||
sort = request.args.get("sort", "top").lower()
|
||||
t = request.args.get('t', 'all').lower()
|
||||
|
||||
criteria=searchparse(query)
|
||||
total, ids = searchcommentlisting(criteria, v=v, page=page, t=t, sort=sort)
|
||||
|
||||
next_exists = (len(ids) == 26)
|
||||
ids = ids[0:25]
|
||||
|
||||
comments = get_comments(ids, v=v)
|
||||
|
||||
return {"html":lambda:render_template("search_comments.html",
|
||||
v=v,
|
||||
query=query,
|
||||
total=total,
|
||||
page=page,
|
||||
comments=comments,
|
||||
sort=sort,
|
||||
t=t,
|
||||
next_exists=next_exists,
|
||||
),
|
||||
"api":lambda:jsonify({"data":[x.json for x in comments]})
|
||||
}
|
||||
|
||||
@app.route("/search/users", methods=["GET"])
|
||||
@app.route("/api/v1/search/users", methods=["GET"])
|
||||
@app.route("/api/vue/search/users")
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def searchusers(v, search_type="posts"):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
query = request.args.get("q", '').strip()
|
||||
|
||||
page = max(1, int(request.args.get("page", 1)))
|
||||
sort = request.args.get("sort", "top").lower()
|
||||
t = request.args.get('t', 'all').lower()
|
||||
term=query.lstrip('@')
|
||||
term=term.replace('\\','')
|
||||
term=term.replace('_','\_')
|
||||
|
||||
now=int(time.time())
|
||||
users=g.db.query(User).filter(User.username.ilike(f'%{term}%'))
|
||||
|
||||
users=users.order_by(User.username.ilike(term).desc(), User.stored_subscriber_count.desc())
|
||||
|
||||
total=users.count()
|
||||
|
||||
users=[x for x in users.offset(25 * (page-1)).limit(26)]
|
||||
next_exists=(len(users)==26)
|
||||
users=users[0:25]
|
||||
|
||||
|
||||
|
||||
return {"html":lambda:render_template("search_users.html",
|
||||
v=v,
|
||||
query=query,
|
||||
total=total,
|
||||
page=page,
|
||||
users=users,
|
||||
sort=sort,
|
||||
t=t,
|
||||
next_exists=next_exists
|
||||
),
|
||||
"api":lambda:jsonify({"data":[x.json for x in users]})
|
||||
}
|
|
@ -0,0 +1,723 @@
|
|||
from __future__ import unicode_literals
|
||||
from ruqqus.helpers.alerts import *
|
||||
from ruqqus.helpers.sanitize import *
|
||||
from ruqqus.helpers.filters import filter_comment_html
|
||||
from ruqqus.helpers.markdown import *
|
||||
from ruqqus.helpers.discord import remove_user, set_nick
|
||||
from ruqqus.mail import *
|
||||
from .front import frontlist
|
||||
from ruqqus.__main__ import app, cache
|
||||
import youtube_dl
|
||||
|
||||
valid_username_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$")
|
||||
valid_title_regex = re.compile("^((?!<).){3,100}$")
|
||||
valid_password_regex = re.compile("^.{8,100}$")
|
||||
|
||||
youtubekey = environ.get("youtubekey").strip()
|
||||
|
||||
@app.route("/settings/profile", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_profile_post(v):
|
||||
updated = False
|
||||
|
||||
if request.values.get("slurreplacer", v.slurreplacer) != v.slurreplacer:
|
||||
updated = True
|
||||
v.slurreplacer = request.values.get("slurreplacer", None) == 'true'
|
||||
|
||||
if request.values.get("hidevotedon", v.hidevotedon) != v.hidevotedon:
|
||||
updated = True
|
||||
v.hidevotedon = request.values.get("hidevotedon", None) == 'true'
|
||||
|
||||
if request.values.get("newtab", v.newtab) != v.newtab:
|
||||
updated = True
|
||||
v.newtab = request.values.get("newtab", None) == 'true'
|
||||
|
||||
if request.values.get("newtabexternal", v.newtabexternal) != v.newtabexternal:
|
||||
updated = True
|
||||
v.newtabexternal = request.values.get("newtabexternal", None) == 'true'
|
||||
|
||||
if request.values.get("oldreddit", v.oldreddit) != v.oldreddit:
|
||||
updated = True
|
||||
v.oldreddit = request.values.get("oldreddit", None) == 'true'
|
||||
|
||||
if request.values.get("over18", v.over_18) != v.over_18:
|
||||
updated = True
|
||||
v.over_18 = request.values.get("over18", None) == 'true'
|
||||
|
||||
if request.values.get("hide_offensive",
|
||||
v.hide_offensive) != v.hide_offensive:
|
||||
updated = True
|
||||
v.hide_offensive = request.values.get("hide_offensive", None) == 'true'
|
||||
|
||||
if request.values.get("hide_bot",
|
||||
v.hide_bot) != v.hide_bot:
|
||||
updated = True
|
||||
v.hide_bot = request.values.get("hide_bot", None) == 'true'
|
||||
|
||||
if request.values.get("filter_nsfw", v.filter_nsfw) != v.filter_nsfw:
|
||||
updated = True
|
||||
v.filter_nsfw = not request.values.get("filter_nsfw", None) == 'true'
|
||||
|
||||
if request.values.get("private", v.is_private) != v.is_private:
|
||||
updated = True
|
||||
v.is_private = request.values.get("private", None) == 'true'
|
||||
|
||||
if request.values.get("nofollow", v.is_nofollow) != v.is_nofollow:
|
||||
updated = True
|
||||
v.is_nofollow = request.values.get("nofollow", None) == 'true'
|
||||
|
||||
if request.values.get("bio") is not None:
|
||||
bio = request.values.get("bio")[0:1500]
|
||||
|
||||
if bio == v.bio:
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error="You didn't change anything")
|
||||
|
||||
for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif))', bio, re.MULTILINE): bio = bio.replace(i.group(1), f'![]({i.group(1)})')
|
||||
with CustomRenderer() as renderer:
|
||||
bio_html = renderer.render(mistletoe.Document(bio))
|
||||
bio_html = sanitize(bio_html, linkgen=True)
|
||||
|
||||
# Run safety filter
|
||||
bans = filter_comment_html(bio_html)
|
||||
|
||||
if bans:
|
||||
ban = bans[0]
|
||||
reason = f"Remove the {ban.domain} link from your bio and try again."
|
||||
if ban.reason:
|
||||
reason += f" {ban.reason_text}"
|
||||
|
||||
#auto ban for digitally malicious content
|
||||
if any([x.reason==4 for x in bans]):
|
||||
v.ban(days=30, reason="Digitally malicious content is not allowed.")
|
||||
return jsonify({"error": reason}), 401
|
||||
|
||||
v.bio = bio
|
||||
v.bio_html=bio_html
|
||||
g.db.add(v)
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
msg="Your bio has been updated.")
|
||||
|
||||
if request.values.get("filters") is not None:
|
||||
|
||||
filters=request.values.get("filters")[0:1000].strip()
|
||||
|
||||
if filters==v.custom_filter_list:
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error="You didn't change anything")
|
||||
|
||||
v.custom_filter_list=filters
|
||||
g.db.add(v)
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
msg="Your custom filters have been updated.")
|
||||
|
||||
|
||||
|
||||
x = request.values.get("title_id", None)
|
||||
if x:
|
||||
x = int(x)
|
||||
if x == 0:
|
||||
v.title_id = None
|
||||
updated = True
|
||||
elif x > 0:
|
||||
title = get_title(x)
|
||||
if bool(eval(title.qualification_expr)):
|
||||
v.title_id = title.id
|
||||
updated = True
|
||||
else:
|
||||
return jsonify({"error": f"You don't meet the requirements for title `{title.text}`."}), 403
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
defaultsortingcomments = request.values.get("defaultsortingcomments")
|
||||
if defaultsortingcomments:
|
||||
if defaultsortingcomments in ["new", "old", "controversial", "top", "bottom", "random"]:
|
||||
v.defaultsortingcomments = defaultsortingcomments
|
||||
updated = True
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
defaultsorting = request.values.get("defaultsorting")
|
||||
if defaultsorting:
|
||||
if defaultsorting in ["hot", "new", "old", "comments", "controversial", "top", "bottom", "random"]:
|
||||
v.defaultsorting = defaultsorting
|
||||
updated = True
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
defaulttime = request.values.get("defaulttime")
|
||||
if defaulttime:
|
||||
if defaulttime in ["hour", "day", "week", "month", "year", "all"]:
|
||||
v.defaulttime = defaulttime
|
||||
updated = True
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
theme = request.values.get("theme")
|
||||
if theme:
|
||||
v.theme = theme
|
||||
if theme == "coffee": v.themecolor = "38a169"
|
||||
elif theme == "tron": v.themecolor = "80ffff"
|
||||
g.db.add(v)
|
||||
return "", 204
|
||||
|
||||
if updated:
|
||||
g.db.add(v)
|
||||
|
||||
return jsonify({"message": "Your settings have been updated."})
|
||||
|
||||
else:
|
||||
return jsonify({"error": "You didn't change anything."}), 400
|
||||
|
||||
@app.route("/changelogsub", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def changelogsub(v):
|
||||
v.changelogsub = not v.changelogsub
|
||||
g.db.add(v)
|
||||
cache.delete_memoized(frontlist)
|
||||
return "", 204
|
||||
|
||||
@app.route("/settings/namecolor", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def namecolor(v):
|
||||
color = str(request.form.get("color", "")).strip()
|
||||
if color not in ['ff66ac','805ad5','62ca56','38a169','80ffff','2a96f3','eb4963','ff0000','f39731','30409f','3e98a7','e4432d','7b9ae4','ec72de','7f8fa6', 'f8db58']: abort(400)
|
||||
v.namecolor = color
|
||||
g.db.add(v)
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.route("/settings/themecolor", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def themecolor(v):
|
||||
themecolor = str(request.form.get("themecolor", "")).strip()
|
||||
if themecolor not in ['ff66ac','805ad5','62ca56','38a169','80ffff','2a96f3','eb4963','ff0000','f39731','30409f','3e98a7','e4432d','7b9ae4','ec72de','7f8fa6', 'f8db58']: abort(400)
|
||||
v.themecolor = themecolor
|
||||
g.db.add(v)
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.route("/settings/titlecolor", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def titlecolor(v):
|
||||
titlecolor = str(request.form.get("titlecolor", "")).strip()
|
||||
if titlecolor not in ['ff66ac','805ad5','62ca56','38a169','80ffff','2a96f3','eb4963','ff0000','f39731','30409f','3e98a7','e4432d','7b9ae4','ec72de','7f8fa6', 'f8db58']: abort(400)
|
||||
v.titlecolor = titlecolor
|
||||
g.db.add(v)
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.route("/settings/security", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_security_post(v):
|
||||
if request.form.get("new_password"):
|
||||
if request.form.get(
|
||||
"new_password") != request.form.get("cnf_password"):
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("Passwords do not match."))
|
||||
|
||||
if not re.match(valid_password_regex, request.form.get("new_password")):
|
||||
#print(f"signup fail - {username } - invalid password")
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("Password must be between 8 and 100 characters."))
|
||||
|
||||
if not v.verifyPass(request.form.get("old_password")):
|
||||
return render_template(
|
||||
"settings_security.html", v=v, error="Incorrect password")
|
||||
|
||||
v.passhash = v.hash_password(request.form.get("new_password"))
|
||||
|
||||
g.db.add(v)
|
||||
|
||||
return redirect("/settings/security?msg=" +
|
||||
escape("Your password has been changed."))
|
||||
|
||||
if request.form.get("new_email"):
|
||||
|
||||
if not v.verifyPass(request.form.get('password')):
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("Invalid password."))
|
||||
|
||||
new_email = request.form.get("new_email","").strip()
|
||||
#counteract gmail username+2 and extra period tricks - convert submitted email to actual inbox
|
||||
if new_email.endswith("@gmail.com"):
|
||||
gmail_username=new_email.split('@')[0]
|
||||
gmail_username=gmail_username.split("+")[0]
|
||||
gmail_username=gmail_username.replace('.','')
|
||||
new_email=f"{gmail_username}@gmail.com"
|
||||
if new_email == v.email:
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("That email is already yours!"))
|
||||
|
||||
# check to see if email is in use
|
||||
existing = g.db.query(User).filter(User.id != v.id,
|
||||
func.lower(User.email) == new_email.lower()).first()
|
||||
if existing:
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("That email address is already in use."))
|
||||
|
||||
url = f"https://{app.config['SERVER_NAME']}/activate"
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
token = generate_hash(f"{new_email}+{v.id}+{now}")
|
||||
params = f"?email={quote(new_email)}&id={v.id}&time={now}&token={token}"
|
||||
|
||||
link = url + params
|
||||
|
||||
send_mail(to_address=new_email,
|
||||
subject="Verify your email address.",
|
||||
html=render_template("email/email_change.html",
|
||||
action_url=link,
|
||||
v=v)
|
||||
)
|
||||
|
||||
return redirect("/settings/security?msg=" + escape(
|
||||
"Check your email and click the verification link to complete the email change."))
|
||||
|
||||
if request.form.get("2fa_token", ""):
|
||||
|
||||
if not v.verifyPass(request.form.get('password')):
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("Invalid password or token."))
|
||||
|
||||
secret = request.form.get("2fa_secret")
|
||||
x = pyotp.TOTP(secret)
|
||||
if not x.verify(request.form.get("2fa_token"), valid_window=1):
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("Invalid password or token."))
|
||||
|
||||
v.mfa_secret = secret
|
||||
g.db.add(v)
|
||||
|
||||
return redirect("/settings/security?msg=" +
|
||||
escape("Two-factor authentication enabled."))
|
||||
|
||||
if request.form.get("2fa_remove", ""):
|
||||
|
||||
if not v.verifyPass(request.form.get('password')):
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("Invalid password or token."))
|
||||
|
||||
token = request.form.get("2fa_remove")
|
||||
|
||||
if not v.validate_2fa(token):
|
||||
return redirect("/settings/security?error=" +
|
||||
escape("Invalid password or token."))
|
||||
|
||||
v.mfa_secret = None
|
||||
g.db.add(v)
|
||||
|
||||
return redirect("/settings/security?msg=" +
|
||||
escape("Two-factor authentication disabled."))
|
||||
|
||||
@app.route("/settings/log_out_all_others", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_log_out_others(v):
|
||||
|
||||
submitted_password = request.form.get("password", "")
|
||||
|
||||
if not v.verifyPass(submitted_password):
|
||||
return render_template("settings_security.html",
|
||||
v=v, error="Incorrect Password"), 401
|
||||
|
||||
# increment account's nonce
|
||||
v.login_nonce += 1
|
||||
|
||||
# update cookie accordingly
|
||||
session["login_nonce"] = v.login_nonce
|
||||
|
||||
g.db.add(v)
|
||||
|
||||
return render_template("settings_security.html", v=v,
|
||||
msg="All other devices have been logged out")
|
||||
|
||||
|
||||
@app.route("/settings/images/profile", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_images_profile(v):
|
||||
if v.can_upload_avatar:
|
||||
|
||||
if request.content_length > 16 * 1024 * 1024:
|
||||
g.db.rollback()
|
||||
abort(413)
|
||||
|
||||
v.set_profile(request.files["profile"])
|
||||
return render_template("settings_profile.html",
|
||||
v=v, msg="Profile picture successfully updated.")
|
||||
|
||||
return render_template("settings_profile.html", v=v,
|
||||
msg="Avatars require 300 reputation.")
|
||||
|
||||
|
||||
@app.route("/settings/images/banner", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_images_banner(v):
|
||||
if v.can_upload_banner:
|
||||
if request.content_length > 16 * 1024 * 1024:
|
||||
g.db.rollback()
|
||||
abort(413)
|
||||
|
||||
v.set_banner(request.files["banner"])
|
||||
|
||||
return render_template("settings_profile.html",
|
||||
v=v, msg="Banner successfully updated.")
|
||||
|
||||
return render_template("settings_profile.html", v=v,
|
||||
msg="Banners require 500 reputation.")
|
||||
|
||||
|
||||
@app.route("/settings/delete/profile", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_delete_profile(v):
|
||||
|
||||
v.del_profile()
|
||||
|
||||
return render_template("settings_profile.html", v=v,
|
||||
msg="Profile picture successfully removed.")
|
||||
|
||||
@app.route("/settings/delete/banner", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_delete_banner(v):
|
||||
|
||||
v.del_banner()
|
||||
|
||||
return render_template("settings_profile.html", v=v,
|
||||
msg="Banner successfully removed.")
|
||||
|
||||
|
||||
@app.route("/settings/toggle_collapse", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_toggle_collapse(v):
|
||||
|
||||
session["sidebar_collapsed"] = not session.get("sidebar_collapsed", False)
|
||||
|
||||
return "", 204
|
||||
|
||||
|
||||
@app.route("/settings/read_announcement", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def update_announcement(v):
|
||||
|
||||
v.read_announcement_utc = int(time.time())
|
||||
g.db.add(v)
|
||||
|
||||
return "", 204
|
||||
|
||||
|
||||
@app.route("/settings/blocks", methods=["GET"])
|
||||
@auth_required
|
||||
def settings_blockedpage(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
#users=[x.target for x in v.blocked]
|
||||
|
||||
return render_template("settings_blocks.html",
|
||||
v=v)
|
||||
|
||||
@app.route("/settings/css", methods=["GET"])
|
||||
@auth_required
|
||||
def settings_css_get(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
return render_template("settings_css.html", v=v)
|
||||
|
||||
@app.route("/settings/css", methods=["POST"])
|
||||
@auth_required
|
||||
def settings_css(v):
|
||||
css = request.form.get("css").replace('\\', '')[0:50000]
|
||||
|
||||
if not v.agendaposter:
|
||||
v.css = css
|
||||
else:
|
||||
v.css = 'body *::before, body *::after { content: "Trans rights are human rights!"; }'
|
||||
g.db.add(v)
|
||||
return render_template("settings_css.html", v=v)
|
||||
|
||||
@app.route("/settings/profilecss", methods=["GET"])
|
||||
@auth_required
|
||||
def settings_profilecss_get(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
if v.dramacoins < 1000: return "You must have +1000 dramacoins to set profile css."
|
||||
return render_template("settings_profilecss.html", v=v)
|
||||
|
||||
@app.route("/settings/profilecss", methods=["POST"])
|
||||
@auth_required
|
||||
def settings_profilecss(v):
|
||||
if v.dramacoins < 1000: return "You must have +1000 dramacoins to set profile css."
|
||||
profilecss = request.form.get("profilecss").replace('\\', '')[0:50000]
|
||||
v.profilecss = profilecss
|
||||
g.db.add(v)
|
||||
return render_template("settings_profilecss.html", v=v)
|
||||
|
||||
@app.route("/settings/block", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_block_user(v):
|
||||
|
||||
user = get_user(request.values.get("username"), graceful=True)
|
||||
|
||||
if not user:
|
||||
return jsonify({"error": "That user doesn't exist."}), 404
|
||||
|
||||
if user.id == v.id:
|
||||
return jsonify({"error": "You can't block yourself."}), 409
|
||||
|
||||
if v.has_block(user):
|
||||
return jsonify({"error": f"You have already blocked @{user.username}."}), 409
|
||||
|
||||
if user.id == 1046:
|
||||
return jsonify({"error": "You can't block @Drama."}), 409
|
||||
|
||||
new_block = UserBlock(user_id=v.id,
|
||||
target_id=user.id,
|
||||
created_utc=int(time.time())
|
||||
)
|
||||
g.db.add(new_block)
|
||||
|
||||
cache.delete_memoized(frontlist)
|
||||
|
||||
existing = g.db.query(Notification).filter_by(blocksender=v.id, user_id=user.id).first()
|
||||
if not existing: send_block_notif(v.id, user.id, f"@{v.username} has blocked you!")
|
||||
|
||||
if request.args.get("notoast"): return "", 204
|
||||
return jsonify({"message": f"@{user.username} blocked."})
|
||||
|
||||
|
||||
@app.route("/settings/unblock", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_unblock_user(v):
|
||||
|
||||
user = get_user(request.values.get("username"))
|
||||
|
||||
x = v.has_block(user)
|
||||
|
||||
if not x: abort(409)
|
||||
|
||||
g.db.delete(x)
|
||||
|
||||
cache.delete_memoized(frontlist)
|
||||
|
||||
existing = g.db.query(Notification).filter_by(unblocksender=v.id, user_id=user.id).first()
|
||||
if not existing: send_unblock_notif(v.id, user.id, f"@{v.username} has unblocked you!")
|
||||
|
||||
if request.args.get("notoast"): return "", 204
|
||||
return jsonify({"message": f"@{user.username} unblocked."})
|
||||
|
||||
|
||||
@app.route("/settings/apps", methods=["GET"])
|
||||
@auth_required
|
||||
def settings_apps(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
return render_template("settings_apps.html", v=v)
|
||||
|
||||
|
||||
@app.route("/settings/remove_discord", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_remove_discord(v):
|
||||
|
||||
if v.admin_level>1:
|
||||
return render_template("settings_filters.html", v=v, error="Admins can't disconnect Discord.")
|
||||
|
||||
remove_user(v)
|
||||
|
||||
v.discord_id=None
|
||||
g.db.add(v)
|
||||
g.db.commit()
|
||||
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.route("/settings/content", methods=["GET"])
|
||||
@auth_required
|
||||
def settings_content_get(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
return render_template("settings_filters.html", v=v)
|
||||
|
||||
@app.route("/settings/name_change", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_name_change(v):
|
||||
|
||||
new_name=request.form.get("name").strip()
|
||||
|
||||
#make sure name is different
|
||||
if new_name==v.username:
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error="You didn't change anything")
|
||||
|
||||
#verify acceptability
|
||||
if not re.match(valid_username_regex, new_name):
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error=f"This isn't a valid username.")
|
||||
|
||||
#verify availability
|
||||
name=new_name.replace('_','\_')
|
||||
|
||||
x= g.db.query(User).options(
|
||||
lazyload('*')
|
||||
).filter(
|
||||
or_(
|
||||
User.username.ilike(name),
|
||||
User.original_username.ilike(name)
|
||||
)
|
||||
).first()
|
||||
|
||||
if x and x.id != v.id:
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error=f"Username `{new_name}` is already in use.")
|
||||
|
||||
v=g.db.query(User).with_for_update().options(lazyload('*')).filter_by(id=v.id).first()
|
||||
|
||||
v.username=new_name
|
||||
v.name_changed_utc=int(time.time())
|
||||
|
||||
set_nick(v, new_name)
|
||||
|
||||
g.db.add(v)
|
||||
g.db.commit()
|
||||
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.route("/settings/song_change", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_song_change(v):
|
||||
song=request.form.get("song").strip()
|
||||
|
||||
if song == "" and v.song and os.path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User).filter_by(song=v.song).count() == 1:
|
||||
os.remove(f"/songs/{v.song}.mp3")
|
||||
v.song=None
|
||||
g.db.add(v)
|
||||
return redirect("/settings/profile")
|
||||
|
||||
song = song.replace("https://music.youtube.com", "https://youtube.com")
|
||||
if song.startswith(("https://www.youtube.com/watch?v=", "https://youtube.com/watch?v=", "https://m.youtube.com/watch?v=")):
|
||||
id = song.split("v=")[1]
|
||||
elif song.startswith("https://youtu.be/"):
|
||||
id = song.split("https://youtu.be/")[1]
|
||||
else:
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error=f"Not a youtube link.")
|
||||
|
||||
if "?" in id: id = id.split("?")[0]
|
||||
if "&" in id: id = id.split("&")[0]
|
||||
|
||||
if os.path.isfile(f'/songs/{id}.mp3'):
|
||||
v.song=id
|
||||
g.db.add(v)
|
||||
g.db.commit()
|
||||
return redirect("/settings/profile")
|
||||
|
||||
|
||||
req = requests.get(f"https://www.googleapis.com/youtube/v3/videos?id={id}&key={youtubekey}&part=contentDetails").json()
|
||||
try: duration = req['items'][0]['contentDetails']['duration']
|
||||
except:
|
||||
print(req)
|
||||
abort(400)
|
||||
if "H" in duration:
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error=f"Duration of the video must not exceed 10 minutes.")
|
||||
|
||||
if "M" in duration:
|
||||
duration = int(duration.split("PT")[1].split("M")[0])
|
||||
if duration > 10:
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error=f"Duration of the video must not exceed 10 minutes.")
|
||||
|
||||
|
||||
if v.song and os.path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User).filter_by(song=v.song).count() == 1:
|
||||
os.remove(f"/songs/{v.song}.mp3")
|
||||
|
||||
ydl_opts = {
|
||||
'outtmpl': '/songs/%(title)s.%(ext)s',
|
||||
'format': 'bestaudio/best',
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': '192',
|
||||
}],
|
||||
}
|
||||
|
||||
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||
try: ydl.download([f"https://youtube.com/watch?v={id}"])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error=f"Age-restricted videos aren't allowed.")
|
||||
|
||||
files = os.listdir("/songs/")
|
||||
paths = [os.path.join("/songs/", basename) for basename in files]
|
||||
songfile = max(paths, key=os.path.getctime)
|
||||
os.rename(songfile, f"/songs/{id}.mp3")
|
||||
|
||||
v.song=id
|
||||
g.db.add(v)
|
||||
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.route("/settings/title_change", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_title_change(v):
|
||||
|
||||
if v.flairchanged: abort(403)
|
||||
|
||||
new_name=request.form.get("title").strip()
|
||||
|
||||
#verify acceptability
|
||||
if not re.match(valid_title_regex, new_name):
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error=f"This isn't a valid flair.")
|
||||
|
||||
#make sure name is different
|
||||
if new_name==v.customtitle:
|
||||
return render_template("settings_profile.html",
|
||||
v=v,
|
||||
error="You didn't change anything")
|
||||
|
||||
v.customtitleplain = new_name
|
||||
new_name = new_name.replace('_','\_')
|
||||
new_name = sanitize(new_name, linkgen=True, flair=True)
|
||||
|
||||
v = g.db.query(User).with_for_update().options(lazyload('*')).filter_by(id=v.id).first()
|
||||
v.customtitle = new_name
|
||||
|
||||
g.db.add(v)
|
||||
g.db.commit()
|
||||
|
||||
return redirect("/settings/profile")
|
||||
|
||||
@app.route("/settings/badges", methods=["POST"])
|
||||
@auth_required
|
||||
@validate_formkey
|
||||
def settings_badge_recheck(v):
|
||||
|
||||
v.refresh_selfset_badges()
|
||||
|
||||
return jsonify({"message":"Badges Refreshed"})
|
|
@ -0,0 +1,194 @@
|
|||
from ruqqus.mail import *
|
||||
from ruqqus.__main__ import app, limiter
|
||||
from ruqqus.helpers.alerts import *
|
||||
|
||||
@app.route("/sex")
|
||||
def index():
|
||||
return render_template("index.html", **{"greeting": "Hello from Flask!"})
|
||||
|
||||
@app.route("/assets/favicon.ico", methods=["GET"])
|
||||
def favicon():
|
||||
return send_file("./assets/images/favicon.png")
|
||||
|
||||
@app.route("/oauthhelp", methods=["GET"])
|
||||
@auth_desired
|
||||
def oauthhelp(v):
|
||||
return render_template("oauthhelp.html", v=v)
|
||||
|
||||
@app.route("/contact", methods=["GET"])
|
||||
@auth_desired
|
||||
def contact(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
return render_template("contact.html", v=v)
|
||||
|
||||
@app.route("/contact", methods=["POST"])
|
||||
@auth_desired
|
||||
def submit_contact(v):
|
||||
message = f'This message has been sent automatically to all admins via https://rdrama.net/contact, user email is "{v.email}"\n\nMessage:\n\n' + request.form.get("message", "")
|
||||
send_admin(v.id, message)
|
||||
return render_template("contact.html", v=v, msg="Your message has been sent.")
|
||||
|
||||
@app.route('/archives')
|
||||
@limiter.exempt
|
||||
def archivesindex():
|
||||
return redirect("/archives/index.html")
|
||||
|
||||
@app.route('/archives/<path:path>')
|
||||
@limiter.exempt
|
||||
def archives(path):
|
||||
resp = make_response(send_from_directory('/archives', path))
|
||||
resp.headers.add("Cache-Control", "public")
|
||||
if request.path.endswith('.css'): resp.headers.add("Content-Type", "text/css")
|
||||
return resp
|
||||
|
||||
@app.route('/assets/<path:path>')
|
||||
@limiter.exempt
|
||||
def static_service(path):
|
||||
resp = make_response(send_from_directory('./assets', path))
|
||||
resp.headers.add("Cache-Control", "public")
|
||||
|
||||
if request.path.endswith('.css'):
|
||||
resp.headers.add("Content-Type", "text/css")
|
||||
return resp
|
||||
|
||||
@app.route("/robots.txt", methods=["GET"])
|
||||
def robots_txt():
|
||||
return send_file("./assets/robots.txt")
|
||||
|
||||
@app.route("/settings", methods=["GET"])
|
||||
@auth_required
|
||||
def settings(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
return redirect("/settings/profile")
|
||||
|
||||
|
||||
@app.route("/settings/profile", methods=["GET"])
|
||||
@auth_required
|
||||
def settings_profile(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
return render_template("settings_profile.html",
|
||||
v=v)
|
||||
|
||||
|
||||
@app.route("/titles", methods=["GET"])
|
||||
@auth_desired
|
||||
def titles(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
titles = [x for x in g.db.query(Title).order_by(text("id asc")).all()]
|
||||
return render_template("/titles.html",
|
||||
v=v,
|
||||
titles=titles)
|
||||
|
||||
@app.route("/badges", methods=["GET"])
|
||||
@auth_desired
|
||||
def badges(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
badges = [
|
||||
x for x in g.db.query(BadgeDef).order_by(
|
||||
text("rank asc, id asc")).all()]
|
||||
return render_template("badges.html",
|
||||
v=v,
|
||||
badges=badges)
|
||||
|
||||
@app.route("/blocks", methods=["GET"])
|
||||
@auth_desired
|
||||
def blocks(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
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.route("/banned", methods=["GET"])
|
||||
@auth_desired
|
||||
def banned(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
users = [x for x in g.db.query(User).filter(User.is_banned != 0, User.unban_utc == 0).all()]
|
||||
return render_template("banned.html", v=v, users=users)
|
||||
|
||||
@app.route("/formatting", methods=["GET"])
|
||||
@auth_desired
|
||||
def formatting(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
return render_template("formatting.html", v=v)
|
||||
|
||||
@app.route("/.well-known/brave-rewards-verification.txt", methods=["GET"])
|
||||
def brave():
|
||||
with open(".well-known/brave-rewards-verification.txt", "r") as f: return Response(f.read(), mimetype='text/plain')
|
||||
|
||||
@app.route("/.well-known/assetlinks.json", methods=["GET"])
|
||||
def googleplayapp():
|
||||
with open(".well-known/assetlinks.json", "r") as f: return Response(f.read(), mimetype='application/json')
|
||||
|
||||
@app.route("/service-worker.js")
|
||||
def serviceworker():
|
||||
with open(".well-known/service-worker.js", "r") as f: return Response(f.read(), mimetype='application/javascript')
|
||||
|
||||
@app.route("/badmins", methods=["GET"])
|
||||
@auth_desired
|
||||
def help_admins(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
admins = g.db.query(User).filter(
|
||||
User.admin_level > 1,
|
||||
User.id > 1).order_by(
|
||||
User.id.asc()).all()
|
||||
admins = [x for x in admins]
|
||||
|
||||
exadmins = g.db.query(User).filter_by(
|
||||
admin_level=1).order_by(
|
||||
User.id.asc()).all()
|
||||
exadmins = [x for x in exadmins]
|
||||
|
||||
return render_template("admins.html",
|
||||
v=v,
|
||||
admins=admins,
|
||||
exadmins=exadmins
|
||||
)
|
||||
|
||||
|
||||
@app.route("/settings/security", methods=["GET"])
|
||||
@auth_required
|
||||
def settings_security(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
return render_template("settings_security.html",
|
||||
v=v,
|
||||
mfa_secret=pyotp.random_base32() if not v.mfa_secret else None,
|
||||
error=request.args.get("error") or None,
|
||||
msg=request.args.get("msg") or None
|
||||
)
|
||||
|
||||
@app.route("/imagehosts", methods=["GET"])
|
||||
def info_image_hosts():
|
||||
|
||||
sites = g.db.query(Domain).filter_by(
|
||||
show_thumbnail=True).order_by(
|
||||
Domain.domain.asc()).all()
|
||||
|
||||
sites = [x.domain for x in sites]
|
||||
|
||||
text = "\n".join(sites)
|
||||
|
||||
resp = make_response(text)
|
||||
resp.mimetype = "text/plain"
|
||||
return resp
|
||||
|
||||
@app.route("/dismiss_mobile_tip", methods=["POST"])
|
||||
def dismiss_mobile_tip():
|
||||
|
||||
session["tooltip_last_dismissed"]=int(time.time())
|
||||
session.modified=True
|
||||
|
||||
return "", 204
|
|
@ -0,0 +1,522 @@
|
|||
import qrcode
|
||||
import io
|
||||
from datetime import datetime
|
||||
|
||||
from ruqqus.helpers.alerts import *
|
||||
from ruqqus.helpers.sanitize import *
|
||||
from ruqqus.helpers.markdown import *
|
||||
from ruqqus.mail import *
|
||||
from flask import *
|
||||
from ruqqus.__main__ import app, cache, limiter, db_session
|
||||
from pusher_push_notifications import PushNotifications
|
||||
|
||||
PUSHER_KEY = environ.get("PUSHER_KEY", "").strip()
|
||||
|
||||
beams_client = PushNotifications(
|
||||
instance_id='02ddcc80-b8db-42be-9022-44c546b4dce6',
|
||||
secret_key=PUSHER_KEY,
|
||||
)
|
||||
|
||||
@app.route("/api/v1/user/<username>", methods=["GET"])
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def user_info(v, username):
|
||||
user = get_user(username, v=v)
|
||||
return jsonify(user.json)
|
||||
|
||||
|
||||
@app.route("/leaderboard", methods=["GET"])
|
||||
@auth_desired
|
||||
def leaderboard(v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
users1, users2 = leaderboard()
|
||||
return render_template("leaderboard.html", v=v, users1=users1, users2=users2)
|
||||
if request.path == "/": render_template("mods.html", v=v, b=board, me=me)
|
||||
else: return jsonify({"data":[x.json for x in board.mods_list]})
|
||||
|
||||
@cache.memoize(timeout=86400)
|
||||
def leaderboard():
|
||||
users = g.db.query(User).options(lazyload('*'))
|
||||
users1= sorted(users, key=lambda x: x.dramacoins, reverse=True)[:100]
|
||||
users2 = sorted(users1, key=lambda x: x.follower_count, reverse=True)[:10]
|
||||
return users1[:25], users2
|
||||
|
||||
@app.get("/@<username>/css")
|
||||
def get_css(username):
|
||||
user = get_user(username)
|
||||
if user.css: css = user.css
|
||||
else: css = ""
|
||||
resp=make_response(css)
|
||||
resp.headers.add("Content-Type", "text/css")
|
||||
return resp
|
||||
|
||||
@app.get("/@<username>/profilecss")
|
||||
def get_profilecss(username):
|
||||
user = get_user(username)
|
||||
if user.profilecss: profilecss = user.profilecss
|
||||
else: profilecss = ""
|
||||
resp=make_response(profilecss)
|
||||
resp.headers.add("Content-Type", "text/css")
|
||||
return resp
|
||||
|
||||
@app.route("/@<username>/reply/<id>", methods=["POST"])
|
||||
@auth_required
|
||||
def messagereply(v, username, id):
|
||||
message = request.form.get("message", "")
|
||||
user = get_user(username)
|
||||
with CustomRenderer() as renderer: text_html = renderer.render(mistletoe.Document(message))
|
||||
text_html = sanitize(text_html, linkgen=True)
|
||||
parent = get_comment(int(id), v=v)
|
||||
new_comment = Comment(author_id=v.id,
|
||||
parent_submission=None,
|
||||
parent_fullname=parent.fullname,
|
||||
parent_comment_id=id,
|
||||
level=parent.level + 1,
|
||||
sentto=user.id
|
||||
)
|
||||
g.db.add(new_comment)
|
||||
g.db.flush()
|
||||
new_aux = CommentAux(id=new_comment.id, body=message, body_html=text_html)
|
||||
g.db.add(new_aux)
|
||||
notif = Notification(comment_id=new_comment.id, user_id=user.id)
|
||||
g.db.add(notif)
|
||||
g.db.commit()
|
||||
return redirect('/notifications?all=true')
|
||||
|
||||
@app.route("/songs/<id>", methods=["GET"])
|
||||
def songs(id):
|
||||
try: id = int(id)
|
||||
except: return '', 400
|
||||
user = g.db.query(User).filter_by(id=id).first()
|
||||
return send_from_directory('/songs/', f'{user.song}.mp3')
|
||||
|
||||
@app.route("/subscribe/<post_id>", methods=["POST"])
|
||||
@auth_required
|
||||
def subscribe(v, post_id):
|
||||
new_sub = Subscription(user_id=v.id, submission_id=post_id)
|
||||
g.db.add(new_sub)
|
||||
g.db.commit()
|
||||
return "", 204
|
||||
|
||||
@app.route("/unsubscribe/<post_id>", methods=["POST"])
|
||||
@auth_required
|
||||
def unsubscribe(v, post_id):
|
||||
sub=g.db.query(Subscription).filter_by(user_id=v.id, submission_id=post_id).first()
|
||||
g.db.delete(sub)
|
||||
return "", 204
|
||||
|
||||
@app.route("/@<username>/message", methods=["POST"])
|
||||
@auth_required
|
||||
def message2(v, username):
|
||||
user = get_user(username, v=v)
|
||||
if user.is_blocking: return jsonify({"error": "You're blocking this user."}), 403
|
||||
if user.is_blocked: return jsonify({"error": "This user is blocking you."}), 403
|
||||
message = request.form.get("message", "")
|
||||
send_pm(v.id, user, message)
|
||||
beams_client.publish_to_interests(
|
||||
interests=[str(user.id)],
|
||||
publish_body={
|
||||
'web': {
|
||||
'notification': {
|
||||
'title': f'New message from @{v.username}',
|
||||
'body': message,
|
||||
'deep_link': f'https://rdrama.net/notifications',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
return redirect('/notifications?sent=true')
|
||||
|
||||
@app.route("/2faqr/<secret>", methods=["GET"])
|
||||
@auth_required
|
||||
def mfa_qr(secret, v):
|
||||
x = pyotp.TOTP(secret)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L
|
||||
)
|
||||
qr.add_data(x.provisioning_uri(v.username, issuer_name="Drama"))
|
||||
img = qr.make_image(fill_color="#FF66AC", back_color="white")
|
||||
|
||||
mem = io.BytesIO()
|
||||
|
||||
img.save(mem, format="PNG")
|
||||
mem.seek(0, 0)
|
||||
return send_file(mem, mimetype="image/png", as_attachment=False)
|
||||
|
||||
|
||||
@app.route("/api/is_available/<name>", methods=["GET"])
|
||||
@app.route("/api/v1/is_available/<name>", methods=["GET"])
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def api_is_available(name, v):
|
||||
|
||||
name=name.strip()
|
||||
|
||||
if len(name)<3 or len(name)>25:
|
||||
return jsonify({name:False})
|
||||
|
||||
name=name.replace('_','\_')
|
||||
|
||||
x= g.db.query(User).options(
|
||||
lazyload('*')
|
||||
).filter(
|
||||
or_(
|
||||
User.username.ilike(name),
|
||||
User.original_username.ilike(name)
|
||||
)
|
||||
).first()
|
||||
|
||||
if x:
|
||||
return jsonify({name: False})
|
||||
else:
|
||||
return jsonify({name: True})
|
||||
|
||||
|
||||
@app.route("/uid/<uid>", methods=["GET"])
|
||||
def user_uid(uid):
|
||||
|
||||
user = get_account(uid)
|
||||
|
||||
return redirect(user.permalink)
|
||||
|
||||
@app.route("/id/<uid>", methods=["GET"])
|
||||
def user_uid2(uid):
|
||||
|
||||
user = get_account(int(uid))
|
||||
return redirect(user.permalink)
|
||||
|
||||
# Allow Id of user to be queryied, and then redirect the bot to the
|
||||
# actual user api endpoint.
|
||||
# So they get the data and then there will be no need to reinvent
|
||||
# the wheel.
|
||||
@app.route("/api/v1/uid/<uid>", methods=["GET"])
|
||||
@auth_desired
|
||||
@api("read")
|
||||
def user_by_uid(uid, v=None):
|
||||
user=get_account(uid)
|
||||
|
||||
return redirect(f'/api/v1/user/{user.username}/info')
|
||||
|
||||
@app.route("/u/<username>", methods=["GET"])
|
||||
def redditor_moment_redirect(username):
|
||||
return redirect(f"/@{username}")
|
||||
|
||||
@app.route("/@<username>/followers", methods=["GET"])
|
||||
@auth_required
|
||||
def followers(username, v):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
u = get_user(username, v=v)
|
||||
users = [x.user for x in u.followers]
|
||||
return render_template("followers.html", v=v, u=u, users=users)
|
||||
|
||||
@app.route("/@<username>", methods=["GET"])
|
||||
@app.route("/api/v1/user/<username>/listing", methods=["GET"])
|
||||
@auth_desired
|
||||
@public("read")
|
||||
def u_username(username, v=None):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
# username is unique so at most this returns one result. Otherwise 404
|
||||
|
||||
# case insensitive search
|
||||
|
||||
u = get_user(username, v=v)
|
||||
|
||||
# check for wrong cases
|
||||
|
||||
if username != u.username:
|
||||
return redirect(request.path.replace(username, u.username))
|
||||
|
||||
if u.reserved:
|
||||
return {'html': lambda: render_template("userpage_reserved.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": f"That username is reserved for: {u.reserved}"}
|
||||
}
|
||||
|
||||
if u.is_deleted and (not v or v.admin_level < 3):
|
||||
return {'html': lambda: render_template("userpage_deleted.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": "That user deactivated their account."}
|
||||
}
|
||||
|
||||
if u.is_private and (not v or (v.id != u.id and v.admin_level < 3)):
|
||||
return {'html': lambda: render_template("userpage_private.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": "That userpage is private"}
|
||||
}
|
||||
|
||||
if u.is_blocking and (not v or v.admin_level < 3):
|
||||
return {'html': lambda: render_template("userpage_blocking.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": f"You are blocking @{u.username}."}
|
||||
}
|
||||
|
||||
if u.is_blocked and (not v or v.admin_level < 3):
|
||||
return {'html': lambda: render_template("userpage_blocked.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": "This person is blocking you."}
|
||||
}
|
||||
|
||||
sort = request.args.get("sort", "new")
|
||||
t = request.args.get("t", "all")
|
||||
page = int(request.args.get("page", "1"))
|
||||
page = max(page, 1)
|
||||
|
||||
ids = u.userpagelisting(v=v, page=page, sort=sort, t=t)
|
||||
|
||||
# we got 26 items just to see if a next page exists
|
||||
next_exists = (len(ids) == 26)
|
||||
ids = ids[0:25]
|
||||
|
||||
# If page 1, check for sticky
|
||||
if page == 1:
|
||||
sticky = []
|
||||
sticky = g.db.query(Submission).filter_by(is_pinned=True, author_id=u.id).all()
|
||||
if sticky:
|
||||
for p in sticky:
|
||||
ids = [p.id] + ids
|
||||
|
||||
listing = get_posts(ids, v=v, sort="new")
|
||||
|
||||
if u.unban_utc:
|
||||
unban = datetime.fromtimestamp(u.unban_utc).strftime('%c')
|
||||
return {'html': lambda: render_template("userpage.html",
|
||||
unban=unban,
|
||||
u=u,
|
||||
v=v,
|
||||
listing=listing,
|
||||
page=page,
|
||||
sort=sort,
|
||||
t=t,
|
||||
next_exists=next_exists,
|
||||
is_following=(v and u.has_follower(v))),
|
||||
'api': lambda: jsonify({"data": [x.json for x in listing]})
|
||||
}
|
||||
|
||||
return {'html': lambda: render_template("userpage.html",
|
||||
u=u,
|
||||
v=v,
|
||||
listing=listing,
|
||||
page=page,
|
||||
sort=sort,
|
||||
t=t,
|
||||
next_exists=next_exists,
|
||||
is_following=(v and u.has_follower(v))),
|
||||
'api': lambda: jsonify({"data": [x.json for x in listing]})
|
||||
}
|
||||
|
||||
|
||||
@app.route("/@<username>/comments", methods=["GET"])
|
||||
@app.route("/api/v1/user/<username>/comments", methods=["GET"])
|
||||
@auth_desired
|
||||
@public("read")
|
||||
def u_username_comments(username, v=None):
|
||||
if v and v.is_banned and not v.unban_utc: return render_template("seized.html")
|
||||
|
||||
# username is unique so at most this returns one result. Otherwise 404
|
||||
|
||||
# case insensitive search
|
||||
|
||||
user = get_user(username, v=v)
|
||||
|
||||
# check for wrong cases
|
||||
|
||||
if username != user.username: return redirect(f'{user.url}/comments')
|
||||
|
||||
u = user
|
||||
|
||||
if u.reserved:
|
||||
return {'html': lambda: render_template("userpage_reserved.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": f"That username is reserved for: {u.reserved}"}
|
||||
}
|
||||
|
||||
if u.is_private and (not v or (v.id != u.id and v.admin_level < 3)):
|
||||
return {'html': lambda: render_template("userpage_private.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": "That userpage is private"}
|
||||
}
|
||||
|
||||
if u.is_blocking and (not v or v.admin_level < 3):
|
||||
return {'html': lambda: render_template("userpage_blocking.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": f"You are blocking @{u.username}."}
|
||||
}
|
||||
|
||||
if u.is_blocked and (not v or v.admin_level < 3):
|
||||
return {'html': lambda: render_template("userpage_blocked.html",
|
||||
u=u,
|
||||
v=v),
|
||||
'api': lambda: {"error": "This person is blocking you."}
|
||||
}
|
||||
|
||||
page = int(request.args.get("page", "1"))
|
||||
sort=request.args.get("sort","new")
|
||||
t=request.args.get("t","all")
|
||||
|
||||
ids = user.commentlisting(
|
||||
v=v,
|
||||
page=page,
|
||||
sort=sort,
|
||||
t=t,
|
||||
)
|
||||
|
||||
# we got 26 items just to see if a next page exists
|
||||
next_exists = (len(ids) == 26)
|
||||
ids = ids[0:25]
|
||||
|
||||
listing = get_comments(ids, v=v)
|
||||
|
||||
is_following = (v and user.has_follower(v))
|
||||
|
||||
board = get_board(1)
|
||||
return {"html": lambda: render_template("userpage_comments.html",
|
||||
u=user,
|
||||
v=v,
|
||||
listing=listing,
|
||||
page=page,
|
||||
sort=sort,
|
||||
t=t,
|
||||
next_exists=next_exists,
|
||||
is_following=is_following,
|
||||
standalone=True),
|
||||
"api": lambda: jsonify({"data": [c.json for c in listing]})
|
||||
}
|
||||
|
||||
@app.route("/api/v1/user/<username>/info", methods=["GET"])
|
||||
@auth_desired
|
||||
@public("read")
|
||||
def u_username_info(username, v=None):
|
||||
|
||||
user=get_user(username, v=v)
|
||||
|
||||
if user.is_blocking:
|
||||
return jsonify({"error": "You're blocking this user."}), 401
|
||||
elif user.is_blocked:
|
||||
return jsonify({"error": "This user is blocking you."}), 403
|
||||
|
||||
return jsonify(user.json)
|
||||
|
||||
|
||||
@app.route("/api/follow/<username>", methods=["POST"])
|
||||
@auth_required
|
||||
def follow_user(username, v):
|
||||
|
||||
target = get_user(username)
|
||||
|
||||
if target.id==v.id:
|
||||
return jsonify({"error": "You can't follow yourself!"}), 400
|
||||
|
||||
# check for existing follow
|
||||
if g.db.query(Follow).filter_by(user_id=v.id, target_id=target.id).first():
|
||||
abort(409)
|
||||
|
||||
new_follow = Follow(user_id=v.id,
|
||||
target_id=target.id)
|
||||
|
||||
g.db.add(new_follow)
|
||||
g.db.flush()
|
||||
target.stored_subscriber_count=target.follower_count
|
||||
g.db.add(target)
|
||||
g.db.commit()
|
||||
|
||||
existing = g.db.query(Notification).filter_by(followsender=v.id, user_id=target.id).first()
|
||||
if not existing: send_follow_notif(v.id, target.id, f"@{v.username} has followed you!")
|
||||
return "", 204
|
||||
|
||||
|
||||
@app.route("/api/unfollow/<username>", methods=["POST"])
|
||||
@auth_required
|
||||
def unfollow_user(username, v):
|
||||
|
||||
target = get_user(username)
|
||||
|
||||
# check for existing follow
|
||||
follow = g.db.query(Follow).filter_by(
|
||||
user_id=v.id, target_id=target.id).first()
|
||||
|
||||
if not follow:
|
||||
abort(409)
|
||||
|
||||
g.db.delete(follow)
|
||||
|
||||
existing = g.db.query(Notification).filter_by(followsender=v.id, user_id=target.id).first()
|
||||
if not existing: send_unfollow_notif(v.id, target.id, f"@{v.username} has unfollowed you!")
|
||||
return "", 204
|
||||
|
||||
|
||||
@app.route("/@<username>/pic/profile")
|
||||
@limiter.exempt
|
||||
def user_profile(username):
|
||||
x = get_user(username)
|
||||
return redirect(x.profile_url)
|
||||
|
||||
@app.route("/uid/<uid>/pic/profile")
|
||||
@limiter.exempt
|
||||
def user_profile_uid(uid):
|
||||
x=get_account(uid)
|
||||
return redirect(x.profile_url)
|
||||
|
||||
|
||||
@app.route("/@<username>/saved/posts", methods=["GET"])
|
||||
@app.route("/api/v1/saved/posts", methods=["GET"])
|
||||
@auth_required
|
||||
@api("read")
|
||||
def saved_posts(v, username):
|
||||
|
||||
page=int(request.args.get("page",1))
|
||||
|
||||
ids=v.saved_idlist(page=page)
|
||||
|
||||
next_exists=len(ids)==26
|
||||
|
||||
ids=ids[0:25]
|
||||
|
||||
listing = get_posts(ids, v=v, sort="new")
|
||||
|
||||
return {'html': lambda: render_template("userpage.html",
|
||||
u=v,
|
||||
v=v,
|
||||
listing=listing,
|
||||
page=page,
|
||||
next_exists=next_exists,
|
||||
),
|
||||
'api': lambda: jsonify({"data": [x.json for x in listing]})
|
||||
}
|
||||
|
||||
|
||||
@app.route("/@<username>/saved/comments", methods=["GET"])
|
||||
@app.route("/api/v1/saved/comments", methods=["GET"])
|
||||
@auth_required
|
||||
@api("read")
|
||||
def saved_comments(v, username):
|
||||
|
||||
page=int(request.args.get("page",1))
|
||||
|
||||
ids=v.saved_comment_idlist(page=page)
|
||||
|
||||
next_exists=len(ids)==26
|
||||
|
||||
ids=ids[0:25]
|
||||
|
||||
listing = get_comments(ids, v=v, sort="new")
|
||||
|
||||
|
||||
return {'html': lambda: render_template("userpage_comments.html",
|
||||
u=v,
|
||||
v=v,
|
||||
listing=listing,
|
||||
page=page,
|
||||
next_exists=next_exists,
|
||||
standalone=True),
|
||||
'api': lambda: jsonify({"data": [x.json for x in listing]})
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
from ruqqus.helpers.wrappers import *
|
||||
from ruqqus.helpers.get import *
|
||||
from ruqqus.classes import *
|
||||
from flask import *
|
||||
from ruqqus.__main__ import app
|
||||
|
||||
|
||||
@app.route("/api/v1/vote/post/<post_id>/<x>", methods=["POST"])
|
||||
@app.route("/api/vote/post/<post_id>/<x>", methods=["POST"])
|
||||
@is_not_banned
|
||||
@no_negative_balance("toast")
|
||||
@api("vote")
|
||||
@validate_formkey
|
||||
def api_vote_post(post_id, x, v):
|
||||
|
||||
if x not in ["-1", "0", "1"]:
|
||||
abort(400)
|
||||
|
||||
# disallow bots
|
||||
if request.headers.get("X-User-Type","") == "Bot":
|
||||
abort(403)
|
||||
|
||||
x = int(x)
|
||||
|
||||
if x==-1:
|
||||
count=g.db.query(Vote).filter(
|
||||
Vote.user_id.in_(
|
||||
tuple(
|
||||
[v.id]+[x.id for x in v.alts]
|
||||
)
|
||||
),
|
||||
Vote.created_utc > (int(time.time())-3600),
|
||||
Vote.vote_type==-1
|
||||
).count()
|
||||
if count >=15 and v.admin_level==0:
|
||||
return jsonify({"error": "You're doing that too much. Try again later."}), 403
|
||||
|
||||
post = get_post(post_id)
|
||||
|
||||
if post.is_banned:
|
||||
return jsonify({"error":"That post has been removed."}), 403
|
||||
elif post.deleted_utc > 0:
|
||||
return jsonify({"error":"That post has been deleted."}), 403
|
||||
elif post.is_archived:
|
||||
return jsonify({"error":"That post is archived and can no longer be voted on."}), 403
|
||||
|
||||
# check for existing vote
|
||||
existing = g.db.query(Vote).filter_by(
|
||||
user_id=v.id, submission_id=post.id).first()
|
||||
if existing:
|
||||
existing.change_to(x)
|
||||
g.db.add(existing)
|
||||
|
||||
else:
|
||||
vote = Vote(user_id=v.id,
|
||||
vote_type=x,
|
||||
submission_id=base36decode(post_id),
|
||||
creation_ip=request.remote_addr,
|
||||
app_id=v.client.application.id if v.client else None
|
||||
)
|
||||
|
||||
g.db.add(vote)
|
||||
|
||||
try:
|
||||
g.db.flush()
|
||||
except:
|
||||
return jsonify({"error":"Vote already exists."}), 422
|
||||
|
||||
posts = []
|
||||
posts.append(post)
|
||||
|
||||
post.upvotes = post.ups
|
||||
post.downvotes = post.downs
|
||||
g.db.add(post)
|
||||
return "", 204
|
||||
|
||||
@app.route("/api/v1/vote/comment/<comment_id>/<x>", methods=["POST"])
|
||||
@app.route("/api/vote/comment/<comment_id>/<x>", methods=["POST"])
|
||||
@is_not_banned
|
||||
@no_negative_balance("toast")
|
||||
@api("vote")
|
||||
@validate_formkey
|
||||
def api_vote_comment(comment_id, x, v):
|
||||
|
||||
if x not in ["-1", "0", "1"]:
|
||||
abort(400)
|
||||
|
||||
# disallow bots
|
||||
if request.headers.get("X-User-Type","") == "Bot":
|
||||
abort(403)
|
||||
|
||||
x = int(x)
|
||||
|
||||
comment = get_comment(comment_id)
|
||||
|
||||
# check for existing vote
|
||||
existing = g.db.query(CommentVote).filter_by(
|
||||
user_id=v.id, comment_id=comment.id).first()
|
||||
if existing:
|
||||
existing.change_to(x)
|
||||
g.db.add(existing)
|
||||
else:
|
||||
|
||||
vote = CommentVote(user_id=v.id,
|
||||
vote_type=x,
|
||||
comment_id=base36decode(comment_id),
|
||||
creation_ip=request.remote_addr,
|
||||
app_id=v.client.application.id if v.client else None
|
||||
)
|
||||
|
||||
g.db.add(vote)
|
||||
try:
|
||||
g.db.flush()
|
||||
except:
|
||||
return jsonify({"error":"Vote already exists."}), 422
|
||||
|
||||
comment.upvotes = comment.ups
|
||||
comment.downvotes = comment.downs
|
||||
g.db.add(comment)
|
||||
return make_response(""), 204
|
|
@ -0,0 +1,7 @@
|
|||
const vm = new Vue({
|
||||
el: '#vm',
|
||||
delimiters: ['[[', ']]'],
|
||||
data: {
|
||||
greeting: 'Hello, Vue!'
|
||||
}
|
||||
})
|
|
@ -0,0 +1,67 @@
|
|||
<!-- 2FA Modal -->
|
||||
|
||||
<div class="modal fade" id="2faModal" tabindex="-1" role="dialog" aria-labelledby="2faModalTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% if mfa_secret %}Setup two-step login{% elif mfa_secret and not v.email %}Email required for two-step login{% else %}Disable two-step login{% endif %}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true"><i class="far fa-times"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
{% if mfa_secret %}
|
||||
<div>
|
||||
<form action="/settings/security" method="post">
|
||||
<input type="hidden" name="formkey" value="{{v.formkey}}">
|
||||
<input type="hidden" name="2fa_secret" value="{{mfa_secret}}">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<span class="font-weight-bold">Step 1:</span> Scan this barcode (or enter the code) using a two-factor authentication app such as Google Authenticator or Authy.
|
||||
</p>
|
||||
<div class="text-center mb-3">
|
||||
<img class="img-fluid" style="width: 175px;" src="/2faqr/{{mfa_secret}}">
|
||||
<div class="text-small text-muted">Or enter this code: {{mfa_secret}}</div>
|
||||
</div>
|
||||
<p>
|
||||
<span class="font-weight-bold">Step 2:</span> Enter the six-digit code generated in the authenticator app and your Drama account password.
|
||||
</p>
|
||||
<label for="2fa_input">6-digit code</label>
|
||||
<input type="text" class="form-control mb-2" id="2fa_input" name="2fa_token" placeholder="# # # # # #" required>
|
||||
|
||||
<label for="2fa_input_password">Password</label>
|
||||
<input type="password" autocomplete="new-password" class="form-control mb-2" id="2fa_input_password" name="password" oninput="document.getElementById('enable2faButton').disabled=false" autocomplete="off" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link text-muted" data-dismiss="modal">Cancel</button>
|
||||
<input id="enable2faButton" class="btn btn-primary" type="submit" value="Enable 2-step login" disabled>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<form action="/settings/security" method="post">
|
||||
<input type="hidden" name="formkey" value="{{v.formkey}}">
|
||||
<input type="hidden" name="2fa_secret" value="{{mfa_secret}}">
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
To disable two-step login, please enter your Drama account password and the 6-digit code generated in your authentication app. If you no longer have your two-step device, <a href="/lost_2fa">click here</a>.
|
||||
</div>
|
||||
|
||||
<label for="2fa_input_password">Password</label>
|
||||
<input type="password" autocomplete="new-password" class="form-control mb-2" id="2fa_input_password" name="password" autocomplete="off" required>
|
||||
|
||||
<label for="2fa_input">6-digit code</label>
|
||||
<input type="text" class="form-control mb-2" id="2fa_input" name="2fa_remove" placeholder="# # # # # #" oninput="document.getElementById('disable2faButton').disabled=false" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link text-muted" data-dismiss="modal">Cancel</button>
|
||||
<input id="disable2faButton" class="btn btn-primary" type="submit" value="Disable 2-step login" disabled>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,40 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>Drama</title>
|
||||
<meta name="description" content="Drama Help">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<pre></pre>
|
||||
<pre></pre>
|
||||
<h4> Admin Tools</h4>
|
||||
{% filter markdown %}
|
||||
* [Advanced Stats](/api/user_stat_data)
|
||||
* [Ban Domain](/admin/domain/enter%20domain%20here)
|
||||
* [Shadowbanned Users](/admin/shadowbanned)
|
||||
* [Users with Agendaposter Theme](/admin/agendaposters)
|
||||
* [Flagged Posts](/admin/flagged/posts)
|
||||
* [Flagged Comments](/admin/flagged/comments)
|
||||
* [Image Posts](/admin/image_posts)
|
||||
* [Removed Posts](/admin/removed)
|
||||
* [Users Feed](/admin/users)
|
||||
* [Remove image from imgur and from cloudflare cache](/admin/image_purge)
|
||||
* [Perceptive Hash Image Ban](/admin/image_ban)
|
||||
* [Multi Vote Analysis](/admin/alt_votes)
|
||||
* [App Queue](/admin/apps)
|
||||
* [App Data](/admin/appdata)
|
||||
* [Badges](/admin/badge_grant)
|
||||
* [Content Stats](/admin/content_stats)
|
||||
|
||||
{% endfilter %}
|
||||
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="disablesignups" name="disablesignups" {% if b.disablesignups %}checked{% endif %} onchange="post('/disablesignups');">
|
||||
<label class="custom-control-label" for="disablesignups">Disable signups</label>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,94 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>Drama</title>
|
||||
<meta name="description" content="Drama 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>
|
||||
|
||||
<p><b>{{u1.username}} Creation IP:</b> {{u1.creation_ip}}</p>
|
||||
<p><b>{{u2.username}} Creation IP:</b> {{u2.creation_ip}}</p>
|
||||
|
||||
|
||||
|
||||
<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 %}
|
|
@ -0,0 +1,76 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>API App Administration</title>
|
||||
<meta name="description" content="Drama 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_secret%}
|
||||
|
||||
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/approve/{{app.base36id}}')">Approve</a>
|
||||
<a href="javascript:void(0)" class="btn btn-secondary mr-0" onclick="post_toast('/admin/app/reject/{{app.base36id}}')">Reject</a>
|
||||
|
||||
{% else %}
|
||||
|
||||
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/revoke/{{app.base36id}}')">Revoke</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if listing %}
|
||||
{% include "submission_listing.html" %}
|
||||
{% elif comments %}
|
||||
{% include "comments.html" %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!----posttoast--->
|
||||
<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-animation="true" data-autohide="true" data-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-animation="true" data-autohide="true" data-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>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,79 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>Drama</title>
|
||||
<meta name="description" content="Drama Help">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<pre>
|
||||
|
||||
|
||||
|
||||
</pre>
|
||||
<h5>App Info</h5>
|
||||
|
||||
<form action="/admin/appdata" method="get" class="mb-6">
|
||||
<label for="link-input">Paste permalink</label>
|
||||
<input id="link-input" type="text" class="form-control mb-2" name="link" value="{{thing.permalink if thing else ''}}">
|
||||
<input type="submit" value="Submit" class="btn btn-primary">
|
||||
</form>
|
||||
|
||||
{% if thing %}
|
||||
|
||||
<h1>Info</h1>
|
||||
<p><a href="{{thing.permalink}}">{{thing.permalink}}</a></p>
|
||||
|
||||
|
||||
<p><b>Author:</b> <a href="{{thing.author.permalink}}">@{{thing.author.username}}</a></p>
|
||||
|
||||
{% if thing.oauth_app %}
|
||||
|
||||
|
||||
<div class="settings-section rounded">
|
||||
<div class="d-lg-flex">
|
||||
<div class="title w-lg-25">
|
||||
<label for="over18"><a href="{{thing.oauth_app.permalink}}" target="_blank">{{thing.oauth_app.app_name}}</a></label>
|
||||
</div>
|
||||
<div class="body w-lg-100">
|
||||
<label for="edit-{{thing.oauth_app.id}}-author" class="mb-0 w-lg-25">User</label>
|
||||
<input id="edit-{{thing.oauth_app.id}}-author" class="form-control" type="text" name="name" value="{{thing.oauth_app.author.username}}" readonly=readonly>
|
||||
|
||||
<label for="edit-{{thing.oauth_app.id}}-name" class="mb-0 w-lg-25">App Name</label>
|
||||
<input id="edit-{{thing.oauth_app.id}}-name" class="form-control" type="text" name="name" value="{{thing.oauth_app.app_name}}" readonly=readonly>
|
||||
|
||||
<label for="edit-{{thing.oauth_app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
|
||||
<input id="edit-{{thing.oauth_app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{thing.oauth_app.redirect_uri}}" readonly="readonly">
|
||||
<label for="edit-{{thing.oauth_app.id}}-desc" class="mb-0 w-lg-25">Description</label>
|
||||
<textarea form="edit-app-{{thing.oauth_app.id}}" class="form-control" name="description" id="edit-{{thing.oauth_app.id}}-desc" maxlength="256" readonly="readonly">{{thing.oauth_app.description}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="d-flex">
|
||||
{% if not thing.oauth_app.client_secret %}
|
||||
|
||||
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/approve/{{thing.oauth_app.base36id}}')">Approve</a>
|
||||
<a href="javascript:void(0)" class="btn btn-secondary mr-0" onclick="post_toast('/admin/app/reject/{{thing.oauth_app.base36id}}')">Reject</a>
|
||||
|
||||
{% else %}
|
||||
|
||||
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/revoke/{{thing.oauth_app.base36id}}')">Revoke</a>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,76 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>API App Administration</title>
|
||||
<meta name="description" content="Drama 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_secret %}
|
||||
<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_secret %}
|
||||
|
||||
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/approve/{{app.base36id}}')">Approve</a>
|
||||
<a href="javascript:void(0)" class="btn btn-secondary mr-0" onclick="post_toast('/admin/app/reject/{{app.base36id}}')">Reject</a>
|
||||
|
||||
{% else %}
|
||||
|
||||
<a href="javascript:void(0)" class="btn btn-primary ml-auto" onclick="post_toast('/admin/app/revoke/{{app.base36id}}')">Revoke</a>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!----posttoast--->
|
||||
<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-animation="true" data-autohide="true" data-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-animation="true" data-autohide="true" data-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>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,76 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>Badge Grant</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block pagetype %}message{% endblock %}
|
||||
|
||||
{% block sidebarblock %}{% 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-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-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 class="d-block" 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 %}
|
|
@ -0,0 +1,27 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>Drama</title>
|
||||
<meta name="description" content="Drama Help">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<pre></pre>
|
||||
<pre></pre>
|
||||
<h5>Cache Dump</h5>
|
||||
<a href="javascript:void(0)" class="btn btn-primary mt-3" onclick="post_toast('/admin/dump_cache')">Clear internal cache</a>
|
||||
|
||||
<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-animation="true" data-autohide="true" data-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-animation="true" data-autohide="true" data-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>
|
||||
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>Drama</title>
|
||||
<meta name="description" content="Drama 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 %}
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "admin/flagged_posts.html" %}
|
||||
|
||||
|
||||
|
||||
{% block listing %}
|
||||
|
||||
|
||||
<div class="posts p-3">
|
||||
{% 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 %}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
{% extends "userpage.html" %}
|
||||
|
||||
{% block adminpanel %}{% endblock %}
|
||||
{% block pagetype %}userpage{% endblock %}
|
||||
{% block banner %}{% endblock %}
|
||||
{% block mobileBanner %}{% endblock %}
|
||||
{% block desktopBanner %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
{% block desktopUserBanner %}{% endblock %}
|
||||
{% block mobileUserBanner %}{% endblock %}
|
||||
|
||||
{% block adminscripts %}
|
||||
<script src="/assets/js/comment_modding.js"></script>
|
||||
{% 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/flagged/posts" %} active{% endif %}" href="/admin/flagged/posts">
|
||||
<div>Posts</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.path=="/admin/flagged/comments" %} active{% endif %}" href="/admin/flagged/comments">
|
||||
<div>Comments</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block enlargeThumbJS %}
|
||||
<script src="/assets/js/enlarge_thumb.js"></script>
|
||||
{% 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>Flagged Posts</title>
|
||||
<meta name="description" content="on Drama">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Post filters bar visible only on medium devices or larger-->
|
||||
|
||||
<div class="row no-gutters">
|
||||
|
||||
<div class="col">
|
||||
|
||||
{% block listing %}
|
||||
<div class="posts p-3">
|
||||
{% 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 %}
|
|
@ -0,0 +1,45 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% 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 %}
|
|
@ -0,0 +1,105 @@
|
|||
{% extends "userpage.html" %}
|
||||
|
||||
{% block adminpanel %}{% endblock %}
|
||||
{% block pagetype %}userpage{% endblock %}
|
||||
{% block banner %}{% endblock %}
|
||||
{% block mobileBanner %}{% endblock %}
|
||||
{% block desktopBanner %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
{% block desktopUserBanner %}{% endblock %}
|
||||
{% block mobileUserBanner %}{% endblock %}
|
||||
|
||||
{% block adminscripts %}
|
||||
<script src="/assets/js/comment_modding.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block postNav %}{% endblock %}
|
||||
|
||||
|
||||
{% block enlargeThumbJS %}
|
||||
<script src="/assets/js/enlarge_thumb.js"></script>
|
||||
{% 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 Drama">
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block sidebarblock %}
|
||||
<pre></pre>
|
||||
<pre></pre>
|
||||
<div class="sidebar-section sidebar-profile-basic">
|
||||
<div class="body">
|
||||
<pre>
|
||||
|
||||
|
||||
|
||||
</pre>
|
||||
<h5 class="h6 d-inline-block mb-0" style="color:black">Image Posts</h5>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Post filters bar visible only on medium devices or larger-->
|
||||
|
||||
<div class="row no-gutters">
|
||||
|
||||
<div class="col">
|
||||
|
||||
{% block listing %}
|
||||
<div class="posts p-3">
|
||||
{% 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 %}
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>Purge Image</title>
|
||||
<meta name="description" content="Purge Image">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Imgur and Cloudflare Image Purge</h1>
|
||||
<p>Paste an image link here to remove it from imgur and from cloudflare cache. You need to enter the full url of the image like this "https://i.imgur.com/63OclpM_d.png?maxwidth=9999"</p>
|
||||
|
||||
<form action="/admin/image_purge" method="post" class="mb-6">
|
||||
<label for="link-input">Image Link</label>
|
||||
<input id="link-input" type="text" class="form-control mb-2" name="url">
|
||||
<input type="submit" value="Delete Image" class="btn btn-primary">
|
||||
</form>
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,53 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>Drama</title>
|
||||
<meta name="description" content="Drama Help">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<input id="domainget" type="text" class="form-control" placeholder="Enter domain name" value="{{domain_name}}">
|
||||
<a class="btn btn-primary" href="javascript:void(0)" onclick="window.location.href='/admin/domain/'+document.getElementById('domainget').value">Get Domain Record</a>
|
||||
|
||||
<h1>{{domain_name}}</h2>
|
||||
|
||||
<h2>Current</h2>
|
||||
|
||||
<h3 class="h5 pt-2">can_submit</h3>
|
||||
<p>{{domain.can_submit}}</p>
|
||||
|
||||
<h3 class="h5 pt-2">can_comment</h3>
|
||||
<p>{{domain.can_comment}}</p>
|
||||
|
||||
<h3 class="h5 pt-2">reason</h3>
|
||||
<p>{{domain.reason_text}}</p>
|
||||
|
||||
<h3 class="h5 pt-2">show_thumbnail</h3>
|
||||
<p>{{domain.show_thumbnail}}</p>
|
||||
|
||||
<h3 class="h5 pt-2">embed_function</h3>
|
||||
<p>{{domain.embed_function}}</p>
|
||||
|
||||
<h3 class="h5 pt-2">embed_template</h3>
|
||||
<p>{{domain.embed_template}}</p>
|
||||
|
||||
<h2>Actions</h2>
|
||||
|
||||
<form action="/admin/ban_domain" method="post">
|
||||
<input type="hidden" name="formkey" value="{{v.formkey}}">
|
||||
<input type="hidden" name="domain" value="{{domain_name}}">
|
||||
<label for="reason_select">Ban reason</label>
|
||||
<select id="reason_select" class="form-control" name="reason" onchange="$('#ban-submit').prop('disabled', false)">
|
||||
<option value="0">---Select Ban Reason---</option>
|
||||
{% for i in reasons %}
|
||||
<option value="{{i}}"{% if i==domain.reason %} selected{% endif %}>{{reasons[i]}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="submit" class="btn btn-primary" value="Ban {{domain_name}}" disabled>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "mine/mine.html" %}
|
||||
|
||||
{% block maincontent %}
|
||||
<img src="{{single_plot}}">
|
||||
<img src="{{multi_plot}}">
|
||||
{% include "user_listing.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebarblock %}
|
||||
<div class="sidebar-section sidebar-about">
|
||||
<div class="title">All Users</div>
|
||||
<div class="body">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block navbar %}{% endblock %}
|
||||
|
||||
{% block sidebar %}{% endblock %}
|
|
@ -0,0 +1,60 @@
|
|||
{% extends "admin/image_posts.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
<title>Removed Content</title>
|
||||
<meta name="description" content="on Drama">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Post filters bar visible only on medium devices or larger-->
|
||||
|
||||
<div class="row justify-content-around mx-lg-5 d-block 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 p-3">
|
||||
{% 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 %}
|
|
@ -0,0 +1,64 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block sidebarblock %}{% endblock %}
|
||||
{% block sidebarLeftblock %}{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
<title>Drama</title>
|
||||
<meta name="description" content="Drama Help">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Vote Info</h1>
|
||||
|
||||
<form action="votes" method="get" class="mb-6">
|
||||
<label for="link-input">Paste permalink</label>
|
||||
<input id="link-input" type="text" class="form-control mb-2" name="link" value="{{thing.permalink if thing else ''}}">
|
||||
<input type="submit" value="Submit" class="btn btn-primary">
|
||||
</form>
|
||||
|
||||
{% if thing %}
|
||||
|
||||
<h1>Info</h1>
|
||||
<p><a href="{{thing.permalink}}">{{thing.permalink}}</a></p>
|
||||
<p><b>Author:</b> <a href="{{thing.author.permalink}}">@{{thing.author.username}}</a></p>
|
||||
<p><b>Author Created At:</b> {{thing.author.created_utc}} ({{thing.author.created_datetime}} UTC)</p>
|
||||
<p><b>Counted Upvotes:</b> {{thing.upvotes}} out of {{ups | length}}</p>
|
||||
<p><b>Counted Downvotes:</b> {{thing.downvotes}} out of {{downs | length}}</p>
|
||||
|
||||
<h2>Upvotes</h2>
|
||||
<table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{% for vote in ups %}
|
||||
<tr>
|
||||
<td style="font-weight:bold;"><a style="color:#{{vote.user.namecolor}}" href="/@{{vote.user.username}}">{{vote.user.username}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h2>Downvotes</h2>
|
||||
<table class="table table-striped mb-5">
|
||||
<thead class="bg-primary text-white">
|
||||
<tr>
|
||||
<th>User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{% for vote in downs %}
|
||||
<tr>
|
||||
<td style="font-weight:bold;"><a style="color:#{{vote.user.namecolor}}" href="/@{{vote.user.username}}">{{vote.user.username}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,131 @@
|
|||
<!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 %}Drama - the free speech social platform{% endblock %}">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>{% block pagetitle %}Drama - the open, free-speech social platform{% endblock %}</title>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
|
||||
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="/assets/fontawesome/css/all.css" rel="stylesheet"> <!--load all styles -->
|
||||
|
||||
<script data-search-pseudo-elements defer src="https://use.fontawesome.com/releases/latest/js/all.js"
|
||||
integrity="sha384-L469/ELG4Bg9sDQbl0hvjMq8pOcqFgkSpwhwnslzvVVGpDjYJ6wJJyYjvG3u8XW7"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Drama CSS -->
|
||||
{% if v %}
|
||||
<link rel="stylesheet" href="/assets/style/{{v.theme}}_{{v.themecolor}}.css">
|
||||
{% if v.agendaposter %}<link rel="stylesheet" href="/assets/style/agendaposter.css">{% elif v.css %}<link rel="stylesheet" href="/@{{v.username}}/css">{% endif %}
|
||||
{% else %}
|
||||
<link rel="stylesheet" href="/assets/style/dark_ff66ac.css">
|
||||
{% endif %}
|
||||
|
||||
</head>
|
||||
|
||||
<body id="login">
|
||||
|
||||
<!-- Navigation -->
|
||||
<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-toggle="collapse" data-target="#navbarResponsive"
|
||||
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
|
||||
<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">Drama</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-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-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>
|
||||
|
||||
<h6 class="splash-text">
|
||||
{{i.text}}
|
||||
</h6>
|
||||
|
||||
<img class="splash-img" src="{{i.path}}"></img>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap core JavaScript -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
|
||||
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
|
||||
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
|
||||
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,5 @@
|
|||
{% if b.url %}
|
||||
<a href="{{b.url}}"><img style="width: 32px; height: 32px" src="{{b.path}}" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="{{b.name}} - {{b.text}}"></a>
|
||||
{% else %}
|
||||
<img style="width: 32px; height: 32px" src="{{b.path}}" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="{{b.name}} - {{b.text}}">
|
||||
{% endif %}
|
|
@ -0,0 +1,6 @@
|
|||
<div>
|
||||
{{bp[0].rendered | safe}}
|
||||
{% if bp|length > 1 %}
|
||||
{{bp[1].rendered | safe}}
|
||||
{% endif %}
|
||||
</div>
|
|
@ -0,0 +1,69 @@
|
|||
{% 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 Drama.</div>
|
||||
<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 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 staff.</div>
|
||||
<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 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>
|
||||
<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 src="{{badge.path}}" style="width:50px;height:50px">
|
||||
<td>{{badge.description}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "guild_settings.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;">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}}">{{user.username}}</a></td>
|
||||
<td style="font-weight:bold;">{% if user.ban_reason %}{{user.ban_reason}}{% endif %}</td>
|
||||
<td><a style="color:#{{user.banned_by.namecolor}}; font-weight:bold;" href="/@{{user.banned_by.username}}">{{user.banned_by.username}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "guild_settings.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}}">{{user.username}}</a></td>
|
||||
<td><a style="font-weight:bold;color:#{{targets[loop.index-1].namecolor}};" href="/@{{targets[loop.index-1].username}}">{{targets[loop.index-1].username}}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue