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@['/api/user/delete']
322fn (mut app App) api_user_delete(mut ctx Context, id int) veb.Result {
323 user := app.whoami(mut ctx) or {
324 ctx.error('you are not logged in!')
325 return ctx.redirect('/login')
326 }
327
328 println('attempting to delete ${id} as ${user.id}')
329
330 if user.admin || user.id == id {
331 // yeet
332 sql app.db {
333 delete from User where id == id
334 delete from Like where user_id == id
335 delete from Notification where user_id == id
336 } or {
337 ctx.error('failed to delete user: ${id}')
338 return ctx.redirect('/')
339 }
340
341 // delete posts and their likes
342 posts_from_this_user := sql app.db {
343 select from Post where author_id == id
344 } or { [] }
345
346 for post in posts_from_this_user {
347 sql app.db {
348 delete from Like where post_id == post.id
349 delete from LikeCache where post_id == post.id
350 } or {
351 eprintln('failed to delete like cache for post during user deletion: ${post.id}')
352 }
353 }
354
355 sql app.db {
356 delete from Post where author_id == id
357 } or {
358 eprintln('failed to delete posts by deleting user: ${user.id}')
359 }
360
361 app.auth.delete_tokens_for_user(id) or {
362 eprintln('failed to delete tokens for user during deletion: ${id}')
363 }
364 // log out
365 if user.id == id {
366 ctx.set_cookie(
367 name: 'token'
368 value: ''
369 same_site: .same_site_none_mode
370 secure: true
371 path: '/'
372 )
373 }
374 println('deleted user ${id}')
375 } else {
376 ctx.error('be nice. deleting other users is off-limits.')
377 }
378
379 return ctx.redirect('/')
380}
381
382////// post //////
383
384@['/api/post/new_post'; post]
385fn (mut app App) api_post_new_post(mut ctx Context, replying_to int, title string, body string) veb.Result {
386 user := app.whoami(mut ctx) or {
387 ctx.error('not logged in!')
388 return ctx.redirect('/login')
389 }
390
391 if user.muted {
392 ctx.error('you are muted!')
393 return ctx.redirect('/post/new')
394 }
395
396 // validate title
397 if !app.validators.post_title.validate(title) {
398 ctx.error('invalid title')
399 return ctx.redirect('/post/new')
400 }
401
402 // validate body
403 if !app.validators.post_body.validate(body) {
404 ctx.error('invalid body')
405 return ctx.redirect('/post/new')
406 }
407
408 mut post := Post{
409 author_id: user.id
410 title: title
411 body: body
412 }
413
414 if replying_to != 0 {
415 // check if replying post exists
416 app.get_post_by_id(replying_to) or {
417 ctx.error('the post you are trying to reply to does not exist')
418 return ctx.redirect('/post/new')
419 }
420 post.replying_to = replying_to
421 }
422
423 sql app.db {
424 insert post into Post
425 } or {
426 ctx.error('failed to post!')
427 println('failed to post: ${post} from user ${user.id}')
428 return ctx.redirect('/post/new')
429 }
430
431 // find the post's id to process mentions with
432 if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) {
433 app.process_post_mentions(x)
434 return ctx.redirect('/post/${x.id}')
435 } else {
436 ctx.error('failed to get_post_by_timestamp_and_author for ${post}')
437 return ctx.redirect('/me')
438 }
439}
440
441@['/api/post/delete'; post]
442fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result {
443 user := app.whoami(mut ctx) or {
444 ctx.error('not logged in!')
445 return ctx.redirect('/login')
446 }
447
448 post := app.get_post_by_id(id) or {
449 ctx.error('post does not exist')
450 return ctx.redirect('/')
451 }
452
453 if user.admin || user.id == post.author_id {
454 sql app.db {
455 delete from Post where id == id
456 delete from Like where post_id == id
457 } or {
458 ctx.error('failed to delete post')
459 eprintln('failed to delete post: ${id}')
460 return ctx.redirect('/')
461 }
462 println('deleted post: ${id}')
463 return ctx.redirect('/')
464 } else {
465 ctx.error('insufficient permissions!')
466 eprintln('insufficient perms to delete post: ${id} (${user.id})')
467 return ctx.redirect('/')
468 }
469}
470
471@['/api/post/like']
472fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result {
473 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
474
475 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
476
477 if app.does_user_like_post(user.id, post.id) {
478 sql app.db {
479 delete from Like where user_id == user.id && post_id == post.id
480 // yeet the old cached like value
481 delete from LikeCache where post_id == post.id
482 } or {
483 eprintln('user ${user.id} failed to unlike post ${id}')
484 return ctx.server_error('failed to unlike post')
485 }
486 return ctx.ok('unliked post')
487 } else {
488 // remove the old dislike, if it exists
489 if app.does_user_dislike_post(user.id, post.id) {
490 sql app.db {
491 delete from Like where user_id == user.id && post_id == post.id
492 } or {
493 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it')
494 }
495 }
496
497 like := Like{
498 user_id: user.id
499 post_id: post.id
500 is_like: true
501 }
502 sql app.db {
503 insert like into Like
504 // yeet the old cached like value
505 delete from LikeCache where post_id == post.id
506 } or {
507 eprintln('user ${user.id} failed to like post ${id}')
508 return ctx.server_error('failed to like post')
509 }
510 return ctx.ok('liked post')
511 }
512}
513
514@['/api/post/dislike']
515fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result {
516 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
517
518 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
519
520 if app.does_user_dislike_post(user.id, post.id) {
521 sql app.db {
522 delete from Like where user_id == user.id && post_id == post.id
523 // yeet the old cached like value
524 delete from LikeCache where post_id == post.id
525 } or {
526 eprintln('user ${user.id} failed to unlike post ${id}')
527 return ctx.server_error('failed to unlike post')
528 }
529 return ctx.ok('undisliked post')
530 } else {
531 // remove the old like, if it exists
532 if app.does_user_like_post(user.id, post.id) {
533 sql app.db {
534 delete from Like where user_id == user.id && post_id == post.id
535 } or {
536 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it')
537 }
538 }
539
540 like := Like{
541 user_id: user.id
542 post_id: post.id
543 is_like: false
544 }
545 sql app.db {
546 insert like into Like
547 // yeet the old cached like value
548 delete from LikeCache where post_id == post.id
549 } or {
550 eprintln('user ${user.id} failed to dislike post ${id}')
551 return ctx.server_error('failed to dislike post')
552 }
553 return ctx.ok('disliked post')
554 }
555}
556
557@['/api/post/get_title']
558fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result {
559 post := app.get_post_by_id(id) or { return ctx.server_error('no such post') }
560 return ctx.text(post.title)
561}
562
563@['/api/post/edit'; post]
564fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result {
565 user := app.whoami(mut ctx) or {
566 ctx.error('not logged in!')
567 return ctx.redirect('/login')
568 }
569 post := app.get_post_by_id(id) or {
570 ctx.error('no such post')
571 return ctx.redirect('/')
572 }
573 if post.author_id != user.id {
574 ctx.error('insufficient permissions')
575 return ctx.redirect('/')
576 }
577
578 sql app.db {
579 update Post set body = body, title = title where id == id
580 } or {
581 eprintln('failed to update post')
582 ctx.error('failed to update post')
583 return ctx.redirect('/')
584 }
585
586 return ctx.redirect('/post/${id}')
587}
588
589////// site //////
590
591@['/api/site/set_motd'; post]
592fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result {
593 user := app.whoami(mut ctx) or {
594 ctx.error('not logged in!')
595 return ctx.redirect('/login')
596 }
597
598 if user.admin {
599 sql app.db {
600 update Site set motd = motd where id == 1
601 } or {
602 ctx.error('failed to set motd')
603 eprintln('failed to set motd: ${motd}')
604 return ctx.redirect('/')
605 }
606 println('set motd to: ${motd}')
607 return ctx.redirect('/')
608 } else {
609 ctx.error('insufficient permissions!')
610 eprintln('insufficient perms to set motd to: ${motd} (${user.id})')
611 return ctx.redirect('/')
612 }
613}