Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: go build -v ./...

- name: Format
run: test -z $(gofmt -l .)
run: test -z "$(gofmt -l .)"

- name: Test
run: go test -v ./...
62 changes: 40 additions & 22 deletions api/answer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"strconv"
"strings"
"time"

"github.com/cartabinaria/auth/pkg/httputil"
"github.com/cartabinaria/auth/pkg/middleware"
Expand All @@ -16,6 +17,24 @@ import (
"gorm.io/gorm"
)

type Answer struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`

Question uint `json:"question"`
Parent *uint `json:"parent"`

User string `json:"user"`
UserAvatarURL string `json:"user_avatar_url"`
Content string `json:"content"`
Upvotes uint32 `json:"upvotes"`
Downvotes uint32 `json:"downvotes"`
Replies []Answer `json:"replies"`
CanIDelete bool `json:"can_i_delete"`
IVoted VoteValue `json:"i_voted"`
}

const RepliesDepth = 2

// createVotesSubquery creates a reusable subquery for vote counting
Expand All @@ -39,7 +58,7 @@ func createPreloadFunction(votesSubquery *gorm.DB) func(db *gorm.DB) *gorm.DB {
}
}

func ConvertAnswerToAPI(answer models.Answer, isAdmin bool, requesterID int) (*models.AnswerResponse, error) {
func ConvertAnswerToAPI(answer models.Answer, isAdmin bool, requesterID int) (*Answer, error) {
db := util.GetDb()
usr, err := util.GetUserByID(db, answer.UserId)
if err != nil {
Expand Down Expand Up @@ -67,7 +86,7 @@ func ConvertAnswerToAPI(answer models.Answer, isAdmin bool, requesterID int) (*m
content = latestVersion.Content
}

var voteValue models.VoteValue
var voteValue VoteValue
var vote models.Vote
err = db.Where("answer = ? AND user_id = ?", answer.ID, requesterID).First(&vote).Error
if err != nil {
Expand All @@ -77,11 +96,11 @@ func ConvertAnswerToAPI(answer models.Answer, isAdmin bool, requesterID int) (*m
voteValue = VoteNone
}
} else {
voteValue = models.VoteValue(vote.Vote)
voteValue = VoteValue(vote.Vote)
}

// recursively convert replies
var replies []models.AnswerResponse
var replies []Answer
for _, reply := range answer.Replies {
reply, err := ConvertAnswerToAPI(reply, isAdmin, requesterID)
if err != nil {
Expand All @@ -90,7 +109,7 @@ func ConvertAnswerToAPI(answer models.Answer, isAdmin bool, requesterID int) (*m
replies = append(replies, *reply)
}

return &models.AnswerResponse{
return &Answer{
ID: answer.ID,
CreatedAt: answer.CreatedAt,
UpdatedAt: answer.UpdatedAt,
Expand All @@ -113,7 +132,7 @@ func ConvertAnswerToAPI(answer models.Answer, isAdmin bool, requesterID int) (*m
// @Tags answer
// @Param answerReq body models.PostAnswerRequest true "Answer data to insert"
// @Produce json
// @Success 200 {object} models.AnswerResponse
// @Success 200 {object} Answer
// @Failure 400 {object} httputil.ApiError
// @Router /answers [post]
func PostAnswerHandler(res http.ResponseWriter, req *http.Request) {
Expand Down Expand Up @@ -204,21 +223,20 @@ func PostAnswerHandler(res http.ResponseWriter, req *http.Request) {
username = user.Username
}

httputil.WriteData(res, http.StatusOK,
models.AnswerResponse{
ID: answer.ID,
CreatedAt: answer.CreatedAt,
UpdatedAt: answer.UpdatedAt,
Question: answer.Question,
Parent: answer.Parent,
User: username,
UserAvatarURL: avatar,
Content: version.Content,
Upvotes: answer.Upvotes,
Downvotes: answer.Downvotes,
CanIDelete: true,
IVoted: 0,
})
httputil.WriteData(res, http.StatusOK, Answer{
ID: answer.ID,
CreatedAt: answer.CreatedAt,
UpdatedAt: answer.UpdatedAt,
Question: answer.Question,
Parent: answer.Parent,
User: username,
UserAvatarURL: avatar,
Content: version.Content,
Upvotes: answer.Upvotes,
Downvotes: answer.Downvotes,
CanIDelete: true,
IVoted: 0,
})
}

// @Summary Delete an answer
Expand Down Expand Up @@ -354,7 +372,7 @@ func UpdateAnswerHandler(res http.ResponseWriter, req *http.Request) {
// @Param id path string true "Answer id"
// @Produce json
// @Success 200 {object} nil
// @Failure 400 {object} models.AnswerResponse[]
// @Failure 400 {object} Answer[]
// @Router /answers/{id}/replies [get]
func GetRepliesHandler(res http.ResponseWriter, req *http.Request) {
// Check method GET is used
Expand Down
12 changes: 7 additions & 5 deletions api/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package api

import (
"bytes"
"encoding/json"
"fmt"
"io"
"log/slog"
Expand All @@ -12,7 +11,6 @@ import (

"github.com/cartabinaria/auth/pkg/httputil"
"github.com/cartabinaria/auth/pkg/middleware"
"github.com/cartabinaria/polleg/models"
"github.com/cartabinaria/polleg/util"
"github.com/google/uuid"
"github.com/kataras/muxie"
Expand All @@ -35,6 +33,11 @@ const (
MAX_NUMBER = 100 // 100 images per user
)

type Image struct {
ID string `json:"id"`
URL string `json:"url"`
}

// checkFileType reads the first few bytes of a file and compares them with known signatures.
// As it takes a reader as input, the caller should ensure to reset the reader's position if needed (e.g., using Seek).
func checkFileType(reader io.Reader) (ImageType, error) {
Expand Down Expand Up @@ -91,7 +94,7 @@ func GetImageHandler(imagesPath string) http.HandlerFunc {
// @Accept multipart/form-data
// @Param image formData file true "Image to upload"
// @Produce json
// @Success 200 {object} models.ImageResponse
// @Success 200 {object} Image
// @Failure 400 {object} httputil.ApiError
// @Router /images [post]
func PostImageHandler(imagesPath string) http.HandlerFunc {
Expand Down Expand Up @@ -221,8 +224,7 @@ func PostImageHandler(imagesPath string) http.HandlerFunc {
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.ImageResponse{
httputil.WriteData(w, http.StatusOK, Image{
ID: uuid.String(),
URL: fullPath,
})
Expand Down
46 changes: 29 additions & 17 deletions api/question.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
package api

import (
"net/http"
"strconv"
"strings"

"github.com/cartabinaria/auth/pkg/httputil"
"github.com/cartabinaria/auth/pkg/middleware"
"github.com/cartabinaria/polleg/models"
"github.com/cartabinaria/polleg/util"
"github.com/kataras/muxie"
"golang.org/x/exp/slog"
"net/http"
"strconv"
"strings"
"time"

"gorm.io/gorm"
)

type Question struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-"`

Document string `json:"document"`
Start uint32 `json:"start"`
End uint32 `json:"end"`
Answers []Answer `json:"answers"`
}

// @Summary Get all answers given a question
// @Description Given a question ID, return the question and all its answers
// @Tags question
// @Param id path string true "Answer id"
// @Produce json
// @Success 200 {array} models.QuestionResponse
// @Success 200 {array} Question
// @Failure 400 {object} httputil.ApiError
// @Router /questions/{id} [get]
func GetQuestionHandler(res http.ResponseWriter, req *http.Request) {
Expand Down Expand Up @@ -72,7 +86,7 @@ func GetQuestionHandler(res http.ResponseWriter, req *http.Request) {
question.Answers = answers

// recursively convert answers
var responseAnswers []models.AnswerResponse
var responseAnswers []Answer
for _, ans := range question.Answers {
ans, err := ConvertAnswerToAPI(ans, isAdmin, requesterID)
if err != nil {
Expand All @@ -81,17 +95,15 @@ func GetQuestionHandler(res http.ResponseWriter, req *http.Request) {
responseAnswers = append(responseAnswers, *ans)
}

httputil.WriteData(res, http.StatusOK,
models.QuestionResponse{
ID: question.ID,
CreatedAt: question.CreatedAt,
UpdatedAt: question.UpdatedAt,
Document: question.Document,
Start: question.Start,
End: question.End,
Answers: responseAnswers,
},
)
httputil.WriteData(res, http.StatusOK, Question{
ID: question.ID,
CreatedAt: question.CreatedAt,
UpdatedAt: question.UpdatedAt,
Document: question.Document,
Start: question.Start,
End: question.End,
Answers: responseAnswers,
})
}

// @Summary Delete a question
Expand Down
40 changes: 28 additions & 12 deletions api/vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"strconv"
"time"

"github.com/cartabinaria/auth/pkg/httputil"
"github.com/cartabinaria/auth/pkg/middleware"
Expand All @@ -14,10 +15,25 @@ import (
"gorm.io/gorm/clause"
)

type VoteValue int8

type Pick struct {
Vote VoteValue `json:"vote"`
}

type Vote struct {
Answer uint `json:"answer"`
User string `json:"user"`
Vote int8 `json:"vote"`

CreatedAt time.Time
UpdatedAt time.Time
}

const (
VoteUp models.VoteValue = 1
VoteNone models.VoteValue = 0
VoteDown models.VoteValue = -1
VoteUp VoteValue = 1
VoteNone VoteValue = 0
VoteDown VoteValue = -1
)

// get given vote to an answer
Expand All @@ -37,13 +53,13 @@ func GetUserVote(res http.ResponseWriter, req *http.Request) {
return
}

var vote models.Vote
var vote Vote
if err = db.First(&vote, "answer = ? and \"user\" = ?", ansID, user.ID).Error; err != nil {
httputil.WriteError(res, http.StatusBadRequest, "the referenced vote does not exist")
return
}

httputil.WriteData(res, http.StatusOK, models.VoteResponse{
httputil.WriteData(res, http.StatusOK, Vote{
Answer: vote.Answer,
User: user.Username,
Vote: int8(vote.Vote),
Expand All @@ -57,7 +73,7 @@ func GetUserVote(res http.ResponseWriter, req *http.Request) {
// @Tags vote
// @Produce json
// @Param id path string true "code query parameter"
// @Success 200 {object} models.VoteResponse
// @Success 200 {object} Vote
// @Failure 400 {object} httputil.ApiError
// @Router /answer/{id}/vote [post]
func PostVote(res http.ResponseWriter, req *http.Request) {
Expand All @@ -80,14 +96,14 @@ func PostVote(res http.ResponseWriter, req *http.Request) {
return
}

var v models.PostVoteRequest
err = json.NewDecoder(req.Body).Decode(&v)
var p Pick
err = json.NewDecoder(req.Body).Decode(&p)
if err != nil {
httputil.WriteError(res, http.StatusBadRequest, fmt.Sprintf("decode error: %v", err))
return
}

var ans models.Answer
var ans Answer
if err = db.First(&ans, ansID).Error; err != nil {
httputil.WriteError(res, http.StatusBadRequest, "the referenced answer does not exist")
return
Expand All @@ -96,9 +112,9 @@ func PostVote(res http.ResponseWriter, req *http.Request) {
vote := models.Vote{
Answer: ans.ID,
UserId: user.ID,
Vote: int8(v.Vote),
Vote: int8(p.Vote),
}
switch v.Vote {
switch p.Vote {
case VoteUp, VoteDown:
// If a vote already exists, and
err := db.Clauses(clause.OnConflict{
Expand Down Expand Up @@ -126,7 +142,7 @@ func PostVote(res http.ResponseWriter, req *http.Request) {
return
}

httputil.WriteData(res, http.StatusOK, models.VoteResponse{
httputil.WriteData(res, http.StatusOK, Vote{
Answer: vote.Answer,
User: user.Username,
Vote: int8(vote.Vote),
Expand Down
Loading