Skip to content
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

[feature] add support for polls + receiving federated status edits #2330

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3086551
copy over new scheduler from feature/add-polls-support
NyaaaWhatsUpDoc Nov 2, 2023
207ff24
remove poll scheduling from startup function -- spoilers!!! ;)
NyaaaWhatsUpDoc Nov 2, 2023
9e796e3
add polls support! now with vastly less commits as they were getting …
NyaaaWhatsUpDoc Nov 4, 2023
87e3a90
fix angry linter + envparsing tests
NyaaaWhatsUpDoc Nov 2, 2023
e0c732a
update GetOpenPolls() to only return LOCAL open polls, by checking st…
NyaaaWhatsUpDoc Nov 2, 2023
ad61fdd
on poll recently closed, send out poll notifications
NyaaaWhatsUpDoc Nov 3, 2023
22196e3
fix incorrect log format directives
NyaaaWhatsUpDoc Nov 3, 2023
8c12f8c
invalidate poll vote IDs in cache hooks
NyaaaWhatsUpDoc Nov 3, 2023
fa6600a
log on expiry handler scheduling
NyaaaWhatsUpDoc Nov 3, 2023
e2c95d3
remove statusable check in update handler
NyaaaWhatsUpDoc Nov 3, 2023
6b69e14
improved poll expiry notification time format
NyaaaWhatsUpDoc Nov 4, 2023
2cc1cab
don't return on most worker errors, only if they prohibit later proce…
NyaaaWhatsUpDoc Nov 4, 2023
9a3262d
fix error formatting directives
NyaaaWhatsUpDoc Nov 4, 2023
187b871
fix slice preallocation to be zero length
NyaaaWhatsUpDoc Nov 4, 2023
19ac64e
fix rebase issue
NyaaaWhatsUpDoc Nov 4, 2023
1a61e00
add code comments for the new wrapping scheduler
NyaaaWhatsUpDoc Nov 6, 2023
35682c2
fix small issues with poll swagger defs, regenerate swagger docs
NyaaaWhatsUpDoc Nov 6, 2023
66e1a74
fix envparsing test
NyaaaWhatsUpDoc Nov 7, 2023
b4b4de8
improved wording
NyaaaWhatsUpDoc Nov 7, 2023
16f2fa9
remove unused code
NyaaaWhatsUpDoc Nov 7, 2023
3ec8a34
appease the angery linter
NyaaaWhatsUpDoc Nov 7, 2023
ec40725
add poll + poll vote test models
NyaaaWhatsUpDoc Nov 7, 2023
7bcf67c
use oklog/ulid generated IDs, fix remaining tests
NyaaaWhatsUpDoc Nov 7, 2023
4b7d298
finish fixing tests
NyaaaWhatsUpDoc Nov 7, 2023
02ee471
add db.Get__By() tests for Poll and PollVote
NyaaaWhatsUpDoc Nov 7, 2023
10601d4
make poll.Voters a ptr type to fit with bun's field tag semantics ...
NyaaaWhatsUpDoc Nov 7, 2023
b928a1c
add test for PutPollVote()
NyaaaWhatsUpDoc Nov 7, 2023
3572744
add polls processor tests
NyaaaWhatsUpDoc Nov 7, 2023
fb24043
use apiutil to parse ID in poll handlers
NyaaaWhatsUpDoc Nov 8, 2023
bc1221b
fix comment typo
NyaaaWhatsUpDoc Nov 8, 2023
6aa8883
update config/gen to use gofumpt instead of deprecated gofumports
NyaaaWhatsUpDoc Nov 8, 2023
1f00cc5
update failed ULID creation at time log msg to be more explanatory, d…
NyaaaWhatsUpDoc Nov 8, 2023
7bd64eb
whitespace formatting
NyaaaWhatsUpDoc Nov 8, 2023
156f1e5
consolidate a bunch of the vote increment / decrement logic, and ensu…
NyaaaWhatsUpDoc Nov 8, 2023
1cb3be4
remove specified database types in order to let bun pick appropriate …
NyaaaWhatsUpDoc Nov 8, 2023
2483682
remove unused function to appease the linter ...
NyaaaWhatsUpDoc Nov 8, 2023
940fcda
update fedi / client API poll vote processing to send out status upda…
NyaaaWhatsUpDoc Nov 8, 2023
11b4c09
fix a stinky ...
NyaaaWhatsUpDoc Nov 8, 2023
51d7784
add a timed wait for account data to be deleted
NyaaaWhatsUpDoc Nov 8, 2023
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
5 changes: 5 additions & 0 deletions cmd/gotosocial/action/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ var Start action.GTSAction = func(ctx context.Context) error {
state.Workers.ProcessFromClientAPI = processor.Workers().ProcessFromClientAPI
state.Workers.ProcessFromFediAPI = processor.Workers().ProcessFromFediAPI

// Schedule tasks for all existing poll expiries.
if err := processor.Polls().ScheduleAll(ctx); err != nil {
return fmt.Errorf("error scheduling poll expiries: %w", err)
}

/*
HTTP router initialization
*/
Expand Down
94 changes: 85 additions & 9 deletions docs/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,10 @@ definitions:
description: 'Statistics about the instance: number of posts, accounts, etc.'
type: object
x-go-name: Stats
terms:
description: Terms and conditions for accounts on this instance.
type: string
x-go-name: Terms
thumbnail:
description: URL of the instance avatar/banner image.
example: https://example.org/files/instance/thumbnail.jpeg
Expand Down Expand Up @@ -1533,6 +1537,10 @@ definitions:
example: https://github.com/superseriousbusiness/gotosocial
type: string
x-go-name: SourceURL
terms:
description: Terms and conditions for accounts on this instance.
type: string
x-go-name: Terms
thumbnail:
$ref: '#/definitions/instanceV2Thumbnail'
title:
Expand Down Expand Up @@ -1993,7 +2001,7 @@ definitions:
type: boolean
x-go-name: Expired
expires_at:
description: When the poll ends. (ISO 8601 Datetime), or null if the poll does not end
description: When the poll ends. (ISO 8601 Datetime).
type: string
x-go-name: ExpiresAt
id:
Expand All @@ -2008,7 +2016,7 @@ definitions:
options:
description: Possible answers for the poll.
items:
$ref: '#/definitions/pollOptions'
$ref: '#/definitions/pollOption'
type: array
x-go-name: Options
own_votes:
Expand All @@ -2023,7 +2031,7 @@ definitions:
type: boolean
x-go-name: Voted
voters_count:
description: How many unique accounts have voted on a multiple-choice poll. Null if multiple is false.
description: How many unique accounts have voted on a multiple-choice poll.
format: int64
type: integer
x-go-name: VotersCount
Expand All @@ -2036,22 +2044,20 @@ definitions:
type: object
x-go-name: Poll
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
pollOptions:
pollOption:
properties:
title:
description: The text value of the poll option. String.
type: string
x-go-name: Title
votes_count:
description: |-
The number of received votes for this option.
Number, or null if results are not published yet.
description: The number of received votes for this option.
format: int64
type: integer
x-go-name: VotesCount
title: PollOptions represents the current vote counts for different poll options.
title: PollOption represents the current vote counts for different poll options.
type: object
x-go-name: PollOptions
x-go-name: PollOption
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
report:
properties:
Expand Down Expand Up @@ -5986,6 +5992,76 @@ paths:
summary: Clear/delete all notifications for currently authorized user.
tags:
- notifications
/api/v1/polls/{id}:
get:
operationId: poll
parameters:
- description: Target poll ID.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: The requested poll.
schema:
$ref: '#/definitions/poll'
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security:
- OAuth2 Bearer:
- read:statuses
summary: View poll with given ID.
tags:
- polls
/api/v1/polls/{id}/vote:
post:
operationId: poll
parameters:
- description: Target poll ID.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: The updated poll with user vote choices.
schema:
$ref: '#/definitions/poll'
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"404":
description: not found
"406":
description: not acceptable
"422":
description: unprocessable entity
"500":
description: internal server error
security:
- OAuth2 Bearer:
- write:statuses
summary: Vote with choices in the given poll.
tags:
- polls
/api/v1/preferences:
get:
description: |-
Expand Down
121 changes: 121 additions & 0 deletions internal/ap/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/url"
"strings"
Expand Down Expand Up @@ -1112,6 +1113,91 @@ func ExtractSharedInbox(withEndpoints WithEndpoints) *url.URL {
return nil
}

// ExtractPoll extracts a placeholder Poll from Pollable interface, with available options and flags populated.
func ExtractPoll(poll Pollable) (*gtsmodel.Poll, error) {
var closed time.Time

// Extract the options (votes if any) and 'multiple choice' flag.
options, votes, multi, err := ExtractPollOptions(poll)
if err != nil {
return nil, err
}

// Check if counts have been hidden from us.
hideCounts := len(options) != len(votes)
if hideCounts {

// Zero out all votes.
for i := range votes {
votes[i] = 0
}
}

// Extract the poll end time.
endTime := GetEndTime(poll)
if endTime.IsZero() {
return nil, errors.New("no poll end time specified")
}

// Extract the poll closed time.
closedSlice := GetClosed(poll)
if len(closedSlice) == 1 {
closed = closedSlice[0]
}

// Extract the number of voters.
voters := GetVotersCount(poll)

return &gtsmodel.Poll{
Options: options,
Multiple: &multi,
HideCounts: &hideCounts,
Votes: votes,
Voters: &voters,
ExpiresAt: endTime,
ClosedAt: closed,
}, nil
}

// ExtractPollOptions extracts poll option name strings, and the 'multiple choice flag' property value from Pollable.
func ExtractPollOptions(poll Pollable) (names []string, votes []int, multi bool, err error) {
var errs gtserror.MultiError

// Iterate the oneOf property and gather poll single-choice options.
IterateOneOf(poll, func(iter vocab.ActivityStreamsOneOfPropertyIterator) {
name, count, err := extractPollOption(iter.GetType())
if err != nil {
errs.Append(err)
return
}
names = append(names, name)
if count != nil {
votes = append(votes, *count)
}
})
if len(names) > 0 || len(errs) > 0 {
return names, votes, false, errs.Combine()
}

// Iterate the anyOf property and gather poll multi-choice options.
IterateAnyOf(poll, func(iter vocab.ActivityStreamsAnyOfPropertyIterator) {
name, count, err := extractPollOption(iter.GetType())
if err != nil {
errs.Append(err)
return
}
names = append(names, name)
if count != nil {
votes = append(votes, *count)
}
})
if len(names) > 0 || len(errs) > 0 {
return names, votes, true, errs.Combine()
}

return nil, nil, false, errors.New("poll without options")
}

// IterateOneOf will attempt to extract oneOf property from given interface, and passes each iterated item to function.
func IterateOneOf(withOneOf WithOneOf, foreach func(vocab.ActivityStreamsOneOfPropertyIterator)) {
if foreach == nil {
Expand Down Expand Up @@ -1158,6 +1244,41 @@ func IterateAnyOf(withAnyOf WithAnyOf, foreach func(vocab.ActivityStreamsAnyOfPr
}
}

// extractPollOption extracts a usable poll option name from vocab.Type, or error.
func extractPollOption(t vocab.Type) (name string, votes *int, err error) {
// Check fulfills PollOptionable type
// (this accounts for nil input type).
optionable, ok := t.(PollOptionable)
if !ok {
return "", nil, fmt.Errorf("incorrect option type: %T", t)
}

// Extract PollOption from interface.
name = ExtractName(optionable)
if name == "" {
return "", nil, errors.New("empty option name")
}

// Check PollOptionable for attached 'replies' property.
repliesProp := optionable.GetActivityStreamsReplies()
if repliesProp != nil {

// Get repliesProp as the AS collection type it should be.
collection := repliesProp.GetActivityStreamsCollection()
if collection != nil {

// Extract integer value from the collection 'totalItems' property.
totalItemsProp := collection.GetActivityStreamsTotalItems()
if totalItemsProp != nil {
i := totalItemsProp.Get()
votes = &i
}
}
}

return name, votes, nil
}

// isPublic checks if at least one entry in the given
// uris slice equals the activitystreams public uri.
func isPublic(uris []*url.URL) bool {
Expand Down
19 changes: 10 additions & 9 deletions internal/ap/properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)

// MustGet performs the given 'Get$Property(with) (T, error)' signature function, panicking on error.
Expand All @@ -36,12 +37,12 @@ import (
// }

// MustSet performs the given 'Set$Property(with, T) error' signature function, panicking on error.
// func MustSet[W, T any](fn func(W, T) error, with W, value T) {
// err := fn(with, value)
// if err != nil {
// panicfAt(3, "error setting property on %T: %w", with, err)
// }
// }
func MustSet[W, T any](fn func(W, T) error, with W, value T) {
err := fn(with, value)
if err != nil {
panicfAt(3, "error setting property on %T: %w", with, err)
}
}

// AppendSet performs the given 'Append$Property(with, ...T) error' signature function, panicking on error.
// func MustAppend[W, T any](fn func(W, ...T) error, with W, values ...T) {
Expand Down Expand Up @@ -320,6 +321,6 @@ func appendIRIs[T TypeOrIRI](getProp func() Property[T], iri ...*url.URL) {
}

// panicfAt panics with a call to gtserror.NewfAt() with given args (+1 to calldepth).
// func panicfAt(calldepth int, msg string, args ...any) {
// panic(gtserror.NewfAt(calldepth+1, msg, args...))
// }
func panicfAt(calldepth int, msg string, args ...any) {
panic(gtserror.NewfAt(calldepth+1, msg, args...))
}
Loading