Skip to content

Commit

Permalink
[feature] add support for polls + receiving federated status edits (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
NyaaaWhatsUpDoc authored Nov 8, 2023
1 parent 7204cce commit e9e5dc5
Show file tree
Hide file tree
Showing 84 changed files with 3,977 additions and 555 deletions.
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

0 comments on commit e9e5dc5

Please sign in to comment.