remotes/1693045480750635534/spooky-22
Aevann1 2021-07-21 03:12:26 +02:00
commit 1fc69b9ce9
207 changed files with 35295 additions and 0 deletions

4
.gitattributes vendored 100644
View File

@ -0,0 +1,4 @@
*.css linguist-detectable=false
*.js linguist-detectable=true
*.html linguist-detectable=false
*.py linguist-detectable=true

3
.github/FUNDING.yml vendored 100644
View File

@ -0,0 +1,3 @@
github: Aevann1
patreon: Aevann
custom: ["https://rdrama.gumroad.com/l/tfcvri"]

115
.gitignore vendored 100644
View File

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

View File

@ -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"]
}
}]

View File

@ -0,0 +1,4 @@
This is a Brave Rewards publisher verification file.
Domain: rdrama.net
Token: 0774158a4aec1e891263f84cf37919c0aa19309b9fba4ad9c4a0aae8946f5d0d

View File

@ -0,0 +1 @@
importScripts("https://js.pusher.com/beams/service-worker.js");

17
Dockerfile 100644
View File

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

373
LICENSE 100644
View File

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

13
appspec.yml 100644
View File

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

8
buildspec.yml 100644
View File

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

7
compilecss.py 100644
View File

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

6
dependabot.yml 100644
View File

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

62
docker-compose.yml 100644
View File

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

17
push.sh 100644
View File

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

3
pushforce.sh 100644
View File

@ -0,0 +1,3 @@
git add .
git commit -m "force push"
git push --force

78
readme.md 100644
View File

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

69
requirements.txt 100644
View File

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

352
ruqqus/__main__.py 100644
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
class PaymentRequired(Exception):
status_code=402
def __init__(self):
Exception.__init__(self)
self.status_code=402

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = '&lt;s&gt;'
end = '&lt;/s&gt;'
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
from .mail import *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
const vm = new Vue({
el: '#vm',
delimiters: ['[[', ']]'],
data: {
greeting: 'Hello, Vue!'
}
})

View File

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

View File

@ -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>&nbsp;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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
<div>
{{bp[0].rendered | safe}}
{% if bp|length > 1 %}
{{bp[1].rendered | safe}}
{% endif %}
</div>

View File

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

View File

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

View File

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