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.
Files changed (67) hide show
  1. openwrite/__init__.py +134 -0
  2. openwrite/cli.py +340 -0
  3. openwrite/db/__init__.py +0 -0
  4. openwrite/db/base.py +3 -0
  5. openwrite/launch_gemini.py +8 -0
  6. openwrite/routes/admin.py +170 -0
  7. openwrite/routes/auth.py +77 -0
  8. openwrite/routes/blog.py +205 -0
  9. openwrite/routes/dashboard.py +408 -0
  10. openwrite/routes/federation.py +284 -0
  11. openwrite/routes/main.py +96 -0
  12. openwrite/routes/upload.py +81 -0
  13. openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  14. openwrite/static/Merriweather.ttf +0 -0
  15. openwrite/static/avatar.png +0 -0
  16. openwrite/static/favicon.ico +0 -0
  17. openwrite/static/logo.png +0 -0
  18. openwrite/static/script/chart.js +14 -0
  19. openwrite/static/script/highlight.min.js +1244 -0
  20. openwrite/static/script/openwrite.js +88 -0
  21. openwrite/static/script/papaparse.min.js +7 -0
  22. openwrite/static/script/showdown.min.js +3 -0
  23. openwrite/static/script/showdown.min.js.map +1 -0
  24. openwrite/static/script/site.compat.min.js +7 -0
  25. openwrite/static/script/site.min.js +7 -0
  26. openwrite/static/style/hl.css +1 -0
  27. openwrite/static/style/style.css +611 -0
  28. openwrite/static/style/themes/light.css +28 -0
  29. openwrite/static/style/themes/obscura.css +27 -0
  30. openwrite/static/style/themes/stickynote.css +50 -0
  31. openwrite/static/style/themes/terminal.css +37 -0
  32. openwrite/static/style/themes/warmnight.css +73 -0
  33. openwrite/static/style/themes/windows98.css +132 -0
  34. openwrite/templates/admin.html +47 -0
  35. openwrite/templates/blog.html +16 -0
  36. openwrite/templates/blog_footer.html +6 -0
  37. openwrite/templates/blog_header.html +45 -0
  38. openwrite/templates/blog_preview.html +44 -0
  39. openwrite/templates/changepw.html +19 -0
  40. openwrite/templates/create.html +46 -0
  41. openwrite/templates/dashboard.html +45 -0
  42. openwrite/templates/discover.html +11 -0
  43. openwrite/templates/edit.html +190 -0
  44. openwrite/templates/footer.html +16 -0
  45. openwrite/templates/header.html +61 -0
  46. openwrite/templates/import.html +142 -0
  47. openwrite/templates/index.html +9 -0
  48. openwrite/templates/instances.html +14 -0
  49. openwrite/templates/login.html +18 -0
  50. openwrite/templates/new_post.html +207 -0
  51. openwrite/templates/post.html +53 -0
  52. openwrite/templates/preview.html +37 -0
  53. openwrite/templates/register.html +42 -0
  54. openwrite/templates/settings.html +63 -0
  55. openwrite/templates/stats.html +242 -0
  56. openwrite/utils/create_db.py +15 -0
  57. openwrite/utils/db.py +30 -0
  58. openwrite/utils/helpers.py +216 -0
  59. openwrite/utils/i18n.json +229 -0
  60. openwrite/utils/models.py +87 -0
  61. openwrite/utils/translations.py +6 -0
  62. openwrite-0.10.1.dist-info/METADATA +684 -0
  63. openwrite-0.10.1.dist-info/RECORD +67 -0
  64. openwrite-0.10.1.dist-info/WHEEL +5 -0
  65. openwrite-0.10.1.dist-info/entry_points.txt +2 -0
  66. openwrite-0.10.1.dist-info/licenses/LICENSE +661 -0
  67. 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"![hello](https://openwrite.b-cdn.net/hello.jpg =500x258)\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
+
File without changes
openwrite/db/base.py ADDED
@@ -0,0 +1,3 @@
1
+ from sqlalchemy.orm import declarative_base
2
+
3
+ Base = declarative_base()
@@ -0,0 +1,8 @@
1
+ from gemini import create_gemini
2
+ import sys
3
+
4
+ if __name__ == "__main__":
5
+ proxy = None
6
+ if len(sys.argv) > 1:
7
+ proxy = "proxy"
8
+ create_gemini(proxy)
@@ -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)