-
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?
Feat/crowdsourced people’s choice judging #286
The head ref may contain hidden characters: "feat/Crowdsourced-People\u2019s-Choice-Judging"
Conversation
…luding a new online debate room.
📝 WalkthroughWalkthroughThis PR implements crowdsourced "People's Choice" voting on debates by adding vote submission and verdict aggregation endpoints, restructuring bot response prompts to use system/user splits with response validation, and adjusting authentication password constraints. Configuration samples are removed and frontend validation is centralized. Changes
Sequence DiagramsequenceDiagram
participant Client as Client
participant Controller as Vote Controller
participant Validator as Validator
participant DB as MongoDB
participant Aggregator as Vote Aggregator
rect rgba(100, 200, 255, 0.5)
Note over Client,Aggregator: Vote Submission Flow
Client->>Controller: POST /debate/:id/vote {vote, voterId}
Controller->>Validator: Validate debateId & payload
Validator-->>Controller: Valid/Invalid
alt Invalid Input
Controller-->>Client: Error 400
else Valid
Controller->>DB: Check debate exists & is finalized
alt Debate not found or not finalized
Controller-->>Client: Error 404/400
else Debate valid
Controller->>DB: Check for duplicate vote by voterId
alt Vote exists
Controller-->>Client: Error 409 (Conflict)
else New vote allowed
Controller->>DB: Insert new Vote document
DB-->>Controller: Insertion successful
Controller-->>Client: Success 201
end
end
end
end
rect rgba(200, 150, 255, 0.5)
Note over Client,Aggregator: Verdict Retrieval Flow
Client->>Controller: GET /debate/:id/verdicts
Controller->>Validator: Validate debateId
Validator-->>Controller: Valid/Invalid
alt Invalid ID
Controller-->>Client: Error 400
else Valid
Controller->>DB: Fetch AI verdict from DebateVsBot
Controller->>DB: Aggregate votes from Votes collection
DB-->>Controller: Verdict & votes
Aggregator->>Aggregator: Compute winner (User/Bot/Draw)
Aggregator->>Aggregator: Count vote distribution
Aggregator-->>Controller: Aggregated result
Controller-->>Client: JSON {debateId, aiVerdict, peoplesChoice}
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 🧪 Unit Test Generation v2 is now available!We have significantly improved our unit test generation capabilities. To enable: Add this to your reviews:
finishing_touches:
unit_tests:
enabled: trueTry it out by using the Have feedback? Share your thoughts on our Discord thread! Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
backend/routes/debatevsbot.go (1)
13-18:⚠️ Potential issue | 🔴 CriticalThe
/vsbotroutes including vote and verdict endpoints require authentication. TheSetupDebateVsBotRoutesfunction is called with theauthRouterGroup (backend/cmd/server/main.go:127), which appliesAuthMiddleware. This means unauthenticated spectators cannot vote as intended. Either exempt the vote and verdict routes from auth, or implement a separate unauthenticated endpoint for spectator voting.backend/db/db.go (1)
17-62:⚠️ Potential issue | 🟠 MajorAdd a unique compound index to enforce duplicate vote prevention.
SubmitVoteuses CountDocuments → InsertOne (vote_controller.go lines 50–71), which is a classic TOCTOU window. Without a unique index, concurrent requests from the same IP can both seecount=0, then both insert successfully, inflating vote counts. Create a unique(debateId, voterId)index inConnectMongoDBand update the error handler to distinguish and return 409 for duplicate-key violations instead of 500.🛠️ Proposed index creation
func ConnectMongoDB(uri string) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ MongoDatabase = client.Database(dbName) DebateVsBotCollection = MongoDatabase.Collection("debates_vs_bot") VotesCollection = MongoDatabase.Collection("votes") + + // Enforce one vote per (debateId, voterId) + _, err = VotesCollection.Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{ + {Key: "debateId", Value: 1}, + {Key: "voterId", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }) + if err != nil { + return fmt.Errorf("failed to create votes index: %w", err) + } return nil }Also update the error handler in SubmitVote (lines 72–74) to catch MongoDB error code 11000 and return 409 instead of 500.
🤖 Fix all issues with AI agents
In `@backend/controllers/vote_controller.go`:
- Around line 25-31: The request binding currently accepts any string for
req.Vote which risks storing invalid values; after binding JSON into the local
req struct (req.Vote) validate that the value is exactly "User" or "Bot" and
return a 400 with a clear error message if it is not; apply the same validation
in the other handler occurrence handling req.Vote (lines ~63-68) so only allowed
Vote values per the Vote model (backend/models/vote.go) are persisted and used
in aggregation.
- Around line 49-71: The current CountDocuments → InsertOne flow in VoteCreate
(uses db.VotesCollection.CountDocuments and db.VotesCollection.InsertOne with
models.Vote) is vulnerable to a TOCTOU race; add a unique compound index on
(debateId, voterId) during DB initialization (use
VotesCollection.Indexes().CreateOne with a mongo.IndexModel keyed by "debateId"
and "voterId" and SetUnique(true)) and then remove the pre-insert count-check in
the controller, instead attempting InsertOne directly and catching duplicate-key
errors (check for mongo.IsDuplicateKeyError or error code 11000) to return HTTP
409 when a duplicate vote is attempted.
In `@backend/models/vote.go`:
- Around line 11-15: The Vote model currently stores raw client IPs in VoterID;
change the usage so VoterID stores a hashed/obfuscated identifier instead (keep
type string but document it as HMAC(IP, serverSecret) or similar). Update
SubmitVote in backend/controllers/vote_controller.go (which currently reads
c.ClientIP()) to compute a stable hash (e.g., hashVoterID or computeVoterIDHash
using a server secret from env/config) before any DB queries or when setting
Vote.VoterID, and use that hashed value for duplicate checks and persistence;
ensure hashing uses HMAC with a secret and consider optional truncation/TTL if
desired.
In `@backend/services/debatevsbot.go`:
- Around line 124-131: The code can set currentPhase to an empty string when
lastMsg.Phase is empty, causing the later switch to hit the generic default;
update the logic after calling findLastUserMessage so that if lastMsg.Phase ==
"" (or strings.TrimSpace(lastMsg.Phase) == "") you assign a sensible fallback
(e.g., "Opening Statement" or the previous known phase) to currentPhase before
any phase normalization (the code around currentPhase and the existing
normalization for "first rebuttal"/"second rebuttal" should be reused).
In `@backend/structs/auth.go`:
- Line 5: The Password JSON binding tags in backend/structs/auth.go were
weakened to `min=6`; revert the minimum password length to `min=8` for all
Password struct fields (the Password field definitions used in signup/reset
request structs) by updating their `binding` tags back to `min=8` so the signup
and reset flows enforce the stronger policy; locate each Password field (same
name "Password") in this file and change the tag value from 6 to 8.
🧹 Nitpick comments (7)
frontend/src/Pages/Authentication/forms.tsx (1)
8-8: Centralize password-length config to avoid drift.Consider moving
MIN_PASSWORD_LENGTHto a shared auth validation/config module so client and server changes stay aligned in one place.backend/models/vote.go (1)
9-14: ConstrainVotevalues to typed constants to avoid inconsistent data.
The model comment allows multiple values, butGetVerdictsonly tallies"User"/"Bot"(backend/controllers/vote_controller.go lines 80‑135). Consider a typed enum plus validation inSubmitVoteto avoid silent miscounts.♻️ Suggested typing
+type VoteChoice string + +const ( + VoteUser VoteChoice = "User" + VoteBot VoteChoice = "Bot" +) + 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" + Vote VoteChoice `json:"vote" bson:"vote"` VoterID string `json:"voterId" bson:"voterId"` // IP address or fingerprint Timestamp time.Time `json:"timestamp" bson:"timestamp"` }backend/services/gemini.go (1)
21-21: UnusedmodelNameparameter.The
modelNameparameter is accepted but never used—Line 43 always passesdefaultGeminiModel. Either remove the parameter or use it in the API call.♻️ Option A: Use the parameter
- resp, err := geminiClient.Models.GenerateContent(ctx, defaultGeminiModel, genai.Text(prompt), config) + resp, err := geminiClient.Models.GenerateContent(ctx, modelName, genai.Text(prompt), config)♻️ Option B: Remove the unused parameter
-func generateModelText(ctx context.Context, modelName, systemInstruction, prompt string) (string, error) { +func generateModelText(ctx context.Context, systemInstruction, prompt string) (string, error) {Then update the call in
generateDefaultModelText:- return generateModelText(ctx, defaultGeminiModel, systemInstruction, prompt) + return generateModelText(ctx, systemInstruction, prompt)Also applies to: 43-43
backend/controllers/vote_controller.go (2)
97-116: Consider MongoDB aggregation pipeline for efficiency.Fetching all votes and iterating client-side is inefficient at scale. MongoDB's aggregation pipeline can compute counts server-side in a single query.
♻️ Proposed aggregation approach
pipeline := mongo.Pipeline{ {{Key: "$match", Value: bson.M{"debateId": debateID}}}, {{Key: "$group", Value: bson.M{ "_id": "$vote", "count": bson.M{"$sum": 1}, }}}, } cursor, err := db.VotesCollection.Aggregate(context.Background(), pipeline) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to aggregate votes"}) return } defer cursor.Close(context.Background()) userVotes, botVotes := 0, 0 for cursor.Next(context.Background()) { var result struct { ID string `bson:"_id"` Count int `bson:"count"` } if err := cursor.Decode(&result); err == nil { switch result.ID { case "User": userVotes = result.Count case "Bot": botVotes = result.Count } } }
107-116: Silent error swallowing on cursor decode.Decode errors at Line 109 are silently ignored. While this prevents crashes, it could mask data corruption issues. Consider logging decode failures for observability.
♻️ Add logging for decode errors
for cursor.Next(context.Background()) { var vote models.Vote - if err := cursor.Decode(&vote); err == nil { + if err := cursor.Decode(&vote); err != nil { + log.Printf("Failed to decode vote: %v", err) + continue + } if vote.Vote == "User" { userVotes++ } else if vote.Vote == "Bot" { botVotes++ } - } }backend/services/debatevsbot.go (2)
186-188: Dead code invalidateResponse.The
casualFillervariable is defined as[]uintptrwith character codes, then immediately discarded with_ = casualFiller. This appears to be leftover/debug code.🧹 Remove dead code
// Reject obvious casual filler if it's the only thing there - casualFiller := []uintptr{'h', 'e', 'l', 'l', 'o'} - _ = casualFiller // just avoiding unused if strings.Contains(lowerText, "hello there") && len(text) < 30 { return false }
178-194: Validation logic is intentionally weak.The inline comment acknowledges this is a "weak check." For Phase 1, this may be acceptable, but consider tracking a follow-up to implement more robust validation (e.g., topic keyword matching, response length distribution) if response quality issues persist in production.
Would you like me to open an issue to track improving response validation with topic keyword matching or other heuristics?
| 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 | ||
| } |
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.Vote field accepts any string, but the Vote model comment indicates only "User" or "Bot" are valid (per backend/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
In `@backend/controllers/vote_controller.go` around lines 25 - 31, The request
binding currently accepts any string for req.Vote which risks storing invalid
values; after binding JSON into the local req struct (req.Vote) validate that
the value is exactly "User" or "Bot" and return a 400 with a clear error message
if it is not; apply the same validation in the other handler occurrence handling
req.Vote (lines ~63-68) so only allowed Vote values per the Vote model
(backend/models/vote.go) are persisted and used in aggregation.
| // 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) |
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.
Race condition in duplicate vote prevention (TOCTOU).
The check-then-insert pattern (CountDocuments → InsertOne) allows duplicate votes if two requests arrive concurrently for the same voter/debate. Between the count check and the insert, another request could complete its insert.
Consider using a unique index on (debateId, voterId) in MongoDB to enforce uniqueness atomically, then handle the duplicate key error.
🔒 Proposed approach
- Create unique index in
backend/db/db.goduring initialization:
indexModel := mongo.IndexModel{
Keys: bson.D{{Key: "debateId", Value: 1}, {Key: "voterId", Value: 1}},
Options: options.Index().SetUnique(true),
}
VotesCollection.Indexes().CreateOne(ctx, indexModel)- Update the controller to handle duplicate key errors:
- 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
In `@backend/controllers/vote_controller.go` around lines 49 - 71, The current
CountDocuments → InsertOne flow in VoteCreate (uses
db.VotesCollection.CountDocuments and db.VotesCollection.InsertOne with
models.Vote) is vulnerable to a TOCTOU race; add a unique compound index on
(debateId, voterId) during DB initialization (use
VotesCollection.Indexes().CreateOne with a mongo.IndexModel keyed by "debateId"
and "voterId" and SetUnique(true)) and then remove the pre-insert count-check in
the controller, instead attempting InsertOne directly and catching duplicate-key
errors (check for mongo.IsDuplicateKeyError or error code 11000) to return HTTP
409 when a duplicate vote is attempted.
| 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"` |
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.
Avoid storing raw IPs in VoterID (PII).
The field comment suggests persisting client IPs; that’s sensitive data. For best‑effort duplicate prevention, store a hashed/obfuscated identifier (e.g., HMAC(IP, server‑secret) with optional truncation/TTL). SubmitVote currently uses c.ClientIP() (backend/controllers/vote_controller.go lines 36‑47), so that path should hash before storing and querying.
🔒 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
In `@backend/models/vote.go` around lines 11 - 15, The Vote model currently stores
raw client IPs in VoterID; change the usage so VoterID stores a
hashed/obfuscated identifier instead (keep type string but document it as
HMAC(IP, serverSecret) or similar). Update SubmitVote in
backend/controllers/vote_controller.go (which currently reads c.ClientIP()) to
compute a stable hash (e.g., hashVoterID or computeVoterIDHash using a server
secret from env/config) before any DB queries or when setting Vote.VoterID, and
use that hashed value for duplicate checks and persistence; ensure hashing uses
HMAC with a secret and consider optional truncation/TTL if desired.
| currentPhase := "Opening Statement" | ||
| if len(history) > 1 { | ||
| lastMsg := findLastUserMessage(history) | ||
| currentPhase = lastMsg.Phase | ||
| if strings.ToLower(currentPhase) == "first rebuttal" || strings.ToLower(currentPhase) == "second rebuttal" { | ||
| currentPhase = "Cross Examination" | ||
| } | ||
| } |
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if Phase field is always populated when messages are created
rg -n -A5 'Phase.*=' --type=go -g '!*_test.go' | head -100Repository: AOSSIE-Org/DebateAI
Length of output: 6781
🏁 Script executed:
# Get the full context around the flagged lines
cat -n backend/services/debatevsbot.go | sed -n '120,145p'Repository: AOSSIE-Org/DebateAI
Length of output: 1618
🏁 Script executed:
# Look for the findLastUserMessage function definition
rg -n -A20 'func.*findLastUserMessage' --type=goRepository: AOSSIE-Org/DebateAI
Length of output: 1720
🏁 Script executed:
# Check for Phase field definition in message structures
rg -n 'Phase\s+string' --type=goRepository: AOSSIE-Org/DebateAI
Length of output: 503
Edge case when lastMsg.Phase is empty: falls through to default switch case.
If the user's last message has an empty Phase field, currentPhase becomes "" and the switch at line 134 falls through to the default case, which provides generic debate instructions. While this behavior is handled gracefully and doesn't cause errors, it means phase-specific instructions won't be applied.
🤖 Prompt for AI Agents
In `@backend/services/debatevsbot.go` around lines 124 - 131, The code can set
currentPhase to an empty string when lastMsg.Phase is empty, causing the later
switch to hit the generic default; update the logic after calling
findLastUserMessage so that if lastMsg.Phase == "" (or
strings.TrimSpace(lastMsg.Phase) == "") you assign a sensible fallback (e.g.,
"Opening Statement" or the previous known phase) to currentPhase before any
phase normalization (the code around currentPhase and the existing normalization
for "first rebuttal"/"second rebuttal" should be reused).
| type SignUpRequest struct { | ||
| Email string `json:"email" binding:"required,email"` | ||
| Password string `json:"password" binding:"required,min=8"` | ||
| Password string `json:"password" binding:"required,min=6"` |
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.
Avoid weakening password policy to 6 characters without explicit security sign‑off.
Line 5, Line 15, and Line 25 reduce the minimum length across signup and reset flows, which materially weakens password strength. Unless security requirements explicitly allow 6, please keep 8 (or higher) to preserve baseline protection.
🔐 Suggested revert
- Password string `json:"password" binding:"required,min=6"`
+ Password string `json:"password" binding:"required,min=8"`- Password string `json:"password" binding:"required,min=6"`
+ Password string `json:"password" binding:"required,min=8"`- NewPassword string `json:"newPassword" binding:"required,min=6"`
+ NewPassword string `json:"newPassword" binding:"required,min=8"`Also applies to: 15-15, 25-25
🤖 Prompt for AI Agents
In `@backend/structs/auth.go` at line 5, The Password JSON binding tags in
backend/structs/auth.go were weakened to `min=6`; revert the minimum password
length to `min=8` for all Password struct fields (the Password field definitions
used in signup/reset request structs) by updating their `binding` tags back to
`min=8` so the signup and reset flows enforce the stronger policy; locate each
Password field (same name "Password") in this file and change the tag value from
6 to 8.
Summary
This PR implements Phase 1 of Crowdsourced People’s Choice Judging in DebateAI.
Spectators can now vote on completed User-vs-Bot debates, and the aggregated audience verdict is returned alongside the existing AI Judge verdict.
The implementation is backend-only, minimal in scope, and does not modify core debate flow or AI judging logic.
Motivation
Debates are often judged differently by experts and general audiences. While DebateAI already provides AI-based judging focused on logical rigor, this feature introduces a social layer by exposing how popular opinion compares with the AI Judge’s verdict. This improves engagement and educational value without affecting debate integrity.
Implementation
User/Bot), IP-based duplicate prevention, and timestamp.votescollection during database initialization.API Endpoints
Submit Vote
POST /vsbot/debate/:id/vote { "vote": "User" }Design Scope (Phase 1)
Verification
Manual testing verified:
Voting on finalized debates
Duplicate vote prevention
Invalid debate handling
Pre-finalization vote rejection
closes #285
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.