| 1 | // Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved. |
| 2 | // Use of this source code is governed by a GPL license that can be found in the LICENSE file. |
| 3 | module main |
| 4 | |
| 5 | import veb |
| 6 | import validation |
| 7 | |
| 8 | struct DiscussionWithUser { |
| 9 | item Discussion |
| 10 | user User |
| 11 | } |
| 12 | |
| 13 | struct DiscussionCommentWithUser { |
| 14 | item DiscussionComment |
| 15 | user User |
| 16 | } |
| 17 | |
| 18 | @['/:username/:repo_name/discussions'] |
| 19 | pub fn (mut app App) handle_get_repo_discussions(mut ctx Context, username string, repo_name string) veb.Result { |
| 20 | repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } |
| 21 | if !app.has_user_repo_read_access(ctx, ctx.user.id, repo.id) && !repo.is_public { |
| 22 | return ctx.not_found() |
| 23 | } |
| 24 | discussions := app.list_repo_discussions(repo.id) |
| 25 | mut rows := []DiscussionWithUser{} |
| 26 | for d in discussions { |
| 27 | u := app.get_user_by_id(d.author_id) or { continue } |
| 28 | rows << DiscussionWithUser{ |
| 29 | item: d |
| 30 | user: u |
| 31 | } |
| 32 | } |
| 33 | return $veb.html('templates/discussions.html') |
| 34 | } |
| 35 | |
| 36 | @['/:username/:repo_name/discussions/new'] |
| 37 | pub fn (mut app App) new_discussion(mut ctx Context, username string, repo_name string) veb.Result { |
| 38 | if !ctx.logged_in { |
| 39 | return ctx.redirect_to_login() |
| 40 | } |
| 41 | repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } |
| 42 | if !app.has_user_repo_read_access(ctx, ctx.user.id, repo.id) && !repo.is_public { |
| 43 | return ctx.not_found() |
| 44 | } |
| 45 | return $veb.html('templates/new/discussion.html') |
| 46 | } |
| 47 | |
| 48 | @['/:username/:repo_name/discussions'; post] |
| 49 | pub fn (mut app App) handle_create_discussion(mut ctx Context, username string, repo_name string) veb.Result { |
| 50 | if !ctx.logged_in { |
| 51 | return ctx.redirect_to_login() |
| 52 | } |
| 53 | repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } |
| 54 | title := ctx.form['title'] |
| 55 | body := ctx.form['body'] |
| 56 | category := ctx.form['category'] |
| 57 | if validation.is_string_empty(title) { |
| 58 | return ctx.redirect('/${username}/${repo_name}/discussions/new') |
| 59 | } |
| 60 | cat := if category in ['general', 'qa', 'announcement', 'idea'] { category } else { 'general' } |
| 61 | id := app.add_discussion(repo.id, ctx.user.id, title, body, cat) or { |
| 62 | ctx.error('Could not create discussion') |
| 63 | return ctx.redirect('/${username}/${repo_name}/discussions/new') |
| 64 | } |
| 65 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 66 | } |
| 67 | |
| 68 | @['/:username/:repo_name/discussions/:id'] |
| 69 | pub fn (mut app App) view_discussion(mut ctx Context, username string, repo_name string, id string) veb.Result { |
| 70 | repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } |
| 71 | if !app.has_user_repo_read_access(ctx, ctx.user.id, repo.id) && !repo.is_public { |
| 72 | return ctx.not_found() |
| 73 | } |
| 74 | discussion := app.find_discussion(id.int()) or { return ctx.not_found() } |
| 75 | if discussion.repo_id != repo.id { |
| 76 | return ctx.not_found() |
| 77 | } |
| 78 | author := app.get_user_by_id(discussion.author_id) or { return ctx.not_found() } |
| 79 | raw_comments := app.get_discussion_comments(discussion.id) |
| 80 | mut comments := []DiscussionCommentWithUser{} |
| 81 | for c in raw_comments { |
| 82 | u := app.get_user_by_id(c.author_id) or { continue } |
| 83 | comments << DiscussionCommentWithUser{ |
| 84 | item: c |
| 85 | user: u |
| 86 | } |
| 87 | } |
| 88 | is_owner := ctx.logged_in |
| 89 | && (repo.user_id == ctx.user.id || discussion.author_id == ctx.user.id) |
| 90 | return $veb.html('templates/discussion.html') |
| 91 | } |
| 92 | |
| 93 | @['/:username/:repo_name/discussions/:id/comments'; post] |
| 94 | pub fn (mut app App) handle_add_discussion_comment(mut ctx Context, username string, repo_name string, id string) veb.Result { |
| 95 | if !ctx.logged_in { |
| 96 | return ctx.redirect_to_login() |
| 97 | } |
| 98 | repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } |
| 99 | discussion := app.find_discussion(id.int()) or { return ctx.not_found() } |
| 100 | if discussion.repo_id != repo.id || discussion.is_locked { |
| 101 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 102 | } |
| 103 | text := ctx.form['text'] |
| 104 | if validation.is_string_empty(text) { |
| 105 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 106 | } |
| 107 | app.add_discussion_comment(discussion.id, ctx.user.id, text) or {} |
| 108 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 109 | } |
| 110 | |
| 111 | @['/:username/:repo_name/discussions/:id/lock'; post] |
| 112 | pub fn (mut app App) handle_lock_discussion(mut ctx Context, username string, repo_name string, id string) veb.Result { |
| 113 | if !ctx.logged_in { |
| 114 | return ctx.redirect_to_login() |
| 115 | } |
| 116 | repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } |
| 117 | discussion := app.find_discussion(id.int()) or { return ctx.not_found() } |
| 118 | if discussion.repo_id != repo.id { |
| 119 | return ctx.not_found() |
| 120 | } |
| 121 | if repo.user_id != ctx.user.id { |
| 122 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 123 | } |
| 124 | app.set_discussion_lock(discussion.id, !discussion.is_locked) or {} |
| 125 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 126 | } |
| 127 | |
| 128 | @['/:username/:repo_name/discussions/:id/delete'; post] |
| 129 | pub fn (mut app App) handle_delete_discussion(mut ctx Context, username string, repo_name string, id string) veb.Result { |
| 130 | if !ctx.logged_in { |
| 131 | return ctx.redirect_to_login() |
| 132 | } |
| 133 | repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } |
| 134 | discussion := app.find_discussion(id.int()) or { return ctx.not_found() } |
| 135 | if discussion.repo_id != repo.id { |
| 136 | return ctx.not_found() |
| 137 | } |
| 138 | if repo.user_id != ctx.user.id && discussion.author_id != ctx.user.id { |
| 139 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 140 | } |
| 141 | app.delete_discussion(discussion.id) or {} |
| 142 | return ctx.redirect('/${username}/${repo_name}/discussions') |
| 143 | } |
| 144 | |
| 145 | @['/:username/:repo_name/discussions/:id/answer/:cid'; post] |
| 146 | pub fn (mut app App) handle_mark_answer(mut ctx Context, username string, repo_name string, id string, cid string) veb.Result { |
| 147 | if !ctx.logged_in { |
| 148 | return ctx.redirect_to_login() |
| 149 | } |
| 150 | repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } |
| 151 | discussion := app.find_discussion(id.int()) or { return ctx.not_found() } |
| 152 | if discussion.repo_id != repo.id { |
| 153 | return ctx.not_found() |
| 154 | } |
| 155 | if discussion.author_id != ctx.user.id && repo.user_id != ctx.user.id { |
| 156 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 157 | } |
| 158 | app.mark_discussion_answer(discussion.id, cid.int()) or {} |
| 159 | return ctx.redirect('/${username}/${repo_name}/discussions/${id}') |
| 160 | } |
| 161 | |