openwrite 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openwrite/__init__.py +134 -0
- openwrite/cli.py +340 -0
- openwrite/db/__init__.py +0 -0
- openwrite/db/base.py +3 -0
- openwrite/launch_gemini.py +8 -0
- openwrite/routes/admin.py +170 -0
- openwrite/routes/auth.py +77 -0
- openwrite/routes/blog.py +205 -0
- openwrite/routes/dashboard.py +408 -0
- openwrite/routes/federation.py +284 -0
- openwrite/routes/main.py +96 -0
- openwrite/routes/upload.py +81 -0
- openwrite/static/IBMPlexSans-Regular.ttf +0 -0
- openwrite/static/Merriweather.ttf +0 -0
- openwrite/static/avatar.png +0 -0
- openwrite/static/favicon.ico +0 -0
- openwrite/static/logo.png +0 -0
- openwrite/static/script/chart.js +14 -0
- openwrite/static/script/highlight.min.js +1244 -0
- openwrite/static/script/openwrite.js +88 -0
- openwrite/static/script/papaparse.min.js +7 -0
- openwrite/static/script/showdown.min.js +3 -0
- openwrite/static/script/showdown.min.js.map +1 -0
- openwrite/static/script/site.compat.min.js +7 -0
- openwrite/static/script/site.min.js +7 -0
- openwrite/static/style/hl.css +1 -0
- openwrite/static/style/style.css +611 -0
- openwrite/static/style/themes/light.css +28 -0
- openwrite/static/style/themes/obscura.css +27 -0
- openwrite/static/style/themes/stickynote.css +50 -0
- openwrite/static/style/themes/terminal.css +37 -0
- openwrite/static/style/themes/warmnight.css +73 -0
- openwrite/static/style/themes/windows98.css +132 -0
- openwrite/templates/admin.html +47 -0
- openwrite/templates/blog.html +16 -0
- openwrite/templates/blog_footer.html +6 -0
- openwrite/templates/blog_header.html +45 -0
- openwrite/templates/blog_preview.html +44 -0
- openwrite/templates/changepw.html +19 -0
- openwrite/templates/create.html +46 -0
- openwrite/templates/dashboard.html +45 -0
- openwrite/templates/discover.html +11 -0
- openwrite/templates/edit.html +190 -0
- openwrite/templates/footer.html +16 -0
- openwrite/templates/header.html +61 -0
- openwrite/templates/import.html +142 -0
- openwrite/templates/index.html +9 -0
- openwrite/templates/instances.html +14 -0
- openwrite/templates/login.html +18 -0
- openwrite/templates/new_post.html +207 -0
- openwrite/templates/post.html +53 -0
- openwrite/templates/preview.html +37 -0
- openwrite/templates/register.html +42 -0
- openwrite/templates/settings.html +63 -0
- openwrite/templates/stats.html +242 -0
- openwrite/utils/create_db.py +15 -0
- openwrite/utils/db.py +30 -0
- openwrite/utils/helpers.py +216 -0
- openwrite/utils/i18n.json +229 -0
- openwrite/utils/models.py +87 -0
- openwrite/utils/translations.py +6 -0
- openwrite-0.10.1.dist-info/METADATA +684 -0
- openwrite-0.10.1.dist-info/RECORD +67 -0
- openwrite-0.10.1.dist-info/WHEEL +5 -0
- openwrite-0.10.1.dist-info/entry_points.txt +2 -0
- openwrite-0.10.1.dist-info/licenses/LICENSE +661 -0
- openwrite-0.10.1.dist-info/top_level.txt +1 -0
openwrite/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from flask import Flask, g
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
4
|
+
from werkzeug.exceptions import HTTPException
|
|
5
|
+
import os
|
|
6
|
+
from .utils.helpers import generate_nonce, get_ip
|
|
7
|
+
import time
|
|
8
|
+
from .utils.models import Settings
|
|
9
|
+
|
|
10
|
+
start_time = time.time()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_app(test_config=None):
|
|
14
|
+
if test_config is None:
|
|
15
|
+
load_dotenv()
|
|
16
|
+
db_type = os.getenv("DB_TYPE", "sqlite")
|
|
17
|
+
db_path = os.getenv("DB_PATH", "db.sqlite")
|
|
18
|
+
else:
|
|
19
|
+
db_type = test_config.get("DB_TYPE", "sqlite")
|
|
20
|
+
db_path = test_config.get("DB_PATH", "data.db")
|
|
21
|
+
load_dotenv(test_config.get("env"), override=True)
|
|
22
|
+
app = Flask(__name__, template_folder="templates", subdomain_matching=True, static_url_path='/static')
|
|
23
|
+
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
|
24
|
+
app.secret_key = os.getenv("SECRET_KEY")
|
|
25
|
+
app.config['SERVER_NAME'] = os.getenv("DOMAIN")
|
|
26
|
+
|
|
27
|
+
from .routes.auth import auth_bp
|
|
28
|
+
from .routes.dashboard import dashboard_bp
|
|
29
|
+
from .routes.blog import blog_bp
|
|
30
|
+
from .routes.admin import admin_bp
|
|
31
|
+
from .routes.main import main_bp
|
|
32
|
+
from .routes.upload import upload_bp
|
|
33
|
+
from .routes.federation import federation_bp
|
|
34
|
+
from .utils.translations import load_translations
|
|
35
|
+
from .utils.db import init_engine, SessionLocal
|
|
36
|
+
from flask_cors import CORS
|
|
37
|
+
|
|
38
|
+
translations = load_translations()
|
|
39
|
+
CORS(app)
|
|
40
|
+
|
|
41
|
+
init_engine(db_type, db_path)
|
|
42
|
+
from .utils.db import SessionLocal
|
|
43
|
+
|
|
44
|
+
app.register_blueprint(auth_bp)
|
|
45
|
+
app.register_blueprint(dashboard_bp)
|
|
46
|
+
app.register_blueprint(blog_bp)
|
|
47
|
+
app.register_blueprint(admin_bp)
|
|
48
|
+
app.register_blueprint(main_bp)
|
|
49
|
+
app.register_blueprint(upload_bp)
|
|
50
|
+
app.register_blueprint(federation_bp)
|
|
51
|
+
|
|
52
|
+
@app.before_request
|
|
53
|
+
def before():
|
|
54
|
+
from flask import request, session
|
|
55
|
+
lang = request.cookies.get("lang")
|
|
56
|
+
if not lang or lang not in translations:
|
|
57
|
+
accept = request.headers.get("Accept-Language", "")
|
|
58
|
+
for part in accept.split(","):
|
|
59
|
+
code = part.split("-")[0].strip().lower()
|
|
60
|
+
if code in translations:
|
|
61
|
+
lang = code
|
|
62
|
+
break
|
|
63
|
+
if lang not in translations:
|
|
64
|
+
lang = "en"
|
|
65
|
+
|
|
66
|
+
g.mode = os.getenv("MODE")
|
|
67
|
+
f_abs_path = os.path.abspath(__file__)
|
|
68
|
+
g.mainpath = "/".join(f_abs_path.split("/")[:-1])
|
|
69
|
+
g.trans = translations[lang]
|
|
70
|
+
g.alltrans = translations
|
|
71
|
+
g.lang = lang
|
|
72
|
+
g.db = SessionLocal
|
|
73
|
+
g.main_domain = os.getenv("DOMAIN")
|
|
74
|
+
g.blog_limit = os.getenv("BLOG_LIMIT")
|
|
75
|
+
g.register_enabled = os.getenv("SELF_REGISTER", "no") == "yes"
|
|
76
|
+
g.upload_enabled = os.getenv("MEDIA_UPLOAD", "no") == "yes"
|
|
77
|
+
g.captcha = os.getenv("CAPTCHA_ENABLED", "no") == "yes"
|
|
78
|
+
g.fcaptcha_sitekey = os.getenv("FRIENDLY_CAPTCHA_SITEKEY", "key")
|
|
79
|
+
g.fcaptcha_apikey = os.getenv("FRIENDLY_CAPTCHA_APIKEY", "api_key")
|
|
80
|
+
g.staticdir = app.static_folder
|
|
81
|
+
g.staticurl = app.static_url_path
|
|
82
|
+
g.settings = g.db.query(Settings).all()
|
|
83
|
+
|
|
84
|
+
if session.get("userid") is not None:
|
|
85
|
+
g.user = session.get("userid")
|
|
86
|
+
g.isadmin = session.get("admin")
|
|
87
|
+
else:
|
|
88
|
+
g.user = None
|
|
89
|
+
|
|
90
|
+
g.nonce = generate_nonce()
|
|
91
|
+
|
|
92
|
+
@app.context_processor
|
|
93
|
+
def inject_globals():
|
|
94
|
+
return {
|
|
95
|
+
'current_lang': g.lang,
|
|
96
|
+
'available_languages': {
|
|
97
|
+
'en': {'name': 'English', 'flag': '🇬🇧'},
|
|
98
|
+
'pl': {'name': 'Polski', 'flag': '🇵🇱'}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@app.after_request
|
|
103
|
+
def after(response):
|
|
104
|
+
nonce = g.nonce
|
|
105
|
+
#response.headers["Content-Security-Policy"] = (
|
|
106
|
+
# f"default-src 'none'; "
|
|
107
|
+
# f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net http://{g.main_domain}"
|
|
108
|
+
# f"style-src 'self'; "
|
|
109
|
+
# f"style-src-elem 'self' http://{g.main_domain}; "
|
|
110
|
+
# f"style-src-attr 'unsafe-inline';"
|
|
111
|
+
# f"script-src-attr 'unsafe-inline';"
|
|
112
|
+
# f"img-src 'self' data: http://{g.main_domain}; "
|
|
113
|
+
# f"font-src 'self'; "
|
|
114
|
+
# f"connect-src 'self'; "
|
|
115
|
+
# f"base-uri 'none'; "
|
|
116
|
+
# f"form-action 'self'; "
|
|
117
|
+
# f"frame-ancestors 'none';"
|
|
118
|
+
# f"frame-src https://global.frcapi.com ;"
|
|
119
|
+
#)
|
|
120
|
+
return response
|
|
121
|
+
|
|
122
|
+
@app.errorhandler(Exception)
|
|
123
|
+
def handle_exception(e):
|
|
124
|
+
if isinstance(e, HTTPException):
|
|
125
|
+
return e
|
|
126
|
+
|
|
127
|
+
app.logger.exception(f"Unhandled exception! {e}")
|
|
128
|
+
return "Internal Server Error", 500
|
|
129
|
+
|
|
130
|
+
@app.teardown_appcontext
|
|
131
|
+
def shutdown_session(exception=None):
|
|
132
|
+
g.db.remove()
|
|
133
|
+
|
|
134
|
+
return app
|
openwrite/cli.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import requests
|
|
6
|
+
import time
|
|
7
|
+
import sys
|
|
8
|
+
from multiprocessing import Process
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
from .utils.create_db import init_db
|
|
11
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
13
|
+
from cryptography.hazmat.primitives import serialization
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
import bcrypt
|
|
16
|
+
|
|
17
|
+
load_dotenv()
|
|
18
|
+
f_abs_path = os.path.abspath(__file__)
|
|
19
|
+
cwd = "/".join(f_abs_path.split("/")[:-2])
|
|
20
|
+
env = f"{cwd}/.env"
|
|
21
|
+
|
|
22
|
+
def print_banner():
|
|
23
|
+
click.secho(r"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_ _
|
|
27
|
+
(_) |
|
|
28
|
+
___ _ __ ___ _ ____ ___ __ _| |_ ___
|
|
29
|
+
/ _ \| '_ \ / _ \ '_ \ \ /\ / / '__| | __/ _ \
|
|
30
|
+
| (_) | |_) | __/ | | \ V V /| | | | || __/
|
|
31
|
+
\___/| .__/ \___|_| |_|\_/\_/ |_| |_|\__\___|
|
|
32
|
+
| |
|
|
33
|
+
|_|
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
quiet place for loud thoughts
|
|
37
|
+
""", fg="cyan")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@click.group()
|
|
41
|
+
def cli():
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@cli.command()
|
|
46
|
+
def init():
|
|
47
|
+
if os.path.exists(env):
|
|
48
|
+
click.confirm(f"{env} already exists. Overwrite?", abort=True)
|
|
49
|
+
|
|
50
|
+
mode = click.prompt("How are you willing to run openwrite?\n1. Multi-user\n2. Single user\n", type=int, default=1)
|
|
51
|
+
domain = click.prompt("Choose a domain (ex. openwrite.io)")
|
|
52
|
+
key = os.urandom(16).hex()
|
|
53
|
+
upload = click.confirm("Enable media upload?", default=True)
|
|
54
|
+
if upload:
|
|
55
|
+
upload_storage = click.prompt("Storage type: bunny / local", default="local")
|
|
56
|
+
if upload_storage == "bunny":
|
|
57
|
+
bunny_api = click.prompt("Bunny.net API key")
|
|
58
|
+
bunny_storagezone = click.prompt("Bunny.net storage zone")
|
|
59
|
+
bunny_storageurl = click.prompt("Bunny.net storage URL")
|
|
60
|
+
else:
|
|
61
|
+
upload_path = click.prompt("Path to save files?", default=f"{cwd}/uploads")
|
|
62
|
+
if mode == 1:
|
|
63
|
+
register = click.prompt("Allow self-register?", default=True)
|
|
64
|
+
dbtype = click.prompt("Choose database type: sqlite / mysql", default="sqlite")
|
|
65
|
+
if dbtype == "mysql":
|
|
66
|
+
mysql_user = click.prompt("MySQL user")
|
|
67
|
+
mysql_password = click.prompt("MySQL password")
|
|
68
|
+
mysql_host = click.prompt("MySQL host IP")
|
|
69
|
+
mysql_database = click.prompt("MySQL database name")
|
|
70
|
+
dbpath = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}/{mysql_database}"
|
|
71
|
+
else:
|
|
72
|
+
dbpath = click.prompt("sqlite database path", default=f"{cwd}/db.sqlite")
|
|
73
|
+
listen_ip = click.prompt("What IP should openwrite listen on?", default=str("0.0.0.0"))
|
|
74
|
+
listen_port = click.prompt("What port should openwrite listen on?", default="8081")
|
|
75
|
+
blog_limit = 0
|
|
76
|
+
if mode == 1:
|
|
77
|
+
blog_limit = click.prompt("Limit blogs per user? Set to 0 for no limit", default="3")
|
|
78
|
+
gemini = click.confirm("Run gemini service too?", default=True)
|
|
79
|
+
if gemini:
|
|
80
|
+
gemini_host = click.prompt("What IP gemini should listen on?", default="0.0.0.0")
|
|
81
|
+
gemini_port = click.prompt("What port gemini should listen on?", default="1965")
|
|
82
|
+
click.echo("[+] Generating certificate for Gemini...\n\n")
|
|
83
|
+
os.makedirs(f"{cwd}/gemini", exist_ok=True)
|
|
84
|
+
os.makedirs(f"{cwd}/gemini/mod", exist_ok=True)
|
|
85
|
+
os.makedirs(f"{cwd}/gemini/.certs", exist_ok=True)
|
|
86
|
+
subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:4096", "-keyout", f"{cwd}/gemini/.certs/key.pem", "-out", f"{cwd}/gemini/.certs/cert.pem", "-days", "365", "-nodes", "-subj", f"/CN={domain}"], check=True)
|
|
87
|
+
if mode == 1:
|
|
88
|
+
os.rename(f"{cwd}/gemini_multi.py", f"{cwd}/gemini/mod/10_openwrite.py")
|
|
89
|
+
os.remove(f"{cwd}/gemini_single.py")
|
|
90
|
+
elif mode == 2:
|
|
91
|
+
os.rename(f"{cwd}/gemini_single.py", f"{cwd}/gemini/mod/10_openwrite.py")
|
|
92
|
+
os.remove(f"{cwd}/gemini_multi.py")
|
|
93
|
+
|
|
94
|
+
logs_enabled = click.confirm("Enable logging?", default=True)
|
|
95
|
+
if logs_enabled:
|
|
96
|
+
logs_dir = click.prompt("Path to save logs", default=f"{cwd}/logs/")
|
|
97
|
+
captcha_enabled = click.confirm("Enable captcha (Friendly catpcha)?", default=False)
|
|
98
|
+
if captcha_enabled:
|
|
99
|
+
captcha_sitekey = click.prompt("Friendly captcha sitekey")
|
|
100
|
+
captcha_apikey = click.prompt("Friendly captcha API key")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
with open(env, "w") as f:
|
|
106
|
+
f.write(f"IP={listen_ip}\n")
|
|
107
|
+
f.write(f"PORT={listen_port}\n")
|
|
108
|
+
f.write(f"BLOG_LIMIT={blog_limit}\n")
|
|
109
|
+
f.write(f"DOMAIN={domain}\n")
|
|
110
|
+
f.write(f"SECRET_KEY={key}\n")
|
|
111
|
+
f.write(f"MEDIA_UPLOAD={'yes' if upload else 'no'}\n")
|
|
112
|
+
if upload:
|
|
113
|
+
f.write(f"UPLOAD_STORAGE={upload_storage}\n")
|
|
114
|
+
if upload_storage == "bunny":
|
|
115
|
+
f.write(f"BUNNY_API_KEY={bunny_api}\n")
|
|
116
|
+
f.write(f"BUNNY_STORAGE_ZONE={bunny_storagezone}\n")
|
|
117
|
+
f.write(f"BUNNY_STORAGEURL={bunny_storageurl}\n")
|
|
118
|
+
else:
|
|
119
|
+
f.write(f"UPLOAD_PATH={upload_path}\n")
|
|
120
|
+
f.write("BLOG_LIMIT=3\n")
|
|
121
|
+
if mode == 1:
|
|
122
|
+
f.write(f"SELF_REGISTER={'yes' if register else 'no'}\n")
|
|
123
|
+
f.write(f"DB_TYPE={dbtype}\n")
|
|
124
|
+
f.write(f"DB_PATH={dbpath}\n")
|
|
125
|
+
if mode == 1:
|
|
126
|
+
f.write(f"MODE=multi\n")
|
|
127
|
+
else:
|
|
128
|
+
f.write(f"MODE=single\n")
|
|
129
|
+
|
|
130
|
+
f.write(f"GEMINI={'yes' if gemini else 'no'}\n")
|
|
131
|
+
if gemini:
|
|
132
|
+
with open(f"{cwd}/gemini.ini", "w") as gem:
|
|
133
|
+
gem.write("[server]\n")
|
|
134
|
+
gem.write(f"host = {domain}\n")
|
|
135
|
+
gem.write(f"address = {gemini_host}\n")
|
|
136
|
+
gem.write(f"port = {gemini_port}\n")
|
|
137
|
+
gem.write(f"certs = {cwd}/gemini/.certs\n")
|
|
138
|
+
gem.write(f"modules = {cwd}/gemini/mod\n")
|
|
139
|
+
f.write(f"LOGS={'yes' if logs_enabled else 'no'}\n")
|
|
140
|
+
if logs_enabled:
|
|
141
|
+
f.write(f"LOGS_DIR={logs_dir}")
|
|
142
|
+
f.write(f"CAPTCHA_ENABLED={'yes' if captcha_enabled else 'no'}\n")
|
|
143
|
+
if captcha_enabled:
|
|
144
|
+
f.write(f"FRIENDLY_CAPTCHA_SITEKEY={captcha_sitekey}\n")
|
|
145
|
+
f.write(f"FRIENDLY_CAPTCHA_APIKEY={captcha_apikey}\n")
|
|
146
|
+
|
|
147
|
+
click.echo("[+] .env file created")
|
|
148
|
+
click.echo("[*] Initializing database..")
|
|
149
|
+
init_db(dbtype, dbpath)
|
|
150
|
+
click.echo("[+] Database initialized")
|
|
151
|
+
click.echo("[*] Adding admin user...")
|
|
152
|
+
from .utils.models import User, Blog, Post, Settings
|
|
153
|
+
from .utils.db import init_engine, SessionLocal
|
|
154
|
+
init_engine(dbtype, dbpath)
|
|
155
|
+
from .utils.db import SessionLocal
|
|
156
|
+
admin_password = os.urandom(16).hex()
|
|
157
|
+
hashed = bcrypt.hashpw(admin_password.encode("utf-8"), bcrypt.gensalt())
|
|
158
|
+
admin_user = User(username="admin", email="", password_hash=hashed.decode("utf-8"), verified=1, admin=1)
|
|
159
|
+
SessionLocal.add(admin_user)
|
|
160
|
+
SessionLocal.commit()
|
|
161
|
+
SessionLocal.close()
|
|
162
|
+
click.echo(f"[+] Admin user added! Your credentials:\n\nLogin: admin\nPassword: {admin_password}")
|
|
163
|
+
if logs_enabled and not os.path.exists(logs_dir):
|
|
164
|
+
os.makedirs(logs_dir, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
if mode == 2:
|
|
167
|
+
key = rsa.generate_private_key(
|
|
168
|
+
public_exponent=65537,
|
|
169
|
+
key_size=2048
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
private_pem = key.private_bytes(
|
|
173
|
+
encoding=serialization.Encoding.PEM,
|
|
174
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
175
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
176
|
+
).decode()
|
|
177
|
+
|
|
178
|
+
public_pem = key.public_key().public_bytes(
|
|
179
|
+
encoding=serialization.Encoding.PEM,
|
|
180
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
181
|
+
).decode()
|
|
182
|
+
|
|
183
|
+
now = datetime.now(timezone.utc)
|
|
184
|
+
|
|
185
|
+
new_blog = Blog(
|
|
186
|
+
owner=1,
|
|
187
|
+
name="default",
|
|
188
|
+
title=domain,
|
|
189
|
+
index="on",
|
|
190
|
+
access="domain",
|
|
191
|
+
description_raw=f"\n\n# Hello there! 👋\n\nYou can edit your blog description in [dashboard](http://{domain}/dashboard/edit/default)",
|
|
192
|
+
description_html=f"<p><img src=\"https://openwrite.b-cdn.net/hello.jpg\" width=\"500\" height=\"258\"></p><h1>Hello there! 👋</h1><p>You can edit your blog description in <a href=\"http://{domain}/dashboard/edit/default\">dashboard</a></p>",
|
|
193
|
+
css="",
|
|
194
|
+
pub_key=public_pem,
|
|
195
|
+
priv_key=private_pem,
|
|
196
|
+
theme="default",
|
|
197
|
+
created=now
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
new_setting = Settings(name="logo", value="/static/logo.png")
|
|
201
|
+
|
|
202
|
+
SessionLocal.add(new_blog)
|
|
203
|
+
SessionLocal.add(new_setting)
|
|
204
|
+
SessionLocal.commit()
|
|
205
|
+
|
|
206
|
+
@cli.command()
|
|
207
|
+
@click.option("-d", "--daemon", is_flag=True, help="Run in background (daemon)")
|
|
208
|
+
def run(daemon):
|
|
209
|
+
print_banner()
|
|
210
|
+
|
|
211
|
+
ip = os.getenv("IP", "0.0.0.0")
|
|
212
|
+
port = int(os.getenv("PORT", 8080))
|
|
213
|
+
gemini = os.getenv("GEMINI", "yes").lower() == "yes"
|
|
214
|
+
gemini_port = int(os.getenv("GEMINI_PORT", 1965))
|
|
215
|
+
gemini_proxy = os.getenv("GEMINI_PROXY", "no").lower() == "yes"
|
|
216
|
+
|
|
217
|
+
logs_enabled = os.getenv("LOGS", "no").lower() == "yes"
|
|
218
|
+
logs_dir = os.getenv("LOGS_DIR", "./logs")
|
|
219
|
+
|
|
220
|
+
os.makedirs(logs_dir, exist_ok=True)
|
|
221
|
+
|
|
222
|
+
gunicorn_access_log = os.path.join(logs_dir, "openwrite_access.log")
|
|
223
|
+
gunicorn_error_log = os.path.join(logs_dir, "openwrite_error.log")
|
|
224
|
+
|
|
225
|
+
gunicorn_cmd = [
|
|
226
|
+
"gunicorn",
|
|
227
|
+
"-w", "4",
|
|
228
|
+
"openwrite:create_app()",
|
|
229
|
+
"--bind", f"{ip}:{port}"
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
gunicorn_logs = [
|
|
233
|
+
"--access-logfile", f"{gunicorn_access_log}",
|
|
234
|
+
"--error-logfile", f"{gunicorn_error_log}"
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
gemini_cmd = [
|
|
238
|
+
"gmcapsuled", "-c", f"{cwd}/gemini.ini"
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
gemini_log = open(os.path.join(logs_dir, "gemini.log"), "a") if logs_enabled else subprocess.PIPE
|
|
242
|
+
|
|
243
|
+
if gemini_proxy:
|
|
244
|
+
gemini_cmd = gemini_cmd + ["proxy"]
|
|
245
|
+
|
|
246
|
+
if logs_enabled:
|
|
247
|
+
gunicorn_cmd = gunicorn_cmd + gunicorn_logs
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if daemon:
|
|
251
|
+
click.echo(f"[+] Openwrite listening on {ip}:{port}")
|
|
252
|
+
subprocess.Popen(gunicorn_cmd + ["--daemon"])
|
|
253
|
+
|
|
254
|
+
if gemini:
|
|
255
|
+
click.echo(f"[+] Gemini listening on {ip}:{gemini_port}")
|
|
256
|
+
|
|
257
|
+
subprocess.Popen(gemini_cmd, stdout=gemini_log, stderr=subprocess.STDOUT)
|
|
258
|
+
|
|
259
|
+
else:
|
|
260
|
+
gunicorn_proc = subprocess.Popen(
|
|
261
|
+
gunicorn_cmd
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if gemini:
|
|
265
|
+
click.echo(f"[+] Gemini started on {ip}:{gemini_port}")
|
|
266
|
+
gemini_proc = subprocess.Popen(gemini_cmd, stdout=gemini_log)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
gunicorn_proc.wait()
|
|
270
|
+
gemini_proc.wait()
|
|
271
|
+
except KeyboardInterrupt:
|
|
272
|
+
gunicorn_proc.terminate()
|
|
273
|
+
gemini_proc.terminate()
|
|
274
|
+
|
|
275
|
+
@cli.command()
|
|
276
|
+
def debugrun():
|
|
277
|
+
from openwrite import create_app
|
|
278
|
+
print_banner()
|
|
279
|
+
ip = os.getenv("IP", "0.0.0.0")
|
|
280
|
+
port = int(os.getenv("PORT", 8080))
|
|
281
|
+
app = create_app()
|
|
282
|
+
app.run(host=ip, port=port)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@cli.command()
|
|
286
|
+
def install_service():
|
|
287
|
+
service_name = "openwrite"
|
|
288
|
+
service_file = f"""[Unit]
|
|
289
|
+
Description=openwrite instance
|
|
290
|
+
After=network.target
|
|
291
|
+
|
|
292
|
+
[Service]
|
|
293
|
+
WorkingDirectory={os.getcwd()}
|
|
294
|
+
ExecStart={shutil.which('openwrite')} run
|
|
295
|
+
Restart=always
|
|
296
|
+
User={os.getenv("USER") or os.getlogin()}
|
|
297
|
+
|
|
298
|
+
[Install]
|
|
299
|
+
WantedBy=multi-user.target
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
path = f"/etc/systemd/system/{service_name}.service"
|
|
303
|
+
if not os.geteuid() == 0:
|
|
304
|
+
click.echo("[-] You need to run command as root: sudo openwrite install-service")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
with open(path, "w") as f:
|
|
308
|
+
f.write(service_file)
|
|
309
|
+
|
|
310
|
+
os.system(f"systemctl daemon-reexec")
|
|
311
|
+
click.echo(f"[+] Service {service_name} installed")
|
|
312
|
+
|
|
313
|
+
@cli.command()
|
|
314
|
+
@click.option("--service", is_flag=True, help="Stop systemd service")
|
|
315
|
+
def stop(service):
|
|
316
|
+
if service:
|
|
317
|
+
click.echo("[*] Stopping systemd service: openwrite")
|
|
318
|
+
os.system("sudo systemctl stop openwrite")
|
|
319
|
+
else:
|
|
320
|
+
click.echo("[*] Searching Gunicorn processes")
|
|
321
|
+
try:
|
|
322
|
+
out = subprocess.check_output(["pgrep", "-f", "gunicorn.*openwrite:create_app"], text=True)
|
|
323
|
+
out_gemini = subprocess.check_output(["pgrep", "-f", "gemini\.py"], text=True)
|
|
324
|
+
pids = out.strip().splitlines()
|
|
325
|
+
pids_gemini = out_gemini.strip().splitlines()
|
|
326
|
+
for pid in pids:
|
|
327
|
+
click.echo(f"[*] Killing {pid}")
|
|
328
|
+
os.kill(int(pid), 15)
|
|
329
|
+
for gemini_pid in pids_gemini:
|
|
330
|
+
click.echo(f"[*] Killing {gemini_pid}")
|
|
331
|
+
os.kill(int(gemini_pid), 15)
|
|
332
|
+
click.echo("[+] Gunicorn stopped")
|
|
333
|
+
click.echo("[+] Gemini stoppted")
|
|
334
|
+
except subprocess.CalledProcessError:
|
|
335
|
+
click.echo("[-] Could not find any openwrite processes")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
cli()
|
|
340
|
+
|
openwrite/db/__init__.py
ADDED
|
File without changes
|
openwrite/db/base.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from flask import Blueprint, render_template, redirect, g, request
|
|
2
|
+
from openwrite.utils.models import Blog, User, Settings, Home
|
|
3
|
+
import re
|
|
4
|
+
import bcrypt
|
|
5
|
+
from werkzeug.utils import secure_filename
|
|
6
|
+
from PIL import Image
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
admin_bp = Blueprint("admin", __name__)
|
|
10
|
+
|
|
11
|
+
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
|
|
12
|
+
|
|
13
|
+
@admin_bp.route("/admin")
|
|
14
|
+
def admin():
|
|
15
|
+
if g.user is None or g.isadmin == 0 or g.mode == "single":
|
|
16
|
+
return redirect("/")
|
|
17
|
+
|
|
18
|
+
blogs = g.db.query(Blog).all()
|
|
19
|
+
for b in blogs:
|
|
20
|
+
u = g.db.query(User).filter_by(id=b.owner).first()
|
|
21
|
+
b.ownername = u.username
|
|
22
|
+
users = g.db.query(User).all()
|
|
23
|
+
|
|
24
|
+
return render_template("admin.html", blogs=blogs, users=users)
|
|
25
|
+
|
|
26
|
+
@admin_bp.route("/admin/delete_blog/<blog>")
|
|
27
|
+
def admin_delete_blog(blog):
|
|
28
|
+
if g.user is None or g.isadmin == 0 or g.mode == "single":
|
|
29
|
+
return redirect("/")
|
|
30
|
+
|
|
31
|
+
b = g.db.query(Blog).filter_by(name=blog).first()
|
|
32
|
+
if b:
|
|
33
|
+
g.db.delete(b)
|
|
34
|
+
g.db.commit()
|
|
35
|
+
|
|
36
|
+
return redirect("/admin")
|
|
37
|
+
|
|
38
|
+
@admin_bp.route("/admin/delete_user/<username>")
|
|
39
|
+
def admin_delete_user(username):
|
|
40
|
+
if g.user is None or g.isadmin == 0 or g.mode == "single":
|
|
41
|
+
return redirect("/")
|
|
42
|
+
|
|
43
|
+
if username == "admin":
|
|
44
|
+
return redirect("/admin")
|
|
45
|
+
|
|
46
|
+
user = g.db.query(User).filter_by(username=username).first()
|
|
47
|
+
if not user:
|
|
48
|
+
return redirect("/admin")
|
|
49
|
+
|
|
50
|
+
user_blogs = g.db.query(Blog).filter_by(owner=user.id).all()
|
|
51
|
+
for b in user_blogs:
|
|
52
|
+
g.db.delete(b)
|
|
53
|
+
|
|
54
|
+
g.db.delete(user)
|
|
55
|
+
g.db.commit()
|
|
56
|
+
|
|
57
|
+
return redirect("/admin")
|
|
58
|
+
|
|
59
|
+
@admin_bp.route("/admin/make_admin/<username>")
|
|
60
|
+
def admin_make_admin(username):
|
|
61
|
+
if g.user is None or g.isadmin == 0 or g.mode == "single":
|
|
62
|
+
return redirect("/")
|
|
63
|
+
|
|
64
|
+
user = g.db.query(User).filter_by(username=username).first()
|
|
65
|
+
if not user:
|
|
66
|
+
return redirect("/admin")
|
|
67
|
+
|
|
68
|
+
user.admin = "1"
|
|
69
|
+
g.db.commit()
|
|
70
|
+
|
|
71
|
+
return redirect("/admin")
|
|
72
|
+
|
|
73
|
+
@admin_bp.route("/admin/remove_admin/<username>")
|
|
74
|
+
def admin_remove_admin(username):
|
|
75
|
+
if g.user is None or g.isadmin == 0 or g.mode == "single":
|
|
76
|
+
return redirect("/")
|
|
77
|
+
|
|
78
|
+
if username == "admin":
|
|
79
|
+
return redirect("/admin")
|
|
80
|
+
|
|
81
|
+
user = g.db.query(User).filter_by(username=username).first()
|
|
82
|
+
if not user:
|
|
83
|
+
return redirect("/admin")
|
|
84
|
+
|
|
85
|
+
user.admin = "0"
|
|
86
|
+
g.db.commit()
|
|
87
|
+
|
|
88
|
+
return redirect("/admin")
|
|
89
|
+
|
|
90
|
+
@admin_bp.route("/admin/add_user", methods=['GET', 'POST'])
|
|
91
|
+
def admin_add_user():
|
|
92
|
+
if g.user is None or g.isadmin == 0 or g.mode == "single":
|
|
93
|
+
return redirect("/")
|
|
94
|
+
if request.method == "GET":
|
|
95
|
+
return render_template("register.html", add="1", captcha="0")
|
|
96
|
+
|
|
97
|
+
form_username = request.form.get('username')
|
|
98
|
+
if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])?$", form_username):
|
|
99
|
+
return render_template('register.html', error=g.trans['wrong_username'], add="1", captcha="0")
|
|
100
|
+
form_password = request.form.get('password')
|
|
101
|
+
form_password2 = request.form.get('password2')
|
|
102
|
+
|
|
103
|
+
if form_password != form_password2:
|
|
104
|
+
return render_template("register.html", error=g.trans['password_dont_match'], add="1", captcha="0")
|
|
105
|
+
|
|
106
|
+
user = g.db.query(User).filter_by(username=form_username).first()
|
|
107
|
+
if user:
|
|
108
|
+
return render_template('register.html', error=g.trans["user_exists"], add="1", captcha="0")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
hashed = bcrypt.hashpw(form_password.encode('utf-8'), bcrypt.gensalt())
|
|
112
|
+
new_user = User(username=form_username, email="", password_hash=hashed.decode('utf-8'), verified=0, admin=0)
|
|
113
|
+
g.db.add(new_user)
|
|
114
|
+
g.db.commit()
|
|
115
|
+
return redirect("/admin")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(e)
|
|
118
|
+
g.db.rollback()
|
|
119
|
+
return render_template('register.html', error=g.trans["error"], add="1", captcha="0")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@admin_bp.route("/admin/settings", methods=['GET', 'POST'])
|
|
123
|
+
def admin_settings():
|
|
124
|
+
if g.user is None or g.isadmin == 0 or g.mode == "single":
|
|
125
|
+
return redirect("/")
|
|
126
|
+
|
|
127
|
+
settings = g.db.query(Settings).all()
|
|
128
|
+
hometext = g.db.query(Home).filter_by(name="hometext").all()
|
|
129
|
+
settings_dict = {s.name: s.value for s in settings}
|
|
130
|
+
|
|
131
|
+
if request.method == "GET":
|
|
132
|
+
return render_template("settings.html", settings=settings_dict, hometext=hometext)
|
|
133
|
+
|
|
134
|
+
hometexts = {}
|
|
135
|
+
pattern = re.compile(r'^hometext_(\w+)$')
|
|
136
|
+
for key, value in request.form.items():
|
|
137
|
+
match = pattern.match(key)
|
|
138
|
+
if match:
|
|
139
|
+
lang = match.group(1)
|
|
140
|
+
hometexts[lang] = value
|
|
141
|
+
|
|
142
|
+
if "logo" in request.files and request.files['logo'].filename != "":
|
|
143
|
+
file = request.files['logo']
|
|
144
|
+
filename = secure_filename(file.filename)
|
|
145
|
+
if "." not in filename:
|
|
146
|
+
return render_template("settings.html", settings=settings_dict, hometext=hometext, error="Wrong file!")
|
|
147
|
+
|
|
148
|
+
extension = filename.rsplit('.', 1)[1].lower()
|
|
149
|
+
if extension not in ALLOWED_EXTENSIONS:
|
|
150
|
+
return render_template("settings.html", settings=settings_dict, hometext=hometext, error="Wrong extension!")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
img = Image.open(file.stream)
|
|
154
|
+
img.verify()
|
|
155
|
+
except Exception:
|
|
156
|
+
return render_template("settings.html", settings=settings_dict, hometext=hometext, error="File is not image!")
|
|
157
|
+
|
|
158
|
+
file.stream.seek(0)
|
|
159
|
+
|
|
160
|
+
filepath = os.path.join(g.staticdir, filename)
|
|
161
|
+
file.save(filepath)
|
|
162
|
+
logo_file = filepath
|
|
163
|
+
logo_q = g.db.query(Settings).filter_by(name="logo").first()
|
|
164
|
+
logo_q.value = f"{g.staticurl}/{filename}"
|
|
165
|
+
g.db.commit()
|
|
166
|
+
settings_dict['logo'] = f"{g.staticurl}/{filename}"
|
|
167
|
+
for h in hometext:
|
|
168
|
+
h.content = hometexts[h.language]
|
|
169
|
+
g.db.commit()
|
|
170
|
+
return render_template("settings.html", settings=settings_dict, hometext=hometext)
|