[feature] unending polls (#3592)

* adds support for unending polls to be created locally

* remove unused argument
This commit is contained in:
kim 2024-12-04 09:35:48 +00:00 committed by GitHub
parent 79f2e85f51
commit 3e18d97a6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 53 additions and 30 deletions

View file

@ -88,12 +88,15 @@ func (p *pollDB) getPoll(ctx context.Context, lookup string, dbQuery func(*gtsmo
func (p *pollDB) GetOpenPolls(ctx context.Context) ([]*gtsmodel.Poll, error) { func (p *pollDB) GetOpenPolls(ctx context.Context) ([]*gtsmodel.Poll, error) {
var pollIDs []string var pollIDs []string
// Select all polls with unset `closed_at` time. // Select all polls with:
// - UNSET `closed_at`
// - SET `expires_at`
if err := p.db.NewSelect(). if err := p.db.NewSelect().
Table("polls"). Table("polls").
Column("polls.id"). Column("polls.id").
Join("JOIN ? ON ? = ?", bun.Ident("statuses"), bun.Ident("polls.id"), bun.Ident("statuses.poll_id")). Join("JOIN ? ON ? = ?", bun.Ident("statuses"), bun.Ident("polls.id"), bun.Ident("statuses.poll_id")).
Where("? = true", bun.Ident("statuses.local")). Where("? = true", bun.Ident("statuses.local")).
Where("? IS NOT NULL", bun.Ident("polls.expires_at")).
Where("? IS NULL", bun.Ident("polls.closed_at")). Where("? IS NULL", bun.Ident("polls.closed_at")).
Scan(ctx, &pollIDs); err != nil { Scan(ctx, &pollIDs); err != nil {
return nil, err return nil, err

View file

@ -85,25 +85,8 @@ func (p *Processor) Create(
PendingApproval: util.Ptr(false), PendingApproval: util.Ptr(false),
} }
if form.Poll != nil { // Process any attached poll.
// Update the status AS type to "Question". p.processPoll(status, form.Poll)
status.ActivityStreamsType = ap.ActivityQuestion
// Create new poll for status from form.
secs := time.Duration(form.Poll.ExpiresIn)
status.Poll = &gtsmodel.Poll{
ID: id.NewULID(),
Multiple: &form.Poll.Multiple,
HideCounts: &form.Poll.HideTotals,
Options: form.Poll.Options,
StatusID: statusID,
Status: status,
ExpiresAt: now.Add(secs * time.Second),
}
// Set poll ID on the status.
status.PollID = status.Poll.ID
}
// Check + attach in-reply-to status. // Check + attach in-reply-to status.
if errWithCode := p.processInReplyTo(ctx, if errWithCode := p.processInReplyTo(ctx,
@ -153,6 +136,14 @@ func (p *Processor) Create(
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
if status.Poll != nil && !status.Poll.ExpiresAt.IsZero() {
// Now that the status is inserted, and side effects queued,
// attempt to schedule an expiry handler for the status poll.
if err := p.polls.ScheduleExpiry(ctx, status.Poll); err != nil {
log.Errorf(ctx, "error scheduling poll expiry: %v", err)
}
}
// send it back to the client API worker for async side-effects. // send it back to the client API worker for async side-effects.
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ObjectNote, APObjectType: ap.ObjectNote,
@ -161,14 +152,6 @@ func (p *Processor) Create(
Origin: requester, Origin: requester,
}) })
if status.Poll != nil {
// Now that the status is inserted, and side effects queued,
// attempt to schedule an expiry handler for the status poll.
if err := p.polls.ScheduleExpiry(ctx, status.Poll); err != nil {
log.Errorf(ctx, "error scheduling poll expiry: %v", err)
}
}
// If the new status replies to a status that // If the new status replies to a status that
// replies to us, use our reply as an implicit // replies to us, use our reply as an implicit
// accept of any pending interaction. // accept of any pending interaction.
@ -189,6 +172,43 @@ func (p *Processor) Create(
return p.c.GetAPIStatus(ctx, requester, status) return p.c.GetAPIStatus(ctx, requester, status)
} }
func (p *Processor) processPoll(status *gtsmodel.Status, poll *apimodel.PollRequest) {
if poll == nil {
// No poll set.
// Nothing to do.
return
}
var expiresAt time.Time
// Now will have been set
// as the status creation.
now := status.CreatedAt
// Update the status AS type to "Question".
status.ActivityStreamsType = ap.ActivityQuestion
// Set an expiry time if one given.
if in := poll.ExpiresIn; in > 0 {
expiresIn := time.Duration(in)
expiresAt = now.Add(expiresIn * time.Second)
}
// Create new poll for status.
status.Poll = &gtsmodel.Poll{
ID: id.NewULID(),
Multiple: &poll.Multiple,
HideCounts: &poll.HideTotals,
Options: poll.Options,
StatusID: status.ID,
Status: status,
ExpiresAt: expiresAt,
}
// Set poll ID on the status.
status.PollID = status.Poll.ID
}
func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status, inReplyToID string) gtserror.WithCode { func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status, inReplyToID string) gtserror.WithCode {
if inReplyToID == "" { if inReplyToID == "" {
// Not a reply. // Not a reply.

View file

@ -444,7 +444,7 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat
poll := streams.NewActivityStreamsQuestion() poll := streams.NewActivityStreamsQuestion()
// Add required status poll data to AS Question. // Add required status poll data to AS Question.
if err := c.addPollToAS(ctx, s.Poll, poll); err != nil { if err := c.addPollToAS(s.Poll, poll); err != nil {
return nil, gtserror.Newf("error converting poll: %w", err) return nil, gtserror.Newf("error converting poll: %w", err)
} }
@ -708,7 +708,7 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat
return status, nil return status, nil
} }
func (c *Converter) addPollToAS(ctx context.Context, poll *gtsmodel.Poll, dst ap.Pollable) error { func (c *Converter) addPollToAS(poll *gtsmodel.Poll, dst ap.Pollable) error {
var optionsProp interface { var optionsProp interface {
// the minimum interface for appending AS Notes // the minimum interface for appending AS Notes
// to an AS type options property of some kind. // to an AS type options property of some kind.