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