Legal Vote
Terminology
- User - A user is an account inside OpenTalk, a single user may be inside a room as multiple participants.
- Participant - A unique connection to a room. Participants may be a guest or logged-in users.
Overview
The legal vote module allows users to participate in a legally sound vote. When a vote is started, a selection of
allowed participants can cast the vote options Yes
, No
and Abstain
on a defined voting topic. An allowed
participant cannot be a guest, as each participant needs to have an underlying user id to cast a vote.
While a vote is active, the occurring vote events are saved in a vote protocol
which is moved to the database once the
vote is complete. This provides a foundation to backtrack results, votes and errors after a vote has ended. The result
and protocol of a vote can be accessed in redis for the lifetime of the room where the vote had taken place in.
The state of a vote is held in a redis instance. There can only be one active vote at a time, every completed vote gets
moved to the vote history
for later access. Any operation that accesses more than one redis key is done with a Lua
script in order to make those operations atomic.
Vote kind
The voting procedure may follow different patterns depending on their kind. In order to choose the vote kind, the corresponding identifier needs to be set in the Start message.
Live roll call
Identifier: "live_roll_call"
In a live roll call, every vote that is handed in gets published to all room participants immediately. This allows everybody to follow the current state of the voting procedure live, immediately revealing who voted what. The report will contain a list of user ids and the vote options they chose.
Roll call
Identifier: "roll_call"
A roll call is private while it is running, while the details get published once it is finished. No updates will be sent out during the voting procedure, but votes are counted by the server. The report will contain a list of users and the vote options they chose.
Pseudonymous
Identifier: "pseudonymous"
A pseudonymous vote allows the users to keep their vote option private. Each user gets a single use token which is consumed when the the vote option is counted. The token is revealed to the user, who should keep it private. The report will contain a list of tokens and the vote options that were handed in with them. This allows every user to verify that their vote has been counted correctly. Because the tokens are published with the association of the chosen vote option, this is only pseudonymous, not anonymous.
Joining the room
JoinSuccess
When joining a room, the join_success
control event contains the module-specific fields described below.
Fields
Field | Type | Always | Description |
---|---|---|---|
votes | VoteSummary[] | yes | A list of current and past votes. |
VoteSummary
fields:
Field | Type | Required | Description |
---|---|---|---|
kind | enum | yes | The exhaustive list of kind can be found in section Vote Kind. |
initiator_id | string | yes | Id of the participant which started the vote. |
legal_vote_id | string | yes | Id of the vote. |
start_time | string | yes | RFC 3339 timestamp when the vote started. |
max_votes | int | yes | The maximum number of possible votes. |
name | string | yes | General name of the vote. |
subtitle | string | no | A subtitle for the vote |
topic | string | no | Detailed topic that will be voted on. |
allowed_participants | string[] | yes | An array of participant ids, where each contained participant is allowed to cast a vote. |
enable_abstain | bool | yes | Enable/Disable the 'Abstain' option on this vote. |
auto_close | bool | yes | When set, the vote will automatically close when every allowed participant casted a vote. |
create_pdf | bool | yes | Automatically create a protocol PDF when the vote ends. |
duration | int | no | Duration of the vote in seconds, counting from the start_time . |
token | string | no | Optional. Only users who participate in the voting procedure receive a token. |
state | string | yes | The state of the vote. Valid values are "started" , "finished" , "canceled" , "invalid" . |
end_time | string | no | RFC 3339 timestamp when the vote ended. |
When state
is "finished"
:
Field | Type | Required | Description |
---|---|---|---|
stop_kind | enum | yes | Describes how the vote finished. Valid values are "by_user" , "auto" , "expired" . |
stopped_by | string | no | The id of the participant who stopped the vote. Only present if stop_kind is "by_user" . |
yes | int | yes | Number of "yes" votes |
no | int | yes | Number of "no" votes |
abstain | int | when enable_abstain is true | Number of "abstain" votes |
voting_record | map | yes | Mapping of participant or token including their vote option |
When state
is "canceled"
:
Field | Type | Required | Description |
---|---|---|---|
issuer | string | yes | The id of the participant who canceled the vote |
reason | string | yes | Either "room_destroyed" , "initiator_left" or "custom" |
custom | string | no | A custom cancel reason, only present when reason is "custom" |
When state
is "invalid"
:
Field | Type | Required | Description |
---|---|---|---|
reason | enum | yes | Either "abstain_disabled" or "vote_count_inconsistent" |
Example
Stopped with valid results on a vote which reveals the users:
[
{
"legal_vote_id": "00000000-0000-0000-0000-000000000123",
"kind": "live_roll_call",
"initiator_id": "00000000-0000-0000-0000-000000000001",
"start_time": "1970-01-01T00:00:00Z",
"max_votes": 5,
"name": "Yes or no",
"subtitle": "Choose either yes or no",
"allowed_participants": [
"00000000-0000-0000-0000-000000000001",
"00000000-0000-0000-0000-000000000002",
"00000000-0000-0000-0000-000000000003",
"00000000-0000-0000-0000-000000000004",
"00000000-0000-0000-0000-000000000005"
],
"enable_abstain": true,
"auto_close": true,
"create_pdf": true,
"duration": 300,
"state": "finished",
"stop_kind": "auto",
"yes": 1,
"no": 2,
"abstain": 2,
"voting_record": {
"00000000-0000-0000-0000-000000000001": "yes",
"00000000-0000-0000-0000-000000000002": "no",
"00000000-0000-0000-0000-000000000003": "abstain",
"00000000-0000-0000-0000-000000000004": "abstain",
"00000000-0000-0000-0000-000000000005": "no"
},
"end_time": "1970-01-01T00:05:00Z"
},
{
"legal_vote_id": "00000000-0000-0000-0000-000000000456",
"kind": "roll_call",
"initiator_id": "00000000-0000-0000-0000-000000000001",
"start_time": "1970-01-01T01:00:00Z",
"max_votes": 3,
"name": "Vote Test",
"subtitle": "Yes or No?",
"allowed_participants": [
"00000000-0000-0000-0000-000000000001",
"00000000-0000-0000-0000-000000000002",
"00000000-0000-0000-0000-000000000003"
],
"enable_abstain": false,
"auto_close": false,
"create_pdf": true,
"timezone": "CET",
"duration": 60,
"state": "canceled",
"issued_by": "00000000-0000-0000-0000-000000000001",
"reason": "initiator_left"
}
]
Joined
When joining a room, the joined
control event sent to all other participants contains the module-specific fields described below.
Commands
Commands are issued by a participant to start or interact with a vote.
Start
The Start
message can be sent by a moderator to start a new legal vote.
Fields
Field | Type | Required | Validation | Description |
---|---|---|---|---|
action | enum | yes | Must be "start" . | |
kind | enum | yes | The exhaustive list of kind can be found in section Vote Kind. | |
name | string | yes | max 150 chars | General name of the vote |
subtitle | string | no | max 255 chars | A subtitle for the vote |
topic | string | no | max 500 chars | Detailed topic that will be voted on. |
allowed_participants | string[] | yes | min 1 participant | An array of participant ids, where each contained participant is allowed to cast a vote. |
enable_abstain | bool | yes | Enable/Disable the 'Abstain' option on this vote | |
auto_close | bool | yes | When set, the vote will automatically close when every allowed participant casted a vote. | |
create_pdf | bool | yes | Automatically create a protocol PDF when the vote ends. | |
timezone | string | no | max 150 chars | Timezone used in the protocol, defaults to UTC, IANA format, e.g."CET" or "Europe/Vienna". |
duration | int | no | min 5 seconds | Duration of the vote in seconds. |
Example
{
"action": "start",
"kind": "roll_call",
"name": "Vote Test",
"topic": "Yes or No?",
"allowed_participants": [
"00000000-0000-0000-0000-000000000001",
"00000000-0000-0000-0000-000000000002",
"00000000-0000-0000-0000-000000000003"
],
"enable_abstain": false,
"auto_close": false,
"create_pdf": true,
"timezone": "CET",
"duration": 60
}
Response
A Started message is sent to all participants that are currently in the room.
Stop
Stop the currently active vote. Will only succeed when the issuer is the vote initiator.
Fields
Field | Type | Required | Description |
---|---|---|---|
action | enum | yes | Must be "stop" . |
legal_vote_id | string | yes | The vote that shall be stopped |
Example
{
"action": "stop",
"legal_vote_id": "00000000-0000-0000-0000-000000000000"
}
Response
A Stopped message is sent to all participants that are currently in the room.
Cancel
Cancel the currently active vote when the provided legal_vote_id
matches. This command may only be issued by a moderator.
The vote protocol will still be saved in the database and in the room-vote-history, but the vote results should be handled as invalid.
This command may be triggered by the controller itself when an invalid state or error was detected. See Canceled for more details on which server events may cause this.
Fields
Field | Type | Required | Validation | Description |
---|---|---|---|---|
action | enum | yes | Must be "cancel" . | |
legal_vote_id | string | yes | The vote that shall be canceled. | |
reason | string | yes | max 255 chars | The reason for the cancel. |
Example
{
"action": "cancel",
"vote_id": "00000000-0000-0000-0000-000000000000",
"reason": "A very descriptive reason",
}
Response
A Canceled message is sent to all participants that are currently in the room.
Vote
Cast a vote on the specified legal_vote_id
. Each user is allowed to only vote once.
Fields
Field | Type | Required | Description |
---|---|---|---|
action | enum | yes | Must be "vote" . |
legal_vote_id | string | yes | The vote that shall be voted on. |
option | enum | yes | The chosen vote option, may be "yes" , "no" or "abstain" when the abstain option is enabled. |
token | string | yes | The token that was handed to the user with the Started message. |
Example
{
"action": "vote",
"legal_vote_id": "00000000-0000-0000-0000-000000000000",
"option": "yes",
"token": "2QNav7b3FJw"
}
Response
When the vote is successful, a Voted message is sent to each participant that is logged in under the same user id.
When the vote failed, a Voted response is sent to the issuer.
ReportIssue
Report an issue to the vote creator while the vote is active.
Can be sent by any vote participant during the vote. These events will be saved and displayed in the vote protocol.
Fields
Field | Type | Required | Description |
---|---|---|---|
action | enum | yes | Must be "report_issue" . |
legal_vote_id | string | yes | The ID of the related legal vote |
kind | enum | no | Either "audio" , "video" or "screenshare" . |
description | string | no | An optional message to the vote creator. Is mandatory when no "kind" is provided |
Example
{
"action": "report_issue",
"kind": "audio",
"description": "Hello, my audio is not working :("
}
{
"action": "report_issue",
"description": "Hello, something else is not working :("
}
Response
No response is sent to the issuer. The moderator will receive a ReportedIssue
message.
GeneratePdf
Generate a PDF of the protocol of the specified vote_id
. Only passed votes can be generated as a PDF document.
Fields
Field | Type | Required | Description |
---|---|---|---|
action | enum | yes | Must be "generate_pdf". |
vote_id | string | yes | The selected vote. |
timezone | string | no | Timezone used in the protocol, defaults to UTC, IANA format, e.g."CET" or "Europe/Vienna". |
Example
{
"action": "generate_pdf",
"vote_id": "00000000-0000-0000-0000-000000000000",
"timezone": "CET"
}
Response
When the PDF got created, a PdfAsset response is sent to the issuer.
Events
Events are received by participants when the vote state is changed. Events may be a 'direct' response to an issued command or unrelated to the actions of the receiving participant.
Started
A vote has been started by a moderator.
This message will also be received when joining a room that has an active vote going.
Fields
Field | Type | Required | Description |
---|---|---|---|
message | enum | yes | Is "started" . |
kind | enum | yes | The exhaustive list of kind can be found in section Vote Kind. |
initiator_id | string | yes | Id of the participant which started the vote. |
legal_vote_id | string | yes | Id of the vote. |
start_time | string | yes | RFC 3339 timestamp when the vote started. |
max_votes | int | yes | The maximum number of possible votes. |
name | string | yes | General name of the vote. |
subtitle | string | no | A subtitle for the vote |
topic | string | no | Detailed topic that will be voted on. |
allowed_participants | string[] | yes | An array of participant ids, where each contained participant is allowed to cast a vote. |
enable_abstain | bool | yes | Enable/Disable the 'Abstain' option on this vote. |
auto_close | bool | yes | When set, the vote will automatically close when every allowed participant casted a vote. |
create_pdf | bool | yes | Automatically create a protocol PDF when the vote ends. |
duration | int | no | Duration of the vote in seconds, counting from the start_time . |
token | string | no | Optional. Only users who participate in the voting procedure receive a token. |
Example
{
"message": "started",
"kind": "roll_call",
"initiator_id": "00000000-0000-0000-0000-000000000004",
"legal_vote_id": "00000000-0000-0000-0000-000000000123",
"start_time": "1970-01-01T00:00:00Z",
"max_votes": 3,
"name": "Vote Test",
"topic": "Yes or No?",
"allowed_participants": [
"00000000-0000-0000-0000-000000000001",
"00000000-0000-0000-0000-000000000002",
"00000000-0000-0000-0000-000000000003"
],
"enable_abstain": false,
"auto_close": false,
"duration": 60,
"token": "2QNav7b3FJw"
}
Voted
Event received by a user whenever they voted. Usually understood as a response to the Vote command. Since every user may only vote once, each participant logged in as that user will receive this message after any of them successfully cast their vote.
Fields
Field | Type | Required | Description |
---|---|---|---|
message | enum | yes | Is "voted" . |
legal_vote_id | string | yes | Id of the vote. |
response | enum | yes | Either "success" or "failed" |
vote_option | enum | when response is success | The option the issuer voted for |
issuer | string | when response is success | Id of the participant which voted. |
consumed_token | string | when response is success | The token that is consumed once a user has voted |
reason | enum | when response is failed | Reason why the vote failed, see table below |
Failure reason:
Reason | Description |
---|---|
invalid_vote_id | the field legal_vote_id contained an invalid or unknown id |
ineligible | the user is not eligible to vote |
invalid_option | the given vote_option field contained an unknown option |
Examples
Successful vote:
{
"message": "voted",
"legal_vote_id": "00000000-0000-0000-0000-000000000123",
"response": "success",
"vote_option": "yes",
"issuer": "00000000-0000-0000-0000-000000000001",
"consumed_token": "2QNav7b3FJw"
}
Failed vote:
{
"message": "voted",
"legal_vote_id": "00000000-0000-0000-0000-000000000123",
"response": "failed",
"reason": "ineligible"
}
Updated
Update to an ongoing vote which supports live updates, signaling the newest results. Used to visualize the UI.
Fields
Field | Type | Required | Description |
---|---|---|---|
message | enum | yes | Is "updated" . |
legal_vote_id | string | yes | Id of the vote. |
yes | int | yes | Number of "yes" votes |
no | int | yes | Number of "no" votes |
abstain | int | when enable_abstain is true | Number of "abstain" votes |
voting_record | map | yes | Mapping of participant which voted to their vote option |
Example
{
"message": "updated",
"legal_vote_id": "00000000-0000-0000-0000-000000000123",
"yes": 1,
"no": 2,
"abstain": 2,
"voting_record": {
"00000000-0000-0000-0000-000000000001": "yes",
"00000000-0000-0000-0000-000000000002": "no",
"00000000-0000-0000-0000-000000000003": "abstain",
"00000000-0000-0000-0000-000000000004": "abstain",
"00000000-0000-0000-0000-000000000005": "no"
}
}
Stopped
An ongoing vote has been finished and the results are being distributed with this event. The vote results may be invalid, this happens when the final vote results are not consistent or altered unexpectedly.
Fields
Field | Type | Required | Description |
---|---|---|---|
message | enum | yes | Is "stopped" . |
legal_vote_id | string | yes | Id of the vote. |
kind | enum | yes | Either "by_participant" , "auto" or "expired" |
issuer | string | when kind is by_participant | Id of the participant which issued the stop command |
results | enum | yes | Is either valid or invalid . This field changes the rest of the fields to one of the following tables. |
end_time | string | yes | RFC 3339 timestamp when the vote ended. |
When results
is valid
:
Field | Type | Required | Description |
---|---|---|---|
yes | int | yes | Number of "yes" votes |
no | int | yes | Number of "no" votes |
abstain | int | when enable_abstain is true | Number of "abstain" votes |
voting_record | map | yes | Mapping of participant or token including their vote option |
When invalid
:
Field | Type | Required | Description |
---|---|---|---|
reason | enum | yes | Either "abstain_disabled" or "vote_count_inconsistent" |
Example
Stopped with valid results on a vote which reveals the users:
{
"message": "stopped",
"legal_vote_id": "00000000-0000-0000-0000-000000000123",
"kind": "by_participant",
"issuer": "00000000-0000-0000-0000-000000000001",
"results": "valid",
"yes": 1,
"no": 2,
"abstain": 2,
"voting_record": {
"00000000-0000-0000-0000-000000000001": "yes",
"00000000-0000-0000-0000-000000000002": "no",
"00000000-0000-0000-0000-000000000003": "abstain",
"00000000-0000-0000-0000-000000000004": "abstain",
"00000000-0000-0000-0000-000000000005": "no"
},
"end_time": "1970-01-01T00:00:00Z"
}
Stopped with valid results on a vote which reveals the tokens:
{
"message": "stopped",
"legal_vote_id": "00000000-0000-0000-0000-000000000123",
"kind": "by_participant",
"issuer": "00000000-0000-0000-0000-000000000001",
"results": "valid",
"yes": 1,
"no": 2,
"abstain": 2,
"voting_record": {
"9AMndyeorvB": "yes",
"G9rLx7vkeMD": "no",
"Mypgay5rhRj": "abstain",
"TjR94viayBf": "abstain",
"UuLLU1sxgPw": "no"
},
"end_time": "1970-01-01T00:00:00Z"
}
Stopped with invalid results:
{
"message": "stopped",
"legal_vote_id": "00000000-0000-0000-0000-000000000123",
"kind": "by_participant",
"issuer": "00000000-0000-0000-0000-000000000001",
"results": "invalid",
"reason": "vote_count_inconsistent",
"end_time": "1970-01-01T00:00:00Z"
}
Canceled
An ongoing vote with legal_vote_id
has been canceled. This may either be issued by a moderator or by the server when some
error or inconsistency occurred.
Fields
Field | Type | Required | Description |
---|---|---|---|
message | enum | yes | Is "canceled" . |
legal_vote_id | string | yes | Id of the vote. |
reason | enum | yes | Either "custom" , "room_destroyed" or "initiator_left" |
custom | string | when reason is custom | The reason for the cancel, set by the moderator. |
Examples
{
"message": "canceled",
"legal_vote_id": "00000000-0000-0000-0000-000000000000",
"reason": "initiator_left"
}
With custom reason:
{
"message": "canceled",
"legal_vote_id": "00000000-0000-0000-0000-000000000000",
"reason": "custom",
"custom": "Some important voters left"
}
ReportedIssue
Received by the vote creator when a participant reports an issue via the ReportIssue
action.
The reported issues are saved and displayed in the vote protocol.
Fields
Field | Type | Required | Description |
---|---|---|---|
message | enum | yes | Is "reported_issue" . |
legal_vote_id | string | yes | The ID of the related legal vote |
participant_id | string | no | The participant ID of the user that issued the report. Omitted when the vote is pseudonymous |
kind | enum | no | Either "audio" , "video" or "screenshare" . |
description | string | no | An optional message to the vote creator. Is mandatory when no kind is provided |
Example
{
"message": "reported_issue",
"participant_id": "00000000-0000-0000-0000-000000000000",
"kind": "video",
"description": "Hello, my video is not working"
}
{
"message": "reported_issue",
"participant_id": "00000000-0000-0000-0000-000000000000",
"description": "Something else is not working"
}
PdfAsset
A PDF document has been created for the specified vote.
This message is sent to the issuing moderator when the vote ends and create_pdf
field in the Start message
is set. In case of an automatic stop, the message is sent to the vote initiator.
Fields
Field | Type | Required | Description |
---|---|---|---|
message | enum | yes | Is "pdf_asset". |
legal_vote_id | string | yes | Id of the vote. |
asset_id | string | yes | Id of the created asset. |
Examples
{
"message": "pdf_asset",
"legal_vote_id": "00000000-0000-0000-0000-000000000000",
"asset_id": "00000000-0000-0000-0000-000000000000"
}
Error
The error event is a message that may be triggered by syntactically correct but invalid commands inside the legal-vote
namespace
and therefore could be considered a kind of response. Errors must be handled outside of any context as they are considered events
that can happen at any time. (e.g. an internal
error may occur at any time to signal an internal problem)
Field | Type | Required | Description |
---|---|---|---|
message | enum | yes | Is "error" . |
error | enum | yes | Exhaustive list of error strings, see table below |
guests | string[] | when error is "allowlist_contains_guests" | A list of participants that where found to be quests |
fields | string[] | when error is "bad_request" | A list of fields that ignored validation constraints |
Error | Description |
---|---|
vote_already_active | Start command sent while a vote was already active |
no_vote_active | A vote related related request was sent while no vote is active |
invalid_vote_id | An invalid vote id was references inside a command |
ineligible | The requesting user is ineligible for the issued command |
allowlist_contains_guests | The allow_list of a Start provided start message contained guests |
bad_request | The input validation failed for one or more of the provided fields |
permission_error | Failed to set permissions when creating backend resources |
insufficient_permissions | The requesting user has insufficient permissions (E.g. a command requires the moderator role |
internal | Backend services encountered an internal error and any active vote should be considered invalid |
Example
{
"message": "error",
"error": "vote_already_active"
}
Error for an invalid Start request, where the allowlist contains guests:
{
"message": "error",
"error": "allowlist_contains_guest",
"guests": ["00000000-0000-0000-0000-000000000123", "00000000-0000-0000-0000-000000011311"]
}
Error for an invalid Start request, where topic
and duration
constraints where ignored:
{
"message": "error",
"error": "bad_request",
"guests": ["topic", "duration"]
}