mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-02-24 00:38:51 +01:00
[bugfix] update fedi api to support multiple separate votes in same multiple choice poll (#3809)
This commit is contained in:
parent
2f5629143d
commit
a03a35a5d6
6 changed files with 142 additions and 44 deletions
|
@ -380,8 +380,8 @@ func (p *pollDB) PutPollVote(ctx context.Context, vote *gtsmodel.PollVote) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment poll votes for choices.
|
// Increment vote choices and voters count.
|
||||||
poll.IncrementVotes(vote.Choices)
|
poll.IncrementVotes(vote.Choices, true)
|
||||||
|
|
||||||
// Finally, update the poll entry.
|
// Finally, update the poll entry.
|
||||||
_, err := tx.NewUpdate().
|
_, err := tx.NewUpdate().
|
||||||
|
@ -394,6 +394,17 @@ func (p *pollDB) PutPollVote(ctx context.Context, vote *gtsmodel.PollVote) error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *pollDB) UpdatePollVote(ctx context.Context, vote *gtsmodel.PollVote, cols ...string) error {
|
||||||
|
return p.state.Caches.DB.PollVote.Store(vote, func() error {
|
||||||
|
_, err := p.db.NewUpdate().
|
||||||
|
Model(vote).
|
||||||
|
Column(cols...).
|
||||||
|
Where("? = ?", bun.Ident("id"), vote.ID).
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (p *pollDB) DeletePollVoteBy(ctx context.Context, pollID string, accountID string) error {
|
func (p *pollDB) DeletePollVoteBy(ctx context.Context, pollID string, accountID string) error {
|
||||||
// Gather necessary fields from
|
// Gather necessary fields from
|
||||||
// deleted for cache invaliation.
|
// deleted for cache invaliation.
|
||||||
|
@ -487,8 +498,8 @@ func updatePollCounts(ctx context.Context, tx bun.Tx, deleted *gtsmodel.PollVote
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrement votes for these choices.
|
// Decrement vote choices and voters count.
|
||||||
poll.DecrementVotes(deleted.Choices)
|
poll.DecrementVotes(deleted.Choices, true)
|
||||||
|
|
||||||
// Finally, update the poll entry.
|
// Finally, update the poll entry.
|
||||||
if _, err := tx.NewUpdate().
|
if _, err := tx.NewUpdate().
|
||||||
|
|
|
@ -58,6 +58,9 @@ type Poll interface {
|
||||||
// PutPollVote puts the given PollVote in the database.
|
// PutPollVote puts the given PollVote in the database.
|
||||||
PutPollVote(ctx context.Context, vote *gtsmodel.PollVote) error
|
PutPollVote(ctx context.Context, vote *gtsmodel.PollVote) error
|
||||||
|
|
||||||
|
// UpdatePollVote updates the given poll vote in the database, using only given columns (else, all).
|
||||||
|
UpdatePollVote(ctx context.Context, vote *gtsmodel.PollVote, cols ...string) error
|
||||||
|
|
||||||
// DeletePollVoteBy deletes the PollVote in Poll with ID, by account ID, from the database.
|
// DeletePollVoteBy deletes the PollVote in Poll with ID, by account ID, from the database.
|
||||||
DeletePollVoteBy(ctx context.Context, pollID string, accountID string) error
|
DeletePollVoteBy(ctx context.Context, pollID string, accountID string) error
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
|
@ -138,6 +139,10 @@ func (f *federatingDB) createPollOptionables(
|
||||||
// then re-used in each further iteration.
|
// then re-used in each further iteration.
|
||||||
inReplyTo *gtsmodel.Status
|
inReplyTo *gtsmodel.Status
|
||||||
|
|
||||||
|
// existing poll vote by requesting user
|
||||||
|
// in receiving user's poll, if it exists.
|
||||||
|
vote *gtsmodel.PollVote
|
||||||
|
|
||||||
// the resulting slices of Poll.Option
|
// the resulting slices of Poll.Option
|
||||||
// choice indices passed into the new
|
// choice indices passed into the new
|
||||||
// created PollVote object.
|
// created PollVote object.
|
||||||
|
@ -169,15 +174,17 @@ func (f *federatingDB) createPollOptionables(
|
||||||
case inReplyTo.PollID == "":
|
case inReplyTo.PollID == "":
|
||||||
return gtserror.Newf("poll vote in status %s without poll", statusURI)
|
return gtserror.Newf("poll vote in status %s without poll", statusURI)
|
||||||
|
|
||||||
// We don't own the poll ...
|
// Ensure poll isn't closed.
|
||||||
case !*inReplyTo.Local:
|
case inReplyTo.Poll.Closed():
|
||||||
return gtserror.Newf("poll vote in remote status %s", statusURI)
|
return gtserror.Newf("poll vote in closed poll %s", statusURI)
|
||||||
|
|
||||||
|
// Ensure receiver actually owns poll.
|
||||||
|
case inReplyTo.AccountID != receiver.ID:
|
||||||
|
return gtserror.Newf("receiving account %s does not own poll %s", receiver.URI, statusURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether user has already vote in this poll.
|
// Try load any existing vote(s) by user.
|
||||||
// (we only check this for the first object, as multiple
|
vote, err = f.state.DB.GetPollVoteBy(
|
||||||
// may be sent in response to a multiple-choice poll).
|
|
||||||
vote, err := f.state.DB.GetPollVoteBy(
|
|
||||||
gtscontext.SetBarebones(ctx),
|
gtscontext.SetBarebones(ctx),
|
||||||
inReplyTo.PollID,
|
inReplyTo.PollID,
|
||||||
requester.ID,
|
requester.ID,
|
||||||
|
@ -185,11 +192,6 @@ func (f *federatingDB) createPollOptionables(
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("error getting status %s poll votes from database: %w", statusURI, err)
|
return gtserror.Newf("error getting status %s poll votes from database: %w", statusURI, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if vote != nil {
|
|
||||||
log.Warnf(ctx, "%s has already voted in poll %s", requester.URI, statusURI)
|
|
||||||
return nil // this is a useful warning for admins to report to us from logs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if statusURI != inReplyTo.URI {
|
if statusURI != inReplyTo.URI {
|
||||||
|
@ -210,21 +212,60 @@ func (f *federatingDB) createPollOptionables(
|
||||||
choices = append(choices, choice)
|
choices = append(choices, choice)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enqueue message to the fedi API worker with poll vote(s).
|
if vote != nil {
|
||||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
// Ensure this isn't a multiple vote
|
||||||
APActivityType: ap.ActivityCreate,
|
// by same account in the same poll.
|
||||||
APObjectType: ap.ActivityQuestion,
|
if !*inReplyTo.Poll.Multiple {
|
||||||
GTSModel: >smodel.PollVote{
|
log.Warnf(ctx, "%s has already voted in single-choice poll %s", requester.URI, inReplyTo.URI)
|
||||||
ID: id.NewULID(),
|
return nil // this is a useful warning for admins to report to us from logs
|
||||||
Choices: choices,
|
}
|
||||||
AccountID: requester.ID,
|
|
||||||
Account: requester,
|
// Drop all existing votes from the new slice of choices.
|
||||||
PollID: inReplyTo.PollID,
|
choices = slices.DeleteFunc(choices, func(choice int) bool {
|
||||||
Poll: inReplyTo.Poll,
|
return slices.Contains(vote.Choices, choice)
|
||||||
},
|
})
|
||||||
Receiving: receiver,
|
|
||||||
Requesting: requester,
|
// Update poll with new choices but *not* voters.
|
||||||
})
|
inReplyTo.Poll.IncrementVotes(choices, false)
|
||||||
|
|
||||||
|
// Append new vote choices to existing
|
||||||
|
// vote, then sort them by index size.
|
||||||
|
vote.Choices = append(vote.Choices, choices...)
|
||||||
|
slices.Sort(vote.Choices)
|
||||||
|
|
||||||
|
// Populate the poll vote,
|
||||||
|
// later used in fedi worker.
|
||||||
|
vote.Poll = inReplyTo.Poll
|
||||||
|
vote.Account = requester
|
||||||
|
|
||||||
|
// TODO: would it be useful to store an UpdatedAt field
|
||||||
|
// on poll votes? i'm not sure we'd actually use it...
|
||||||
|
|
||||||
|
// Enqueue an update event for poll vote to fedi API worker.
|
||||||
|
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||||
|
APActivityType: ap.ActivityUpdate,
|
||||||
|
APObjectType: ap.ActivityQuestion,
|
||||||
|
GTSModel: vote,
|
||||||
|
Receiving: receiver,
|
||||||
|
Requesting: requester,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Create new poll vote and enqueue create to fedi API worker.
|
||||||
|
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||||
|
APActivityType: ap.ActivityCreate,
|
||||||
|
APObjectType: ap.ActivityQuestion,
|
||||||
|
GTSModel: >smodel.PollVote{
|
||||||
|
ID: id.NewULID(),
|
||||||
|
Choices: choices,
|
||||||
|
AccountID: requester.ID,
|
||||||
|
Account: requester,
|
||||||
|
PollID: inReplyTo.PollID,
|
||||||
|
Poll: inReplyTo.Poll,
|
||||||
|
},
|
||||||
|
Receiving: receiver,
|
||||||
|
Requesting: requester,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,8 +60,8 @@ func (p *Poll) Closed() bool {
|
||||||
time.Now().After(p.ClosedAt)
|
time.Now().After(p.ClosedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IncrementVotes increments Poll vote and voter counts for given choices.
|
// IncrementVotes increments Poll vote counts for given choices, and voters if 'isNew' is set.
|
||||||
func (p *Poll) IncrementVotes(choices []int) {
|
func (p *Poll) IncrementVotes(choices []int, isNew bool) {
|
||||||
if len(choices) == 0 {
|
if len(choices) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -69,11 +69,13 @@ func (p *Poll) IncrementVotes(choices []int) {
|
||||||
for _, choice := range choices {
|
for _, choice := range choices {
|
||||||
p.Votes[choice]++
|
p.Votes[choice]++
|
||||||
}
|
}
|
||||||
(*p.Voters)++
|
if isNew {
|
||||||
|
(*p.Voters)++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecrementVotes decrements Poll vote and voter counts for given choices.
|
// DecrementVotes decrements Poll vote counts for given choices, and voters if 'withVoter' is set.
|
||||||
func (p *Poll) DecrementVotes(choices []int) {
|
func (p *Poll) DecrementVotes(choices []int, withVoter bool) {
|
||||||
if len(choices) == 0 {
|
if len(choices) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -83,7 +85,8 @@ func (p *Poll) DecrementVotes(choices []int) {
|
||||||
p.Votes[choice]--
|
p.Votes[choice]--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (*p.Voters) != 0 {
|
if (*p.Voters) != 0 &&
|
||||||
|
withVoter {
|
||||||
(*p.Voters)--
|
(*p.Voters)--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ func (p *Processor) PollVote(ctx context.Context, requester *gtsmodel.Account, p
|
||||||
// Before enqueuing it, increment the poll
|
// Before enqueuing it, increment the poll
|
||||||
// vote counts on the copy attached to the
|
// vote counts on the copy attached to the
|
||||||
// PollVote (that we also later return).
|
// PollVote (that we also later return).
|
||||||
poll.IncrementVotes(choices)
|
poll.IncrementVotes(choices, true)
|
||||||
|
|
||||||
// Enqueue worker task to handle side-effects of user poll vote(s).
|
// Enqueue worker task to handle side-effects of user poll vote(s).
|
||||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||||
|
|
|
@ -124,6 +124,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
|
||||||
// UPDATE ACCOUNT
|
// UPDATE ACCOUNT
|
||||||
case ap.ActorPerson:
|
case ap.ActorPerson:
|
||||||
return p.fediAPI.UpdateAccount(ctx, fMsg)
|
return p.fediAPI.UpdateAccount(ctx, fMsg)
|
||||||
|
|
||||||
|
// UPDATE QUESTION
|
||||||
|
case ap.ActivityQuestion:
|
||||||
|
return p.fediAPI.UpdatePollVote(ctx, fMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACCEPT SOMETHING
|
// ACCEPT SOMETHING
|
||||||
|
@ -355,7 +359,8 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
|
||||||
return gtserror.Newf("cannot cast %T -> *gtsmodel.PollVote", fMsg.GTSModel)
|
return gtserror.Newf("cannot cast %T -> *gtsmodel.PollVote", fMsg.GTSModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the new poll vote in the database.
|
// Insert the new poll vote in the database, note this
|
||||||
|
// will handle updating votes on the poll model itself.
|
||||||
if err := p.state.DB.PutPollVote(ctx, vote); err != nil {
|
if err := p.state.DB.PutPollVote(ctx, vote); err != nil {
|
||||||
return gtserror.Newf("error inserting poll vote in db: %w", err)
|
return gtserror.Newf("error inserting poll vote in db: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -376,9 +381,9 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
|
||||||
status.Poll = vote.Poll
|
status.Poll = vote.Poll
|
||||||
|
|
||||||
if *status.Local {
|
if *status.Local {
|
||||||
// Before federating it, increment the
|
// Before federating it, increment the poll vote
|
||||||
// poll vote counts on our local copy.
|
// and voter counts, *only on our local copy*.
|
||||||
status.Poll.IncrementVotes(vote.Choices)
|
status.Poll.IncrementVotes(vote.Choices, true)
|
||||||
|
|
||||||
// These were poll votes in a local status, we need to
|
// These were poll votes in a local status, we need to
|
||||||
// federate the updated status model with latest vote counts.
|
// federate the updated status model with latest vote counts.
|
||||||
|
@ -387,8 +392,43 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interaction counts changed on the source status, uncache from timelines.
|
// Interaction counts changed, uncache from timelines.
|
||||||
p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
|
p.surface.invalidateStatusFromTimelines(ctx, status.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *fediAPI) UpdatePollVote(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||||
|
// Cast poll vote type from the worker message.
|
||||||
|
vote, ok := fMsg.GTSModel.(*gtsmodel.PollVote)
|
||||||
|
if !ok {
|
||||||
|
return gtserror.Newf("cannot cast %T -> *gtsmodel.PollVote", fMsg.GTSModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update poll vote model (specifically only choices) in the database.
|
||||||
|
if err := p.state.DB.UpdatePollVote(ctx, vote, "choices"); err != nil {
|
||||||
|
return gtserror.Newf("error updating poll vote in db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the vote counts on the poll model itself. These will have
|
||||||
|
// been updated by message pusher as we can't know which were new.
|
||||||
|
if err := p.state.DB.UpdatePoll(ctx, vote.Poll, "votes"); err != nil {
|
||||||
|
return gtserror.Newf("error updating poll in db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the origin status.
|
||||||
|
status := vote.Poll.Status
|
||||||
|
|
||||||
|
if *status.Local {
|
||||||
|
// These were poll votes in a local status, we need to
|
||||||
|
// federate the updated status model with latest vote counts.
|
||||||
|
if err := p.federate.UpdateStatus(ctx, status); err != nil {
|
||||||
|
log.Errorf(ctx, "error federating status update: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interaction counts changed, uncache from timelines.
|
||||||
|
p.surface.invalidateStatusFromTimelines(ctx, status.ID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue