+2
-2
src/database/post.v
+2
-2
src/database/post.v
···
109
110
// update_post updates the given post's title and body with the given title and
111
// body, returns true if this succeeds and false otherwise.
112
-
pub fn (app &DatabaseAccess) update_post(post_id int, new_title string, new_body string) bool {
113
sql app.db {
114
-
update Post set body = new_body, title = new_title where id == post_id
115
} or {
116
return false
117
}
···
109
110
// update_post updates the given post's title and body with the given title and
111
// body, returns true if this succeeds and false otherwise.
112
+
pub fn (app &DatabaseAccess) update_post(post_id int, new_title string, new_body string, new_nsfw bool) bool {
113
sql app.db {
114
+
update Post set body = new_body, title = new_title, nsfw = new_nsfw where id == post_id
115
} or {
116
return false
117
}
+52
src/static/js/form.js
+52
src/static/js/form.js
···
···
1
+
async function _submit(event, element)
2
+
{
3
+
event.preventDefault();
4
+
5
+
/* debug */
6
+
console.log(`submitting form:`);
7
+
console.log(element)
8
+
console.log(`destination: ${element.action}`);
9
+
const formdata = new FormData(element);
10
+
console.log(`data:`);
11
+
console.log(formdata);
12
+
13
+
try
14
+
{
15
+
await fetch(element.action, {
16
+
method: "POST",
17
+
headers: {
18
+
"Content-Type": "application/x-www-form-urlencoded",
19
+
},
20
+
body: new URLSearchParams(new FormData(element)),
21
+
}).then(async response => {
22
+
console.log(response);
23
+
const ok = response.status == 200;
24
+
const text = await response.text();
25
+
notify(text, ok ? 'ok' : 'error'); /* /static/js/notify.js */
26
+
if (ok)
27
+
{
28
+
if (element.hasAttribute("beep-redirect"))
29
+
window.location.href = element.getAttribute("beep-redirect");
30
+
else if (element.hasAttribute('beep-redirect-js'))
31
+
window.location.href = eval(element.getAttribute("beep-redirect-js"))(
32
+
response,
33
+
text
34
+
);
35
+
}
36
+
});
37
+
}
38
+
catch (error)
39
+
{
40
+
console.error(error.message);
41
+
}
42
+
}
43
+
44
+
const e = document.getElementsByTagName('form');
45
+
for (let i = 0 ; i < e.length ; i++)
46
+
{
47
+
const element = e.item(i);
48
+
if (element.method == 'post')
49
+
{
50
+
element.onsubmit = event => _submit(event, element);
51
+
}
52
+
}
+18
src/static/js/notify.js
+18
src/static/js/notify.js
···
···
1
+
const errors = document.getElementById('errors')
2
+
3
+
const notify = (msg, level = 'ok') => {
4
+
const p = document.createElement('p');
5
+
p.classList.add(level);
6
+
7
+
const button = document.createElement('button');
8
+
button.innerText = 'X';
9
+
button.style.display = 'inline';
10
+
button.onclick = () => errors.removeChild(p);
11
+
12
+
const span = document.createElement('span');
13
+
span.innerText = `${level != 'ok' ? `${level}: ` : ''}${msg}`;
14
+
15
+
p.appendChild(button);
16
+
p.appendChild(span);
17
+
errors.appendChild(p);
18
+
}
+31
src/static/js/password.js
+31
src/static/js/password.js
···
···
1
+
const add_password_checkers = (password_id, confirm_id, match_id) => {
2
+
const password = document.getElementById(password_id);
3
+
const confirm_password = document.getElementById(confirm_id);
4
+
const matches = document.getElementById(match_id);
5
+
6
+
const a = () => {
7
+
matches.innerText = password.value==confirm_password.value ? "yes" : "no";
8
+
};
9
+
password.addEventListener('input', a);
10
+
confirm_password.addEventListener('input', a);
11
+
12
+
const view_password = document.getElementById(`view-${password_id}`);
13
+
const view_confirm_password = document.getElementById(`view-${confirm_id}`);
14
+
15
+
const b = (elm, btn) => {
16
+
return _ => {
17
+
if (elm.getAttribute('type') == 'password')
18
+
{
19
+
elm.setAttribute('type', 'text');
20
+
btn.value = 'hide';
21
+
}
22
+
else
23
+
{
24
+
elm.setAttribute('type', 'password')
25
+
btn.value = 'show';
26
+
}
27
+
};
28
+
};
29
+
view_password.addEventListener('click', b(password, view_password));
30
+
view_confirm_password.addEventListener('click', b(confirm_password, view_confirm_password));
31
+
}
+5
src/static/style.css
+5
src/static/style.css
···
37
margin-left: 6px;
38
}
39
40
+
details>summary:hover {
41
+
cursor: pointer;
42
+
}
43
+
44
/*
45
* some themes make input fields display: block, which overrides my hidden
46
* attribute. to resolve that, i will just override the override.
47
*/
48
input[hidden] {
49
display: none !important;
50
+
visibility: none !important;
51
}
+159
-26
src/static/themes/default.css
+159
-26
src/static/themes/default.css
···
1
-
@import url('https://fonts.googleapis.com/css2?family=Nova+Mono&family=Oxygen+Mono&display=swap');
2
3
:root {
4
-
--c-bg: #222;
5
-
--c-panel-bg: #2c2c2c;
6
-
--c-fg: #ffffff;
7
-
--c-nsfw-border: #ff6666;
8
-
--c-link: #6666ff;
9
-
--c-accent: #faaeff;
10
}
11
12
html {
···
15
margin: 0;
16
17
width: 100vw;
18
19
display: flex;
20
flex-direction: column;
···
23
background-color: var(--c-bg);
24
color: var(--c-fg);
25
26
-
font-family: "Oxygen Mono", sans-serif;
27
-
font-weight: 400;
28
-
font-style: normal;
29
-
font-size: 20px;
30
}
31
32
body {
33
-
padding: 16px 0 0 0;
34
offset: 0;
35
margin: 0;
36
-
37
-
width: 80vw;
38
}
39
40
header {
41
-
padding-bottom: 16px;
42
}
43
44
footer {
45
-
padding-top: 16px;
46
}
47
48
main {
49
-
padding: 16px;
50
-
51
background-color: var(--c-panel-bg);
52
53
display: flex;
54
flex-direction: column;
55
-
gap: 12px;
56
}
57
58
form {
59
display: flex;
60
flex-direction: column;
61
-
gap: 12px;
62
}
63
64
input,
···
67
background-color: var(--c-panel-bg);
68
color: var(--c-fg);
69
70
-
border: 2px solid var(--c-accent);
71
padding: 6px;
72
}
73
74
h1, h2, h3, h4, h5, h6, p {
···
76
}
77
78
h1, header, footer {
79
-
font-family: "Nova Mono", sans-serif;
80
}
81
82
a {
83
color: var(--c-link);
84
}
85
86
hr {
87
width: 100%;
88
}
89
90
.post {
91
border: none;
92
-
border-left: 2px solid var(--c-fg);
93
}
94
95
.post + .post,
···
97
margin-top: 18px;
98
}
99
100
-
form,
101
#recent-posts,
102
#pinned-posts {
103
-
border-top: 2px solid var(--c-accent);
104
-
border-bottom: 2px solid var(--c-accent);
105
padding: 16px 24px 16px 24px;
106
}
···
1
+
@import url('https://fonts.googleapis.com/css2?family=Onest:wght@100..900&family=Oxygen+Mono&display=swap');
2
3
:root {
4
+
/* palette */
5
+
/* greys */
6
+
--p-black: #333333;
7
+
--p-grey0: #414141;
8
+
--p-grey1: #4a4a4a;
9
+
--p-grey2: #4f4f4f;
10
+
--p-grey3: #5c5c5c;
11
+
--p-grey4: #5f5f5f;
12
+
--p-white: #e7e7e7;
13
+
/* rainbow */
14
+
--p-red: #faa; /* == light red */
15
+
--p-orange: #fa7;
16
+
--p-yellow: #ffa; /* == light-orange */
17
+
--p-teal: #7fa;
18
+
--p-green: #af7;
19
+
--p-blue: #7af;
20
+
--p-purple: #a7f;
21
+
--p-pink: #f7a;
22
+
/* light rainbow */
23
+
--p-light-red: #faa;
24
+
--p-light-blue: #aaf;
25
+
--p-light-green: #afa;
26
+
--p-light-orange: #ffa; /* == yellow */
27
+
--p-light-purple: #faf;
28
+
--p-light-blue: #aff;
29
+
30
+
/* colours */
31
+
--c-bg: var(--p-black);
32
+
--c-panel-bg: var(--p-grey0);
33
+
--c-panel-border: var(--p-grey2);
34
+
--c-panel2-bg: var(--p-grey1);
35
+
--c-panel2-border: var(--p-grey3);
36
+
--c-panel3-bg: var(--p-grey2);
37
+
--c-panel3-border: var(--p-grey4);
38
+
--c-fg: var(--p-white);
39
+
--c-nsfw-border: var(--p-orange);
40
+
--c-link: var(--p-blue);
41
+
--c-link-hover: var(--p-light-blue);
42
+
--c-accent: var(--p-light-green);
43
+
--c-notify-ok: var(--p-light-green);
44
+
--c-notify-error: var(--p-light-red);
45
+
46
+
/* text */
47
+
--t-font: 'Onest', Arial, serif;
48
+
--t-post-font: Garamond, 'Times New Roman', var(--t-font);
49
+
--t-mono-font: 'Oxygen Mono', monospace;
50
+
--t-h-font: 'Oxygen Mono', var(--t-post-font);
51
+
--t-font-weight: 400;
52
+
--t-font-style: normal;
53
+
--t-font-size: 20px;
54
+
55
+
/* layout */
56
+
--l-body-padding: 16px;
57
+
--l-body-gap: 12px;
58
+
--l-body-width: 75vw;
59
+
--l-border-width: 2px;
60
+
--l-border-style: solid;
61
+
--l-border-radius: 0px;
62
}
63
64
html {
···
67
margin: 0;
68
69
width: 100vw;
70
+
overflow-x: hidden;
71
72
display: flex;
73
flex-direction: column;
···
76
background-color: var(--c-bg);
77
color: var(--c-fg);
78
79
+
font-family: var(--t-font);
80
+
font-weight: var(--t-font-weight);
81
+
font-style: var(--t-font-style);
82
+
font-size: var(--t-font-size);
83
}
84
85
body {
86
+
padding: var(--l-body-padding) 0 var(--l-body-padding) 0;
87
offset: 0;
88
margin: 0;
89
+
width: var(--l-body-width);
90
}
91
92
header {
93
+
padding-bottom: var(--l-body-padding);
94
}
95
96
footer {
97
+
padding-top: var(--l-body-padding);
98
}
99
100
main {
101
+
padding: var(--l-body-padding);
102
background-color: var(--c-panel-bg);
103
+
border: var(--l-border-width) var(--l-border-style) var(--c-panel-border);
104
+
border-radius: var(--l-border-radius);
105
106
display: flex;
107
flex-direction: column;
108
+
gap: var(--l-body-gap);
109
}
110
111
form {
112
display: flex;
113
flex-direction: column;
114
+
gap: var(--l-body-gap);
115
+
}
116
+
117
+
button:hover {
118
+
cursor: pointer;
119
}
120
121
input,
···
124
background-color: var(--c-panel-bg);
125
color: var(--c-fg);
126
127
+
border: var(--l-border-width) var(--l-border-style) var(--c-accent);
128
+
border-radius: var(--l-border-radius);
129
padding: 6px;
130
+
131
+
font-family: var(--t-font);
132
+
}
133
+
134
+
input:hover,
135
+
textarea:hover,
136
+
button:hover {
137
+
border-color: var(--c-fg);
138
+
}
139
+
140
+
input:focus,
141
+
textarea:focus,
142
+
button:focus {
143
+
background-color: var(--c-accent);
144
+
color: var(--c-bg);
145
}
146
147
h1, h2, h3, h4, h5, h6, p {
···
149
}
150
151
h1, header, footer {
152
+
font-family: var(--t-h-font);
153
}
154
155
a {
156
color: var(--c-link);
157
+
transition: 0.15s linear color;
158
+
}
159
+
160
+
a:hover {
161
+
color: var(--c-link-hover);
162
}
163
164
hr {
165
width: 100%;
166
}
167
168
+
pre {
169
+
font-family: var(--t-mono-font);
170
+
}
171
+
172
.post {
173
border: none;
174
+
border-left: var(--l-border-width) var(--l-border-style) var(--c-fg);
175
+
}
176
+
177
+
.post>pre {
178
+
font-family: var(--t-post-font);
179
}
180
181
.post + .post,
···
183
margin-top: 18px;
184
}
185
186
+
form:not(.form-inline),
187
#recent-posts,
188
#pinned-posts {
189
padding: 16px 24px 16px 24px;
190
+
background-color: var(--c-panel2-bg);
191
+
border: var(--l-border-width) var(--l-border-style) var(--c-panel2-border);
192
+
border-radius: var(--l-border-radius);
193
+
}
194
+
195
+
#errors:empty {
196
+
display: none;
197
+
visibility: hidden;
198
+
}
199
+
200
+
#errors {
201
+
display: flex;
202
+
flex-direction: column;
203
+
gap: var(--l-body-gap);
204
+
}
205
+
206
+
#errors>p {
207
+
background-color: var(--c-panel3-bg);
208
+
border: var(--l-border-width) var(--l-border-style) var(--c-panel3-border);
209
+
border-radius: var(--l-border-radius);
210
+
211
+
padding: 8px;
212
+
width: calc(100% - 16px);
213
+
214
+
display: inline-flex;
215
+
align-items: center;
216
+
justify-content: center;
217
+
gap: 12px;
218
+
}
219
+
220
+
#errors>p>button {
221
+
border-color: inherit;
222
+
flex-grow: 0;
223
+
}
224
+
225
+
#errors>p>button:hover {
226
+
border-color: var(--c-fg);
227
+
}
228
+
229
+
#errors>p>span {
230
+
flex-grow: 1;
231
+
}
232
+
233
+
#errors>p.ok {
234
+
border-color: var(--c-notify-ok);
235
+
}
236
+
237
+
#errors>p.error {
238
+
border-color: var(--c-notify-error);
239
}
+7
-1
src/templates/components/new_post.html
+7
-1
src/templates/components/new_post.html
···
1
<script src="/static/js/text_area_counter.js"></script>
2
<div>
3
+
<form action="/api/post/new_post" method="post"
4
+
beep-redirect-js="(_,t)=>{return'/post/'+t.split('=')[1];}">
5
+
<!--
6
+
the above JS snippet will redirect the user to the new post. It's a liiiitle convoluted but whatever.
7
+
A successful new_post response will always respond with `posted. id=<id>`. I could just return JSON but honestly I don't care lmao.
8
+
TODO: return json because it's definitely better practice. also it would be useful for custom clients :p
9
+
-->
10
<h2>new post:</h2>
11
12
@if replying
+19
-2
src/templates/edit.html
+19
-2
src/templates/edit.html
···
7
<h1>edit post</h1>
8
9
<div class="post post-full">
10
-
<form action="/api/post/edit" method="post">
11
<input
12
type="number"
13
name="id"
···
47
>@post.body</textarea>
48
<br>
49
50
<input type="submit" value="save">
51
</form>
52
</div>
···
55
56
<div>
57
<h2>danger zone:</h2>
58
-
<form action="/api/post/delete" method="post">
59
<input
60
type="number"
61
name="id"
···
7
<h1>edit post</h1>
8
9
<div class="post post-full">
10
+
<form action="/api/post/edit" method="post" beep-redirect="/post/@post.id">
11
<input
12
type="number"
13
name="id"
···
47
>@post.body</textarea>
48
<br>
49
50
+
@if app.config.post.allow_nsfw
51
+
<div>
52
+
<label for="nsfw">is nsfw:</label>
53
+
<input
54
+
type="checkbox"
55
+
name="nsfw"
56
+
id="nsfw"
57
+
@if post.nsfw
58
+
checked aria-checked
59
+
@end
60
+
/>
61
+
</div>
62
+
<br>
63
+
@else
64
+
<input type="checkbox" name="nsfw" id="nsfw" hidden aria-hidden />
65
+
@end
66
+
67
<input type="submit" value="save">
68
</form>
69
</div>
···
72
73
<div>
74
<h2>danger zone:</h2>
75
+
<form action="/api/post/delete" method="post" beep-redirect="/">
76
<input
77
type="number"
78
name="id"
+10
-3
src/templates/inbox.html
+10
-3
src/templates/inbox.html
···
9
@if notifications.len == 0
10
<p>your inbox is empty!</p>
11
@else
12
-
<a href="/api/user/notification/clear_all">clear all</a>
13
<hr>
14
@for notification in notifications.reverse()
15
<div class="notification">
16
-
<p><strong>@notification.summary</strong></p>
17
<pre id="notif-@{notification.id}">@notification.body</pre>
18
-
<a href="/api/user/notification/clear?id=@{notification.id}">clear</a>
19
<script>
20
render_body('notif-@{notification.id}')
21
</script>
···
9
@if notifications.len == 0
10
<p>your inbox is empty!</p>
11
@else
12
+
<form action="/api/user/notification/clear_all" method="post" beep-redirect="/inbox">
13
+
<button>clear all</button>
14
+
</form>
15
<hr>
16
@for notification in notifications.reverse()
17
<div class="notification">
18
+
<div style="display: flex; flex-direction: row; align-items: center; gap: 12px;">
19
+
<p><strong>@notification.summary</strong></p>
20
+
<form action="/api/user/notification/clear" method="post" beep-redirect="/inbox" class="form-inline" style="display: inline;">
21
+
<input type="number" value="@{notification.id}" name="id" required aria-required hidden aria-hidden readonly aria-readonly />
22
+
<button style="display: inline;">clear</button>
23
+
</form>
24
+
</div>
25
<pre id="notif-@{notification.id}">@notification.body</pre>
26
<script>
27
render_body('notif-@{notification.id}')
28
</script>
+2
-2
src/templates/login.html
+2
-2
src/templates/login.html
···
11
<p>you are already logged in as @{user.get_name()}!</p>
12
<a href="/api/user/logout">log out</a>
13
@else
14
+
<form action="/api/user/login" method="post" beep-redirect="/me">
15
<label for="username">username:</label>
16
<input
17
type="text"
···
39
@end
40
</div>
41
42
+
@include 'partial/footer.html'
+4
-6
src/templates/partial/header.html
+4
-6
src/templates/partial/header.html
+1
-3
src/templates/post.html
+1
-3
src/templates/post.html
···
3
<script src="/static/js/post.js"></script>
4
<script src="/static/js/render_body.js"></script>
5
6
-
<br>
7
-
8
<div class="post post-full">
9
<h2>
10
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>
···
99
<input type="submit" value="pin">
100
</form>
101
102
-
<form action="/api/post/delete" method="post">
103
<input
104
type="number"
105
name="id"
···
3
<script src="/static/js/post.js"></script>
4
<script src="/static/js/render_body.js"></script>
5
6
<div class="post post-full">
7
<h2>
8
<a href="/user/@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).username}"><strong>@{(app.get_user_by_id(post.author_id) or { app.get_unknown_user() }).get_name()}</strong></a>
···
97
<input type="submit" value="pin">
98
</form>
99
100
+
<form action="/api/post/delete" method="post" beep-redirect="/">
101
<input
102
type="number"
103
name="id"
+4
-30
src/templates/register.html
+4
-30
src/templates/register.html
···
1
@include 'partial/header.html'
2
3
<h1>register</h1>
4
5
<div>
···
11
<p>you are already logged in as @{user.get_name()}!</p>
12
<a href="/api/user/logout">log out</a>
13
@else
14
-
<form action="/api/user/register" method="post">
15
<label for="username">username:</label>
16
<input
17
type="text"
···
63
</div>
64
65
<script>
66
-
const password = document.getElementById('password');
67
-
const confirm_password = document.getElementById('confirm-password');
68
-
const matches = document.getElementById('passwords-match');
69
-
70
-
const a = () => {
71
-
matches.innerText = password.value==confirm_password.value ? "yes" : "no";
72
-
};
73
-
password.addEventListener('input', a);
74
-
confirm_password.addEventListener('input', a);
75
-
76
-
const view_password = document.getElementById('view-password');
77
-
const view_confirm_password = document.getElementById('view-confirm-password');
78
-
79
-
const b = (elm, btn) => {
80
-
return event => {
81
-
if (elm.getAttribute('type') == 'password')
82
-
{
83
-
elm.setAttribute('type', 'text');
84
-
btn.innerText = 'hide';
85
-
}
86
-
else
87
-
{
88
-
elm.setAttribute('type', 'password')
89
-
btn.innerText = 'show';
90
-
}
91
-
};
92
-
};
93
-
view_password.addEventListener('click', b(password, view_password));
94
-
view_confirm_password.addEventListener('click', b(confirm_password, view_confirm_password));
95
</script>
96
97
@include 'partial/footer.html'
···
1
@include 'partial/header.html'
2
3
+
<script src="/static/js/password.js"></script>
4
+
5
<h1>register</h1>
6
7
<div>
···
13
<p>you are already logged in as @{user.get_name()}!</p>
14
<a href="/api/user/logout">log out</a>
15
@else
16
+
<form action="/api/user/register" method="post" beep-redirect="/me">
17
<label for="username">username:</label>
18
<input
19
type="text"
···
65
</div>
66
67
<script>
68
+
add_password_checkers('password', 'confirm-password', 'passwords-match');
69
</script>
70
71
@include 'partial/footer.html'
+22
-3
src/templates/settings.html
+22
-3
src/templates/settings.html
···
2
3
@if ctx.is_logged_in()
4
<script src="/static/js/text_area_counter.js"></script>
5
6
<h1>user settings:</h1>
7
···
127
128
<details>
129
<summary>change password (click to reveal)</summary>
130
-
<form action="/api/user/set_password" method="post">
131
<p>changing your password will log you out of all devices, so you will need to log in again after changing it.</p>
132
<label for="current_password">current password:</label>
133
<input
···
141
autocomplete="off" aria-autocomplete="off"
142
>
143
<br>
144
-
<label for="new_password">new password:</label>
145
<input
146
type="password"
147
name="new_password"
···
152
required aria-required
153
autocomplete="off" aria-autocomplete="off"
154
>
155
<input type="submit" value="save">
156
</form>
157
</details>
···
160
161
<details>
162
<summary>account deletion (click to reveal)</summary>
163
-
<form action="/api/user/delete" autocomplete="off">
164
<input
165
type="number"
166
name="id"
···
194
</form>
195
</details>
196
</details>
197
198
@else
199
<p>uh oh, you need to be logged in to view this page!</p>
···
2
3
@if ctx.is_logged_in()
4
<script src="/static/js/text_area_counter.js"></script>
5
+
<script src="/static/js/password.js"></script>
6
7
<h1>user settings:</h1>
8
···
128
129
<details>
130
<summary>change password (click to reveal)</summary>
131
+
<form action="/api/user/set_password" method="post" beep-redirect="/login">
132
<p>changing your password will log you out of all devices, so you will need to log in again after changing it.</p>
133
<label for="current_password">current password:</label>
134
<input
···
142
autocomplete="off" aria-autocomplete="off"
143
>
144
<br>
145
+
<label for="new_password">new password: <input type="button" id="view-new_password" style="display: inline;" value="view"></input></label>
146
<input
147
type="password"
148
name="new_password"
···
153
required aria-required
154
autocomplete="off" aria-autocomplete="off"
155
>
156
+
<label for="confirm_password">confirm password: <input type="button" id="view-confirm_password" style="display: inline;" value="view"></input></label>
157
+
<input
158
+
type="password"
159
+
name="confirm_password"
160
+
id="confirm_password"
161
+
pattern="@app.config.user.password_pattern"
162
+
minlength="@app.config.user.password_min_len"
163
+
maxlength="@app.config.user.password_max_len"
164
+
required aria-required
165
+
autocomplete="off" aria-autocomplete="off"
166
+
>
167
+
<br>
168
+
<p>passwords match: <span id="passwords-match">yes</span></p>
169
+
<br>
170
<input type="submit" value="save">
171
</form>
172
</details>
···
175
176
<details>
177
<summary>account deletion (click to reveal)</summary>
178
+
<form action="/api/user/delete" autocomplete="off" beep-redirect="/">
179
<input
180
type="number"
181
name="id"
···
209
</form>
210
</details>
211
</details>
212
+
213
+
<script>
214
+
add_password_checkers('new_password', 'confirm_password', 'passwords-match');
215
+
</script>
216
217
@else
218
<p>uh oh, you need to be logged in to view this page!</p>
+158
-195
src/webapp/api.v
+158
-195
src/webapp/api.v
···
10
// search_hard_limit is the maximum limit for a search query, used to prevent
11
// people from requesting searches with huge limits and straining the SQL server
12
pub const search_hard_limit = 50
13
14
////// user //////
15
···
29
'remoteip': ctx.ip()
30
'response': token
31
}) or {
32
-
ctx.error('failed to post hcaptcha response: ${err}')
33
-
return ctx.redirect('/register')
34
}
35
data := json.decode(HcaptchaResponse, response.body) or {
36
-
ctx.error('failed to decode hcaptcha response: ${err}')
37
-
return ctx.redirect('/register')
38
}
39
if !data.success {
40
-
ctx.error('failed to verify hcaptcha: ${data}')
41
-
return ctx.redirect('/register')
42
}
43
}
44
45
if app.config.instance.invite_only && ctx.form['invite-code'] != app.config.instance.invite_code {
46
-
ctx.error('invalid invite code')
47
-
return ctx.redirect('/register')
48
}
49
50
if app.get_user_by_name(username) != none {
51
-
ctx.error('username taken')
52
-
return ctx.redirect('/register')
53
}
54
55
// validate username
56
if !app.validators.username.validate(username) {
57
-
ctx.error('invalid username')
58
-
return ctx.redirect('/register')
59
}
60
61
// validate password
62
if !app.validators.password.validate(password) {
63
-
ctx.error('invalid password')
64
-
return ctx.redirect('/register')
65
}
66
67
if password != ctx.form['confirm-password'] {
68
-
ctx.error('passwords do not match')
69
-
return ctx.redirect('/register')
70
}
71
72
salt := auth.generate_salt()
···
84
app.send_notification_to(x.id, app.config.welcome.summary.replace('%s', x.get_name()),
85
app.config.welcome.body.replace('%s', x.get_name()))
86
token := app.auth.add_token(x.id) or {
87
-
eprintln(err)
88
-
ctx.error('api_user_register: could not create token for user with id ${x.id}')
89
-
return ctx.redirect('/')
90
}
91
ctx.set_cookie(
92
name: 'token'
···
97
)
98
} else {
99
eprintln('api_user_register: could not log into newly-created user: ${user}')
100
-
ctx.error('could not log into newly-created user.')
101
}
102
103
-
return ctx.redirect('/')
104
}
105
106
@['/api/user/set_username'; post]
107
fn (mut app App) api_user_set_username(mut ctx Context, new_username string) veb.Result {
108
user := app.whoami(mut ctx) or {
109
-
ctx.error('you are not logged in!')
110
-
return ctx.redirect('/login')
111
}
112
113
if app.get_user_by_name(new_username) != none {
114
-
ctx.error('username taken')
115
-
return ctx.redirect('/settings')
116
}
117
118
// validate username
119
if !app.validators.username.validate(new_username) {
120
-
ctx.error('invalid username')
121
-
return ctx.redirect('/settings')
122
}
123
124
if !app.set_username(user.id, new_username) {
125
-
ctx.error('failed to update username')
126
}
127
128
-
return ctx.redirect('/settings')
129
}
130
131
@['/api/user/set_password'; post]
132
fn (mut app App) api_user_set_password(mut ctx Context, current_password string, new_password string) veb.Result {
133
user := app.whoami(mut ctx) or {
134
-
ctx.error('you are not logged in!')
135
-
return ctx.redirect('/login')
136
}
137
138
if !auth.compare_password_with_hash(current_password, user.password_salt, user.password) {
139
-
ctx.error('current_password is incorrect')
140
-
return ctx.redirect('/settings')
141
}
142
143
// validate password
144
if !app.validators.password.validate(new_password) {
145
-
ctx.error('invalid password')
146
-
return ctx.redirect('/settings')
147
}
148
149
hashed_new_password := auth.hash_password_with_salt(new_password, user.password_salt)
150
if !app.set_password(user.id, hashed_new_password) {
151
-
ctx.error('failed to update password')
152
-
return ctx.redirect('/settings')
153
}
154
155
// invalidate tokens and log out
156
app.auth.delete_tokens_for_user(user.id) or {
157
eprintln('failed to yeet tokens during password deletion for ${user.id} (${err})')
158
-
return ctx.redirect('/settings')
159
}
160
ctx.set_cookie(
161
name: 'token'
···
165
path: '/'
166
)
167
168
-
return ctx.redirect('/login')
169
}
170
171
@['/api/user/login'; post]
172
fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result {
173
user := app.get_user_by_name(username) or {
174
-
ctx.error('invalid credentials')
175
-
return ctx.redirect('/login')
176
}
177
178
if !auth.compare_password_with_hash(password, user.password_salt, user.password) {
179
-
ctx.error('invalid credentials')
180
-
return ctx.redirect('/login')
181
}
182
183
token := app.auth.add_token(user.id) or {
184
eprintln('failed to add token on log in: ${err}')
185
-
ctx.error('could not create token for user with id ${user.id}')
186
-
return ctx.redirect('/login')
187
}
188
189
ctx.set_cookie(
···
194
path: '/'
195
)
196
197
-
return ctx.redirect('/')
198
}
199
200
-
@['/api/user/logout']
201
fn (mut app App) api_user_logout(mut ctx Context) veb.Result {
202
if token := ctx.get_cookie('token') {
203
if user := app.get_user_by_token(token) {
···
207
// }
208
app.auth.delete_tokens_for_value(token) or {
209
eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()} (${err})')
210
-
return ctx.redirect('/login')
211
}
212
} else {
213
eprintln('failed to get user for token for logout')
···
224
path: '/'
225
)
226
227
-
return ctx.redirect('/login')
228
}
229
230
-
@['/api/user/full_logout']
231
fn (mut app App) api_user_full_logout(mut ctx Context) veb.Result {
232
if token := ctx.get_cookie('token') {
233
if user := app.get_user_by_token(token) {
234
app.auth.delete_tokens_for_user(user.id) or {
235
eprintln('failed to yeet tokens for ${user.id}')
236
-
return ctx.redirect('/login')
237
}
238
} else {
239
eprintln('failed to get user for token for full_logout')
···
250
path: '/'
251
)
252
253
-
return ctx.redirect('/login')
254
}
255
256
@['/api/user/set_nickname'; post]
257
fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result {
258
user := app.whoami(mut ctx) or {
259
-
ctx.error('you are not logged in!')
260
-
return ctx.redirect('/login')
261
}
262
263
mut clean_nickname := ?string(nickname.trim_space())
···
267
268
// validate
269
if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) {
270
-
ctx.error('invalid nickname')
271
-
return ctx.redirect('/settings')
272
}
273
274
if !app.set_nickname(user.id, clean_nickname) {
275
eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})')
276
-
return ctx.redirect('/settings')
277
}
278
279
-
return ctx.redirect('/settings')
280
}
281
282
@['/api/user/set_muted'; post]
283
fn (mut app App) api_user_set_muted(mut ctx Context, id int, muted bool) veb.Result {
284
user := app.whoami(mut ctx) or {
285
-
ctx.error('you are not logged in!')
286
-
return ctx.redirect('/login')
287
}
288
289
to_mute := app.get_user_by_id(id) or {
290
-
ctx.error('no such user')
291
-
return ctx.redirect('/')
292
}
293
294
if user.admin {
295
if !app.set_muted(to_mute.id, muted) {
296
-
ctx.error('failed to change mute status')
297
-
return ctx.redirect('/user/${to_mute.username}')
298
}
299
-
return ctx.redirect('/user/${to_mute.username}')
300
} else {
301
-
ctx.error('insufficient permissions!')
302
eprintln('insufficient perms to update mute status for ${to_mute} (${to_mute.muted} -> ${muted})')
303
-
return ctx.redirect('/user/${to_mute.username}')
304
}
305
}
306
307
@['/api/user/set_automated'; post]
308
fn (mut app App) api_user_set_automated(mut ctx Context, is_automated bool) veb.Result {
309
user := app.whoami(mut ctx) or {
310
-
ctx.error('you are not logged in!')
311
-
return ctx.redirect('/login')
312
}
313
314
if !app.set_automated(user.id, is_automated) {
315
-
ctx.error('failed to set automated status.')
316
}
317
318
-
return ctx.redirect('/settings')
319
}
320
321
@['/api/user/set_theme'; post]
322
fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result {
323
if !app.config.instance.allow_changing_theme {
324
-
ctx.error('this instance disallows changing themes :(')
325
-
return ctx.redirect('/settings')
326
}
327
328
user := app.whoami(mut ctx) or {
329
-
ctx.error('you are not logged in!')
330
-
return ctx.redirect('/login')
331
}
332
333
mut theme := ?string(none)
···
338
}
339
340
if !app.set_theme(user.id, theme) {
341
-
ctx.error('failed to change theme')
342
-
return ctx.redirect('/settings')
343
}
344
345
-
return ctx.redirect('/settings')
346
}
347
348
@['/api/user/set_css'; post]
349
fn (mut app App) api_user_set_css(mut ctx Context, css string) veb.Result {
350
if !app.config.instance.allow_changing_theme {
351
-
ctx.error('this instance disallows changing themes :(')
352
-
return ctx.redirect('/settings')
353
}
354
355
user := app.whoami(mut ctx) or {
356
-
ctx.error('you are not logged in!')
357
-
return ctx.redirect('/login')
358
}
359
360
c := if css.trim_space() == '' { app.config.instance.default_css } else { css.trim_space() }
361
362
if !app.set_css(user.id, c) {
363
-
ctx.error('failed to change css')
364
-
return ctx.redirect('/settings')
365
}
366
367
-
return ctx.redirect('/settings')
368
}
369
370
@['/api/user/set_pronouns'; post]
371
fn (mut app App) api_user_set_pronouns(mut ctx Context, pronouns string) veb.Result {
372
user := app.whoami(mut ctx) or {
373
-
ctx.error('you are not logged in!')
374
-
return ctx.redirect('/login')
375
}
376
377
clean_pronouns := pronouns.trim_space()
378
if !app.validators.pronouns.validate(clean_pronouns) {
379
-
ctx.error('invalid pronouns')
380
-
return ctx.redirect('/settings')
381
}
382
383
if !app.set_pronouns(user.id, clean_pronouns) {
384
-
ctx.error('failed to change pronouns')
385
-
return ctx.redirect('/settings')
386
}
387
388
-
return ctx.redirect('/settings')
389
}
390
391
@['/api/user/set_bio'; post]
392
fn (mut app App) api_user_set_bio(mut ctx Context, bio string) veb.Result {
393
user := app.whoami(mut ctx) or {
394
-
ctx.error('you are not logged in!')
395
-
return ctx.redirect('/login')
396
}
397
398
clean_bio := bio.trim_space()
399
if !app.validators.user_bio.validate(clean_bio) {
400
-
ctx.error('invalid bio')
401
-
return ctx.redirect('/settings')
402
}
403
404
if !app.set_bio(user.id, clean_bio) {
405
eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})')
406
-
return ctx.redirect('/settings')
407
}
408
409
-
return ctx.redirect('/settings')
410
}
411
412
-
@['/api/user/get_name']
413
fn (mut app App) api_user_get_name(mut ctx Context, username string) veb.Result {
414
user := app.get_user_by_name(username) or { return ctx.server_error('no such user') }
415
return ctx.text(user.get_name())
416
}
417
418
-
@['/api/user/delete']
419
fn (mut app App) api_user_delete(mut ctx Context, id int) veb.Result {
420
user := app.whoami(mut ctx) or {
421
-
ctx.error('you are not logged in!')
422
-
return ctx.redirect('/login')
423
}
424
425
-
println('attempting to delete ${id} as ${user.id}')
426
427
-
if user.admin || user.id == id {
428
// yeet
429
if !app.delete_user(user.id) {
430
-
ctx.error('failed to delete user: ${id}')
431
-
return ctx.redirect('/')
432
}
433
434
app.auth.delete_tokens_for_user(id) or {
···
445
)
446
}
447
println('deleted user ${id}')
448
} else {
449
-
ctx.error('be nice. deleting other users is off-limits.')
450
}
451
-
452
-
return ctx.redirect('/')
453
}
454
455
@['/api/user/search'; get]
456
fn (mut app App) api_user_search(mut ctx Context, query string, limit int, offset int) veb.Result {
457
-
_ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
458
if limit >= search_hard_limit {
459
-
return ctx.text('limit exceeds hard limit (${search_hard_limit})')
460
}
461
users := app.search_for_users(query, limit, offset)
462
return ctx.json[[]User](users)
···
464
465
@['/api/user/whoami'; get]
466
fn (mut app App) api_user_whoami(mut ctx Context) veb.Result {
467
-
user := app.whoami(mut ctx) or { return ctx.text('not logged in') }
468
return ctx.text(user.username)
469
}
470
471
/// user/notification ///
472
473
-
@['/api/user/notification/clear']
474
fn (mut app App) api_user_notification_clear(mut ctx Context, id int) veb.Result {
475
user := app.whoami(mut ctx) or {
476
-
ctx.error('you are not logged in!')
477
-
return ctx.redirect('/login')
478
}
479
480
if notification := app.get_notification_by_id(id) {
481
if notification.user_id != user.id {
482
-
ctx.error('no such notification for user')
483
-
return ctx.redirect('/inbox')
484
-
} else {
485
-
if !app.delete_notification(id) {
486
-
ctx.error('failed to delete notification')
487
-
return ctx.redirect('/inbox')
488
-
}
489
}
490
} else {
491
-
ctx.error('no such notification for user')
492
}
493
494
-
return ctx.redirect('/inbox')
495
}
496
497
-
@['/api/user/notification/clear_all']
498
fn (mut app App) api_user_notification_clear_all(mut ctx Context) veb.Result {
499
user := app.whoami(mut ctx) or {
500
-
ctx.error('you are not logged in!')
501
-
return ctx.redirect('/login')
502
}
503
if !app.delete_notifications_for_user(user.id) {
504
-
ctx.error('failed to delete notifications')
505
-
return ctx.redirect('/inbox')
506
}
507
-
return ctx.redirect('/inbox')
508
}
509
510
////// post //////
···
512
@['/api/post/new_post'; post]
513
fn (mut app App) api_post_new_post(mut ctx Context, replying_to int, title string, body string) veb.Result {
514
user := app.whoami(mut ctx) or {
515
-
ctx.error('not logged in!')
516
-
return ctx.redirect('/login')
517
}
518
519
if user.muted {
520
-
ctx.error('you are muted!')
521
-
return ctx.redirect('/post/new')
522
}
523
524
// validate title
525
if !app.validators.post_title.validate(title) {
526
-
ctx.error('invalid title')
527
-
return ctx.redirect('/post/new')
528
}
529
530
// validate body
531
if !app.validators.post_body.validate(body) {
532
-
ctx.error('invalid body')
533
-
return ctx.redirect('/post/new')
534
}
535
536
nsfw := 'nsfw' in ctx.form
537
if nsfw && !app.config.post.allow_nsfw {
538
-
ctx.error('nsfw posts are not allowed on this instance')
539
-
return ctx.redirect('/post/new')
540
}
541
542
mut post := Post{
···
549
if replying_to != 0 {
550
// check if replying post exists
551
app.get_post_by_id(replying_to) or {
552
-
ctx.error('the post you are trying to reply to does not exist')
553
-
return ctx.redirect('/post/new')
554
}
555
post.replying_to = replying_to
556
}
557
558
if !app.add_post(post) {
559
-
ctx.error('failed to post!')
560
println('failed to post: ${post} from user ${user.id}')
561
-
return ctx.redirect('/post/new')
562
}
563
564
// find the post's id to process mentions with
565
if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) {
566
app.process_post_mentions(x)
567
-
return ctx.redirect('/post/${x.id}')
568
} else {
569
-
ctx.error('failed to get_post_by_timestamp_and_author for ${post}')
570
-
return ctx.redirect('/me')
571
}
572
}
573
574
@['/api/post/delete'; post]
575
fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result {
576
user := app.whoami(mut ctx) or {
577
-
ctx.error('not logged in!')
578
-
return ctx.redirect('/login')
579
}
580
581
post := app.get_post_by_id(id) or {
582
-
ctx.error('post does not exist')
583
-
return ctx.redirect('/')
584
}
585
586
if user.admin || user.id == post.author_id {
587
if !app.delete_post(post.id) {
588
-
ctx.error('failed to delete post')
589
-
eprintln('failed to delete post: ${id}')
590
-
return ctx.redirect('/')
591
}
592
println('deleted post: ${id}')
593
-
return ctx.redirect('/')
594
} else {
595
-
ctx.error('insufficient permissions!')
596
eprintln('insufficient perms to delete post: ${id} (${user.id})')
597
-
return ctx.redirect('/')
598
}
599
}
600
601
-
@['/api/post/like']
602
fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result {
603
-
user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
604
605
-
post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
606
607
if app.does_user_like_post(user.id, post.id) {
608
if !app.unlike_post(post.id, user.id) {
···
631
}
632
}
633
634
-
@['/api/post/dislike']
635
fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result {
636
-
user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
637
638
-
post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
639
640
if app.does_user_dislike_post(user.id, post.id) {
641
if !app.unlike_post(post.id, user.id) {
···
664
}
665
}
666
667
-
@['/api/post/save']
668
fn (mut app App) api_post_save(mut ctx Context, id int) veb.Result {
669
-
user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
670
671
if app.get_post_by_id(id) != none {
672
if app.toggle_save_post(user.id, id) {
···
679
}
680
}
681
682
-
@['/api/post/save_for_later']
683
fn (mut app App) api_post_save_for_later(mut ctx Context, id int) veb.Result {
684
-
user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
685
686
if app.get_post_by_id(id) != none {
687
if app.toggle_save_for_later_post(user.id, id) {
···
694
}
695
}
696
697
-
@['/api/post/get_title']
698
fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result {
699
if !app.config.instance.public_data {
700
-
_ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
701
}
702
post := app.get_post_by_id(id) or { return ctx.server_error('no such post') }
703
return ctx.text(post.title)
···
706
@['/api/post/edit'; post]
707
fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result {
708
user := app.whoami(mut ctx) or {
709
-
ctx.error('not logged in!')
710
-
return ctx.redirect('/login')
711
}
712
post := app.get_post_by_id(id) or {
713
-
ctx.error('no such post')
714
-
return ctx.redirect('/')
715
}
716
if post.author_id != user.id {
717
-
ctx.error('insufficient permissions')
718
-
return ctx.redirect('/')
719
}
720
721
-
if !app.update_post(id, title, body) {
722
eprintln('failed to update post')
723
-
ctx.error('failed to update post')
724
-
return ctx.redirect('/')
725
}
726
727
-
return ctx.redirect('/post/${id}')
728
}
729
730
@['/api/post/pin'; post]
731
fn (mut app App) api_post_pin(mut ctx Context, id int) veb.Result {
732
user := app.whoami(mut ctx) or {
733
-
ctx.error('not logged in!')
734
-
return ctx.redirect('/login')
735
}
736
737
if user.admin {
738
if !app.pin_post(id) {
739
eprintln('failed to pin post: ${id}')
740
-
ctx.error('failed to pin post')
741
-
return ctx.redirect('/post/${id}')
742
}
743
-
return ctx.redirect('/post/${id}')
744
} else {
745
-
ctx.error('insufficient permissions!')
746
eprintln('insufficient perms to pin post: ${id} (${user.id})')
747
-
return ctx.redirect('/')
748
}
749
}
750
751
@['/api/post/get/<id>'; get]
752
fn (mut app App) api_post_get_post(mut ctx Context, id int) veb.Result {
753
if !app.config.instance.public_data {
754
-
_ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
755
}
756
post := app.get_post_by_id(id) or { return ctx.text('no such post') }
757
return ctx.json[Post](post)
···
759
760
@['/api/post/search'; get]
761
fn (mut app App) api_post_search(mut ctx Context, query string, limit int, offset int) veb.Result {
762
-
_ := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
763
if limit >= search_hard_limit {
764
return ctx.text('limit exceeds hard limit (${search_hard_limit})')
765
}
···
772
@['/api/site/set_motd'; post]
773
fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result {
774
user := app.whoami(mut ctx) or {
775
-
ctx.error('not logged in!')
776
-
return ctx.redirect('/login')
777
}
778
779
if user.admin {
780
if !app.set_motd(motd) {
781
-
ctx.error('failed to set motd')
782
eprintln('failed to set motd: ${motd}')
783
-
return ctx.redirect('/')
784
}
785
println('set motd to: ${motd}')
786
-
return ctx.redirect('/')
787
} else {
788
-
ctx.error('insufficient permissions!')
789
eprintln('insufficient perms to set motd to: ${motd} (${user.id})')
790
-
return ctx.redirect('/')
791
}
792
}
···
10
// search_hard_limit is the maximum limit for a search query, used to prevent
11
// people from requesting searches with huge limits and straining the SQL server
12
pub const search_hard_limit = 50
13
+
pub const not_logged_in_msg = 'you are not logged in!'
14
15
////// user //////
16
···
30
'remoteip': ctx.ip()
31
'response': token
32
}) or {
33
+
return ctx.server_error('failed to post hcaptcha response: ${err}')
34
}
35
data := json.decode(HcaptchaResponse, response.body) or {
36
+
return ctx.server_error('failed to decode hcaptcha response: ${err}')
37
}
38
if !data.success {
39
+
return ctx.server_error('failed to verify hcaptcha: ${data}')
40
}
41
}
42
43
if app.config.instance.invite_only && ctx.form['invite-code'] != app.config.instance.invite_code {
44
+
return ctx.server_error('invalid invite code')
45
}
46
47
if app.get_user_by_name(username) != none {
48
+
return ctx.server_error('username taken')
49
}
50
51
// validate username
52
if !app.validators.username.validate(username) {
53
+
return ctx.server_error('invalid username')
54
}
55
56
// validate password
57
if !app.validators.password.validate(password) {
58
+
return ctx.server_error('invalid password')
59
}
60
61
if password != ctx.form['confirm-password'] {
62
+
return ctx.server_error('passwords do not match')
63
}
64
65
salt := auth.generate_salt()
···
77
app.send_notification_to(x.id, app.config.welcome.summary.replace('%s', x.get_name()),
78
app.config.welcome.body.replace('%s', x.get_name()))
79
token := app.auth.add_token(x.id) or {
80
+
eprintln('api_user_register: could not create token for user with id ${x.id}: ${err}')
81
+
return ctx.server_error('could not create token for user')
82
}
83
ctx.set_cookie(
84
name: 'token'
···
89
)
90
} else {
91
eprintln('api_user_register: could not log into newly-created user: ${user}')
92
+
return ctx.server_error('could not log into newly-created user.')
93
}
94
95
+
return ctx.ok('user registered')
96
}
97
98
@['/api/user/set_username'; post]
99
fn (mut app App) api_user_set_username(mut ctx Context, new_username string) veb.Result {
100
user := app.whoami(mut ctx) or {
101
+
return ctx.unauthorized(not_logged_in_msg)
102
}
103
104
if app.get_user_by_name(new_username) != none {
105
+
return ctx.server_error('username taken')
106
}
107
108
// validate username
109
if !app.validators.username.validate(new_username) {
110
+
return ctx.server_error('invalid username')
111
}
112
113
if !app.set_username(user.id, new_username) {
114
+
return ctx.server_error('failed to update username')
115
}
116
117
+
return ctx.ok('username updated')
118
}
119
120
@['/api/user/set_password'; post]
121
fn (mut app App) api_user_set_password(mut ctx Context, current_password string, new_password string) veb.Result {
122
user := app.whoami(mut ctx) or {
123
+
return ctx.unauthorized(not_logged_in_msg)
124
}
125
126
if !auth.compare_password_with_hash(current_password, user.password_salt, user.password) {
127
+
return ctx.server_error('current_password is incorrect')
128
}
129
130
// validate password
131
if !app.validators.password.validate(new_password) {
132
+
return ctx.server_error('invalid password')
133
+
}
134
+
135
+
if new_password != ctx.form['confirm_password'] {
136
+
return ctx.server_error('passwords do not match')
137
}
138
139
hashed_new_password := auth.hash_password_with_salt(new_password, user.password_salt)
140
if !app.set_password(user.id, hashed_new_password) {
141
+
return ctx.server_error('failed to update password')
142
}
143
144
// invalidate tokens and log out
145
app.auth.delete_tokens_for_user(user.id) or {
146
eprintln('failed to yeet tokens during password deletion for ${user.id} (${err})')
147
+
return ctx.server_error('failed to delete tokens during password deletion')
148
}
149
ctx.set_cookie(
150
name: 'token'
···
154
path: '/'
155
)
156
157
+
return ctx.ok('password updated')
158
}
159
160
@['/api/user/login'; post]
161
fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result {
162
user := app.get_user_by_name(username) or {
163
+
return ctx.server_error('invalid credentials')
164
}
165
166
if !auth.compare_password_with_hash(password, user.password_salt, user.password) {
167
+
return ctx.server_error('invalid credentials')
168
}
169
170
token := app.auth.add_token(user.id) or {
171
eprintln('failed to add token on log in: ${err}')
172
+
return ctx.server_error('could not create token for user with id ${user.id}')
173
}
174
175
ctx.set_cookie(
···
180
path: '/'
181
)
182
183
+
return ctx.ok('logged in')
184
}
185
186
+
@['/api/user/logout'; post]
187
fn (mut app App) api_user_logout(mut ctx Context) veb.Result {
188
if token := ctx.get_cookie('token') {
189
if user := app.get_user_by_token(token) {
···
193
// }
194
app.auth.delete_tokens_for_value(token) or {
195
eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()} (${err})')
196
}
197
} else {
198
eprintln('failed to get user for token for logout')
···
209
path: '/'
210
)
211
212
+
return ctx.ok('logged out')
213
}
214
215
+
@['/api/user/full_logout'; post]
216
fn (mut app App) api_user_full_logout(mut ctx Context) veb.Result {
217
if token := ctx.get_cookie('token') {
218
if user := app.get_user_by_token(token) {
219
app.auth.delete_tokens_for_user(user.id) or {
220
eprintln('failed to yeet tokens for ${user.id}')
221
}
222
} else {
223
eprintln('failed to get user for token for full_logout')
···
234
path: '/'
235
)
236
237
+
return ctx.ok('logged out')
238
}
239
240
@['/api/user/set_nickname'; post]
241
fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result {
242
user := app.whoami(mut ctx) or {
243
+
return ctx.unauthorized(not_logged_in_msg)
244
}
245
246
mut clean_nickname := ?string(nickname.trim_space())
···
250
251
// validate
252
if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) {
253
+
return ctx.server_error('invalid nickname')
254
}
255
256
if !app.set_nickname(user.id, clean_nickname) {
257
eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})')
258
+
return ctx.server_error('failed to update nickname')
259
}
260
261
+
return ctx.ok('updated nickname')
262
}
263
264
@['/api/user/set_muted'; post]
265
fn (mut app App) api_user_set_muted(mut ctx Context, id int, muted bool) veb.Result {
266
user := app.whoami(mut ctx) or {
267
+
return ctx.unauthorized(not_logged_in_msg)
268
}
269
270
to_mute := app.get_user_by_id(id) or {
271
+
return ctx.server_error('no such user')
272
}
273
274
if user.admin {
275
if !app.set_muted(to_mute.id, muted) {
276
+
return ctx.server_error('failed to change mute status')
277
}
278
+
return ctx.ok('muted user')
279
} else {
280
eprintln('insufficient perms to update mute status for ${to_mute} (${to_mute.muted} -> ${muted})')
281
+
return ctx.unauthorized('insufficient permissions')
282
}
283
}
284
285
@['/api/user/set_automated'; post]
286
fn (mut app App) api_user_set_automated(mut ctx Context, is_automated bool) veb.Result {
287
user := app.whoami(mut ctx) or {
288
+
return ctx.unauthorized(not_logged_in_msg)
289
}
290
291
if !app.set_automated(user.id, is_automated) {
292
+
return ctx.server_error('failed to set automated status.')
293
}
294
295
+
if is_automated {
296
+
return ctx.ok('you\'re now a bot! :D')
297
+
} else {
298
+
return ctx.ok('you\'re no longer a bot :(')
299
+
}
300
}
301
302
@['/api/user/set_theme'; post]
303
fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result {
304
if !app.config.instance.allow_changing_theme {
305
+
return ctx.server_error('this instance disallows changing themes :(')
306
}
307
308
user := app.whoami(mut ctx) or {
309
+
return ctx.unauthorized(not_logged_in_msg)
310
}
311
312
mut theme := ?string(none)
···
317
}
318
319
if !app.set_theme(user.id, theme) {
320
+
return ctx.server_error('failed to change theme')
321
}
322
323
+
return ctx.ok('theme updated')
324
}
325
326
@['/api/user/set_css'; post]
327
fn (mut app App) api_user_set_css(mut ctx Context, css string) veb.Result {
328
if !app.config.instance.allow_changing_theme {
329
+
return ctx.server_error('this instance disallows changing themes :(')
330
}
331
332
user := app.whoami(mut ctx) or {
333
+
return ctx.unauthorized(not_logged_in_msg)
334
}
335
336
c := if css.trim_space() == '' { app.config.instance.default_css } else { css.trim_space() }
337
338
if !app.set_css(user.id, c) {
339
+
return ctx.server_error('failed to change css')
340
}
341
342
+
return ctx.ok('css updated')
343
}
344
345
@['/api/user/set_pronouns'; post]
346
fn (mut app App) api_user_set_pronouns(mut ctx Context, pronouns string) veb.Result {
347
user := app.whoami(mut ctx) or {
348
+
return ctx.unauthorized(not_logged_in_msg)
349
}
350
351
clean_pronouns := pronouns.trim_space()
352
if !app.validators.pronouns.validate(clean_pronouns) {
353
+
return ctx.server_error('invalid pronouns')
354
}
355
356
if !app.set_pronouns(user.id, clean_pronouns) {
357
+
return ctx.server_error('failed to change pronouns')
358
}
359
360
+
return ctx.ok('pronouns updated')
361
}
362
363
@['/api/user/set_bio'; post]
364
fn (mut app App) api_user_set_bio(mut ctx Context, bio string) veb.Result {
365
user := app.whoami(mut ctx) or {
366
+
return ctx.unauthorized(not_logged_in_msg)
367
}
368
369
clean_bio := bio.trim_space()
370
if !app.validators.user_bio.validate(clean_bio) {
371
+
return ctx.server_error('invalid bio')
372
}
373
374
if !app.set_bio(user.id, clean_bio) {
375
eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})')
376
+
return ctx.server_error('failed to update bio')
377
}
378
379
+
return ctx.ok('bio updated')
380
}
381
382
+
@['/api/user/get_name'; get]
383
fn (mut app App) api_user_get_name(mut ctx Context, username string) veb.Result {
384
+
if !app.config.instance.public_data {
385
+
return ctx.server_error('no such error')
386
+
}
387
user := app.get_user_by_name(username) or { return ctx.server_error('no such user') }
388
return ctx.text(user.get_name())
389
}
390
391
+
@['/api/user/delete'; post]
392
fn (mut app App) api_user_delete(mut ctx Context, id int) veb.Result {
393
user := app.whoami(mut ctx) or {
394
+
return ctx.unauthorized(not_logged_in_msg)
395
}
396
397
+
if user.admin || user.id == id {
398
+
println('attempting to delete ${id} as ${user.id}')
399
400
// yeet
401
if !app.delete_user(user.id) {
402
+
return ctx.server_error('failed to delete user: ${id}')
403
}
404
405
app.auth.delete_tokens_for_user(id) or {
···
416
)
417
}
418
println('deleted user ${id}')
419
+
return ctx.ok('user deleted')
420
} else {
421
+
return ctx.unauthorized('be nice. deleting other users is off-limits.')
422
}
423
}
424
425
@['/api/user/search'; get]
426
fn (mut app App) api_user_search(mut ctx Context, query string, limit int, offset int) veb.Result {
427
+
_ := app.whoami(mut ctx) or { return ctx.unauthorized(not_logged_in_msg) }
428
if limit >= search_hard_limit {
429
+
return ctx.server_error('limit exceeds hard limit (${search_hard_limit})')
430
}
431
users := app.search_for_users(query, limit, offset)
432
return ctx.json[[]User](users)
···
434
435
@['/api/user/whoami'; get]
436
fn (mut app App) api_user_whoami(mut ctx Context) veb.Result {
437
+
user := app.whoami(mut ctx) or {
438
+
return ctx.unauthorized(not_logged_in_msg)
439
+
}
440
return ctx.text(user.username)
441
}
442
443
/// user/notification ///
444
445
+
@['/api/user/notification/clear'; post]
446
fn (mut app App) api_user_notification_clear(mut ctx Context, id int) veb.Result {
447
user := app.whoami(mut ctx) or {
448
+
return ctx.unauthorized(not_logged_in_msg)
449
}
450
451
if notification := app.get_notification_by_id(id) {
452
if notification.user_id != user.id {
453
+
return ctx.server_error('no such notification for user')
454
+
} else if !app.delete_notification(id) {
455
+
return ctx.server_error('failed to delete notification')
456
}
457
} else {
458
+
return ctx.server_error('no such notification for user')
459
}
460
461
+
return ctx.ok('cleared notification')
462
}
463
464
+
@['/api/user/notification/clear_all'; post]
465
fn (mut app App) api_user_notification_clear_all(mut ctx Context) veb.Result {
466
user := app.whoami(mut ctx) or {
467
+
return ctx.unauthorized(not_logged_in_msg)
468
}
469
if !app.delete_notifications_for_user(user.id) {
470
+
return ctx.server_error('failed to delete notifications')
471
}
472
+
return ctx.ok('cleared notifications')
473
}
474
475
////// post //////
···
477
@['/api/post/new_post'; post]
478
fn (mut app App) api_post_new_post(mut ctx Context, replying_to int, title string, body string) veb.Result {
479
user := app.whoami(mut ctx) or {
480
+
return ctx.unauthorized(not_logged_in_msg)
481
}
482
483
if user.muted {
484
+
return ctx.server_error('you are muted!')
485
}
486
487
// validate title
488
if !app.validators.post_title.validate(title) {
489
+
return ctx.server_error('invalid title')
490
}
491
492
// validate body
493
if !app.validators.post_body.validate(body) {
494
+
return ctx.server_error('invalid body')
495
}
496
497
nsfw := 'nsfw' in ctx.form
498
if nsfw && !app.config.post.allow_nsfw {
499
+
return ctx.server_error('nsfw posts are not allowed on this instance')
500
}
501
502
mut post := Post{
···
509
if replying_to != 0 {
510
// check if replying post exists
511
app.get_post_by_id(replying_to) or {
512
+
return ctx.server_error('the post you are trying to reply to does not exist')
513
}
514
post.replying_to = replying_to
515
}
516
517
if !app.add_post(post) {
518
println('failed to post: ${post} from user ${user.id}')
519
+
return ctx.server_error('failed to post')
520
}
521
522
+
//TODO: Can I not just get the ID directly?? This method feels dicey at best.
523
// find the post's id to process mentions with
524
if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) {
525
app.process_post_mentions(x)
526
+
return ctx.ok('posted. id=${x.id}')
527
} else {
528
+
eprintln('api_post_new_post: get_post_by_timestamp_and_author failed for ${post}')
529
+
return ctx.server_error('failed to get post ID, this error should never happen')
530
}
531
}
532
533
@['/api/post/delete'; post]
534
fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result {
535
user := app.whoami(mut ctx) or {
536
+
return ctx.unauthorized(not_logged_in_msg)
537
}
538
539
post := app.get_post_by_id(id) or {
540
+
return ctx.server_error('post does not exist')
541
}
542
543
if user.admin || user.id == post.author_id {
544
if !app.delete_post(post.id) {
545
+
eprintln('api_post_delete: failed to delete post: ${id}')
546
+
return ctx.server_error('failed to delete post')
547
}
548
println('deleted post: ${id}')
549
+
return ctx.ok('post deleted')
550
} else {
551
eprintln('insufficient perms to delete post: ${id} (${user.id})')
552
+
return ctx.unauthorized('insufficient permissions')
553
}
554
}
555
556
+
@['/api/post/like'; post]
557
fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result {
558
+
user := app.whoami(mut ctx) or {
559
+
return ctx.unauthorized(not_logged_in_msg)
560
+
}
561
562
+
post := app.get_post_by_id(id) or {
563
+
return ctx.server_error('post does not exist')
564
+
}
565
566
if app.does_user_like_post(user.id, post.id) {
567
if !app.unlike_post(post.id, user.id) {
···
590
}
591
}
592
593
+
@['/api/post/dislike'; post]
594
fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result {
595
+
user := app.whoami(mut ctx) or {
596
+
return ctx.unauthorized(not_logged_in_msg)
597
+
}
598
599
+
post := app.get_post_by_id(id) or {
600
+
return ctx.server_error('post does not exist')
601
+
}
602
603
if app.does_user_dislike_post(user.id, post.id) {
604
if !app.unlike_post(post.id, user.id) {
···
627
}
628
}
629
630
+
@['/api/post/save'; post]
631
fn (mut app App) api_post_save(mut ctx Context, id int) veb.Result {
632
+
user := app.whoami(mut ctx) or {
633
+
return ctx.unauthorized(not_logged_in_msg)
634
+
}
635
636
if app.get_post_by_id(id) != none {
637
if app.toggle_save_post(user.id, id) {
···
644
}
645
}
646
647
+
@['/api/post/save_for_later'; post]
648
fn (mut app App) api_post_save_for_later(mut ctx Context, id int) veb.Result {
649
+
user := app.whoami(mut ctx) or {
650
+
return ctx.unauthorized(not_logged_in_msg)
651
+
}
652
653
if app.get_post_by_id(id) != none {
654
if app.toggle_save_for_later_post(user.id, id) {
···
661
}
662
}
663
664
+
@['/api/post/get_title'; get]
665
fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result {
666
if !app.config.instance.public_data {
667
+
_ := app.whoami(mut ctx) or { return ctx.unauthorized(not_logged_in_msg) }
668
}
669
post := app.get_post_by_id(id) or { return ctx.server_error('no such post') }
670
return ctx.text(post.title)
···
673
@['/api/post/edit'; post]
674
fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result {
675
user := app.whoami(mut ctx) or {
676
+
return ctx.unauthorized(not_logged_in_msg)
677
}
678
post := app.get_post_by_id(id) or {
679
+
return ctx.server_error('no such post')
680
}
681
if post.author_id != user.id {
682
+
return ctx.unauthorized('insufficient permissions')
683
}
684
685
+
nsfw := if 'nsfw' in ctx.form {
686
+
app.config.post.allow_nsfw
687
+
} else {
688
+
post.nsfw
689
+
}
690
+
691
+
if !app.update_post(id, title, body, nsfw) {
692
eprintln('failed to update post')
693
+
return ctx.server_error('failed to update post')
694
}
695
696
+
return ctx.ok('posted edited')
697
}
698
699
@['/api/post/pin'; post]
700
fn (mut app App) api_post_pin(mut ctx Context, id int) veb.Result {
701
user := app.whoami(mut ctx) or {
702
+
return ctx.unauthorized(not_logged_in_msg)
703
}
704
705
if user.admin {
706
if !app.pin_post(id) {
707
eprintln('failed to pin post: ${id}')
708
+
return ctx.server_error('failed to pin post')
709
}
710
+
return ctx.ok('post pinned')
711
} else {
712
eprintln('insufficient perms to pin post: ${id} (${user.id})')
713
+
return ctx.unauthorized('insufficient permissions')
714
}
715
}
716
717
@['/api/post/get/<id>'; get]
718
fn (mut app App) api_post_get_post(mut ctx Context, id int) veb.Result {
719
if !app.config.instance.public_data {
720
+
_ := app.whoami(mut ctx) or { return ctx.unauthorized(not_logged_in_msg) }
721
}
722
post := app.get_post_by_id(id) or { return ctx.text('no such post') }
723
return ctx.json[Post](post)
···
725
726
@['/api/post/search'; get]
727
fn (mut app App) api_post_search(mut ctx Context, query string, limit int, offset int) veb.Result {
728
+
_ := app.whoami(mut ctx) or { return ctx.unauthorized(not_logged_in_msg) }
729
if limit >= search_hard_limit {
730
return ctx.text('limit exceeds hard limit (${search_hard_limit})')
731
}
···
738
@['/api/site/set_motd'; post]
739
fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result {
740
user := app.whoami(mut ctx) or {
741
+
return ctx.unauthorized(not_logged_in_msg)
742
}
743
744
if user.admin {
745
if !app.set_motd(motd) {
746
eprintln('failed to set motd: ${motd}')
747
+
return ctx.server_error('failed to set motd')
748
}
749
println('set motd to: ${motd}')
750
+
return ctx.ok('motd updated')
751
} else {
752
eprintln('insufficient perms to set motd to: ${motd} (${user.id})')
753
+
return ctx.unauthorized('insufficient permissions')
754
}
755
}