a mini social media app for small communities
1module webapp
2
3import veb
4import db.pg
5import regex
6import auth
7import entity { LikeCache, Like, Post, Site, User, Notification }
8import database { DatabaseAccess }
9
10pub struct App {
11 veb.StaticHandler
12 DatabaseAccess
13pub:
14 config Config
15pub mut:
16 auth auth.Auth[pg.DB]
17 validators struct {
18 pub mut:
19 username StringValidator
20 password StringValidator
21 nickname StringValidator
22 pronouns StringValidator
23 user_bio StringValidator
24 post_title StringValidator
25 post_body StringValidator
26 }
27}
28
29// get_user_by_token returns a user by their token, returns none if the user was
30// not found.
31pub fn (app &App) get_user_by_token(ctx &Context, token string) ?User {
32 user_token := app.auth.find_token(token, ctx.ip()) or {
33 eprintln('no such user corresponding to token')
34 return none
35 }
36 return app.get_user_by_id(user_token.user_id)
37}
38
39// whoami returns the current logged in user, or none if the user is not logged
40// in.
41pub fn (app &App) whoami(mut ctx Context) ?User {
42 token := ctx.get_cookie('token') or { return none }.trim_space()
43 if token == '' {
44 return none
45 }
46 if user := app.get_user_by_token(ctx, token) {
47 if user.username == '' || user.id == 0 {
48 eprintln('a user had a token for the blank user')
49 // Clear token
50 ctx.set_cookie(
51 name: 'token'
52 value: ''
53 same_site: .same_site_none_mode
54 secure: true
55 path: '/'
56 )
57 return none
58 }
59 return user
60 } else {
61 eprintln('a user had a token for a non-existent user (this token may have been expired and left in cookies)')
62 // Clear token
63 ctx.set_cookie(
64 name: 'token'
65 value: ''
66 same_site: .same_site_none_mode
67 secure: true
68 path: '/'
69 )
70 return none
71 }
72}
73
74// get_unknown_user returns a user representing an unknown user
75pub fn (app &App) get_unknown_user() User {
76 return User{
77 username: 'unknown'
78 }
79}
80
81// get_unknown_post returns a post representing an unknown post
82pub fn (app &App) get_unknown_post() Post {
83 return Post{
84 title: 'unknown'
85 }
86}
87
88// logged_in_as returns true if the user is logged in as the provided user id.
89pub fn (app &App) logged_in_as(mut ctx Context, id int) bool {
90 if !ctx.is_logged_in() {
91 return false
92 }
93 return app.whoami(mut ctx) or { return false }.id == id
94}
95
96// get_motd returns the site's message of the day.
97@[inline]
98pub fn (app &App) get_motd() string {
99 site := app.get_or_create_site_config()
100 return site.motd
101}
102
103// get_notification_count_for_frontend returns the notification count for a
104// given user, formatted for usage on the frontend.
105pub fn (app &App) get_notification_count_for_frontend(user_id int, limit int) string {
106 count := app.get_notification_count(user_id, limit)
107 if count == 0 {
108 return ''
109 } else if count > limit {
110 return ' (${count}+)'
111 } else {
112 return ' (${count})'
113 }
114}
115
116// process_post_mentions parses a post's body to send notifications for mentions
117// or replies.
118pub fn (app &App) process_post_mentions(post &Post) {
119 author := app.get_user_by_id(post.author_id) or {
120 eprintln('process_post_mentioned called on a post with a non-existent author: ${post}')
121 return
122 }
123 author_name := author.get_name()
124
125 // used so we do not send more than one notification per post
126 mut notified_users := []int{}
127
128 // notify who we replied to, if applicable
129 if post.replying_to != none {
130 if x := app.get_post_by_id(post.replying_to) {
131 app.send_notification_to(x.author_id, '${author_name} replied to your post!', '${author_name} replied to *(${x.id})')
132 }
133 }
134
135 // find mentions
136 mut re := regex.regex_opt('@\\(${app.config.user.username_pattern}\\)') or {
137 eprintln('failed to compile regex for process_post_mentions (err: ${err})')
138 return
139 }
140 matches := re.find_all_str(post.body)
141 for mat in matches {
142 println('found mentioned user: ${mat}')
143 username := mat#[2..-1]
144 user := app.get_user_by_name(username) or {
145 continue
146 }
147
148 if user.id in notified_users || user.id == author.id {
149 continue
150 }
151 notified_users << user.id
152
153 app.send_notification_to(
154 user.id,
155 '${author_name} mentioned you!',
156 'you have been mentioned in this post: *(${post.id})'
157 )
158 }
159}