a mini social media app for small communities
1module main
2
3import veb
4import db.pg
5import auth
6import entity { LikeCache, Like, Post, Site, User }
7
8pub struct App {
9 veb.StaticHandler
10pub:
11 config Config
12pub mut:
13 db pg.DB
14 auth auth.Auth[pg.DB]
15 validators struct {
16 pub mut:
17 username StringValidator
18 password StringValidator
19 nickname StringValidator
20 pronouns StringValidator
21 user_bio StringValidator
22 post_title StringValidator
23 post_body StringValidator
24 }
25}
26
27pub fn (app &App) get_user_by_name(username string) ?User {
28 users := sql app.db {
29 select from User where username == username
30 } or { [] }
31 if users.len != 1 {
32 return none
33 }
34 return users[0]
35}
36
37pub fn (app &App) get_user_by_id(id int) ?User {
38 users := sql app.db {
39 select from User where id == id
40 } or { [] }
41 if users.len != 1 {
42 return none
43 }
44 return users[0]
45}
46
47pub fn (app &App) get_user_by_token(ctx &Context, token string) ?User {
48 user_token := app.auth.find_token(token, ctx.ip()) or {
49 eprintln('no such user corresponding to token')
50 return none
51 }
52 return app.get_user_by_id(user_token.user_id)
53}
54
55pub fn (app &App) get_recent_posts() []Post {
56 posts := sql app.db {
57 select from Post order by posted_at desc limit 10
58 } or { [] }
59 return posts
60}
61
62// pub fn (app &App) get_popular_posts() []Post {
63// posts := sql app.db {
64// select from Post order by likes desc limit 10
65// } or { [] }
66// return posts
67// }
68
69pub fn (app &App) get_posts_from_user(user_id int) []Post {
70 posts := sql app.db {
71 select from Post where author_id == user_id order by posted_at desc
72 } or { [] }
73 return posts
74}
75
76pub fn (app &App) get_users() []User {
77 users := sql app.db {
78 select from User
79 } or { [] }
80 return users
81}
82
83pub fn (app &App) get_post_by_id(id int) ?Post {
84 posts := sql app.db {
85 select from Post where id == id limit 1
86 } or { [] }
87 if posts.len != 1 {
88 return none
89 }
90 return posts[0]
91}
92
93pub fn (app &App) get_pinned_posts() []Post {
94 posts := sql app.db {
95 select from Post where pinned == true
96 } or { [] }
97 return posts
98}
99
100pub fn (app &App) whoami(mut ctx Context) ?User {
101 token := ctx.get_cookie('token') or { return none }.trim_space()
102 if token == '' {
103 return none
104 }
105 if user := app.get_user_by_token(ctx, token) {
106 if user.username == '' || user.id == 0 {
107 eprintln('a user had a token for the blank user')
108 // Clear token
109 ctx.set_cookie(
110 name: 'token'
111 value: ''
112 same_site: .same_site_none_mode
113 secure: true
114 path: '/'
115 )
116 return none
117 }
118 return user
119 } else {
120 eprintln('a user had a token for a non-existent user (this token may have been expired and left in cookies)')
121 // Clear token
122 ctx.set_cookie(
123 name: 'token'
124 value: ''
125 same_site: .same_site_none_mode
126 secure: true
127 path: '/'
128 )
129 return none
130 }
131}
132
133pub fn (app &App) get_unknown_user() User {
134 return User{
135 username: 'unknown'
136 }
137}
138
139pub fn (app &App) logged_in_as(mut ctx Context, id int) bool {
140 if !ctx.is_logged_in() {
141 return false
142 }
143 return app.whoami(mut ctx) or { return false }.id == id
144}
145
146pub fn (app &App) does_user_like_post(user_id int, post_id int) bool {
147 likes := sql app.db {
148 select from Like where user_id == user_id && post_id == post_id
149 } or { [] }
150 if likes.len > 1 {
151 // something is very wrong lol
152 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
153 } else if likes.len == 0 {
154 return false
155 }
156 return likes.first().is_like
157}
158
159pub fn (app &App) does_user_dislike_post(user_id int, post_id int) bool {
160 likes := sql app.db {
161 select from Like where user_id == user_id && post_id == post_id
162 } or { [] }
163 if likes.len > 1 {
164 // something is very wrong lol
165 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
166 } else if likes.len == 0 {
167 return false
168 }
169 return !likes.first().is_like
170}
171
172pub fn (app &App) does_user_like_or_dislike_post(user_id int, post_id int) bool {
173 likes := sql app.db {
174 select from Like where user_id == user_id && post_id == post_id
175 } or { [] }
176 if likes.len > 1 {
177 // something is very wrong lol
178 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
179 }
180 return likes.len == 1
181}
182
183pub fn (app &App) get_net_likes_for_post(post_id int) int {
184 // check cache
185 cache := sql app.db {
186 select from LikeCache where post_id == post_id limit 1
187 } or { [] }
188
189 mut likes := 0
190
191 if cache.len != 1 {
192 println('calculating net likes for post: ${post_id}')
193 // calculate
194 db_likes := sql app.db {
195 select from Like where post_id == post_id
196 } or { [] }
197
198 for like in db_likes {
199 if like.is_like {
200 likes++
201 } else {
202 likes--
203 }
204 }
205
206 // cache
207 cached := LikeCache{
208 post_id: post_id
209 likes: likes
210 }
211 sql app.db {
212 insert cached into LikeCache
213 } or {
214 eprintln('failed to cache like: ${cached}')
215 return likes
216 }
217 } else {
218 likes = cache.first().likes
219 }
220
221 return likes
222}
223
224pub fn (app &App) get_or_create_site_config() Site {
225 configs := sql app.db {
226 select from Site
227 } or { [] }
228 if configs.len == 0 {
229 // make the site config
230 site_config := Site{}
231 sql app.db {
232 insert site_config into Site
233 } or { panic('failed to create site config (${err})') }
234 } else if configs.len > 1 {
235 // this should never happen
236 panic('there are multiple site configs')
237 }
238 return configs[0]
239}
240
241pub fn (app &App) get_motd() string {
242 site := app.get_or_create_site_config()
243 return site.motd
244}