mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-04 08:12:26 -06:00 
			
		
		
		
	
		
			
	
	
		
			352 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			352 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 
								 | 
							
								// GoToSocial
							 | 
						||
| 
								 | 
							
								// Copyright (C) GoToSocial Authors admin@gotosocial.org
							 | 
						||
| 
								 | 
							
								// SPDX-License-Identifier: AGPL-3.0-or-later
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								// This program is free software: you can redistribute it and/or modify
							 | 
						||
| 
								 | 
							
								// it under the terms of the GNU Affero General Public License as published by
							 | 
						||
| 
								 | 
							
								// the Free Software Foundation, either version 3 of the License, or
							 | 
						||
| 
								 | 
							
								// (at your option) any later version.
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								// This program is distributed in the hope that it will be useful,
							 | 
						||
| 
								 | 
							
								// but WITHOUT ANY WARRANTY; without even the implied warranty of
							 | 
						||
| 
								 | 
							
								// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
							 | 
						||
| 
								 | 
							
								// GNU Affero General Public License for more details.
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								// You should have received a copy of the GNU Affero General Public License
							 | 
						||
| 
								 | 
							
								// along with this program.  If not, see <http://www.gnu.org/licenses/>.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								package status
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import (
							 | 
						||
| 
								 | 
							
									"context"
							 | 
						||
| 
								 | 
							
									"errors"
							 | 
						||
| 
								 | 
							
									"fmt"
							 | 
						||
| 
								 | 
							
									"time"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
							 | 
						||
| 
								 | 
							
									"github.com/superseriousbusiness/gotosocial/internal/config"
							 | 
						||
| 
								 | 
							
									"github.com/superseriousbusiness/gotosocial/internal/db"
							 | 
						||
| 
								 | 
							
									"github.com/superseriousbusiness/gotosocial/internal/gtserror"
							 | 
						||
| 
								 | 
							
									"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
							 | 
						||
| 
								 | 
							
									"github.com/superseriousbusiness/gotosocial/internal/id"
							 | 
						||
| 
								 | 
							
									"github.com/superseriousbusiness/gotosocial/internal/text"
							 | 
						||
| 
								 | 
							
									"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
							 | 
						||
| 
								 | 
							
									"github.com/superseriousbusiness/gotosocial/internal/validate"
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// validateStatusContent will validate the common
							 | 
						||
| 
								 | 
							
								// content fields across status write endpoints against
							 | 
						||
| 
								 | 
							
								// current server configuration (e.g. max char counts).
							 | 
						||
| 
								 | 
							
								func validateStatusContent(
							 | 
						||
| 
								 | 
							
									status string,
							 | 
						||
| 
								 | 
							
									spoiler string,
							 | 
						||
| 
								 | 
							
									mediaIDs []string,
							 | 
						||
| 
								 | 
							
									poll *apimodel.PollRequest,
							 | 
						||
| 
								 | 
							
								) gtserror.WithCode {
							 | 
						||
| 
								 | 
							
									totalChars := len([]rune(status)) +
							 | 
						||
| 
								 | 
							
										len([]rune(spoiler))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if totalChars == 0 && len(mediaIDs) == 0 && poll == nil {
							 | 
						||
| 
								 | 
							
										const text = "status contains no text, media or poll"
							 | 
						||
| 
								 | 
							
										return gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if max := config.GetStatusesMaxChars(); totalChars > max {
							 | 
						||
| 
								 | 
							
										text := fmt.Sprintf("text with spoiler exceed max chars (%d)", max)
							 | 
						||
| 
								 | 
							
										return gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if max := config.GetStatusesMediaMaxFiles(); len(mediaIDs) > max {
							 | 
						||
| 
								 | 
							
										text := fmt.Sprintf("media files exceed max count (%d)", max)
							 | 
						||
| 
								 | 
							
										return gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if poll != nil {
							 | 
						||
| 
								 | 
							
										switch max := config.GetStatusesPollMaxOptions(); {
							 | 
						||
| 
								 | 
							
										case len(poll.Options) == 0:
							 | 
						||
| 
								 | 
							
											const text = "poll cannot have no options"
							 | 
						||
| 
								 | 
							
											return gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										case len(poll.Options) > max:
							 | 
						||
| 
								 | 
							
											text := fmt.Sprintf("poll options exceed max count (%d)", max)
							 | 
						||
| 
								 | 
							
											return gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										max := config.GetStatusesPollOptionMaxChars()
							 | 
						||
| 
								 | 
							
										for i, option := range poll.Options {
							 | 
						||
| 
								 | 
							
											switch l := len([]rune(option)); {
							 | 
						||
| 
								 | 
							
											case l == 0:
							 | 
						||
| 
								 | 
							
												const text = "poll option cannot be empty"
							 | 
						||
| 
								 | 
							
												return gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
											case l > max:
							 | 
						||
| 
								 | 
							
												text := fmt.Sprintf("poll option %d exceed max chars (%d)", i, max)
							 | 
						||
| 
								 | 
							
												return gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
											}
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return nil
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// statusContent encompasses the set of common processed
							 | 
						||
| 
								 | 
							
								// status content fields from status write operations for
							 | 
						||
| 
								 | 
							
								// an easily returnable type, without needing to allocate
							 | 
						||
| 
								 | 
							
								// an entire gtsmodel.Status{} model.
							 | 
						||
| 
								 | 
							
								type statusContent struct {
							 | 
						||
| 
								 | 
							
									Content        string
							 | 
						||
| 
								 | 
							
									ContentWarning string
							 | 
						||
| 
								 | 
							
									PollOptions    []string
							 | 
						||
| 
								 | 
							
									Language       string
							 | 
						||
| 
								 | 
							
									MentionIDs     []string
							 | 
						||
| 
								 | 
							
									Mentions       []*gtsmodel.Mention
							 | 
						||
| 
								 | 
							
									EmojiIDs       []string
							 | 
						||
| 
								 | 
							
									Emojis         []*gtsmodel.Emoji
							 | 
						||
| 
								 | 
							
									TagIDs         []string
							 | 
						||
| 
								 | 
							
									Tags           []*gtsmodel.Tag
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								func (p *Processor) processContent(
							 | 
						||
| 
								 | 
							
									ctx context.Context,
							 | 
						||
| 
								 | 
							
									author *gtsmodel.Account,
							 | 
						||
| 
								 | 
							
									statusID string,
							 | 
						||
| 
								 | 
							
									contentType string,
							 | 
						||
| 
								 | 
							
									content string,
							 | 
						||
| 
								 | 
							
									contentWarning string,
							 | 
						||
| 
								 | 
							
									language string,
							 | 
						||
| 
								 | 
							
									poll *apimodel.PollRequest,
							 | 
						||
| 
								 | 
							
								) (
							 | 
						||
| 
								 | 
							
									*statusContent,
							 | 
						||
| 
								 | 
							
									gtserror.WithCode,
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
									if language == "" {
							 | 
						||
| 
								 | 
							
										// Ensure we have a status language.
							 | 
						||
| 
								 | 
							
										language = author.Settings.Language
							 | 
						||
| 
								 | 
							
										if language == "" {
							 | 
						||
| 
								 | 
							
											const text = "account default language unset"
							 | 
						||
| 
								 | 
							
											return nil, gtserror.NewErrorInternalError(
							 | 
						||
| 
								 | 
							
												errors.New(text),
							 | 
						||
| 
								 | 
							
											)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									var err error
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Validate + normalize determined language.
							 | 
						||
| 
								 | 
							
									language, err = validate.Language(language)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										text := fmt.Sprintf("invalid language tag: %v", err)
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewErrorBadRequest(
							 | 
						||
| 
								 | 
							
											errors.New(text),
							 | 
						||
| 
								 | 
							
											text,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// format is the currently set text formatting
							 | 
						||
| 
								 | 
							
									// function, according to the provided content-type.
							 | 
						||
| 
								 | 
							
									var format text.FormatFunc
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if contentType == "" {
							 | 
						||
| 
								 | 
							
										// If content type wasn't specified, use
							 | 
						||
| 
								 | 
							
										// the author's preferred content-type.
							 | 
						||
| 
								 | 
							
										contentType = author.Settings.StatusContentType
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									switch contentType {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Format status according to text/plain.
							 | 
						||
| 
								 | 
							
									case "", string(apimodel.StatusContentTypePlain):
							 | 
						||
| 
								 | 
							
										format = p.formatter.FromPlain
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Format status according to text/markdown.
							 | 
						||
| 
								 | 
							
									case string(apimodel.StatusContentTypeMarkdown):
							 | 
						||
| 
								 | 
							
										format = p.formatter.FromMarkdown
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Unknown.
							 | 
						||
| 
								 | 
							
									default:
							 | 
						||
| 
								 | 
							
										const text = "invalid status format"
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewErrorBadRequest(
							 | 
						||
| 
								 | 
							
											errors.New(text),
							 | 
						||
| 
								 | 
							
											text,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Allocate a structure to hold the
							 | 
						||
| 
								 | 
							
									// majority of formatted content without
							 | 
						||
| 
								 | 
							
									// needing to alloc a whole gtsmodel.Status{}.
							 | 
						||
| 
								 | 
							
									var status statusContent
							 | 
						||
| 
								 | 
							
									status.Language = language
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// formatInput is a shorthand function to format the given input string with the
							 | 
						||
| 
								 | 
							
									// currently set 'formatFunc', passing in all required args and returning result.
							 | 
						||
| 
								 | 
							
									formatInput := func(formatFunc text.FormatFunc, input string) *text.FormatResult {
							 | 
						||
| 
								 | 
							
										return formatFunc(ctx, p.parseMention, author.ID, statusID, input)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Sanitize input status text and format.
							 | 
						||
| 
								 | 
							
									contentRes := formatInput(format, content)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Gather results of formatted.
							 | 
						||
| 
								 | 
							
									status.Content = contentRes.HTML
							 | 
						||
| 
								 | 
							
									status.Mentions = contentRes.Mentions
							 | 
						||
| 
								 | 
							
									status.Emojis = contentRes.Emojis
							 | 
						||
| 
								 | 
							
									status.Tags = contentRes.Tags
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// From here-on-out just use emoji-only
							 | 
						||
| 
								 | 
							
									// plain-text formatting as the FormatFunc.
							 | 
						||
| 
								 | 
							
									format = p.formatter.FromPlainEmojiOnly
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Sanitize content warning and format.
							 | 
						||
| 
								 | 
							
									warning := text.SanitizeToPlaintext(contentWarning)
							 | 
						||
| 
								 | 
							
									warningRes := formatInput(format, warning)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Gather results of the formatted.
							 | 
						||
| 
								 | 
							
									status.ContentWarning = warningRes.HTML
							 | 
						||
| 
								 | 
							
									status.Emojis = append(status.Emojis, warningRes.Emojis...)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if poll != nil {
							 | 
						||
| 
								 | 
							
										// Pre-allocate slice of poll options of expected length.
							 | 
						||
| 
								 | 
							
										status.PollOptions = make([]string, len(poll.Options))
							 | 
						||
| 
								 | 
							
										for i, option := range poll.Options {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
											// Sanitize each poll option and format.
							 | 
						||
| 
								 | 
							
											option = text.SanitizeToPlaintext(option)
							 | 
						||
| 
								 | 
							
											optionRes := formatInput(format, option)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
											// Gather results of the formatted.
							 | 
						||
| 
								 | 
							
											status.PollOptions[i] = optionRes.HTML
							 | 
						||
| 
								 | 
							
											status.Emojis = append(status.Emojis, optionRes.Emojis...)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// Also update options on the form.
							 | 
						||
| 
								 | 
							
										poll.Options = status.PollOptions
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// We may have received multiple copies of the same emoji, deduplicate these first.
							 | 
						||
| 
								 | 
							
									status.Emojis = xslices.DeduplicateFunc(status.Emojis, func(e *gtsmodel.Emoji) string {
							 | 
						||
| 
								 | 
							
										return e.ID
							 | 
						||
| 
								 | 
							
									})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Gather up the IDs of mentions from parsed content.
							 | 
						||
| 
								 | 
							
									status.MentionIDs = xslices.Gather(nil, status.Mentions,
							 | 
						||
| 
								 | 
							
										func(m *gtsmodel.Mention) string {
							 | 
						||
| 
								 | 
							
											return m.ID
							 | 
						||
| 
								 | 
							
										},
							 | 
						||
| 
								 | 
							
									)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Gather up the IDs of tags from parsed content.
							 | 
						||
| 
								 | 
							
									status.TagIDs = xslices.Gather(nil, status.Tags,
							 | 
						||
| 
								 | 
							
										func(t *gtsmodel.Tag) string {
							 | 
						||
| 
								 | 
							
											return t.ID
							 | 
						||
| 
								 | 
							
										},
							 | 
						||
| 
								 | 
							
									)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Gather up the IDs of emojis in updated content.
							 | 
						||
| 
								 | 
							
									status.EmojiIDs = xslices.Gather(nil, status.Emojis,
							 | 
						||
| 
								 | 
							
										func(e *gtsmodel.Emoji) string {
							 | 
						||
| 
								 | 
							
											return e.ID
							 | 
						||
| 
								 | 
							
										},
							 | 
						||
| 
								 | 
							
									)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return &status, nil
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								func (p *Processor) processMedia(
							 | 
						||
| 
								 | 
							
									ctx context.Context,
							 | 
						||
| 
								 | 
							
									authorID string,
							 | 
						||
| 
								 | 
							
									statusID string,
							 | 
						||
| 
								 | 
							
									mediaIDs []string,
							 | 
						||
| 
								 | 
							
								) (
							 | 
						||
| 
								 | 
							
									[]*gtsmodel.MediaAttachment,
							 | 
						||
| 
								 | 
							
									gtserror.WithCode,
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
									// No media provided!
							 | 
						||
| 
								 | 
							
									if len(mediaIDs) == 0 {
							 | 
						||
| 
								 | 
							
										return nil, nil
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Get configured min/max supported descr chars.
							 | 
						||
| 
								 | 
							
									minChars := config.GetMediaDescriptionMinChars()
							 | 
						||
| 
								 | 
							
									maxChars := config.GetMediaDescriptionMaxChars()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Pre-allocate slice of media attachments of expected length.
							 | 
						||
| 
								 | 
							
									attachments := make([]*gtsmodel.MediaAttachment, len(mediaIDs))
							 | 
						||
| 
								 | 
							
									for i, id := range mediaIDs {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// Look for media attachment by ID in database.
							 | 
						||
| 
								 | 
							
										media, err := p.state.DB.GetAttachmentByID(ctx, id)
							 | 
						||
| 
								 | 
							
										if err != nil && !errors.Is(err, db.ErrNoEntries) {
							 | 
						||
| 
								 | 
							
											err := gtserror.Newf("error getting media from db: %w", err)
							 | 
						||
| 
								 | 
							
											return nil, gtserror.NewErrorInternalError(err)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// Check media exists and is owned by author
							 | 
						||
| 
								 | 
							
										// (this masks finding out media ownership info).
							 | 
						||
| 
								 | 
							
										if media == nil || media.AccountID != authorID {
							 | 
						||
| 
								 | 
							
											text := fmt.Sprintf("media not found: %s", id)
							 | 
						||
| 
								 | 
							
											return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// Check media isn't already attached to another status.
							 | 
						||
| 
								 | 
							
										if (media.StatusID != "" && media.StatusID != statusID) ||
							 | 
						||
| 
								 | 
							
											(media.ScheduledStatusID != "" && media.ScheduledStatusID != statusID) {
							 | 
						||
| 
								 | 
							
											text := fmt.Sprintf("media already attached to status: %s", id)
							 | 
						||
| 
								 | 
							
											return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// Check media description chars within range,
							 | 
						||
| 
								 | 
							
										// this needs to be done here as lots of clients
							 | 
						||
| 
								 | 
							
										// only update media description on status post.
							 | 
						||
| 
								 | 
							
										switch chars := len([]rune(media.Description)); {
							 | 
						||
| 
								 | 
							
										case chars < minChars:
							 | 
						||
| 
								 | 
							
											text := fmt.Sprintf("media description less than min chars (%d)", minChars)
							 | 
						||
| 
								 | 
							
											return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										case chars > maxChars:
							 | 
						||
| 
								 | 
							
											text := fmt.Sprintf("media description exceeds max chars (%d)", maxChars)
							 | 
						||
| 
								 | 
							
											return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										// Set media at index.
							 | 
						||
| 
								 | 
							
										attachments[i] = media
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return attachments, nil
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								func (p *Processor) processPoll(
							 | 
						||
| 
								 | 
							
									ctx context.Context,
							 | 
						||
| 
								 | 
							
									statusID string,
							 | 
						||
| 
								 | 
							
									form *apimodel.PollRequest,
							 | 
						||
| 
								 | 
							
									now time.Time, // used for expiry time
							 | 
						||
| 
								 | 
							
								) (
							 | 
						||
| 
								 | 
							
									*gtsmodel.Poll,
							 | 
						||
| 
								 | 
							
									gtserror.WithCode,
							 | 
						||
| 
								 | 
							
								) {
							 | 
						||
| 
								 | 
							
									var expiresAt time.Time
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Set an expiry time if one given.
							 | 
						||
| 
								 | 
							
									if in := form.ExpiresIn; in > 0 {
							 | 
						||
| 
								 | 
							
										expiresIn := time.Duration(in)
							 | 
						||
| 
								 | 
							
										expiresAt = now.Add(expiresIn * time.Second)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Create new poll model.
							 | 
						||
| 
								 | 
							
									poll := >smodel.Poll{
							 | 
						||
| 
								 | 
							
										ID:         id.NewULIDFromTime(now),
							 | 
						||
| 
								 | 
							
										Multiple:   &form.Multiple,
							 | 
						||
| 
								 | 
							
										HideCounts: &form.HideTotals,
							 | 
						||
| 
								 | 
							
										Options:    form.Options,
							 | 
						||
| 
								 | 
							
										StatusID:   statusID,
							 | 
						||
| 
								 | 
							
										ExpiresAt:  expiresAt,
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Insert the newly created poll model in the database.
							 | 
						||
| 
								 | 
							
									if err := p.state.DB.PutPoll(ctx, poll); err != nil {
							 | 
						||
| 
								 | 
							
										err := gtserror.Newf("error inserting poll in db: %w", err)
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewErrorInternalError(err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return poll, nil
							 | 
						||
| 
								 | 
							
								}
							 |