mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Complete poll implementation with fixes and documentation
Co-authored-by: CPlusPatch <42910258+CPlusPatch@users.noreply.github.com>
This commit is contained in:
parent
31171b5fc7
commit
2e3014582b
|
|
@ -6,6 +6,9 @@ import {
|
||||||
MediasToNotes,
|
MediasToNotes,
|
||||||
Notes,
|
Notes,
|
||||||
NoteToMentions,
|
NoteToMentions,
|
||||||
|
PollOptions,
|
||||||
|
Polls,
|
||||||
|
PollVotes,
|
||||||
Users,
|
Users,
|
||||||
} from "@versia/kit/tables";
|
} from "@versia/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
|
|
@ -56,7 +59,12 @@ type NoteTypeWithRelations = NoteType & {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
liked: boolean;
|
liked: boolean;
|
||||||
reactions: Omit<typeof Reaction.$type, "note" | "author">[];
|
reactions: Omit<typeof Reaction.$type, "note" | "author">[];
|
||||||
poll: typeof Poll.$type | null;
|
poll: (InferSelectModel<typeof Polls> & {
|
||||||
|
options: (InferSelectModel<typeof PollOptions> & {
|
||||||
|
votes: InferSelectModel<typeof PollVotes>[];
|
||||||
|
})[];
|
||||||
|
votes: InferSelectModel<typeof PollVotes>[];
|
||||||
|
}) | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NoteTypeWithoutRecursiveRelations = Omit<
|
export type NoteTypeWithoutRecursiveRelations = Omit<
|
||||||
|
|
@ -693,7 +701,27 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
language: null,
|
language: null,
|
||||||
muted: data.muted,
|
muted: data.muted,
|
||||||
pinned: data.pinned,
|
pinned: data.pinned,
|
||||||
poll: data.poll ? data.poll.toApi(userFetching) : null,
|
poll: data.poll ? {
|
||||||
|
id: data.poll.id,
|
||||||
|
expires_at: data.poll.expiresAt,
|
||||||
|
expired: data.poll.expiresAt ? new Date(data.poll.expiresAt) < new Date() : false,
|
||||||
|
multiple: data.poll.multiple,
|
||||||
|
votes_count: data.poll.votesCount,
|
||||||
|
voters_count: data.poll.votersCount,
|
||||||
|
options: data.poll.options.map((option) => ({
|
||||||
|
title: option.title,
|
||||||
|
votes_count: data.poll.hideTotals && !((data.poll.expiresAt ? new Date(data.poll.expiresAt) < new Date() : false)) ? null : option.votesCount,
|
||||||
|
})),
|
||||||
|
emojis: [], // TODO: Parse emojis from poll options
|
||||||
|
voted: userFetching ? data.poll.votes.some((vote) => vote.userId === userFetching.id) : undefined,
|
||||||
|
own_votes: userFetching ? data.poll.votes
|
||||||
|
.filter((vote) => vote.userId === userFetching.id)
|
||||||
|
.map((vote) => {
|
||||||
|
const option = data.poll.options.find((opt) => opt.id === vote.optionId);
|
||||||
|
return option?.index ?? -1;
|
||||||
|
})
|
||||||
|
.filter((index) => index !== -1) : undefined,
|
||||||
|
} : null,
|
||||||
reblog: data.reblog
|
reblog: data.reblog
|
||||||
? await new Note(data.reblog as NoteTypeWithRelations).toApi(
|
? await new Note(data.reblog as NoteTypeWithRelations).toApi(
|
||||||
userFetching,
|
userFetching,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
type InferSelectModel,
|
type InferSelectModel,
|
||||||
inArray,
|
inArray,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
|
import { randomUUIDv7 } from "bun";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import type { Poll as PollSchema } from "@versia/client/schemas";
|
import type { Poll as PollSchema } from "@versia/client/schemas";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
@ -203,7 +204,7 @@ export class Poll extends BaseInterface<typeof Polls, PollTypeWithRelations> {
|
||||||
|
|
||||||
// Insert poll options
|
// Insert poll options
|
||||||
const optionInserts = options.map((title, index) => ({
|
const optionInserts = options.map((title, index) => ({
|
||||||
id: crypto.randomUUID(),
|
id: randomUUIDv7(),
|
||||||
pollId: insertedPoll.id,
|
pollId: insertedPoll.id,
|
||||||
title,
|
title,
|
||||||
index,
|
index,
|
||||||
|
|
|
||||||
99
docs/polls.md
Normal file
99
docs/polls.md
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
# Poll Feature Implementation
|
||||||
|
|
||||||
|
This document describes the poll feature implementation for posting notes with polls.
|
||||||
|
|
||||||
|
## API Usage
|
||||||
|
|
||||||
|
### Creating a Note with a Poll
|
||||||
|
|
||||||
|
To create a note with a poll, include the following parameters in your POST request to `/api/v1/statuses`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "Which do you prefer?",
|
||||||
|
"poll[options]": ["Option 1", "Option 2", "Option 3"],
|
||||||
|
"poll[expires_in]": 3600,
|
||||||
|
"poll[multiple]": false,
|
||||||
|
"poll[hide_totals]": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `poll[options]`: Array of poll option strings (2-20 options, max 500 chars each)
|
||||||
|
- `poll[expires_in]`: Duration in seconds (60 seconds to 100 days)
|
||||||
|
- `poll[multiple]`: Boolean, allow multiple choice selection (optional, default: false)
|
||||||
|
- `poll[hide_totals]`: Boolean, hide vote counts until poll ends (optional, default: false)
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
|
||||||
|
- Polls cannot be attached to posts with media attachments
|
||||||
|
- Minimum 2 options required
|
||||||
|
- Maximum options and duration are configurable in server settings
|
||||||
|
|
||||||
|
## API Response
|
||||||
|
|
||||||
|
When fetching a note with a poll, the response includes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "note-id",
|
||||||
|
"content": "Which do you prefer?",
|
||||||
|
"poll": {
|
||||||
|
"id": "poll-id",
|
||||||
|
"expires_at": "2025-01-07T14:11:00.000Z",
|
||||||
|
"expired": false,
|
||||||
|
"multiple": false,
|
||||||
|
"votes_count": 6,
|
||||||
|
"voters_count": 3,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"title": "Option 1",
|
||||||
|
"votes_count": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Option 2",
|
||||||
|
"votes_count": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"emojis": [],
|
||||||
|
"voted": true,
|
||||||
|
"own_votes": [0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
The implementation adds three new tables:
|
||||||
|
|
||||||
|
### Polls
|
||||||
|
- `id`: Primary key
|
||||||
|
- `noteId`: Foreign key to Notes table (unique)
|
||||||
|
- `expiresAt`: Poll expiration timestamp (nullable)
|
||||||
|
- `multiple`: Allow multiple choice
|
||||||
|
- `hideTotals`: Hide vote counts until expired
|
||||||
|
- `votesCount`: Total vote count
|
||||||
|
- `votersCount`: Unique voter count
|
||||||
|
|
||||||
|
### PollOptions
|
||||||
|
- `id`: Primary key
|
||||||
|
- `pollId`: Foreign key to Polls table
|
||||||
|
- `title`: Option text
|
||||||
|
- `index`: Option position (0-based)
|
||||||
|
- `votesCount`: Votes for this option
|
||||||
|
|
||||||
|
### PollVotes
|
||||||
|
- `id`: Primary key
|
||||||
|
- `pollId`: Foreign key to Polls table
|
||||||
|
- `optionId`: Foreign key to PollOptions table
|
||||||
|
- `userId`: Foreign key to Users table
|
||||||
|
- Unique constraint on (pollId, userId, optionId)
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
- Polls are created atomically with the note in a database transaction
|
||||||
|
- Poll data is included in note queries via relations
|
||||||
|
- Expiration is checked dynamically when converting to API format
|
||||||
|
- Vote totals can be hidden until poll expires based on `hideTotals` setting
|
||||||
|
- User vote status and choices are included when user context is available
|
||||||
Loading…
Reference in a new issue