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
@@ -0,0 +1,216 @@
1
+ import bleach
2
+ import re
3
+ import unicodedata
4
+ import hashlib
5
+ from flask import request, g
6
+ import os
7
+ import secrets
8
+ import json
9
+ import base64
10
+ import requests
11
+ import datetime
12
+ from urllib.parse import urlparse
13
+ from cryptography.hazmat.primitives import hashes, serialization
14
+ from cryptography.hazmat.primitives.asymmetric import padding
15
+ from cryptography.exceptions import InvalidSignature
16
+ from bs4 import BeautifulSoup
17
+
18
+
19
+ def sanitize_html(content):
20
+ allowed_tags = [
21
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
22
+ 'p', 'br', 'hr',
23
+ 'strong', 'b', 'em', 'i', 'u',
24
+ 'ul', 'ol', 'li',
25
+ 'a', 'img', 'video',
26
+ 'code', 'pre', 'del',
27
+ 'blockquote',
28
+ 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'
29
+ ]
30
+
31
+ allowed_attrs = {
32
+ 'a': ['href', 'title', 'rel', 'target'],
33
+ 'img': ['src', 'alt', 'title', 'width', 'height'],
34
+ 'th': ['align'],
35
+ 'td': ['align'],
36
+ 'video': ['src', 'width', 'height', 'controls']
37
+ }
38
+
39
+ return bleach.clean(content, tags=allowed_tags, attributes=allowed_attrs)
40
+
41
+
42
+ def gen_link(text):
43
+ text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
44
+ text = re.sub(r'[^\w\s-]', '', text.lower())
45
+ text = re.sub(r'[\s_]+', '-', text.strip())
46
+ return text
47
+
48
+
49
+ def get_ip():
50
+ ip = request.headers.get("X-Forwarded-For", request.remote_addr)
51
+ if ip and "," in ip:
52
+ ip = ip.split(",")[0].strip()
53
+ return ip
54
+
55
+
56
+ def anonymize(ip: str, salt: str = None) -> str:
57
+ if salt is None:
58
+ salt = os.getenv("SECRET_KEY", "")
59
+ data = (salt + ip).encode("utf-8")
60
+ return hashlib.sha256(data).hexdigest()
61
+
62
+
63
+ def safe_css(data):
64
+ banned_keywords = [
65
+ r"url\s*\(", r"@import", r"@keyframes", r"expression\s*\(", r"javascript\s*:", r"animation"
66
+ ]
67
+
68
+ for keyword in banned_keywords:
69
+ data = re.sub(keyword, "", data, flags=re.IGNORECASE)
70
+
71
+ return data
72
+
73
+ def generate_nonce():
74
+ return secrets.token_urlsafe(16)
75
+
76
+
77
+ def verify_http_signature(headers, body, blog):
78
+ try:
79
+ digest_header = headers.get("Digest")
80
+ if not digest_header or not digest_header.startswith("SHA-256="):
81
+ return False
82
+
83
+ digest_b64 = digest_header.split("=", 1)[1]
84
+ actual_digest = base64.b64encode(hashlib.sha256(body.encode("utf-8")).digest()).decode()
85
+ if digest_b64 != actual_digest:
86
+ return False
87
+
88
+ sig_header = headers.get("Signature")
89
+ if not sig_header:
90
+ return False
91
+
92
+ sig_parts = {}
93
+ for part in sig_header.split(","):
94
+ k, v = part.strip().split("=", 1)
95
+ sig_parts[k] = v.strip('"')
96
+
97
+ key_id = sig_parts.get("keyId")
98
+ algorithm = sig_parts.get("algorithm")
99
+ header_list = sig_parts.get("headers", "").split()
100
+ signature_b64 = sig_parts.get("signature")
101
+
102
+ if not key_id or not algorithm or not header_list or not signature_b64:
103
+ return False
104
+
105
+ string_lines = []
106
+ for h in header_list:
107
+ if h == "(request-target)":
108
+ method = "post"
109
+ path = f"/inbox/{blog}"
110
+ string_lines.append(f"(request-target): {method} {path}")
111
+ else:
112
+ value = headers.get(h, "")
113
+ string_lines.append(f"{h.lower()}: {value}")
114
+
115
+ string_to_verify = "\n".join(string_lines)
116
+
117
+ actor_url = key_id.split("#")[0]
118
+ res = requests.get(actor_url, headers={"Accept": "application/activity+json"})
119
+ actor_data = res.json()
120
+ public_key_pem = actor_data["publicKey"]["publicKeyPem"]
121
+
122
+ public_key = serialization.load_pem_public_key(public_key_pem.encode())
123
+ signature = base64.b64decode(signature_b64)
124
+
125
+ public_key.verify(
126
+ signature,
127
+ string_to_verify.encode(),
128
+ padding.PKCS1v15(),
129
+ hashes.SHA256()
130
+ )
131
+
132
+ return True
133
+
134
+ except InvalidSignature:
135
+ return False
136
+ except Exception as e:
137
+ return False
138
+
139
+ def send_activity(activity, private_key_pem, from_, to):
140
+
141
+ body = json.dumps(activity)
142
+ digest = "SHA-256=" + base64.b64encode(hashlib.sha256(body.encode()).digest()).decode()
143
+ date = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")
144
+
145
+ to_actor = from_
146
+ inbox_url = to
147
+ parsed = urlparse(inbox_url)
148
+ host = parsed.hostname
149
+ path = parsed.path
150
+
151
+ private_key = serialization.load_pem_private_key(
152
+ private_key_pem.encode(), password=None
153
+ )
154
+
155
+ headers_to_sign = [
156
+ f"(request-target): post {path}",
157
+ f"host: {host}",
158
+ f"date: {date}",
159
+ f"digest: {digest}",
160
+ "content-type: application/activity+json"
161
+ ]
162
+ string_to_sign = "\n".join(headers_to_sign)
163
+
164
+ signature = private_key.sign(
165
+ string_to_sign.encode(),
166
+ padding.PKCS1v15(),
167
+ hashes.SHA256()
168
+ )
169
+ signature_b64 = base64.b64encode(signature).decode()
170
+
171
+ signature_header = (
172
+ f'keyId="{to_actor}#main-key",'
173
+ f'algorithm="rsa-sha256",'
174
+ f'headers="(request-target) host date digest content-type",'
175
+ f'signature="{signature_b64}"'
176
+ )
177
+
178
+ headers = {
179
+ "Host": host,
180
+ "Date": date,
181
+ "Digest": digest,
182
+ "Content-Type": "application/activity+json",
183
+ "Signature": signature_header
184
+ }
185
+ #print(f"""
186
+ # URL:
187
+ # {inbox_url}
188
+
189
+ # Headers:
190
+ # {headers}
191
+
192
+ # Body:
193
+ # {body}
194
+ #""")
195
+
196
+ response = requests.post(inbox_url, headers=headers, data=body)
197
+ #print(f"""
198
+ # [+] Sent to {inbox_url}: {response.status_code}
199
+ # Body:
200
+
201
+ # {response.text}
202
+ #""")
203
+ if response.status_code >= 400:
204
+ print("[-] Response:", response.text)
205
+
206
+ return response.status_code
207
+
208
+ def is_html(content):
209
+ soup = BeautifulSoup(content, "html.parser")
210
+ return bool(soup.find())
211
+
212
+ def get_themes():
213
+ css_files = [f.replace(".css", "") for f in os.listdir(f"{g.mainpath}/static/style/themes/") if f.endswith(".css")]
214
+
215
+ return css_files
216
+
@@ -0,0 +1,229 @@
1
+ {
2
+ "en": {
3
+ "instances": "Instances",
4
+ "hosting": "Hosting",
5
+ "register": "Register",
6
+ "login": "Login",
7
+ "header_text": "<span style=\"color: #f7e3db\">quiet</span> space for <span style=\"color: #8d7bb9\">loud</span> thoughts.",
8
+ "register_here": "Register Here",
9
+ "self_host": "Self Host",
10
+ "username": "Username",
11
+ "password": "Password",
12
+ "email": "Email",
13
+ "not_required": "not required by recommended for 2FA and recovery",
14
+ "register_on_this_instance": "register on this instance",
15
+ "home_text": "<strong>openwrite.io</strong> is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.<br><br>No tracking scripts. No bloated frameworks. No ads. Just you, your thoughts, and the open web.<br><br>Each blog runs on its own terms — you can self-host in minutes, write in clean Markdown, and share your voice without intermediaries.<br><br>Designed for lightness and privacy, openwrite is also prepared for federation. In the future, instances will be able to follow each other, share content, and form a decentralized writing network.<br><br><em>No analytics. No distractions. Just words.</em <br><br><u class=\"centered\">Don't wait up - start blogging now!</u>",
16
+ "or": "or",
17
+ "language": "Language",
18
+ "buy_coffee": "Buy me a coffee",
19
+ "user_exists": "User already exists. Choose different name.",
20
+ "error": "Unknown error occured. Try again later.",
21
+ "registered": "Register successful! You can login now.",
22
+ "invalid_credentials": "Invalid credentials!",
23
+ "logout": "Log out",
24
+ "your_blogs": "your blogs",
25
+ "title": "Title",
26
+ "edit": "Edit",
27
+ "new_post": "New post",
28
+ "delete_blog": "Delete blog",
29
+ "create_new_blog": "Create new blog",
30
+ "description": "Description",
31
+ "save": "Save",
32
+ "upload_image": "Upload image",
33
+ "date": "Date",
34
+ "actions": "Actions",
35
+ "choose_blog_title": "Choose blog title",
36
+ "choose_blog_url": "Choose blog URL",
37
+ "access_blog_how": "How would you like to access your blog",
38
+ "domain_name": "Domain name",
39
+ "path": "Path",
40
+ "create": "Create",
41
+ "index_blog": "Index your blog in search engines",
42
+ "content": "content",
43
+ "preview": "Preview",
44
+ "post": "Post",
45
+ "include_author": "Include author information(username) in post",
46
+ "new_post_on": "new post on blog",
47
+ "posts": "Posts",
48
+ "are_you_sure_blog": "Are you sure to delete blog",
49
+ "this_action_cannot_be_reverted": "This action cannot be reverted",
50
+ "are_you_sure_user": "Are you sure to delete user",
51
+ "edit": "Edit",
52
+ "are_you_sure_post": "Are you sure to delete post",
53
+ "delete": "Delete",
54
+ "editing_post": "editing post",
55
+ "views": "Views",
56
+ "settings": "Settings",
57
+ "mfa": "Multi-factor authentication",
58
+ "verify": "Verify",
59
+ "delete_account": "Delete account",
60
+ "instances_text": "Here you will find all known openwrite instances along with their properties",
61
+ "name": "Name",
62
+ "blogs_running": "Blogs running",
63
+ "users": "Users",
64
+ "media_upload": "Upload media?",
65
+ "password2": "Retype password",
66
+ "passwords_dont_match": "Passwords don't match!",
67
+ "include_in_feed": "Include post in \"Discover\" section",
68
+ "discover": "Discover",
69
+ "discover_text": "Here you can find most recent posts from this instance.",
70
+ "yes": "Yes",
71
+ "no": "No",
72
+ "change_password": "Change Password",
73
+ "migrate_posts": "Import posts",
74
+ "wrong_username": "Wrong username!",
75
+ "invalid_captcha": "Invalid captcha!",
76
+ "uploads_disabled": "Uploads disabled",
77
+ "invalid_filename": "Invalid filename",
78
+ "unsupported_filetype": "Unsupported file type",
79
+ "no_file": "No file",
80
+ "not_image": "Uploaded file is not a valid image",
81
+ "upload_failed": "Upload failed",
82
+ "current_password": "Current password",
83
+ "new_password": "New password",
84
+ "choose_vendor": "Choose blog vendor",
85
+ "select_posts": "Select posts to migrate",
86
+ "migrate_info": "Data is used only for migration and not saved in any place.",
87
+ "invalid_password": "Invalid password!",
88
+ "choose_format": "Choose data format",
89
+ "title_column": "Title column name",
90
+ "content_column": "Content column name",
91
+ "date_column": "Date column name",
92
+ "content_format": "Content format",
93
+ "step": "Step",
94
+ "upload_file": "Upload file",
95
+ "send": "Send",
96
+ "import_info": "In here you can import posts from another platform. Files can be exported from Wordpress(XML) or in general CSV format. <a class=\"link\" href=\"https://main.openwrite.io/posts-migration\">Here</a> you can find information about how to prepare data to import.",
97
+ "import_success1": "Import successful! Go to",
98
+ "import_success2": "blog dashboard",
99
+ "import_success3": "to see your posts.",
100
+ "import_failed": "Error occured! Check your data format.",
101
+ "select_blog_to_import": "Select blog to import data to",
102
+ "remove_admin": "Remove admin",
103
+ "add_admin": "Make admin",
104
+ "are_you_sure_make_admin": "Are you sure to add admin rights to",
105
+ "are_you_sure_remove_admin": "Are you sure to remove admin rights from",
106
+ "add_user": "Add user",
107
+ "select_theme": "Select theme",
108
+ "default": "Default",
109
+ "see_themes": "See themes",
110
+ "custom_css": "Custom CSS",
111
+ "css_unsafe_info": "Unsafe values like url(), @keyframes, @import, expression, javascript and animation will be removed.",
112
+ "your_blog": "your blog",
113
+ "statistics": "Statistics",
114
+ "contact": "Contact"
115
+ },
116
+ "pl": {
117
+ "instances": "Instancje",
118
+ "hosting": "Hosting",
119
+ "register": "Rejestracja",
120
+ "login": "Zaloguj się",
121
+ "header_text": "<span style=\"color: #f7e3db\">cicha</span> przestrzeń na <span style=\"color: #8d7bb9\">głośne</span> myśli.",
122
+ "register_here": "Zarejestruj się tutaj",
123
+ "self_host": "Samodzielny hosting",
124
+ "username": "Nazwa użytkownika",
125
+ "password": "Hasło",
126
+ "email": "E-mail",
127
+ "not_required": "nie wymagany, ale zalecany do 2FA i odzyskiwania konta",
128
+ "register_on_this_instance": "zarejestruj się na tej instancji",
129
+ "home_text": "<strong>openwrite.io</strong> to minimalistyczna platforma blogowa stworzona z myślą o wolności pisania, niezależnym hostowaniu i publikowaniu bez zbędnego hałasu.<br><br>Bez skryptów śledzących. Bez przeładowanych frameworków. Bez reklam. Tylko Ty, Twoje myśli i otwarty internet.<br><br>Każdy blog działa na własnych zasadach — możesz samodzielnie go uruchomić w kilka minut, pisać w czystym Markdownie i dzielić się swoim głosem bez pośredników.<br><br>Stworzony z myślą o lekkości i prywatności, openwrite jest również gotowy na federację. W przyszłości instancje będą mogły obserwować się nawzajem, udostępniać treści i tworzyć zdecentralizowaną sieć pisarską.<br><br><em>Bez analityki. Bez rozpraszaczy. Tylko słowa.</em><br><br><u class=\"centered\">Nie czekaj — zacznij pisać już teraz!</u>",
130
+ "or": "lub",
131
+ "language": "Język",
132
+ "buy_coffee": "Kup mi kawkę",
133
+ "user_exists": "Użytkownik o takiej nazwie istnieje. Wybierz inną.",
134
+ "error": "Wystąpił nieznany błąd. Spróbuj ponownie później.",
135
+ "registered": "Zarejestrowano pomyslnie! Możesz się zalogować.",
136
+ "invalid_credentials": "Login lub hasło nieprawidłowe!",
137
+ "logout": "Wyloguj",
138
+ "your_blogs": "twoje blogi",
139
+ "title": "Tytuł",
140
+ "edit": "Edytuj",
141
+ "new_post": "Nowy wpis",
142
+ "delete_blog": "Usuń bloga",
143
+ "create_new_blog": "Utwórz nowego bloga",
144
+ "description": "Opis",
145
+ "save": "Zapisz",
146
+ "upload_image": "Prześlij obraz",
147
+ "date": "Data",
148
+ "actions": "Akcje",
149
+ "choose_blog_title": "Wybierz tytuł bloga",
150
+ "choose_blog_url": "Wybierz adres URL bloga",
151
+ "access_blog_how": "Jaki chcesz link do bloga",
152
+ "domain_name": "Nazwa domeny",
153
+ "path": "Ścieżka",
154
+ "create": "Utwórz",
155
+ "index_blog": "Indeksuj bloga w wyszukiwarkach",
156
+ "content": "Treść",
157
+ "preview": "Podgląd",
158
+ "post": "Opublikuj",
159
+ "include_author": "Dołącz informacje o autorze (nazwa użytkownika) we wpisie",
160
+ "new_post_on": "nowy wpis na blogu",
161
+ "posts": "Wpisy",
162
+ "are_you_sure_blog": "Czy na pewno chcesz usunąć bloga",
163
+ "this_action_cannot_be_reverted": "Tej operacji nie można cofnąć",
164
+ "are_you_sure_post": "Czy na pewno chcesz usunąć wpis",
165
+ "are_you_sure_user": "Czy na pewno chcesz usunąć użytkownika",
166
+ "delete": "Usuń",
167
+ "editing_post": "edycja wpisu",
168
+ "views": "Wyświetlenia",
169
+ "settings": "Ustawienia",
170
+ "mfa": "Weryfikacja dwuetapowa",
171
+ "verify": "Zweryfikuj",
172
+ "delete_account": "Usuń konto",
173
+ "instances_text": "Tutaj znajdziesz wszystkie znane nam instancje openwrite.io wraz z ich właściwościami",
174
+ "name": "Nazwa",
175
+ "blogs_running": "Uruchomione blogi",
176
+ "users": "Użytkownicy",
177
+ "media_upload": "Przesyłanie mediów?",
178
+ "password2": "Powtórz hasło",
179
+ "password_dont_match": "Hasła się nie zgadzają!",
180
+ "include_in_feed": "Wyświetlać wpis w sekcji \"Odkrywaj\"",
181
+ "discover": "Odkrywaj",
182
+ "discover_text": "Tutaj znajdziesz wszystkie najnowsze posty z tej instancji openwrite.",
183
+ "yes": "Tak",
184
+ "no": "Nie",
185
+ "change_password": "Zmień hasło",
186
+ "migrate_posts": "Importuj posty",
187
+ "wrong_username": "Nieprawidłowa nazwa!",
188
+ "invalid_captcha": "Captcha nieprawidłowa!",
189
+ "uploads_disabled": "Przesyłanie plików wyłączone",
190
+ "invalid_filename": "Nieprawidłowa nazwa pliku",
191
+ "unsupported_filetype": "Nieprawidłowy typ pliku",
192
+ "no_file": "Brak pliku",
193
+ "not_image": "Przesyłany plik nie jest obrazem",
194
+ "upload_failed": "Niepowodzenie w przesyłaniu",
195
+ "current_password": "Aktualne hasło",
196
+ "new_password": "Nowe hasło",
197
+ "choose_vendor": "Wybierz dostawcę obecnego bloga",
198
+ "select_posts": "Wybierz wpisy do migracji",
199
+ "migrate_info": "Dane są używane wyłącznie do migracji wpisów i nie są zapisywane.",
200
+ "invalid_password": "Nieprawidłowe hasło!",
201
+ "choose_format": "Wybierz format danych",
202
+ "title_column": "Nazwa kolumny z tytułem",
203
+ "content_column": "Nazwa kolumny z treścią",
204
+ "date_column": "Nazwa kolumny z datą",
205
+ "content_format": "Format treści wpisu",
206
+ "step": "Krok",
207
+ "upload_file": "Wybierz plik",
208
+ "send": "Wyślij",
209
+ "import_info": "W tym miejscu możesz zaimportować wpisy z innej platformy. Przyjmowane są pliki z Wordpress(XML) oraz CSV. <a class=\"link\" href=\"https://main.openwrite.io/posts-migration\">Tutaj</a> dowiesz się jak przygotować dane do importu.",
210
+ "import_success1": "Import wpisów zakończony sukcesem! Przejdź do",
211
+ "import_success2": "dashboardu bloga",
212
+ "import_success3": "żeby zobaczyć swoje posty.",
213
+ "import_failed": "Wystąpił błąd! Sprawdź przesyłane dane.",
214
+ "select_blog_to_import": "Wybierz blog, do którego trafią wpisy",
215
+ "remove_admin": "Zabierz prawa administratora",
216
+ "add_admin": "Mianuj administratorem",
217
+ "are_you_sure_make_admin": "Czy na pewno chcesz nadać prawa administratora dla",
218
+ "are_you_sure_remove_admin": "Czy na pewno zabrać uprawnienia administratora ",
219
+ "add_user": "Dodaj użytkownika",
220
+ "select_theme": "Wybierz motyw",
221
+ "default": "Domyślny",
222
+ "see_themes": "Zobacz motywy",
223
+ "custom_css": "Własny CSS",
224
+ "css_unsafe_info": "Niebezpieczne wartości jak url(), @keyframes, @import, expression, javascript i animation zostaną usunięte.",
225
+ "your_blog": "twój blog",
226
+ "statistics": "Statystyki",
227
+ "contact": "Contact"
228
+ }
229
+ }
@@ -0,0 +1,87 @@
1
+ from sqlalchemy import Column, Integer, String, Date, Text, DateTime, Index
2
+ from sqlalchemy.sql import func
3
+ from openwrite.db.base import Base
4
+
5
+ class Settings(Base):
6
+ __tablename__ = "settings"
7
+
8
+ id = Column(Integer, primary_key=True)
9
+ name = Column(String(100), nullable=False)
10
+ value = Column(Text, nullable=False)
11
+
12
+ class Home(Base):
13
+ __tablename__ = "home"
14
+
15
+ id = Column(Integer, primary_key=True)
16
+ language = Column(String(10))
17
+ name = Column(String(100), nullable=False)
18
+ type = Column(String(30), nullable=False)
19
+ url = Column(String(200))
20
+ content = Column(Text)
21
+
22
+ class User(Base):
23
+ __tablename__ = "users"
24
+
25
+ id = Column(Integer, primary_key=True)
26
+ username = Column(String(50), unique=True, nullable=False)
27
+ password_hash = Column(String(255), nullable=False)
28
+ email = Column(String(120), nullable=True)
29
+ verified = Column(Integer, nullable=False)
30
+ admin = Column(Integer, nullable=False)
31
+
32
+ class Blog(Base):
33
+ __tablename__ = "blogs"
34
+
35
+ id = Column(Integer, primary_key=True)
36
+ owner = Column(Integer, nullable=False)
37
+ name = Column(String(64), nullable=False)
38
+ title = Column(String(64), nullable=False)
39
+ index = Column(String(10), nullable=False)
40
+ access = Column(String(10), nullable=False)
41
+ description_raw = Column(Text, nullable=False)
42
+ description_html = Column(Text, nullable=False)
43
+ css = Column(Text)
44
+ pub_key = Column(Text, nullable=False)
45
+ priv_key = Column(Text, nullable=False)
46
+ followers = Column(Text)
47
+ created = Column(DateTime, nullable=False)
48
+ updated = Column(DateTime)
49
+ theme = Column(String(50), nullable=False)
50
+
51
+ class Post(Base):
52
+ __tablename__ = "posts"
53
+
54
+ id = Column(Integer, primary_key=True)
55
+ blog = Column(Integer, nullable=False)
56
+ title = Column(String(128), nullable=False)
57
+ link = Column(String(64), nullable=False)
58
+ date = Column(DateTime, default=func.current_date())
59
+ content_raw = Column(Text, nullable=False)
60
+ content_html = Column(Text, nullable=False)
61
+ author = Column(String(10), nullable=False)
62
+ feed = Column(String(10), nullable=False)
63
+ updated = Column(DateTime)
64
+
65
+ class View(Base):
66
+ __tablename__ = "views"
67
+
68
+ id = Column(Integer, primary_key=True)
69
+ hash = Column(String(64), nullable=False)
70
+ blog = Column(Integer, nullable=False)
71
+ post = Column(Integer, nullable=False)
72
+ date = Column(DateTime)
73
+ agent = Column(String(300))
74
+
75
+ __table_args__ = (
76
+ Index("ix_views_blog_post", "blog", "post"),
77
+ )
78
+
79
+ class Like(Base):
80
+ __tablename__ = "likes"
81
+
82
+ id = Column(Integer, primary_key=True)
83
+ hash = Column(String(64), nullable=False)
84
+ blog = Column(Integer, nullable=False)
85
+ post = Column(Integer, nullable=False)
86
+ date = Column(DateTime)
87
+
@@ -0,0 +1,6 @@
1
+ import json
2
+ import importlib.resources
3
+
4
+ def load_translations():
5
+ data = importlib.resources.files(__package__).joinpath("i18n.json").read_text(encoding="utf-8")
6
+ return json.loads(data)