a mini social media app for small communities
1module main 2 3import veb 4import db.pg 5import regex 6import time 7import auth 8import entity { LikeCache, Like, Post, Site, User, Notification } 9 10pub struct App { 11 veb.StaticHandler 12pub: 13 config Config 14pub mut: 15 db pg.DB 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 29pub fn (app &App) get_user_by_name(username string) ?User { 30 users := sql app.db { 31 select from User where username == username 32 } or { [] } 33 if users.len != 1 { 34 return none 35 } 36 return users[0] 37} 38 39pub fn (app &App) get_user_by_id(id int) ?User { 40 users := sql app.db { 41 select from User where id == id 42 } or { [] } 43 if users.len != 1 { 44 return none 45 } 46 return users[0] 47} 48 49pub fn (app &App) get_user_by_token(ctx &Context, token string) ?User { 50 user_token := app.auth.find_token(token, ctx.ip()) or { 51 eprintln('no such user corresponding to token') 52 return none 53 } 54 return app.get_user_by_id(user_token.user_id) 55} 56 57pub fn (app &App) get_recent_posts() []Post { 58 posts := sql app.db { 59 select from Post order by posted_at desc limit 10 60 } or { [] } 61 return posts 62} 63 64// pub fn (app &App) get_popular_posts() []Post { 65// posts := sql app.db { 66// select from Post order by likes desc limit 10 67// } or { [] } 68// return posts 69// } 70 71pub fn (app &App) get_posts_from_user(user_id int) []Post { 72 posts := sql app.db { 73 select from Post where author_id == user_id order by posted_at desc 74 } or { [] } 75 return posts 76} 77 78pub fn (app &App) get_users() []User { 79 users := sql app.db { 80 select from User 81 } or { [] } 82 return users 83} 84 85pub fn (app &App) get_post_by_id(id int) ?Post { 86 posts := sql app.db { 87 select from Post where id == id limit 1 88 } or { [] } 89 if posts.len != 1 { 90 return none 91 } 92 return posts[0] 93} 94 95pub fn (app &App) get_post_by_author_and_timestamp(author_id int, timestamp time.Time) ?Post { 96 posts := sql app.db { 97 select from Post where author_id == author_id && posted_at == timestamp order by posted_at desc limit 1 98 } or { [] } 99 if posts.len == 0 { 100 return none 101 } 102 return posts[0] 103} 104 105pub fn (app &App) get_pinned_posts() []Post { 106 posts := sql app.db { 107 select from Post where pinned == true 108 } or { [] } 109 return posts 110} 111 112pub fn (app &App) whoami(mut ctx Context) ?User { 113 token := ctx.get_cookie('token') or { return none }.trim_space() 114 if token == '' { 115 return none 116 } 117 if user := app.get_user_by_token(ctx, token) { 118 if user.username == '' || user.id == 0 { 119 eprintln('a user had a token for the blank user') 120 // Clear token 121 ctx.set_cookie( 122 name: 'token' 123 value: '' 124 same_site: .same_site_none_mode 125 secure: true 126 path: '/' 127 ) 128 return none 129 } 130 return user 131 } else { 132 eprintln('a user had a token for a non-existent user (this token may have been expired and left in cookies)') 133 // Clear token 134 ctx.set_cookie( 135 name: 'token' 136 value: '' 137 same_site: .same_site_none_mode 138 secure: true 139 path: '/' 140 ) 141 return none 142 } 143} 144 145pub fn (app &App) get_unknown_user() User { 146 return User{ 147 username: 'unknown' 148 } 149} 150 151pub fn (app &App) logged_in_as(mut ctx Context, id int) bool { 152 if !ctx.is_logged_in() { 153 return false 154 } 155 return app.whoami(mut ctx) or { return false }.id == id 156} 157 158pub fn (app &App) does_user_like_post(user_id int, post_id int) bool { 159 likes := sql app.db { 160 select from Like where user_id == user_id && post_id == post_id 161 } or { [] } 162 if likes.len > 1 { 163 // something is very wrong lol 164 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})') 165 } else if likes.len == 0 { 166 return false 167 } 168 return likes.first().is_like 169} 170 171pub fn (app &App) does_user_dislike_post(user_id int, post_id int) bool { 172 likes := sql app.db { 173 select from Like where user_id == user_id && post_id == post_id 174 } or { [] } 175 if likes.len > 1 { 176 // something is very wrong lol 177 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})') 178 } else if likes.len == 0 { 179 return false 180 } 181 return !likes.first().is_like 182} 183 184pub fn (app &App) does_user_like_or_dislike_post(user_id int, post_id int) bool { 185 likes := sql app.db { 186 select from Like where user_id == user_id && post_id == post_id 187 } or { [] } 188 if likes.len > 1 { 189 // something is very wrong lol 190 eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})') 191 } 192 return likes.len == 1 193} 194 195pub fn (app &App) get_net_likes_for_post(post_id int) int { 196 // check cache 197 cache := sql app.db { 198 select from LikeCache where post_id == post_id limit 1 199 } or { [] } 200 201 mut likes := 0 202 203 if cache.len != 1 { 204 println('calculating net likes for post: ${post_id}') 205 // calculate 206 db_likes := sql app.db { 207 select from Like where post_id == post_id 208 } or { [] } 209 210 for like in db_likes { 211 if like.is_like { 212 likes++ 213 } else { 214 likes-- 215 } 216 } 217 218 // cache 219 cached := LikeCache{ 220 post_id: post_id 221 likes: likes 222 } 223 sql app.db { 224 insert cached into LikeCache 225 } or { 226 eprintln('failed to cache like: ${cached}') 227 return likes 228 } 229 } else { 230 likes = cache.first().likes 231 } 232 233 return likes 234} 235 236pub fn (app &App) get_or_create_site_config() Site { 237 configs := sql app.db { 238 select from Site 239 } or { [] } 240 if configs.len == 0 { 241 // make the site config 242 site_config := Site{} 243 sql app.db { 244 insert site_config into Site 245 } or { panic('failed to create site config (${err})') } 246 } else if configs.len > 1 { 247 // this should never happen 248 panic('there are multiple site configs') 249 } 250 return configs[0] 251} 252 253@[inline] 254pub fn (app &App) get_motd() string { 255 site := app.get_or_create_site_config() 256 return site.motd 257} 258 259pub fn (app &App) get_notifications_for(user_id int) []Notification { 260 notifications := sql app.db { 261 select from Notification where user_id == user_id 262 } or { [] } 263 return notifications 264} 265 266pub fn (app &App) get_notification_count(user_id int, limit int) int { 267 notifications := sql app.db { 268 select from Notification where user_id == user_id limit limit 269 } or { [] } 270 return notifications.len 271} 272 273pub fn (app &App) get_notification_count_for_frontend(user_id int, limit int) string { 274 count := app.get_notification_count(user_id, limit) 275 if count == 0 { 276 return '' 277 } else if count > limit { 278 return ' (${count}+)' 279 } else { 280 return ' (${count})' 281 } 282} 283 284pub fn (app &App) send_notification_to(user_id int, summary string, body string) { 285 notification := Notification{ 286 user_id: user_id 287 summary: summary 288 body: body 289 } 290 sql app.db { 291 insert notification into Notification 292 } or { 293 eprintln('failed to send notification ${notification}') 294 } 295} 296 297// sends notifications to each user mentioned in a post 298pub fn (app &App) process_post_mentions(post &Post) { 299 author := app.get_user_by_id(post.author_id) or { 300 eprintln('process_post_mentioned called on a post with a non-existent author: ${post}') 301 return 302 } 303 author_name := author.get_name() 304 305 mut re := regex.regex_opt('@\\(${app.config.user.username_pattern}\\)') or { 306 eprintln('failed to compile regex for process_post_mentions (err: ${err})') 307 return 308 } 309 matches := re.find_all_str(post.body) 310 mut mentioned_users := []int{} 311 for mat in matches { 312 println('found mentioned user: ${mat}') 313 username := mat#[2..-1] 314 user := app.get_user_by_name(username) or { 315 continue 316 } 317 318 if user.id in mentioned_users || user.id == author.id { 319 continue 320 } 321 mentioned_users << user.id 322 323 app.send_notification_to( 324 user.id, 325 '${author_name} mentioned you!', 326 'you have been mentioned in this post: *(${post.id})' 327 ) 328 } 329}