a mini social media app for small communities
1module main
2
3import veb
4import auth
5import entity { Like, LikeCache, Post, Site, User, Notification }
6
7////// user //////
8
9@['/api/user/register'; post]
10fn (mut app App) api_user_register(mut ctx Context, username string, password string) veb.Result {
11 if app.get_user_by_name(username) != none {
12 ctx.error('username taken')
13 return ctx.redirect('/register')
14 }
15
16 // validate username
17 if !app.validators.username.validate(username) {
18 ctx.error('invalid username')
19 return ctx.redirect('/register')
20 }
21
22 // validate password
23 if !app.validators.password.validate(password) {
24 ctx.error('invalid password')
25 return ctx.redirect('/register')
26 }
27
28 salt := auth.generate_salt()
29 mut user := User{
30 username: username
31 password: auth.hash_password_with_salt(password, salt)
32 password_salt: salt
33 }
34
35 if app.config.instance.default_theme != '' {
36 user.theme = app.config.instance.default_theme
37 }
38
39 sql app.db {
40 insert user into User
41 } or {
42 eprintln('failed to insert user ${user}')
43 return ctx.redirect('/')
44 }
45
46 println('reg: ${username}')
47
48 if x := app.get_user_by_name(username) {
49 app.send_notification_to(
50 x.id,
51 app.config.welcome.summary.replace('%s', x.get_name()),
52 app.config.welcome.body.replace('%s', x.get_name())
53 )
54 token := app.auth.add_token(x.id, ctx.ip()) or {
55 eprintln(err)
56 ctx.error('could not create token for user with id ${x.id}')
57 return ctx.redirect('/')
58 }
59 ctx.set_cookie(
60 name: 'token'
61 value: token
62 same_site: .same_site_none_mode
63 secure: true
64 path: '/'
65 )
66 } else {
67 eprintln('could not log into newly-created user: ${user}')
68 ctx.error('could not log into newly-created user.')
69 }
70
71 return ctx.redirect('/')
72}
73
74@['/api/user/login'; post]
75fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result {
76 user := app.get_user_by_name(username) or {
77 ctx.error('invalid credentials')
78 return ctx.redirect('/login')
79 }
80
81 if !auth.compare_password_with_hash(password, user.password_salt, user.password) {
82 ctx.error('invalid credentials')
83 return ctx.redirect('/login')
84 }
85
86 token := app.auth.add_token(user.id, ctx.ip()) or {
87 eprintln('failed to add token on log in: ${err}')
88 ctx.error('could not create token for user with id ${user.id}')
89 return ctx.redirect('/login')
90 }
91
92 ctx.set_cookie(
93 name: 'token'
94 value: token
95 same_site: .same_site_none_mode
96 secure: true
97 path: '/'
98 )
99
100 return ctx.redirect('/')
101}
102
103@['/api/user/logout']
104fn (mut app App) api_user_logout(mut ctx Context) veb.Result {
105 if token := ctx.get_cookie('token') {
106 if user := app.get_user_by_token(ctx, token) {
107 app.auth.delete_tokens_for_ip(ctx.ip()) or {
108 eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()}')
109 return ctx.redirect('/login')
110 }
111 } else {
112 eprintln('failed to get user for token for logout')
113 }
114 } else {
115 eprintln('failed to get token cookie for logout')
116 }
117
118 ctx.set_cookie(
119 name: 'token'
120 value: ''
121 same_site: .same_site_none_mode
122 secure: true
123 path: '/'
124 )
125
126 return ctx.redirect('/login')
127}
128
129@['/api/user/full_logout']
130fn (mut app App) api_user_full_logout(mut ctx Context) veb.Result {
131 if token := ctx.get_cookie('token') {
132 if user := app.get_user_by_token(ctx, token) {
133 app.auth.delete_tokens_for_user(user.id) or {
134 eprintln('failed to yeet tokens for ${user.id}')
135 return ctx.redirect('/login')
136 }
137 } else {
138 eprintln('failed to get user for token for full_logout')
139 }
140 } else {
141 eprintln('failed to get token cookie for full_logout')
142 }
143
144 ctx.set_cookie(
145 name: 'token'
146 value: ''
147 same_site: .same_site_none_mode
148 secure: true
149 path: '/'
150 )
151
152 return ctx.redirect('/login')
153}
154
155@['/api/user/set_nickname'; post]
156fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result {
157 user := app.whoami(mut ctx) or {
158 ctx.error('you are not logged in!')
159 return ctx.redirect('/login')
160 }
161
162 mut clean_nickname := ?string(nickname.trim_space())
163 if clean_nickname or { '' } == '' {
164 clean_nickname = none
165 }
166
167 // validate
168 if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) {
169 ctx.error('invalid nickname')
170 return ctx.redirect('/me')
171 }
172
173 sql app.db {
174 update User set nickname = clean_nickname where id == user.id
175 } or {
176 ctx.error('failed to change nickname')
177 eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})')
178 return ctx.redirect('/me')
179 }
180
181 return ctx.redirect('/me')
182}
183
184@['/api/user/set_muted'; post]
185fn (mut app App) api_user_set_muted(mut ctx Context, muted bool) veb.Result {
186 user := app.whoami(mut ctx) or {
187 ctx.error('you are not logged in!')
188 return ctx.redirect('/login')
189 }
190
191 if user.admin || app.config.dev_mode {
192 sql app.db {
193 update User set muted = muted where id == user.id
194 } or {
195 ctx.error('failed to change mute status')
196 eprintln('failed to update mute status for ${user} (${user.muted} -> ${muted})')
197 return ctx.redirect('/user/${user.username}')
198 }
199 return ctx.redirect('/user/${user.username}')
200 } else {
201 ctx.error('insufficient permissions!')
202 eprintln('insufficient perms to update mute status for ${user} (${user.muted} -> ${muted})')
203 return ctx.redirect('/user/${user.username}')
204 }
205}
206
207@['/api/user/set_theme'; post]
208fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result {
209 if !app.config.instance.allow_changing_theme {
210 ctx.error('this instance disallows changing themes :(')
211 return ctx.redirect('/me')
212 }
213
214 user := app.whoami(mut ctx) or {
215 ctx.error('you are not logged in!')
216 return ctx.redirect('/login')
217 }
218
219 mut theme := ?string(none)
220 if url.trim_space() != '' {
221 theme = url.trim_space()
222 }
223
224 sql app.db {
225 update User set theme = theme where id == user.id
226 } or {
227 ctx.error('failed to change theme')
228 eprintln('failed to update theme for ${user} (${user.theme} -> ${theme})')
229 return ctx.redirect('/me')
230 }
231
232 return ctx.redirect('/me')
233}
234
235@['/api/user/set_pronouns'; post]
236fn (mut app App) api_user_set_pronouns(mut ctx Context, pronouns string) veb.Result {
237 user := app.whoami(mut ctx) or {
238 ctx.error('you are not logged in!')
239 return ctx.redirect('/login')
240 }
241
242 clean_pronouns := pronouns.trim_space()
243 if !app.validators.pronouns.validate(clean_pronouns) {
244 ctx.error('invalid pronouns')
245 return ctx.redirect('/me')
246 }
247
248 sql app.db {
249 update User set pronouns = clean_pronouns where id == user.id
250 } or {
251 ctx.error('failed to change pronouns')
252 eprintln('failed to update pronouns for ${user} (${user.pronouns} -> ${clean_pronouns})')
253 return ctx.redirect('/me')
254 }
255
256 return ctx.redirect('/me')
257}
258
259@['/api/user/set_bio'; post]
260fn (mut app App) api_user_set_bio(mut ctx Context, bio string) veb.Result {
261 user := app.whoami(mut ctx) or {
262 ctx.error('you are not logged in!')
263 return ctx.redirect('/login')
264 }
265
266 clean_bio := bio.trim_space()
267 if !app.validators.user_bio.validate(clean_bio) {
268 ctx.error('invalid bio')
269 return ctx.redirect('/me')
270 }
271
272 sql app.db {
273 update User set bio = clean_bio where id == user.id
274 } or {
275 ctx.error('failed to change bio')
276 eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})')
277 return ctx.redirect('/me')
278 }
279
280 return ctx.redirect('/me')
281}
282
283@['/api/user/get_name']
284fn (mut app App) api_user_get_name(mut ctx Context, username string) veb.Result {
285 user := app.get_user_by_name(username) or { return ctx.server_error('no such user') }
286 return ctx.text(user.get_name())
287}
288
289/// user/notification ///
290
291@['/api/user/notification/clear']
292fn (mut app App) api_user_notification_clear(mut ctx Context, id int) veb.Result {
293 if !ctx.is_logged_in() {
294 ctx.error('you are not logged in!')
295 return ctx.redirect('/login')
296 }
297 sql app.db {
298 delete from Notification where id == id
299 } or {
300 ctx.error('failed to delete notification')
301 return ctx.redirect('/inbox')
302 }
303 return ctx.redirect('/inbox')
304}
305
306@['/api/user/notification/clear_all']
307fn (mut app App) api_user_notification_clear_all(mut ctx Context) veb.Result {
308 user := app.whoami(mut ctx) or {
309 ctx.error('you are not logged in!')
310 return ctx.redirect('/login')
311 }
312 sql app.db {
313 delete from Notification where user_id == user.id
314 } or {
315 ctx.error('failed to delete notifications')
316 return ctx.redirect('/inbox')
317 }
318 return ctx.redirect('/inbox')
319}
320
321////// post //////
322
323@['/api/post/new_post'; post]
324fn (mut app App) api_post_new_post(mut ctx Context, title string, body string) veb.Result {
325 user := app.whoami(mut ctx) or {
326 ctx.error('not logged in!')
327 return ctx.redirect('/')
328 }
329
330 if user.muted {
331 ctx.error('you are muted!')
332 return ctx.redirect('/me')
333 }
334
335 // validate title
336 if !app.validators.post_title.validate(title) {
337 ctx.error('invalid title')
338 return ctx.redirect('/me')
339 }
340
341 // validate body
342 if !app.validators.post_body.validate(body) {
343 ctx.error('invalid body')
344 return ctx.redirect('/me')
345 }
346
347 post := Post{
348 author_id: user.id
349 title: title
350 body: body
351 }
352
353 sql app.db {
354 insert post into Post
355 } or {
356 ctx.error('failed to post!')
357 println('failed to post: ${post} from user ${user.id}')
358 return ctx.redirect('/me')
359 }
360
361 // find the post's id to process mentions with
362 if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) {
363 app.process_post_mentions(x)
364 } else {
365 ctx.error('failed to get_post_by_timestamp_and_author for ${post}')
366 }
367
368 return ctx.redirect('/me')
369}
370
371@['/api/post/delete'; post]
372fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result {
373 user := app.whoami(mut ctx) or {
374 ctx.error('not logged in!')
375 return ctx.redirect('/login')
376 }
377
378 post := app.get_post_by_id(id) or {
379 ctx.error('post does not exist')
380 return ctx.redirect('/')
381 }
382
383 if user.admin || user.id == post.author_id {
384 sql app.db {
385 delete from Post where id == id
386 delete from Like where post_id == id
387 } or {
388 ctx.error('failed to delete post')
389 eprintln('failed to delete post: ${id}')
390 return ctx.redirect('/')
391 }
392 println('deleted post: ${id}')
393 return ctx.redirect('/')
394 } else {
395 ctx.error('insufficient permissions!')
396 eprintln('insufficient perms to delete post: ${id} (${user.id})')
397 return ctx.redirect('/')
398 }
399}
400
401@['/api/post/like']
402fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result {
403 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
404
405 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
406
407 if app.does_user_like_post(user.id, post.id) {
408 sql app.db {
409 delete from Like where user_id == user.id && post_id == post.id
410 // yeet the old cached like value
411 delete from LikeCache where post_id == post.id
412 } or {
413 eprintln('user ${user.id} failed to unlike post ${id}')
414 return ctx.server_error('failed to unlike post')
415 }
416 return ctx.ok('unliked post')
417 } else {
418 // remove the old dislike, if it exists
419 if app.does_user_dislike_post(user.id, post.id) {
420 sql app.db {
421 delete from Like where user_id == user.id && post_id == post.id
422 } or {
423 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it')
424 }
425 }
426
427 like := Like{
428 user_id: user.id
429 post_id: post.id
430 is_like: true
431 }
432 sql app.db {
433 insert like into Like
434 // yeet the old cached like value
435 delete from LikeCache where post_id == post.id
436 } or {
437 eprintln('user ${user.id} failed to like post ${id}')
438 return ctx.server_error('failed to like post')
439 }
440 return ctx.ok('liked post')
441 }
442}
443
444@['/api/post/dislike']
445fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result {
446 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
447
448 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
449
450 if app.does_user_dislike_post(user.id, post.id) {
451 sql app.db {
452 delete from Like where user_id == user.id && post_id == post.id
453 // yeet the old cached like value
454 delete from LikeCache where post_id == post.id
455 } or {
456 eprintln('user ${user.id} failed to unlike post ${id}')
457 return ctx.server_error('failed to unlike post')
458 }
459 return ctx.ok('undisliked post')
460 } else {
461 // remove the old like, if it exists
462 if app.does_user_like_post(user.id, post.id) {
463 sql app.db {
464 delete from Like where user_id == user.id && post_id == post.id
465 } or {
466 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it')
467 }
468 }
469
470 like := Like{
471 user_id: user.id
472 post_id: post.id
473 is_like: false
474 }
475 sql app.db {
476 insert like into Like
477 // yeet the old cached like value
478 delete from LikeCache where post_id == post.id
479 } or {
480 eprintln('user ${user.id} failed to dislike post ${id}')
481 return ctx.server_error('failed to dislike post')
482 }
483 return ctx.ok('disliked post')
484 }
485}
486
487@['/api/post/get_title']
488fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result {
489 post := app.get_post_by_id(id) or { return ctx.server_error('no such post') }
490 return ctx.text(post.title)
491}
492
493@['/api/post/edit'; post]
494fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result {
495 user := app.whoami(mut ctx) or {
496 ctx.error('not logged in!')
497 return ctx.redirect('/login')
498 }
499 post := app.get_post_by_id(id) or {
500 ctx.error('no such post')
501 return ctx.redirect('/')
502 }
503 if post.author_id != user.id {
504 ctx.error('insufficient permissions')
505 return ctx.redirect('/')
506 }
507
508 sql app.db {
509 update Post set body = body, title = title where id == id
510 } or {
511 eprintln('failed to update post')
512 ctx.error('failed to update post')
513 return ctx.redirect('/')
514 }
515
516 return ctx.redirect('/post/${id}')
517}
518
519////// site //////
520
521@['/api/site/set_motd'; post]
522fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result {
523 user := app.whoami(mut ctx) or {
524 ctx.error('not logged in!')
525 return ctx.redirect('/login')
526 }
527
528 if user.admin {
529 sql app.db {
530 update Site set motd = motd where id == 1
531 } or {
532 ctx.error('failed to set motd')
533 eprintln('failed to set motd: ${motd}')
534 return ctx.redirect('/')
535 }
536 println('set motd to: ${motd}')
537 return ctx.redirect('/')
538 } else {
539 ctx.error('insufficient permissions!')
540 eprintln('insufficient perms to set motd to: ${motd} (${user.id})')
541 return ctx.redirect('/')
542 }
543}