+20
-45
src/api.v
src/webapp/api.v
+20
-45
src/api.v
src/webapp/api.v
···
1
-
module main
1
+
module webapp
2
2
3
3
import veb
4
4
import auth
···
36
36
user.theme = app.config.instance.default_theme
37
37
}
38
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) {
39
+
if x := app.new_user(user) {
49
40
app.send_notification_to(
50
41
x.id,
51
42
app.config.welcome.summary.replace('%s', x.get_name()),
···
89
80
return ctx.redirect('/settings')
90
81
}
91
82
92
-
sql app.db {
93
-
update User set username = new_username where id == user.id
94
-
} or {
95
-
eprintln('failed to update username for ${user.id}')
83
+
if !app.set_username(user.id, new_username) {
96
84
ctx.error('failed to update username')
97
85
}
98
86
···
125
113
126
114
hashed_new_password := auth.hash_password_with_salt(new_password, user.password_salt)
127
115
128
-
sql app.db {
129
-
update User set password = hashed_new_password where id == user.id
130
-
} or {
131
-
eprintln('failed to update password for ${user.id}')
116
+
if !app.set_password(user.id, hashed_new_password) {
132
117
ctx.error('failed to update password')
133
118
}
134
119
···
234
219
return ctx.redirect('/me')
235
220
}
236
221
237
-
sql app.db {
238
-
update User set nickname = clean_nickname where id == user.id
239
-
} or {
240
-
ctx.error('failed to change nickname')
222
+
if !app.set_nickname(user.id, clean_nickname) {
241
223
eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})')
242
224
return ctx.redirect('/me')
243
225
}
···
246
228
}
247
229
248
230
@['/api/user/set_muted'; post]
249
-
fn (mut app App) api_user_set_muted(mut ctx Context, muted bool) veb.Result {
231
+
fn (mut app App) api_user_set_muted(mut ctx Context, id int, muted bool) veb.Result {
250
232
user := app.whoami(mut ctx) or {
251
233
ctx.error('you are not logged in!')
252
234
return ctx.redirect('/login')
253
235
}
254
236
255
-
if user.admin || app.config.dev_mode {
256
-
sql app.db {
257
-
update User set muted = muted where id == user.id
258
-
} or {
237
+
to_mute := app.get_user_by_id(id) or {
238
+
ctx.error('no such user')
239
+
return ctx.redirect('/')
240
+
}
241
+
242
+
if user.admin {
243
+
if !app.set_muted(to_mute.id, muted) {
259
244
ctx.error('failed to change mute status')
260
-
eprintln('failed to update mute status for ${user} (${user.muted} -> ${muted})')
261
-
return ctx.redirect('/user/${user.username}')
245
+
return ctx.redirect('/user/${to_mute.username}')
262
246
}
263
-
return ctx.redirect('/user/${user.username}')
247
+
return ctx.redirect('/user/${to_mute.username}')
264
248
} else {
265
249
ctx.error('insufficient permissions!')
266
-
eprintln('insufficient perms to update mute status for ${user} (${user.muted} -> ${muted})')
267
-
return ctx.redirect('/user/${user.username}')
250
+
eprintln('insufficient perms to update mute status for ${to_mute} (${to_mute.muted} -> ${muted})')
251
+
return ctx.redirect('/user/${to_mute.username}')
268
252
}
269
253
}
270
254
···
285
269
theme = url.trim_space()
286
270
}
287
271
288
-
sql app.db {
289
-
update User set theme = theme where id == user.id
290
-
} or {
272
+
if !app.set_theme(user.id, theme) {
291
273
ctx.error('failed to change theme')
292
-
eprintln('failed to update theme for ${user} (${user.theme} -> ${theme})')
293
274
return ctx.redirect('/me')
294
275
}
295
276
···
309
290
return ctx.redirect('/me')
310
291
}
311
292
312
-
sql app.db {
313
-
update User set pronouns = clean_pronouns where id == user.id
314
-
} or {
293
+
if !app.set_pronouns(user.id, clean_pronouns) {
315
294
ctx.error('failed to change pronouns')
316
-
eprintln('failed to update pronouns for ${user} (${user.pronouns} -> ${clean_pronouns})')
317
295
return ctx.redirect('/me')
318
296
}
319
297
···
333
311
return ctx.redirect('/me')
334
312
}
335
313
336
-
sql app.db {
337
-
update User set bio = clean_bio where id == user.id
338
-
} or {
339
-
ctx.error('failed to change bio')
314
+
if !app.set_bio(user.id, clean_bio) {
340
315
eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})')
341
316
return ctx.redirect('/me')
342
317
}
-358
src/app.v
-358
src/app.v
···
1
-
module main
2
-
3
-
import veb
4
-
import db.pg
5
-
import regex
6
-
import time
7
-
import auth
8
-
import entity { LikeCache, Like, Post, Site, User, Notification }
9
-
10
-
pub struct App {
11
-
veb.StaticHandler
12
-
pub:
13
-
config Config
14
-
pub 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
-
29
-
pub 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
-
39
-
pub 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
-
49
-
pub 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
-
57
-
pub 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
-
cached_likes := sql app.db {
66
-
select from LikeCache order by likes desc limit 10
67
-
} or { [] }
68
-
posts := cached_likes.map(fn [app] (it LikeCache) Post {
69
-
return app.get_post_by_id(it.post_id) or {
70
-
eprintln('cached like ${it} does not have a post related to it (from get_popular_posts)')
71
-
return Post{}
72
-
}
73
-
}).filter(it.id != 0)
74
-
return posts
75
-
}
76
-
77
-
pub fn (app &App) get_posts_from_user(user_id int) []Post {
78
-
posts := sql app.db {
79
-
select from Post where author_id == user_id order by posted_at desc
80
-
} or { [] }
81
-
return posts
82
-
}
83
-
84
-
pub fn (app &App) get_users() []User {
85
-
users := sql app.db {
86
-
select from User
87
-
} or { [] }
88
-
return users
89
-
}
90
-
91
-
pub fn (app &App) get_post_by_id(id int) ?Post {
92
-
posts := sql app.db {
93
-
select from Post where id == id limit 1
94
-
} or { [] }
95
-
if posts.len != 1 {
96
-
return none
97
-
}
98
-
return posts[0]
99
-
}
100
-
101
-
pub fn (app &App) get_post_by_author_and_timestamp(author_id int, timestamp time.Time) ?Post {
102
-
posts := sql app.db {
103
-
select from Post where author_id == author_id && posted_at == timestamp order by posted_at desc limit 1
104
-
} or { [] }
105
-
if posts.len == 0 {
106
-
return none
107
-
}
108
-
return posts[0]
109
-
}
110
-
111
-
pub fn (app &App) get_posts_with_tag(tag string, offset int) []Post {
112
-
posts := sql app.db {
113
-
select from Post where body like '%#(${tag})%' order by posted_at desc limit 10 offset offset
114
-
} or { [] }
115
-
return posts
116
-
}
117
-
118
-
pub fn (app &App) get_pinned_posts() []Post {
119
-
posts := sql app.db {
120
-
select from Post where pinned == true
121
-
} or { [] }
122
-
return posts
123
-
}
124
-
125
-
pub fn (app &App) whoami(mut ctx Context) ?User {
126
-
token := ctx.get_cookie('token') or { return none }.trim_space()
127
-
if token == '' {
128
-
return none
129
-
}
130
-
if user := app.get_user_by_token(ctx, token) {
131
-
if user.username == '' || user.id == 0 {
132
-
eprintln('a user had a token for the blank user')
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
-
return user
144
-
} else {
145
-
eprintln('a user had a token for a non-existent user (this token may have been expired and left in cookies)')
146
-
// Clear token
147
-
ctx.set_cookie(
148
-
name: 'token'
149
-
value: ''
150
-
same_site: .same_site_none_mode
151
-
secure: true
152
-
path: '/'
153
-
)
154
-
return none
155
-
}
156
-
}
157
-
158
-
pub fn (app &App) get_unknown_user() User {
159
-
return User{
160
-
username: 'unknown'
161
-
}
162
-
}
163
-
164
-
pub fn (app &App) get_unknown_post() Post {
165
-
return Post{
166
-
title: 'unknown'
167
-
}
168
-
}
169
-
170
-
pub fn (app &App) logged_in_as(mut ctx Context, id int) bool {
171
-
if !ctx.is_logged_in() {
172
-
return false
173
-
}
174
-
return app.whoami(mut ctx) or { return false }.id == id
175
-
}
176
-
177
-
pub fn (app &App) does_user_like_post(user_id int, post_id int) bool {
178
-
likes := sql app.db {
179
-
select from Like where user_id == user_id && post_id == post_id
180
-
} or { [] }
181
-
if likes.len > 1 {
182
-
// something is very wrong lol
183
-
eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
184
-
} else if likes.len == 0 {
185
-
return false
186
-
}
187
-
return likes.first().is_like
188
-
}
189
-
190
-
pub fn (app &App) does_user_dislike_post(user_id int, post_id int) bool {
191
-
likes := sql app.db {
192
-
select from Like where user_id == user_id && post_id == post_id
193
-
} or { [] }
194
-
if likes.len > 1 {
195
-
// something is very wrong lol
196
-
eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
197
-
} else if likes.len == 0 {
198
-
return false
199
-
}
200
-
return !likes.first().is_like
201
-
}
202
-
203
-
pub fn (app &App) does_user_like_or_dislike_post(user_id int, post_id int) bool {
204
-
likes := sql app.db {
205
-
select from Like where user_id == user_id && post_id == post_id
206
-
} or { [] }
207
-
if likes.len > 1 {
208
-
// something is very wrong lol
209
-
eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
210
-
}
211
-
return likes.len == 1
212
-
}
213
-
214
-
pub fn (app &App) get_net_likes_for_post(post_id int) int {
215
-
// check cache
216
-
cache := sql app.db {
217
-
select from LikeCache where post_id == post_id limit 1
218
-
} or { [] }
219
-
220
-
mut likes := 0
221
-
222
-
if cache.len != 1 {
223
-
println('calculating net likes for post: ${post_id}')
224
-
// calculate
225
-
db_likes := sql app.db {
226
-
select from Like where post_id == post_id
227
-
} or { [] }
228
-
229
-
for like in db_likes {
230
-
if like.is_like {
231
-
likes++
232
-
} else {
233
-
likes--
234
-
}
235
-
}
236
-
237
-
// cache
238
-
cached := LikeCache{
239
-
post_id: post_id
240
-
likes: likes
241
-
}
242
-
sql app.db {
243
-
insert cached into LikeCache
244
-
} or {
245
-
eprintln('failed to cache like: ${cached}')
246
-
return likes
247
-
}
248
-
} else {
249
-
likes = cache.first().likes
250
-
}
251
-
252
-
return likes
253
-
}
254
-
255
-
pub fn (app &App) get_or_create_site_config() Site {
256
-
configs := sql app.db {
257
-
select from Site
258
-
} or { [] }
259
-
if configs.len == 0 {
260
-
// make the site config
261
-
site_config := Site{}
262
-
sql app.db {
263
-
insert site_config into Site
264
-
} or { panic('failed to create site config (${err})') }
265
-
} else if configs.len > 1 {
266
-
// this should never happen
267
-
panic('there are multiple site configs')
268
-
}
269
-
return configs[0]
270
-
}
271
-
272
-
@[inline]
273
-
pub fn (app &App) get_motd() string {
274
-
site := app.get_or_create_site_config()
275
-
return site.motd
276
-
}
277
-
278
-
pub fn (app &App) get_notifications_for(user_id int) []Notification {
279
-
notifications := sql app.db {
280
-
select from Notification where user_id == user_id
281
-
} or { [] }
282
-
return notifications
283
-
}
284
-
285
-
pub fn (app &App) get_notification_count(user_id int, limit int) int {
286
-
notifications := sql app.db {
287
-
select from Notification where user_id == user_id limit limit
288
-
} or { [] }
289
-
return notifications.len
290
-
}
291
-
292
-
pub fn (app &App) get_notification_count_for_frontend(user_id int, limit int) string {
293
-
count := app.get_notification_count(user_id, limit)
294
-
if count == 0 {
295
-
return ''
296
-
} else if count > limit {
297
-
return ' (${count}+)'
298
-
} else {
299
-
return ' (${count})'
300
-
}
301
-
}
302
-
303
-
pub fn (app &App) send_notification_to(user_id int, summary string, body string) {
304
-
notification := Notification{
305
-
user_id: user_id
306
-
summary: summary
307
-
body: body
308
-
}
309
-
sql app.db {
310
-
insert notification into Notification
311
-
} or {
312
-
eprintln('failed to send notification ${notification}')
313
-
}
314
-
}
315
-
316
-
// sends notifications to each user mentioned in a post
317
-
pub fn (app &App) process_post_mentions(post &Post) {
318
-
author := app.get_user_by_id(post.author_id) or {
319
-
eprintln('process_post_mentioned called on a post with a non-existent author: ${post}')
320
-
return
321
-
}
322
-
author_name := author.get_name()
323
-
324
-
// used so we do not send more than one notification per post
325
-
mut notified_users := []int{}
326
-
327
-
// notify who we replied to, if applicable
328
-
if post.replying_to != none {
329
-
if x := app.get_post_by_id(post.replying_to) {
330
-
app.send_notification_to(x.author_id, '${author_name} replied to your post!', '${author_name} replied to *(${x.id})')
331
-
}
332
-
}
333
-
334
-
// find mentions
335
-
mut re := regex.regex_opt('@\\(${app.config.user.username_pattern}\\)') or {
336
-
eprintln('failed to compile regex for process_post_mentions (err: ${err})')
337
-
return
338
-
}
339
-
matches := re.find_all_str(post.body)
340
-
for mat in matches {
341
-
println('found mentioned user: ${mat}')
342
-
username := mat#[2..-1]
343
-
user := app.get_user_by_name(username) or {
344
-
continue
345
-
}
346
-
347
-
if user.id in notified_users || user.id == author.id {
348
-
continue
349
-
}
350
-
notified_users << user.id
351
-
352
-
app.send_notification_to(
353
-
user.id,
354
-
'${author_name} mentioned you!',
355
-
'you have been mentioned in this post: *(${post.id})'
356
-
)
357
-
}
358
-
}
+1
-1
src/config.v
src/webapp/config.v
+1
-1
src/config.v
src/webapp/config.v
+1
-1
src/context.v
src/webapp/context.v
+1
-1
src/context.v
src/webapp/context.v
+9
src/database/database.v
+9
src/database/database.v
+45
src/database/like.v
+45
src/database/like.v
···
1
+
module database
2
+
3
+
import entity { Like, LikeCache }
4
+
5
+
// returns the net likes of the given post
6
+
pub fn (app &DatabaseAccess) get_net_likes_for_post(post_id int) int {
7
+
// check cache
8
+
cache := sql app.db {
9
+
select from LikeCache where post_id == post_id limit 1
10
+
} or { [] }
11
+
12
+
mut likes := 0
13
+
14
+
if cache.len != 1 {
15
+
println('calculating net likes for post: ${post_id}')
16
+
// calculate
17
+
db_likes := sql app.db {
18
+
select from Like where post_id == post_id
19
+
} or { [] }
20
+
21
+
for like in db_likes {
22
+
if like.is_like {
23
+
likes++
24
+
} else {
25
+
likes--
26
+
}
27
+
}
28
+
29
+
// cache
30
+
cached := LikeCache{
31
+
post_id: post_id
32
+
likes: likes
33
+
}
34
+
sql app.db {
35
+
insert cached into LikeCache
36
+
} or {
37
+
eprintln('failed to cache like: ${cached}')
38
+
return likes
39
+
}
40
+
} else {
41
+
likes = cache.first().likes
42
+
}
43
+
44
+
return likes
45
+
}
+33
src/database/notification.v
+33
src/database/notification.v
···
1
+
module database
2
+
3
+
import entity { Notification }
4
+
5
+
// get a list of notifications for the given user
6
+
pub fn (app &DatabaseAccess) get_notifications_for(user_id int) []Notification {
7
+
notifications := sql app.db {
8
+
select from Notification where user_id == user_id
9
+
} or { [] }
10
+
return notifications
11
+
}
12
+
13
+
// get the amount of notifications a user has, with a given limit
14
+
pub fn (app &DatabaseAccess) get_notification_count(user_id int, limit int) int {
15
+
notifications := sql app.db {
16
+
select from Notification where user_id == user_id limit limit
17
+
} or { [] }
18
+
return notifications.len
19
+
}
20
+
21
+
// send a notification to the given user
22
+
pub fn (app &DatabaseAccess) send_notification_to(user_id int, summary string, body string) {
23
+
notification := Notification{
24
+
user_id: user_id
25
+
summary: summary
26
+
body: body
27
+
}
28
+
sql app.db {
29
+
insert notification into Notification
30
+
} or {
31
+
eprintln('failed to send notification ${notification}')
32
+
}
33
+
}
+74
src/database/post.v
+74
src/database/post.v
···
1
+
module database
2
+
3
+
import time
4
+
import entity { Post, Like, LikeCache }
5
+
6
+
// get a post by its id, returns none if it does not exist
7
+
pub fn (app &DatabaseAccess) get_post_by_id(id int) ?Post {
8
+
posts := sql app.db {
9
+
select from Post where id == id limit 1
10
+
} or { [] }
11
+
if posts.len != 1 {
12
+
return none
13
+
}
14
+
return posts[0]
15
+
}
16
+
17
+
// get a post by its author and timestamp, returns none if it does not exist
18
+
pub fn (app &DatabaseAccess) get_post_by_author_and_timestamp(author_id int, timestamp time.Time) ?Post {
19
+
posts := sql app.db {
20
+
select from Post where author_id == author_id && posted_at == timestamp order by posted_at desc limit 1
21
+
} or { [] }
22
+
if posts.len == 0 {
23
+
return none
24
+
}
25
+
return posts[0]
26
+
}
27
+
28
+
// get a list of posts given a tag. this performs sql string operations and
29
+
// probably is not very efficient, use sparingly.
30
+
pub fn (app &DatabaseAccess) get_posts_with_tag(tag string, offset int) []Post {
31
+
posts := sql app.db {
32
+
select from Post where body like '%#(${tag})%' order by posted_at desc limit 10 offset offset
33
+
} or { [] }
34
+
return posts
35
+
}
36
+
37
+
// returns a list of all pinned posts
38
+
pub fn (app &DatabaseAccess) get_pinned_posts() []Post {
39
+
posts := sql app.db {
40
+
select from Post where pinned == true
41
+
} or { [] }
42
+
return posts
43
+
}
44
+
45
+
// returns a list of the ten most recent posts.
46
+
pub fn (app &DatabaseAccess) get_recent_posts() []Post {
47
+
posts := sql app.db {
48
+
select from Post order by posted_at desc limit 10
49
+
} or { [] }
50
+
return posts
51
+
}
52
+
53
+
// returns a list of the ten most liked posts.
54
+
// TODO: make this time-gated (i.e, top ten liked posts of the day)
55
+
pub fn (app &DatabaseAccess) get_popular_posts() []Post {
56
+
cached_likes := sql app.db {
57
+
select from LikeCache order by likes desc limit 10
58
+
} or { [] }
59
+
posts := cached_likes.map(fn [app] (it LikeCache) Post {
60
+
return app.get_post_by_id(it.post_id) or {
61
+
eprintln('cached like ${it} does not have a post related to it (from get_popular_posts)')
62
+
return Post{}
63
+
}
64
+
}).filter(it.id != 0)
65
+
return posts
66
+
}
67
+
68
+
// returns a list of all posts from a user in descending order of date
69
+
pub fn (app &DatabaseAccess) 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
+
}
+20
src/database/site.v
+20
src/database/site.v
···
1
+
module database
2
+
3
+
import entity { Site }
4
+
5
+
pub fn (app &DatabaseAccess) get_or_create_site_config() Site {
6
+
configs := sql app.db {
7
+
select from Site
8
+
} or { [] }
9
+
if configs.len == 0 {
10
+
// make the site config
11
+
site_config := Site{}
12
+
sql app.db {
13
+
insert site_config into Site
14
+
} or { panic('failed to create site config (${err})') }
15
+
} else if configs.len > 1 {
16
+
// this should never happen
17
+
panic('there are multiple site configs')
18
+
}
19
+
return configs[0]
20
+
}
+171
src/database/user.v
+171
src/database/user.v
···
1
+
module database
2
+
3
+
import entity { User, Notification, Like, Post }
4
+
5
+
// creates a new user and returns their struct after creation.
6
+
pub fn (app &DatabaseAccess) new_user(user User) ?User {
7
+
sql app.db {
8
+
insert user into User
9
+
} or {
10
+
eprintln('failed to insert user ${user}')
11
+
return none
12
+
}
13
+
14
+
println('reg: ${user.username}')
15
+
16
+
return app.get_user_by_name(user.username)
17
+
}
18
+
19
+
// updates the given user's username, returns true if this succeeded and false
20
+
// otherwise.
21
+
pub fn (app &DatabaseAccess) set_username(user_id int, new_username string) bool {
22
+
sql app.db {
23
+
update User set username = new_username where id == user_id
24
+
} or {
25
+
eprintln('failed to update username for ${user_id}')
26
+
return false
27
+
}
28
+
return true
29
+
}
30
+
31
+
// updates the given user's password, returns true if this succeeded and false
32
+
// otherwise.
33
+
pub fn (app &DatabaseAccess) set_password(user_id int, hashed_new_password string) bool {
34
+
sql app.db {
35
+
update User set password = hashed_new_password where id == user_id
36
+
} or {
37
+
eprintln('failed to update password for ${user_id}')
38
+
return false
39
+
}
40
+
return true
41
+
}
42
+
43
+
// updates the given user's nickname, returns true if this succeeded and false
44
+
// otherwise.
45
+
pub fn (app &DatabaseAccess) set_nickname(user_id int, new_nickname ?string) bool {
46
+
sql app.db {
47
+
update User set nickname = new_nickname where id == user_id
48
+
} or {
49
+
eprintln('failed to update nickname for ${user_id}')
50
+
return false
51
+
}
52
+
return true
53
+
}
54
+
55
+
// updates the given user's muted status, returns true if this succeeded and
56
+
// false otherwise.
57
+
pub fn (app &DatabaseAccess) set_muted(user_id int, muted bool) bool {
58
+
sql app.db {
59
+
update User set muted = muted where id == user_id
60
+
} or {
61
+
eprintln('failed to update muted status for ${user_id}')
62
+
return false
63
+
}
64
+
return true
65
+
}
66
+
67
+
// updates the given user's theme url, returns true if this succeeded and false
68
+
// otherwise.
69
+
pub fn (app &DatabaseAccess) set_theme(user_id int, theme ?string) bool {
70
+
sql app.db {
71
+
update User set theme = theme where id == user_id
72
+
} or {
73
+
eprintln('failed to update theme url for ${user_id}')
74
+
return false
75
+
}
76
+
return true
77
+
}
78
+
79
+
// updates the given user's pronouns, returns true if this succeeded and false
80
+
// otherwise.
81
+
pub fn (app &DatabaseAccess) set_pronouns(user_id int, pronouns string) bool {
82
+
sql app.db {
83
+
update User set pronouns = pronouns where id == user_id
84
+
} or {
85
+
eprintln('failed to update pronouns for ${user_id}')
86
+
return false
87
+
}
88
+
return true
89
+
}
90
+
91
+
// updates the given user's bio, returns true if this succeeded and false
92
+
// otherwise.
93
+
pub fn (app &DatabaseAccess) set_bio(user_id int, bio string) bool {
94
+
sql app.db {
95
+
update User set bio = bio where id == user_id
96
+
} or {
97
+
eprintln('failed to update bio for ${user_id}')
98
+
return false
99
+
}
100
+
return true
101
+
}
102
+
103
+
// get a user by their username, returns none if the user was not found.
104
+
pub fn (app &DatabaseAccess) get_user_by_name(username string) ?User {
105
+
users := sql app.db {
106
+
select from User where username == username
107
+
} or { [] }
108
+
if users.len != 1 {
109
+
return none
110
+
}
111
+
return users[0]
112
+
}
113
+
114
+
// get a user by their id, returns none if the user was not found.
115
+
pub fn (app &DatabaseAccess) get_user_by_id(id int) ?User {
116
+
users := sql app.db {
117
+
select from User where id == id
118
+
} or { [] }
119
+
if users.len != 1 {
120
+
return none
121
+
}
122
+
return users[0]
123
+
}
124
+
125
+
// returns all users
126
+
pub fn (app &DatabaseAccess) get_users() []User {
127
+
users := sql app.db {
128
+
select from User
129
+
} or { [] }
130
+
return users
131
+
}
132
+
133
+
// returns true if a user likes the given post
134
+
pub fn (app &DatabaseAccess) does_user_like_post(user_id int, post_id int) bool {
135
+
likes := sql app.db {
136
+
select from Like where user_id == user_id && post_id == post_id
137
+
} or { [] }
138
+
if likes.len > 1 {
139
+
// something is very wrong lol
140
+
eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
141
+
} else if likes.len == 0 {
142
+
return false
143
+
}
144
+
return likes.first().is_like
145
+
}
146
+
147
+
// returns true if a user dislikes the given post
148
+
pub fn (app &DatabaseAccess) does_user_dislike_post(user_id int, post_id int) bool {
149
+
likes := sql app.db {
150
+
select from Like where user_id == user_id && post_id == post_id
151
+
} or { [] }
152
+
if likes.len > 1 {
153
+
// something is very wrong lol
154
+
eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
155
+
} else if likes.len == 0 {
156
+
return false
157
+
}
158
+
return !likes.first().is_like
159
+
}
160
+
161
+
// returns true if a user likes or dislikes the given post
162
+
pub fn (app &DatabaseAccess) does_user_like_or_dislike_post(user_id int, post_id int) bool {
163
+
likes := sql app.db {
164
+
select from Like where user_id == user_id && post_id == post_id
165
+
} or { [] }
166
+
if likes.len > 1 {
167
+
// something is very wrong lol
168
+
eprintln('a user somehow got two or more likes on the same post (user: ${user_id}, post: ${post_id})')
169
+
}
170
+
return likes.len == 1
171
+
}
+2
-1
src/main.v
+2
-1
src/main.v
···
5
5
import auth
6
6
import entity
7
7
import os
8
+
import webapp { App, Context, StringValidator }
8
9
9
10
fn init_db(db pg.DB) ! {
10
11
sql db {
···
18
19
}
19
20
20
21
fn main() {
21
-
config := load_config_from(os.args[1])
22
+
config := webapp.load_config_from(os.args[1])
22
23
23
24
println('-> connecting to db...')
24
25
mut db := pg.connect(pg.Config{
+15
-15
src/pages.v
src/webapp/pages.v
+15
-15
src/pages.v
src/webapp/pages.v
···
1
-
module main
1
+
module webapp
2
2
3
3
import veb
4
4
import entity { User }
···
9
9
recent_posts := app.get_recent_posts()
10
10
pinned_posts := app.get_pinned_posts()
11
11
motd := app.get_motd()
12
-
return $veb.html()
12
+
return $veb.html('../templates/index.html')
13
13
}
14
14
15
15
fn (mut app App) login(mut ctx Context) veb.Result {
16
16
ctx.title = 'login to ${app.config.instance.name}'
17
17
user := app.whoami(mut ctx) or { User{} }
18
-
return $veb.html()
18
+
return $veb.html('../templates/login.html')
19
19
}
20
20
21
21
fn (mut app App) register(mut ctx Context) veb.Result {
22
22
ctx.title = 'register for ${app.config.instance.name}'
23
23
user := app.whoami(mut ctx) or { User{} }
24
-
return $veb.html()
24
+
return $veb.html('../templates/register.html')
25
25
}
26
26
27
27
fn (mut app App) me(mut ctx Context) veb.Result {
···
39
39
return ctx.redirect('/login')
40
40
}
41
41
ctx.title = '${app.config.instance.name} - settings'
42
-
return $veb.html()
42
+
return $veb.html('../templates/settings.html')
43
43
}
44
44
45
45
fn (mut app App) admin(mut ctx Context) veb.Result {
46
46
ctx.title = '${app.config.instance.name} dashboard'
47
47
user := app.whoami(mut ctx) or { User{} }
48
-
return $veb.html()
48
+
return $veb.html('../templates/admin.html')
49
49
}
50
50
51
51
fn (mut app App) inbox(mut ctx Context) veb.Result {
···
55
55
}
56
56
ctx.title = '${app.config.instance.name} inbox'
57
57
notifications := app.get_notifications_for(user.id)
58
-
return $veb.html()
58
+
return $veb.html('../templates/inbox.html')
59
59
}
60
60
61
61
fn (mut app App) logout(mut ctx Context) veb.Result {
···
64
64
return ctx.redirect('/login')
65
65
}
66
66
ctx.title = '${app.config.instance.name} logout'
67
-
return $veb.html()
67
+
return $veb.html('../templates/logout.html')
68
68
}
69
69
70
70
@['/user/:username']
···
75
75
return ctx.redirect('/')
76
76
}
77
77
ctx.title = '${app.config.instance.name} - ${user.get_name()}'
78
-
return $veb.html()
78
+
return $veb.html('../templates/user.html')
79
79
}
80
80
81
81
@['/post/:post_id']
···
99
99
}
100
100
}
101
101
102
-
return $veb.html()
102
+
return $veb.html('../templates/post.html')
103
103
}
104
104
105
105
@['/post/:post_id/edit']
···
117
117
return ctx.redirect('/post/${post_id}')
118
118
}
119
119
ctx.title = '${app.config.instance.name} - editing ${post.title}'
120
-
return $veb.html()
120
+
return $veb.html('../templates/edit.html')
121
121
}
122
122
123
123
@['/post/:post_id/reply']
···
137
137
ctx.error('no such post')
138
138
return ctx.redirect('/')
139
139
}
140
-
return $veb.html('templates/new_post.html')
140
+
return $veb.html('../templates/new_post.html')
141
141
}
142
142
143
143
@['/post/new']
···
150
150
replying := false
151
151
replying_to := 0
152
152
replying_to_user := User{}
153
-
return $veb.html('templates/new_post.html')
153
+
return $veb.html('../templates/new_post.html')
154
154
}
155
155
156
156
@['/tag/:tag']
···
161
161
}
162
162
ctx.title = '${app.config.instance.name} - #${tag}'
163
163
offset := 0
164
-
return $veb.html()
164
+
return $veb.html('../templates/tag.html')
165
165
}
166
166
167
167
@['/tag/:tag/:offset']
···
171
171
return ctx.redirect('/login')
172
172
}
173
173
ctx.title = '${app.config.instance.name} - #${tag}'
174
-
return $veb.html('templates/tag.html')
174
+
return $veb.html('../templates/tag.html')
175
175
}
+2
src/templates/post.html
+2
src/templates/post.html
+8
-1
src/templates/user.html
+8
-1
src/templates/user.html
···
56
56
@if viewing.bio != ''
57
57
<div>
58
58
<h2>bio:</h2>
59
-
<p>@viewing.bio</p>
59
+
<pre id="bio">@viewing.bio</pre>
60
60
</div>
61
61
@end
62
62
···
105
105
@end
106
106
</form>
107
107
</div>
108
+
@end
109
+
110
+
@if viewing.bio != ''
111
+
<script src="/static/js/render_body.js"></script>
112
+
<script>
113
+
render_body('bio')
114
+
</script>
108
115
@end
109
116
110
117
@include 'partial/footer.html'
+1
-1
src/validation.v
src/webapp/validation.v
+1
-1
src/validation.v
src/webapp/validation.v
+156
src/webapp/app.v
+156
src/webapp/app.v
···
1
+
module webapp
2
+
3
+
import veb
4
+
import db.pg
5
+
import regex
6
+
import auth
7
+
import entity { LikeCache, Like, Post, Site, User, Notification }
8
+
import database { DatabaseAccess }
9
+
10
+
pub struct App {
11
+
veb.StaticHandler
12
+
DatabaseAccess
13
+
pub:
14
+
config Config
15
+
pub 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 a user by their token, returns none if the user was not found.
30
+
pub fn (app &App) get_user_by_token(ctx &Context, token string) ?User {
31
+
user_token := app.auth.find_token(token, ctx.ip()) or {
32
+
eprintln('no such user corresponding to token')
33
+
return none
34
+
}
35
+
return app.get_user_by_id(user_token.user_id)
36
+
}
37
+
38
+
// returns the current logged in user, or none if the user is not logged in.
39
+
pub fn (app &App) whoami(mut ctx Context) ?User {
40
+
token := ctx.get_cookie('token') or { return none }.trim_space()
41
+
if token == '' {
42
+
return none
43
+
}
44
+
if user := app.get_user_by_token(ctx, token) {
45
+
if user.username == '' || user.id == 0 {
46
+
eprintln('a user had a token for the blank user')
47
+
// Clear token
48
+
ctx.set_cookie(
49
+
name: 'token'
50
+
value: ''
51
+
same_site: .same_site_none_mode
52
+
secure: true
53
+
path: '/'
54
+
)
55
+
return none
56
+
}
57
+
return user
58
+
} else {
59
+
eprintln('a user had a token for a non-existent user (this token may have been expired and left in cookies)')
60
+
// Clear token
61
+
ctx.set_cookie(
62
+
name: 'token'
63
+
value: ''
64
+
same_site: .same_site_none_mode
65
+
secure: true
66
+
path: '/'
67
+
)
68
+
return none
69
+
}
70
+
}
71
+
72
+
// get a user representing an unknown user
73
+
pub fn (app &App) get_unknown_user() User {
74
+
return User{
75
+
username: 'unknown'
76
+
}
77
+
}
78
+
79
+
// get a post representing an unknown post
80
+
pub fn (app &App) get_unknown_post() Post {
81
+
return Post{
82
+
title: 'unknown'
83
+
}
84
+
}
85
+
86
+
// returns true if the user is logged in as the provided user id.
87
+
pub fn (app &App) logged_in_as(mut ctx Context, id int) bool {
88
+
if !ctx.is_logged_in() {
89
+
return false
90
+
}
91
+
return app.whoami(mut ctx) or { return false }.id == id
92
+
}
93
+
94
+
// get the site's message of the day.
95
+
@[inline]
96
+
pub fn (app &App) get_motd() string {
97
+
site := app.get_or_create_site_config()
98
+
return site.motd
99
+
}
100
+
101
+
// get the notification count for a given user, formatted for usage on the
102
+
// frontend.
103
+
pub fn (app &App) get_notification_count_for_frontend(user_id int, limit int) string {
104
+
count := app.get_notification_count(user_id, limit)
105
+
if count == 0 {
106
+
return ''
107
+
} else if count > limit {
108
+
return ' (${count}+)'
109
+
} else {
110
+
return ' (${count})'
111
+
}
112
+
}
113
+
114
+
// processes a post's body to send notifications for mentions or replies.
115
+
pub fn (app &App) process_post_mentions(post &Post) {
116
+
author := app.get_user_by_id(post.author_id) or {
117
+
eprintln('process_post_mentioned called on a post with a non-existent author: ${post}')
118
+
return
119
+
}
120
+
author_name := author.get_name()
121
+
122
+
// used so we do not send more than one notification per post
123
+
mut notified_users := []int{}
124
+
125
+
// notify who we replied to, if applicable
126
+
if post.replying_to != none {
127
+
if x := app.get_post_by_id(post.replying_to) {
128
+
app.send_notification_to(x.author_id, '${author_name} replied to your post!', '${author_name} replied to *(${x.id})')
129
+
}
130
+
}
131
+
132
+
// find mentions
133
+
mut re := regex.regex_opt('@\\(${app.config.user.username_pattern}\\)') or {
134
+
eprintln('failed to compile regex for process_post_mentions (err: ${err})')
135
+
return
136
+
}
137
+
matches := re.find_all_str(post.body)
138
+
for mat in matches {
139
+
println('found mentioned user: ${mat}')
140
+
username := mat#[2..-1]
141
+
user := app.get_user_by_name(username) or {
142
+
continue
143
+
}
144
+
145
+
if user.id in notified_users || user.id == author.id {
146
+
continue
147
+
}
148
+
notified_users << user.id
149
+
150
+
app.send_notification_to(
151
+
user.id,
152
+
'${author_name} mentioned you!',
153
+
'you have been mentioned in this post: *(${post.id})'
154
+
)
155
+
}
156
+
}