openwrite 0.10.5__tar.gz → 0.11.0__tar.gz
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-0.10.5/openwrite.egg-info → openwrite-0.11.0}/PKG-INFO +4 -3
- {openwrite-0.10.5 → openwrite-0.11.0}/README.md +3 -2
- openwrite-0.11.0/openwrite/__init__.py +273 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/cli.py +10 -61
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/gemini_multi.py +0 -2
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/gemini_single.py +0 -2
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/admin.py +33 -3
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/auth.py +1 -1
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/dashboard.py +3 -5
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/main.py +4 -1
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/upload.py +1 -1
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/admin.html +7 -6
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/blog.html +1 -1
- openwrite-0.11.0/openwrite/templates/changepw.html +19 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/create.html +7 -7
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/dashboard.html +10 -10
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/discover.html +2 -2
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/edit.html +15 -16
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/footer.html +4 -4
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/header.html +6 -6
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/import.html +7 -7
- openwrite-0.11.0/openwrite/templates/index.html +17 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/instances.html +3 -3
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/login.html +2 -2
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/new_post.html +9 -9
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/register.html +5 -5
- openwrite-0.11.0/openwrite/templates/settings.html +96 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/stats.html +4 -4
- openwrite-0.11.0/openwrite/templates/translations.html +87 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/i18n.json +3 -5
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/models.py +7 -0
- {openwrite-0.10.5 → openwrite-0.11.0/openwrite.egg-info}/PKG-INFO +4 -3
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/SOURCES.txt +2 -2
- {openwrite-0.10.5 → openwrite-0.11.0}/pyproject.toml +1 -1
- openwrite-0.10.5/openwrite/__init__.py +0 -134
- openwrite-0.10.5/openwrite/templates/changepw.html +0 -19
- openwrite-0.10.5/openwrite/templates/index.html +0 -9
- openwrite-0.10.5/openwrite/templates/settings.html +0 -63
- openwrite-0.10.5/openwrite/utils/translations.py +0 -6
- {openwrite-0.10.5 → openwrite-0.11.0}/LICENSE +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/MANIFEST.in +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/db/__init__.py +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/db/base.py +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/blog.py +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/routes/federation.py +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/Merriweather.ttf +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/avatar.png +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/favicon.ico +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/logo.png +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/chart.js +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/highlight.min.js +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/openwrite.js +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/papaparse.min.js +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/showdown.min.js +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/showdown.min.js.map +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/site.compat.min.js +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/script/site.min.js +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/hl.css +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/style.css +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/light.css +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/obscura.css +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/stickynote.css +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/terminal.css +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/warmnight.css +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/static/style/themes/windows98.css +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/blog_footer.html +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/blog_header.html +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/blog_preview.html +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/post.html +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/templates/preview.html +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/create_db.py +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/db.py +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite/utils/helpers.py +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/dependency_links.txt +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/entry_points.txt +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/requires.txt +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/openwrite.egg-info/top_level.txt +0 -0
- {openwrite-0.10.5 → openwrite-0.11.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openwrite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
5
5
|
Version 3, 19 November 2007
|
|
6
6
|
|
|
@@ -718,6 +718,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
|
|
|
718
718
|
- Blog themes
|
|
719
719
|
- Gemini protocol
|
|
720
720
|
- Statistics per post
|
|
721
|
+
- Multilanguage (currently English and Polish)
|
|
721
722
|
|
|
722
723
|
|
|
723
724
|
## Installation
|
|
@@ -734,7 +735,7 @@ pip install openwrite
|
|
|
734
735
|
openwrite init
|
|
735
736
|
```
|
|
736
737
|
|
|
737
|
-
3. Answer few questions about your instance to generate .env
|
|
738
|
+
3. Answer few questions about your instance to generate .env and create all necessary directories.
|
|
738
739
|
4. Run:
|
|
739
740
|
|
|
740
741
|
```
|
|
@@ -749,7 +750,7 @@ openwrite run -d
|
|
|
749
750
|
|
|
750
751
|
to run it in background.
|
|
751
752
|
|
|
752
|
-
5. Enjoy!
|
|
753
|
+
5. Default user is `admin` with password `openwrite`. You can (and should!) change it in your dashboard. Enjoy!
|
|
753
754
|
|
|
754
755
|
|
|
755
756
|
#### Docker
|
|
@@ -29,6 +29,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
|
|
|
29
29
|
- Blog themes
|
|
30
30
|
- Gemini protocol
|
|
31
31
|
- Statistics per post
|
|
32
|
+
- Multilanguage (currently English and Polish)
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
## Installation
|
|
@@ -45,7 +46,7 @@ pip install openwrite
|
|
|
45
46
|
openwrite init
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
3. Answer few questions about your instance to generate .env
|
|
49
|
+
3. Answer few questions about your instance to generate .env and create all necessary directories.
|
|
49
50
|
4. Run:
|
|
50
51
|
|
|
51
52
|
```
|
|
@@ -60,7 +61,7 @@ openwrite run -d
|
|
|
60
61
|
|
|
61
62
|
to run it in background.
|
|
62
63
|
|
|
63
|
-
5. Enjoy!
|
|
64
|
+
5. Default user is `admin` with password `openwrite`. You can (and should!) change it in your dashboard. Enjoy!
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
#### Docker
|
|
@@ -0,0 +1,273 @@
|
|
|
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, Home
|
|
9
|
+
from sqlalchemy import distinct
|
|
10
|
+
import bcrypt
|
|
11
|
+
import json
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
13
|
+
from cryptography.hazmat.primitives import serialization
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
|
|
16
|
+
start_time = time.time()
|
|
17
|
+
|
|
18
|
+
def create_app(test_config=None):
|
|
19
|
+
if test_config is None:
|
|
20
|
+
load_dotenv()
|
|
21
|
+
db_type = os.getenv("DB_TYPE", "sqlite")
|
|
22
|
+
db_path = os.getenv("DB_PATH", "db.sqlite")
|
|
23
|
+
else:
|
|
24
|
+
db_type = test_config.get("DB_TYPE", "sqlite")
|
|
25
|
+
db_path = test_config.get("DB_PATH", "data.db")
|
|
26
|
+
load_dotenv(test_config.get("env"), override=True)
|
|
27
|
+
|
|
28
|
+
if not os.path.exists(".initialized") or test_config is not None:
|
|
29
|
+
mode = os.getenv("MODE")
|
|
30
|
+
domain = os.getenv("DOMAIN")
|
|
31
|
+
f_abs_path = os.path.abspath(__file__)
|
|
32
|
+
file_cwd = "/".join(f_abs_path.split("/")[:-1])
|
|
33
|
+
from .utils.create_db import init_db
|
|
34
|
+
init_db(db_type, db_path)
|
|
35
|
+
from .utils.models import User, Blog, Post, Settings, Home
|
|
36
|
+
from .utils.db import init_engine, SessionLocal
|
|
37
|
+
init_engine(db_type, db_path)
|
|
38
|
+
from .utils.db import SessionLocal
|
|
39
|
+
admin_password = os.urandom(16).hex()
|
|
40
|
+
hashed = bcrypt.hashpw("openwrite".encode("utf-8"), bcrypt.gensalt())
|
|
41
|
+
admin_user = User(username="admin", email="", password_hash=hashed.decode("utf-8"), verified=1, admin=1)
|
|
42
|
+
SessionLocal.add(admin_user)
|
|
43
|
+
SessionLocal.commit()
|
|
44
|
+
SessionLocal.close()
|
|
45
|
+
|
|
46
|
+
if mode == "single":
|
|
47
|
+
key = rsa.generate_private_key(
|
|
48
|
+
public_exponent=65537,
|
|
49
|
+
key_size=2048
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
private_pem = key.private_bytes(
|
|
53
|
+
encoding=serialization.Encoding.PEM,
|
|
54
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
55
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
56
|
+
).decode()
|
|
57
|
+
|
|
58
|
+
public_pem = key.public_key().public_bytes(
|
|
59
|
+
encoding=serialization.Encoding.PEM,
|
|
60
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
61
|
+
).decode()
|
|
62
|
+
|
|
63
|
+
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
64
|
+
|
|
65
|
+
new_blog = Blog(
|
|
66
|
+
owner=1,
|
|
67
|
+
name="default",
|
|
68
|
+
title=domain,
|
|
69
|
+
index="on",
|
|
70
|
+
access="domain",
|
|
71
|
+
description_raw=f"\n\n# Hello there! 👋\n\nYou can edit your blog description in [dashboard](http://{domain}/dashboard/edit/default)",
|
|
72
|
+
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>",
|
|
73
|
+
css="",
|
|
74
|
+
pub_key=public_pem,
|
|
75
|
+
priv_key=private_pem,
|
|
76
|
+
theme="default",
|
|
77
|
+
created=now
|
|
78
|
+
)
|
|
79
|
+
SessionLocal.add(new_blog)
|
|
80
|
+
|
|
81
|
+
hometext_pl = """
|
|
82
|
+
<h1 class="centered"><span style="color: #f2d2c8">cicha</span> przestrzeń na <span style="color: #5a36bf">głośne</span> myśli.</h1>
|
|
83
|
+
|
|
84
|
+
<article class="main-content" style="text-align: center">
|
|
85
|
+
<p><strong>Openwrite</strong> to lekka, otwartoźródłowa platforma blogowa stworzona z myślą o wolności pisania – bez reklam, trackerów i zbędnego balastu.</p><br>
|
|
86
|
+
|
|
87
|
+
- Pisz w Markdownie<br>
|
|
88
|
+
- Zainstaluj w kilka minut<br>
|
|
89
|
+
- Publikuj do sieci, przez Gemini lub na Mastodonie – jak chcesz<br>
|
|
90
|
+
|
|
91
|
+
<br>
|
|
92
|
+
<p>Bez analityki. Bez ukrytych skryptów.<br>
|
|
93
|
+
Tylko Ty, Twoje myśli i miejsce, w którym możesz je wyrazić.</p>
|
|
94
|
+
<p>Wybierz motyw. Dodaj obrazki. Śledź wyświetlenia.<br>
|
|
95
|
+
Zbuduj bloga po swojemu – minimalistycznego lub z dodatkami.</p>
|
|
96
|
+
<p><strong>To Twoje słowa. Twój blog. Twoje zasady.</strong><br><br>
|
|
97
|
+
Zacznij pisać — i odzyskaj głos.</p>
|
|
98
|
+
</p>
|
|
99
|
+
</article>
|
|
100
|
+
<div class="action-links">
|
|
101
|
+
<a class="btn purple" href="/register">Zarejestruj się tutaj</a>
|
|
102
|
+
<span class="or-separator">lub</span>
|
|
103
|
+
<a class="btn empty" href="https://main.openwrite.io/hosting">Samodzielny hosting</a>
|
|
104
|
+
</div>
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
hometext_en = """
|
|
108
|
+
<h1 class="centered"><span style="color: #f2d2c8">quiet</span> space for <span style="color: #5a36bf">loud</span> thoughts.</h1>
|
|
109
|
+
|
|
110
|
+
<article class="main-content" style="text-align: center">
|
|
111
|
+
<p><strong>Openwrite</strong> is a lightweight, open-source blogging platform built for people who just want to write — without ads, trackers, or bloated nonsense.</p><br>
|
|
112
|
+
|
|
113
|
+
- Write in Markdown<br>
|
|
114
|
+
- Self-host in minutes<br>
|
|
115
|
+
- Publish to the open web, Gemini, or Mastodon — your choice<br>
|
|
116
|
+
|
|
117
|
+
<br>
|
|
118
|
+
<p>There’s no analytics. No hidden scripts. No engagement traps.<br>
|
|
119
|
+
Just you, your thoughts, and an honest place to share them.</p>
|
|
120
|
+
<p>Choose a theme. Add images. Track views.<br>
|
|
121
|
+
Build your blog your way — minimal or feature-rich.</p>
|
|
122
|
+
<p><strong>It’s your voice. Your blog. Your rules.</strong><br><br>
|
|
123
|
+
Start writing — and own your words.</p>
|
|
124
|
+
</article>
|
|
125
|
+
<div class="action-links">
|
|
126
|
+
<a class="btn purple" href="/register">Register Here</a>
|
|
127
|
+
<span class="or-separator">or</span>
|
|
128
|
+
<a class="btn empty" href="https://main.openwrite.io/hosting">Self Host</a>
|
|
129
|
+
</div>
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
with open(f"{file_cwd}/utils/i18n.json", "r", encoding='utf-8') as f:
|
|
133
|
+
data = json.load(f)
|
|
134
|
+
|
|
135
|
+
for lang, entries in data.items():
|
|
136
|
+
for key, value in entries.items():
|
|
137
|
+
SessionLocal.add(Home(language=lang, name=key, type="translation", content=value))
|
|
138
|
+
|
|
139
|
+
new_setting = Settings(name="logo", value="/static/logo.png")
|
|
140
|
+
new_home_pl = Home(language="pl", name="hometext", type="text", content=hometext_pl)
|
|
141
|
+
new_home_en = Home(language="en", name="hometext", type="text", content=hometext_en)
|
|
142
|
+
SessionLocal.add(new_setting)
|
|
143
|
+
SessionLocal.add(new_home_pl)
|
|
144
|
+
SessionLocal.add(new_home_en)
|
|
145
|
+
SessionLocal.commit()
|
|
146
|
+
|
|
147
|
+
if test_config is None:
|
|
148
|
+
with open(".initialized", "w") as f:
|
|
149
|
+
f.write("ok")
|
|
150
|
+
|
|
151
|
+
app = Flask(__name__, template_folder="templates", subdomain_matching=True, static_url_path='/static')
|
|
152
|
+
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
|
153
|
+
app.secret_key = os.getenv("SECRET_KEY")
|
|
154
|
+
app.config['SERVER_NAME'] = os.getenv("DOMAIN")
|
|
155
|
+
|
|
156
|
+
from .routes.auth import auth_bp
|
|
157
|
+
from .routes.dashboard import dashboard_bp
|
|
158
|
+
from .routes.blog import blog_bp
|
|
159
|
+
from .routes.admin import admin_bp
|
|
160
|
+
from .routes.main import main_bp
|
|
161
|
+
from .routes.upload import upload_bp
|
|
162
|
+
from .routes.federation import federation_bp
|
|
163
|
+
from .utils.db import init_engine, SessionLocal
|
|
164
|
+
from flask_cors import CORS
|
|
165
|
+
|
|
166
|
+
CORS(app)
|
|
167
|
+
|
|
168
|
+
init_engine(db_type, db_path)
|
|
169
|
+
from .utils.db import SessionLocal
|
|
170
|
+
|
|
171
|
+
app.register_blueprint(auth_bp)
|
|
172
|
+
app.register_blueprint(dashboard_bp)
|
|
173
|
+
app.register_blueprint(blog_bp)
|
|
174
|
+
app.register_blueprint(admin_bp)
|
|
175
|
+
app.register_blueprint(main_bp)
|
|
176
|
+
app.register_blueprint(upload_bp)
|
|
177
|
+
app.register_blueprint(federation_bp)
|
|
178
|
+
|
|
179
|
+
def _(key: str):
|
|
180
|
+
return g.trans.get(key, key)
|
|
181
|
+
|
|
182
|
+
app.jinja_env.globals['_'] = _
|
|
183
|
+
|
|
184
|
+
@app.before_request
|
|
185
|
+
def before():
|
|
186
|
+
from flask import request, session
|
|
187
|
+
from openwrite.utils.models import Home, Settings
|
|
188
|
+
g.db = SessionLocal
|
|
189
|
+
t_query = g.db.query(distinct(Home.language)).all()
|
|
190
|
+
translations = [row[0] for row in t_query]
|
|
191
|
+
lang = request.cookies.get("lang")
|
|
192
|
+
if not lang or lang not in translations:
|
|
193
|
+
accept = request.headers.get("Accept-Language", "")
|
|
194
|
+
for part in accept.split(","):
|
|
195
|
+
code = part.split("-")[0].strip().lower()
|
|
196
|
+
if code in translations:
|
|
197
|
+
lang = code
|
|
198
|
+
break
|
|
199
|
+
if lang not in translations:
|
|
200
|
+
lang = "en"
|
|
201
|
+
|
|
202
|
+
trans = g.db.query(Home).filter(Home.type == "translation", Home.language == lang).all()
|
|
203
|
+
|
|
204
|
+
g.mode = os.getenv("MODE")
|
|
205
|
+
f_abs_path = os.path.abspath(__file__)
|
|
206
|
+
g.mainpath = "/".join(f_abs_path.split("/")[:-1])
|
|
207
|
+
g.trans = {t.name: t.content for t in trans if len(t.content) > 1}
|
|
208
|
+
g.alltrans = translations
|
|
209
|
+
g.lang = lang
|
|
210
|
+
g.main_domain = os.getenv("DOMAIN")
|
|
211
|
+
g.blog_limit = os.getenv("BLOG_LIMIT")
|
|
212
|
+
g.register_enabled = os.getenv("SELF_REGISTER", "no") == "yes"
|
|
213
|
+
g.upload_enabled = os.getenv("MEDIA_UPLOAD", "no") == "yes"
|
|
214
|
+
g.captcha = os.getenv("CAPTCHA_ENABLED", "no") == "yes"
|
|
215
|
+
g.fcaptcha_sitekey = os.getenv("FRIENDLY_CAPTCHA_SITEKEY", "key")
|
|
216
|
+
g.fcaptcha_apikey = os.getenv("FRIENDLY_CAPTCHA_APIKEY", "api_key")
|
|
217
|
+
g.staticdir = app.static_folder
|
|
218
|
+
g.staticurl = app.static_url_path
|
|
219
|
+
g.settings = g.db.query(Settings).all()
|
|
220
|
+
|
|
221
|
+
if session.get("userid") is not None:
|
|
222
|
+
g.user = session.get("userid")
|
|
223
|
+
g.isadmin = session.get("admin")
|
|
224
|
+
else:
|
|
225
|
+
g.user = None
|
|
226
|
+
|
|
227
|
+
g.nonce = generate_nonce()
|
|
228
|
+
|
|
229
|
+
@app.context_processor
|
|
230
|
+
def inject_globals():
|
|
231
|
+
from openwrite.utils.models import Home
|
|
232
|
+
langs = g.db.query(Home).filter_by(name="lang_name").all()
|
|
233
|
+
lang_obj = {}
|
|
234
|
+
for l in langs:
|
|
235
|
+
lang_obj[l.language] = {"name": l.content}
|
|
236
|
+
return {
|
|
237
|
+
'current_lang': g.lang,
|
|
238
|
+
'available_languages': lang_obj
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@app.after_request
|
|
242
|
+
def after(response):
|
|
243
|
+
nonce = g.nonce
|
|
244
|
+
#response.headers["Content-Security-Policy"] = (
|
|
245
|
+
# f"default-src 'none'; "
|
|
246
|
+
# f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net http://{g.main_domain}"
|
|
247
|
+
# f"style-src 'self'; "
|
|
248
|
+
# f"style-src-elem 'self' http://{g.main_domain}; "
|
|
249
|
+
# f"style-src-attr 'unsafe-inline';"
|
|
250
|
+
# f"script-src-attr 'unsafe-inline';"
|
|
251
|
+
# f"img-src 'self' data: http://{g.main_domain}; "
|
|
252
|
+
# f"font-src 'self'; "
|
|
253
|
+
# f"connect-src 'self'; "
|
|
254
|
+
# f"base-uri 'none'; "
|
|
255
|
+
# f"form-action 'self'; "
|
|
256
|
+
# f"frame-ancestors 'none';"
|
|
257
|
+
# f"frame-src https://global.frcapi.com ;"
|
|
258
|
+
#)
|
|
259
|
+
return response
|
|
260
|
+
|
|
261
|
+
@app.errorhandler(Exception)
|
|
262
|
+
def handle_exception(e):
|
|
263
|
+
if isinstance(e, HTTPException):
|
|
264
|
+
return e
|
|
265
|
+
|
|
266
|
+
app.logger.exception(f"Unhandled exception! {e}")
|
|
267
|
+
return "Internal Server Error", 500
|
|
268
|
+
|
|
269
|
+
@app.teardown_appcontext
|
|
270
|
+
def shutdown_session(exception=None):
|
|
271
|
+
g.db.remove()
|
|
272
|
+
|
|
273
|
+
return app
|
|
@@ -13,6 +13,8 @@ from cryptography.hazmat.primitives.asymmetric import rsa
|
|
|
13
13
|
from cryptography.hazmat.primitives import serialization
|
|
14
14
|
from datetime import datetime, timezone
|
|
15
15
|
import bcrypt
|
|
16
|
+
import json
|
|
17
|
+
import shutil
|
|
16
18
|
|
|
17
19
|
load_dotenv()
|
|
18
20
|
f_abs_path = os.path.abspath(__file__)
|
|
@@ -87,9 +89,9 @@ def init():
|
|
|
87
89
|
os.makedirs(f"{cwd}/gemini/.certs", exist_ok=True)
|
|
88
90
|
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)
|
|
89
91
|
if mode == 1:
|
|
90
|
-
|
|
92
|
+
shutil.copyfile(f"{lib_cwd}/gemini_multi.py", f"{cwd}/gemini/mod/10_openwrite.py")
|
|
91
93
|
elif mode == 2:
|
|
92
|
-
|
|
94
|
+
shutil.copyfile(f"{lib_cwd}/gemini_single.py", f"{cwd}/gemini/mod/10_openwrite.py")
|
|
93
95
|
|
|
94
96
|
logs_enabled = click.confirm("Enable logging?", default=True)
|
|
95
97
|
if logs_enabled:
|
|
@@ -138,70 +140,15 @@ def init():
|
|
|
138
140
|
gem.write(f"modules = {cwd}/gemini/mod\n")
|
|
139
141
|
f.write(f"LOGS={'yes' if logs_enabled else 'no'}\n")
|
|
140
142
|
if logs_enabled:
|
|
141
|
-
f.write(f"LOGS_DIR={logs_dir}")
|
|
143
|
+
f.write(f"LOGS_DIR={logs_dir}\n")
|
|
142
144
|
f.write(f"CAPTCHA_ENABLED={'yes' if captcha_enabled else 'no'}\n")
|
|
143
145
|
if captcha_enabled:
|
|
144
146
|
f.write(f"FRIENDLY_CAPTCHA_SITEKEY={captcha_sitekey}\n")
|
|
145
147
|
f.write(f"FRIENDLY_CAPTCHA_APIKEY={captcha_apikey}\n")
|
|
146
148
|
|
|
147
149
|
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
150
|
if logs_enabled and not os.path.exists(logs_dir):
|
|
164
151
|
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
152
|
|
|
206
153
|
@cli.command()
|
|
207
154
|
@click.option("-d", "--daemon", is_flag=True, help="Run in background (daemon)")
|
|
@@ -267,10 +214,12 @@ def run(daemon):
|
|
|
267
214
|
|
|
268
215
|
try:
|
|
269
216
|
gunicorn_proc.wait()
|
|
270
|
-
|
|
217
|
+
if gemini:
|
|
218
|
+
gemini_proc.wait()
|
|
271
219
|
except KeyboardInterrupt:
|
|
272
220
|
gunicorn_proc.terminate()
|
|
273
|
-
|
|
221
|
+
if gemini:
|
|
222
|
+
gemini_proc.terminate()
|
|
274
223
|
|
|
275
224
|
@cli.command()
|
|
276
225
|
def debugrun():
|
|
@@ -320,7 +269,7 @@ def stop(service):
|
|
|
320
269
|
click.echo("[*] Searching Gunicorn processes")
|
|
321
270
|
try:
|
|
322
271
|
out = subprocess.check_output(["pgrep", "-f", "gunicorn.*openwrite:create_app"], text=True)
|
|
323
|
-
out_gemini = subprocess.check_output(["pgrep", "-f", "
|
|
272
|
+
out_gemini = subprocess.check_output(["pgrep", "-f", "gmcapsuled"], text=True)
|
|
324
273
|
pids = out.strip().splitlines()
|
|
325
274
|
pids_gemini = out_gemini.strip().splitlines()
|
|
326
275
|
for pid in pids:
|
|
@@ -5,6 +5,7 @@ import bcrypt
|
|
|
5
5
|
from werkzeug.utils import secure_filename
|
|
6
6
|
from PIL import Image
|
|
7
7
|
import os
|
|
8
|
+
from collections import defaultdict
|
|
8
9
|
|
|
9
10
|
admin_bp = Blueprint("admin", __name__)
|
|
10
11
|
|
|
@@ -164,7 +165,36 @@ def admin_settings():
|
|
|
164
165
|
logo_q.value = f"{g.staticurl}/{filename}"
|
|
165
166
|
g.db.commit()
|
|
166
167
|
settings_dict['logo'] = f"{g.staticurl}/{filename}"
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
g.db.query(Home).filter_by(name="hometext").delete()
|
|
169
|
+
for h in hometexts.keys():
|
|
170
|
+
g.db.add(Home(name="hometext", language=h, type="text", content=hometexts[h]))
|
|
169
171
|
g.db.commit()
|
|
170
|
-
|
|
172
|
+
new_hometexts = g.db.query(Home).filter_by(name="hometext").all()
|
|
173
|
+
return render_template("settings.html", settings=settings_dict, hometext=new_hometexts)
|
|
174
|
+
|
|
175
|
+
@admin_bp.route("/admin/translations", methods=['GET', 'POST'])
|
|
176
|
+
def translations():
|
|
177
|
+
if request.method == "GET":
|
|
178
|
+
trans = g.db.query(Home).filter_by(type="translation").all()
|
|
179
|
+
grouped = defaultdict(dict)
|
|
180
|
+
for t in trans:
|
|
181
|
+
grouped[t.name][t.language] = t.content
|
|
182
|
+
return render_template("translations.html", trans=grouped, langs=g.alltrans)
|
|
183
|
+
|
|
184
|
+
trans_array = []
|
|
185
|
+
for key, val in request.form.items():
|
|
186
|
+
if key.startswith("val_"):
|
|
187
|
+
_, name, lang = key.split("___", 2)
|
|
188
|
+
trans_array.append({
|
|
189
|
+
"name": name,
|
|
190
|
+
"language": lang,
|
|
191
|
+
"content": val
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
g.db.query(Home).filter(Home.type == "translation").delete()
|
|
195
|
+
|
|
196
|
+
for tr in trans_array:
|
|
197
|
+
g.db.add(Home(name=tr['name'], language=tr['language'], content=tr['content'], type="translation"))
|
|
198
|
+
|
|
199
|
+
g.db.commit()
|
|
200
|
+
return redirect("/admin/translations")
|
|
@@ -25,7 +25,7 @@ def register():
|
|
|
25
25
|
form_password2 = request.form.get('password2')
|
|
26
26
|
|
|
27
27
|
if form_password != form_password2:
|
|
28
|
-
return render_template("register.html", error=g.trans['
|
|
28
|
+
return render_template("register.html", error=g.trans['passwords_dont_match'])
|
|
29
29
|
|
|
30
30
|
user = g.db.query(User).filter_by(username=form_username).first()
|
|
31
31
|
if user:
|
|
@@ -123,6 +123,7 @@ def edit_blog(name):
|
|
|
123
123
|
p.views = v
|
|
124
124
|
|
|
125
125
|
themes = get_themes()
|
|
126
|
+
themes.append("default")
|
|
126
127
|
|
|
127
128
|
if request.method == "GET":
|
|
128
129
|
return render_template("edit.html", blog=blog, posts=posts, themes=themes)
|
|
@@ -136,10 +137,7 @@ def edit_blog(name):
|
|
|
136
137
|
blog.updated = now
|
|
137
138
|
blog.title = request.form.get("title")
|
|
138
139
|
selected_theme = request.form.get("theme")
|
|
139
|
-
|
|
140
|
-
themes_plus.append("default")
|
|
141
|
-
if selected_theme not in themes_plus:
|
|
142
|
-
print(f"{selected_theme} in {themes}?")
|
|
140
|
+
if selected_theme not in themes:
|
|
143
141
|
return render_template("edit.html", blog=blog, posts=posts, themes=themes, error="Wrong theme!")
|
|
144
142
|
blog.theme = selected_theme
|
|
145
143
|
g.db.commit()
|
|
@@ -192,7 +190,7 @@ def new_post(name):
|
|
|
192
190
|
url = f"https://openwrite.io/b/{blog.name}/{link}"
|
|
193
191
|
|
|
194
192
|
followers = []
|
|
195
|
-
if blog.followers
|
|
193
|
+
if blog.followers not in (None, "null", "NULL"):
|
|
196
194
|
followers = json.loads(blog.followers)
|
|
197
195
|
|
|
198
196
|
for actor in followers:
|
|
@@ -14,8 +14,11 @@ main_bp = Blueprint("main", __name__)
|
|
|
14
14
|
@main_bp.route("/")
|
|
15
15
|
def index():
|
|
16
16
|
if g.mode == "multi":
|
|
17
|
+
langs = []
|
|
17
18
|
home = g.db.query(Home).filter_by(name="hometext").all()
|
|
18
|
-
|
|
19
|
+
for h in home:
|
|
20
|
+
langs.append(h.language)
|
|
21
|
+
return render_template('index.html', home=home, langs=langs)
|
|
19
22
|
|
|
20
23
|
elif g.mode == "single":
|
|
21
24
|
blog = g.db.query(Blog).filter_by(name="default").first()
|
|
@@ -47,7 +47,7 @@ def upload_image():
|
|
|
47
47
|
|
|
48
48
|
m = hashlib.md5()
|
|
49
49
|
m.update(filename.encode() + user.username.encode())
|
|
50
|
-
filename = f"{
|
|
50
|
+
filename = f"{m.hexdigest()}.{extension}"
|
|
51
51
|
|
|
52
52
|
if STORAGE_BACKEND == "bunny":
|
|
53
53
|
url = f"https://storage.bunnycdn.com/{BUNNY_ZONE}/{filename}"
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
{% endif %}
|
|
5
5
|
<h1 class="centered">Actions</h1>
|
|
6
6
|
<div style="display: flex; justify-content: center; gap: 1rem;">
|
|
7
|
-
<a style="width: 5rem;" class="btn purple" href="/admin/add_user">{{
|
|
7
|
+
<a style="width: 5rem;" class="btn purple" href="/admin/add_user">{{ _('add_user') }}</a>
|
|
8
8
|
<a style="width: 5rem;" class="btn purple" href="/admin/settings">Settings</a>
|
|
9
|
+
<a style="width: 5rem;" class="btn purple" href="/admin/translations">Translations</a>
|
|
9
10
|
</div>
|
|
10
11
|
<h1 class="centered">Blogs</h1>
|
|
11
12
|
<table class="table">
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
</thead>
|
|
15
16
|
<tbody>
|
|
16
17
|
{% for blog in blogs %}
|
|
17
|
-
<tr><td>{{ blog.title }}</td><td>{{ blog.name }}</td><td>{{ blog.ownername }}</td><td><a class="action" href="/admin/delete_blog/{{ blog.name }}" onclick="return confirm('{{
|
|
18
|
+
<tr><td>{{ blog.title }}</td><td>{{ blog.name }}</td><td>{{ blog.ownername }}</td><td><a class="action" href="/admin/delete_blog/{{ blog.name }}" onclick="return confirm('{{ _('are_you_sure_blog') }} {{ blog.name }}?\n{{ _('this_action_cannot_be_reverted') }}.')">Delete</a></td></tr>
|
|
18
19
|
{% endfor %}
|
|
19
20
|
</tbody>
|
|
20
21
|
</table>
|
|
@@ -27,15 +28,15 @@
|
|
|
27
28
|
<tbody>
|
|
28
29
|
{% for user in users %}
|
|
29
30
|
<tr><td>{{ user.username }}</td>
|
|
30
|
-
<td>{% if user.admin > 0 %}{{
|
|
31
|
+
<td>{% if user.admin > 0 %}{{ _('yes') }}{% else %}{{ _('no') }}{% endif %}</td>
|
|
31
32
|
<td>
|
|
32
|
-
<a class="action" href="/admin/delete_user/{{ user.username }}" onclick="return confirm('{{
|
|
33
|
+
<a class="action" href="/admin/delete_user/{{ user.username }}" onclick="return confirm('{{ _('are_you_sure_user') }} {{ user.username }}?\n{{ _('this_action_cannot_be_reverted') }}.')">Delete</a>
|
|
33
34
|
{% if user.admin > 0 %}
|
|
34
35
|
{% if user.username != "admin" %}
|
|
35
|
-
<a class="action" onclick="return confirm('{{
|
|
36
|
+
<a class="action" onclick="return confirm('{{ _('are_you_sure_remove_admin') }} {{ user.username }}?')" href="/admin/remove_admin/{{ user.username }}">{{ _('remove_admin') }}</a>
|
|
36
37
|
{% endif %}
|
|
37
38
|
{% else %}
|
|
38
|
-
<a class="action" onclick="return confirm('{{
|
|
39
|
+
<a class="action" onclick="return confirm('{{ _('are_you_sure_make_admin') }} {{ user.username }}?')" href="/admin/make_admin/{{ user.username }}">{{ _('add_admin') }}</a>
|
|
39
40
|
{% endif %}
|
|
40
41
|
</td>
|
|
41
42
|
</tr>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{% include 'header.html' %}
|
|
2
|
+
|
|
3
|
+
<h1 class="centered">{{ _('change_password') }}</h1>
|
|
4
|
+
|
|
5
|
+
<article class="main-content">
|
|
6
|
+
{% if error %}
|
|
7
|
+
<div class="error">{{ error }}</div>
|
|
8
|
+
{% endif %}
|
|
9
|
+
<form method="post" action="/dashboard/changepw">
|
|
10
|
+
<span class="form-field">{{ _('current_password') }}:</span>
|
|
11
|
+
<input type="password" name="current_pass">
|
|
12
|
+
<span class="form-field">{{ _('new_password') }}:</span>
|
|
13
|
+
<input type="password" name="new_pass">
|
|
14
|
+
<span class="form-field">{{ _('password2') }}:</span>
|
|
15
|
+
<input type="password" name="new_pass2">
|
|
16
|
+
<input type="submit" name="sub" value="{{ _('change_password') }}" class="btn purple" style="margin-top: 1rem;">
|
|
17
|
+
</form>
|
|
18
|
+
</article>
|
|
19
|
+
{% include 'footer.html' %}
|