-
Notifications
You must be signed in to change notification settings - Fork 160
Feat/crowdsourced people’s choice judging #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
The head ref may contain hidden characters: "feat/Crowdsourced-People\u2019s-Choice-Judging"
Changes from all commits
a5a1731
efa2adc
3348133
6b202a7
948c4ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| package controllers | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "time" | ||
|
|
||
| "arguehub/db" | ||
| "arguehub/models" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| "go.mongodb.org/mongo-driver/bson" | ||
| "go.mongodb.org/mongo-driver/bson/primitive" | ||
| ) | ||
|
|
||
| // SubmitVote handles a spectator's vote on a debate | ||
| func SubmitVote(c *gin.Context) { | ||
| debateIDHex := c.Param("id") | ||
| debateID, err := primitive.ObjectIDFromHex(debateIDHex) | ||
| if err != nil { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid debate ID"}) | ||
| return | ||
| } | ||
|
|
||
| var req struct { | ||
| Vote string `json:"vote" binding:"required"` // "User" or "Bot" | ||
| } | ||
| if err := c.ShouldBindJSON(&req); err != nil { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) | ||
| return | ||
| } | ||
|
|
||
| // Validate debate exists and is in a votable state (has an outcome) | ||
| var debate models.DebateVsBot | ||
| err = db.DebateVsBotCollection.FindOne(context.Background(), bson.M{"_id": debateID}).Decode(&debate) | ||
| if err != nil { | ||
| c.JSON(http.StatusNotFound, gin.H{"error": "Debate not found"}) | ||
| return | ||
| } | ||
|
|
||
| if debate.Outcome == "" { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "Debate must be finalized before voting"}) | ||
| return | ||
| } | ||
|
|
||
| // Basic duplicate prevention using IP | ||
| voterID := c.ClientIP() | ||
|
|
||
| // Check if this voter has already voted for this debate | ||
| count, err := db.VotesCollection.CountDocuments(context.Background(), bson.M{ | ||
| "debateId": debateID, | ||
| "voterId": voterID, | ||
| }) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing votes"}) | ||
| return | ||
| } | ||
| if count > 0 { | ||
| c.JSON(http.StatusConflict, gin.H{"error": "You have already voted on this debate"}) | ||
| return | ||
| } | ||
|
|
||
| vote := models.Vote{ | ||
| ID: primitive.NewObjectID(), | ||
| DebateID: debateID, | ||
| Vote: req.Vote, | ||
| VoterID: voterID, | ||
| Timestamp: time.Now(), | ||
| } | ||
|
|
||
| _, err = db.VotesCollection.InsertOne(context.Background(), vote) | ||
|
Comment on lines
+49
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Race condition in duplicate vote prevention (TOCTOU). The check-then-insert pattern ( Consider using a unique index on 🔒 Proposed approach
indexModel := mongo.IndexModel{
Keys: bson.D{{Key: "debateId", Value: 1}, {Key: "voterId", Value: 1}},
Options: options.Index().SetUnique(true),
}
VotesCollection.Indexes().CreateOne(ctx, indexModel)
- count, err := db.VotesCollection.CountDocuments(context.Background(), bson.M{
- "debateId": debateID,
- "voterId": voterID,
- })
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing votes"})
- return
- }
- if count > 0 {
- c.JSON(http.StatusConflict, gin.H{"error": "You have already voted on this debate"})
- return
- }
-
vote := models.Vote{
ID: primitive.NewObjectID(),
DebateID: debateID,
Vote: req.Vote,
VoterID: voterID,
Timestamp: time.Now(),
}
_, err = db.VotesCollection.InsertOne(context.Background(), vote)
if err != nil {
+ if mongo.IsDuplicateKeyError(err) {
+ c.JSON(http.StatusConflict, gin.H{"error": "You have already voted on this debate"})
+ return
+ }
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to submit vote"})
return
}🤖 Prompt for AI Agents |
||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to submit vote"}) | ||
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{"message": "Vote submitted successfully"}) | ||
| } | ||
|
|
||
| // GetVerdicts combines AI and People's Choice verdicts | ||
| func GetVerdicts(c *gin.Context) { | ||
| debateIDHex := c.Param("id") | ||
| debateID, err := primitive.ObjectIDFromHex(debateIDHex) | ||
| if err != nil { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid debate ID"}) | ||
| return | ||
| } | ||
|
|
||
| // Fetch debate for AI outcome | ||
| var debate models.DebateVsBot | ||
| err = db.DebateVsBotCollection.FindOne(context.Background(), bson.M{"_id": debateID}).Decode(&debate) | ||
| if err != nil { | ||
| c.JSON(http.StatusNotFound, gin.H{"error": "Debate not found"}) | ||
| return | ||
| } | ||
|
|
||
| // Aggregate People's Choice votes | ||
| cursor, err := db.VotesCollection.Find(context.Background(), bson.M{"debateId": debateID}) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch votes"}) | ||
| return | ||
| } | ||
| defer cursor.Close(context.Background()) | ||
|
|
||
| userVotes := 0 | ||
| botVotes := 0 | ||
| for cursor.Next(context.Background()) { | ||
| var vote models.Vote | ||
| if err := cursor.Decode(&vote); err == nil { | ||
| if vote.Vote == "User" { | ||
| userVotes++ | ||
| } else if vote.Vote == "Bot" { | ||
| botVotes++ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| peoplesChoice := "Draw" | ||
| if userVotes > botVotes { | ||
| peoplesChoice = "User" | ||
| } else if botVotes > userVotes { | ||
| peoplesChoice = "Bot" | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "debateId": debateIDHex, | ||
| "aiVerdict": debate.Outcome, | ||
| "peoplesChoice": gin.H{ | ||
| "winner": peoplesChoice, | ||
| "counts": gin.H{ | ||
| "user": userVotes, | ||
| "bot": botVotes, | ||
| }, | ||
| }, | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package models | ||
|
|
||
| import ( | ||
| "time" | ||
|
|
||
| "go.mongodb.org/mongo-driver/bson/primitive" | ||
| ) | ||
|
|
||
| // Vote represents a spectator's vote on a debate outcome | ||
| type Vote struct { | ||
| ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` | ||
| DebateID primitive.ObjectID `json:"debateId" bson:"debateId"` | ||
| Vote string `json:"vote" bson:"vote"` // "User", "Bot", or "for", "against" | ||
| VoterID string `json:"voterId" bson:"voterId"` // IP address or fingerprint | ||
| Timestamp time.Time `json:"timestamp" bson:"timestamp"` | ||
|
Comment on lines
+11
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid storing raw IPs in 🔒 Suggested model adjustment (also update controller logic) type Vote struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
DebateID primitive.ObjectID `json:"debateId" bson:"debateId"`
Vote string `json:"vote" bson:"vote"` // "User", "Bot", or "for", "against"
- VoterID string `json:"voterId" bson:"voterId"` // IP address or fingerprint
+ VoterHash string `json:"voterHash" bson:"voterHash"` // HMAC(IP, secret) or anonymized fingerprint
Timestamp time.Time `json:"timestamp" bson:"timestamp"`
}🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation for vote value.
The
req.Votefield accepts any string, but theVotemodel comment indicates only "User" or "Bot" are valid (perbackend/models/vote.go). Invalid values will be stored and silently ignored in aggregation, corrupting vote counts.🛡️ Proposed fix
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) return } + + if req.Vote != "User" && req.Vote != "Bot" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Vote must be 'User' or 'Bot'"}) + return + }Also applies to: 63-68
🤖 Prompt for AI Agents