+15
src/database/database.v
+15
src/database/database.v
···
2
2
module database
3
3
4
4
import db.pg
5
+
import entity { User, Post }
5
6
6
7
// DatabaseAccess handles all interactions with the database.
7
8
pub struct DatabaseAccess {
8
9
pub mut:
9
10
db pg.DB
10
11
}
12
+
13
+
// get_unknown_user returns a user representing an unknown user
14
+
pub fn (app &DatabaseAccess) get_unknown_user() User {
15
+
return User{
16
+
username: 'unknown'
17
+
}
18
+
}
19
+
20
+
// get_unknown_post returns a post representing an unknown post
21
+
pub fn (app &DatabaseAccess) get_unknown_post() Post {
22
+
return Post{
23
+
title: 'unknown'
24
+
}
25
+
}
+41
-1
src/database/post.v
+41
-1
src/database/post.v
···
1
1
module database
2
2
3
3
import time
4
-
import entity { Post, Like, LikeCache }
4
+
import entity { Post, User, Like, LikeCache }
5
5
6
6
// add_post adds a new post to the database, returns true if this succeeded and
7
7
// false otherwise.
···
130
130
}
131
131
return true
132
132
}
133
+
134
+
////// searching //////
135
+
136
+
// PostSearchResult represents a search result for a post.
137
+
pub struct PostSearchResult {
138
+
pub mut:
139
+
post Post
140
+
author User
141
+
}
142
+
143
+
@[inline]
144
+
pub fn PostSearchResult.from_post(app &DatabaseAccess, post &Post) PostSearchResult {
145
+
return PostSearchResult{
146
+
post: post
147
+
author: app.get_user_by_id(post.author_id) or { app.get_unknown_user() }
148
+
}
149
+
}
150
+
151
+
@[inline]
152
+
pub fn PostSearchResult.from_post_list(app &DatabaseAccess, posts []Post) []PostSearchResult {
153
+
mut results := []PostSearchResult{
154
+
cap: posts.len,
155
+
len: posts.len
156
+
}
157
+
for index, post in posts {
158
+
results[index] = PostSearchResult.from_post(app, post)
159
+
}
160
+
return results
161
+
}
162
+
163
+
// search_for_post searches for posts matching the given query.
164
+
// todo: query options/filters, such as user:beep, !excluded-text, etc
165
+
pub fn (app &DatabaseAccess) search_for_posts(query string, limit int, offset int) []PostSearchResult {
166
+
println('searching, q=${query},l=${limit},o=${offset}')
167
+
posts := sql app.db {
168
+
select from Post where title like '%${query}%' order by posted_at desc limit limit offset offset
169
+
} or { [] }
170
+
println('search results: ${posts.len}')
171
+
return PostSearchResult.from_post_list(app, posts)
172
+
}
+2
-2
src/database/user.v
+2
-2
src/database/user.v
···
186
186
187
187
// delete posts and their likes
188
188
posts_from_this_user := sql app.db {
189
-
select from Post where author_id == id
189
+
select from Post where author_id == user_id
190
190
} or { [] }
191
191
192
192
for post in posts_from_this_user {
···
199
199
}
200
200
201
201
sql app.db {
202
-
delete from Post where author_id == id
202
+
delete from Post where author_id == user_id
203
203
} or {
204
204
eprintln('failed to delete posts by deleting user: ${user_id}')
205
205
}
+9
src/static/js/search.js
+9
src/static/js/search.js
+1
src/static/js/user_utils.js
+1
src/static/js/user_utils.js
···
1
+
const get_display_name = user => user.nickname == undefined ? user.username : user.nickname
+5
-2
src/templates/partial/header.html
+5
-2
src/templates/partial/header.html
···
22
22
<body>
23
23
24
24
<header>
25
+
@if ctx.is_logged_in()
26
+
<a href="/me">@@@user.get_name()</a>
27
+
-
28
+
@end
29
+
25
30
@if app.config.dev_mode
26
31
<span><strong>dev mode</strong></span>
27
32
-
···
31
36
-
32
37
33
38
@if ctx.is_logged_in()
34
-
<a href="/me">profile</a>
35
-
-
36
39
<a href="/inbox">inbox@{app.get_notification_count_for_frontend(user.id, 99)}</a>
37
40
@else
38
41
<a href="/login">log in</a>
+55
src/templates/search.html
+55
src/templates/search.html
···
1
+
@include 'partial/header.html'
2
+
3
+
<script src="/static/js/user_utils.js"></script>
4
+
<script src="/static/js/search.js"></script>
5
+
6
+
<h1>search</h1>
7
+
8
+
<div>
9
+
<input type="text" name="query" id="query">
10
+
<button id="search">search</button>
11
+
</div>
12
+
13
+
<br>
14
+
15
+
<div id="results">
16
+
</div>
17
+
18
+
<script>
19
+
const query = document.getElementById("query")
20
+
const results = document.getElementById("results")
21
+
22
+
document.getElementById("search").addEventListener("click", async () => {
23
+
results.innerHTML = '' // yeet the children!
24
+
const search_results = await search(query.value, 10, 0)
25
+
if (search_results.length >= 0) {
26
+
for (result of search_results) {
27
+
// same as components/post_mini.html except js
28
+
const element = document.createElement('div')
29
+
element.classList.add('post', 'post-mini')
30
+
const p = document.createElement('p')
31
+
32
+
const user_link = document.createElement('a')
33
+
user_link.href = '/user/' + result.author.username
34
+
const user_text = document.createElement('strong')
35
+
user_text.innerText = get_display_name(result.author)
36
+
user_link.appendChild(user_text)
37
+
p.appendChild(user_link)
38
+
39
+
p.innerText += ': '
40
+
41
+
const post_link = document.createElement('a')
42
+
post_link.href = '/post/' + result.post.id
43
+
post_link.innerText = result.post.title
44
+
p.appendChild(post_link)
45
+
46
+
element.appendChild(p)
47
+
results.appendChild(element)
48
+
}
49
+
} else {
50
+
results.innerText = 'No results!'
51
+
}
52
+
})
53
+
</script>
54
+
55
+
@include 'partial/footer.html'
+22
src/webapp/api.v
+22
src/webapp/api.v
···
3
3
import veb
4
4
import auth
5
5
import entity { Like, LikeCache, Post, Site, User, Notification }
6
+
import database { PostSearchResult }
6
7
7
8
////// user //////
8
9
···
602
603
}
603
604
}
604
605
606
+
@['/api/post/get/<id>'; get]
607
+
fn (mut app App) api_post_get_post(mut ctx Context, id int) veb.Result {
608
+
post := app.get_post_by_id(id) or {
609
+
return ctx.text('no such post')
610
+
}
611
+
return ctx.json[Post](post)
612
+
}
613
+
605
614
////// site //////
606
615
607
616
@['/api/site/set_motd'; post]
···
625
634
return ctx.redirect('/')
626
635
}
627
636
}
637
+
638
+
////// Misc //////
639
+
640
+
pub const search_hard_limit := 50
641
+
642
+
@['/api/search'; get]
643
+
fn (mut app App) api_search(mut ctx Context, query string, limit int, offset int) veb.Result {
644
+
if limit >= search_hard_limit {
645
+
return ctx.text('limit exceeds hard limit (${search_hard_limit})')
646
+
}
647
+
posts := app.search_for_posts(query, limit, offset)
648
+
return ctx.json[[]PostSearchResult](posts)
649
+
}
-14
src/webapp/app.v
-14
src/webapp/app.v
···
71
71
}
72
72
}
73
73
74
-
// get_unknown_user returns a user representing an unknown user
75
-
pub 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
82
-
pub fn (app &App) get_unknown_post() Post {
83
-
return Post{
84
-
title: 'unknown'
85
-
}
86
-
}
87
-
88
74
// logged_in_as returns true if the user is logged in as the provided user id.
89
75
pub fn (app &App) logged_in_as(mut ctx Context, id int) bool {
90
76
if !ctx.is_logged_in() {
+10
src/webapp/pages.v
+10
src/webapp/pages.v
···
173
173
ctx.title = '${app.config.instance.name} - #${tag}'
174
174
return $veb.html('../templates/tag.html')
175
175
}
176
+
177
+
@['/search']
178
+
fn (mut app App) search(mut ctx Context) veb.Result {
179
+
user := app.whoami(mut ctx) or {
180
+
ctx.error('not logged in')
181
+
return ctx.redirect('/login')
182
+
}
183
+
ctx.title = '${app.config.instance.name} - search'
184
+
return $veb.html('../templates/search.html')
185
+
}