moving stuff around
This commit is contained in:
@ -80,7 +80,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht
|
||||
}
|
||||
|
||||
// put it in our channel to queue it for async processing
|
||||
p.FromFederator() <- gtsmodel.FromFederator{
|
||||
p.fromFederator <- gtsmodel.FromFederator{
|
||||
APObjectType: gtsmodel.ActivityStreamsProfile,
|
||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||
GTSModel: requestingAccount,
|
||||
|
||||
@ -32,8 +32,8 @@ import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/status"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/timeline"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
@ -44,14 +44,6 @@ import (
|
||||
// fire messages into the processor and not wait for a reply before proceeding with other work. This allows
|
||||
// for clean distribution of messages without slowing down the client API and harming the user experience.
|
||||
type Processor interface {
|
||||
// ToClientAPI returns a channel for putting in messages that need to go to the gts client API.
|
||||
// ToClientAPI() chan gtsmodel.ToClientAPI
|
||||
// FromClientAPI returns a channel for putting messages in that come from the client api going to the processor
|
||||
FromClientAPI() chan gtsmodel.FromClientAPI
|
||||
// ToFederator returns a channel for putting in messages that need to go to the federator (activitypub).
|
||||
// ToFederator() chan gtsmodel.ToFederator
|
||||
// FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor
|
||||
FromFederator() chan gtsmodel.FromFederator
|
||||
// Start starts the Processor, reading from its channels and passing messages back and forth.
|
||||
Start() error
|
||||
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
|
||||
@ -227,22 +219,6 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
|
||||
}
|
||||
}
|
||||
|
||||
// func (p *processor) ToClientAPI() chan gtsmodel.ToClientAPI {
|
||||
// return p.toClientAPI
|
||||
// }
|
||||
|
||||
func (p *processor) FromClientAPI() chan gtsmodel.FromClientAPI {
|
||||
return p.fromClientAPI
|
||||
}
|
||||
|
||||
// func (p *processor) ToFederator() chan gtsmodel.ToFederator {
|
||||
// return p.toFederator
|
||||
// }
|
||||
|
||||
func (p *processor) FromFederator() chan gtsmodel.FromFederator {
|
||||
return p.fromFederator
|
||||
}
|
||||
|
||||
// Start starts the Processor, reading from its channels and passing messages back and forth.
|
||||
func (p *processor) Start() error {
|
||||
go func() {
|
||||
|
||||
@ -60,7 +60,7 @@ func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Appli
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// send it to the processor for async processing
|
||||
// send it back to the processor for async processing
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsAnnounce,
|
||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||
@ -80,7 +80,7 @@ func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Appl
|
||||
}
|
||||
}
|
||||
|
||||
// put the new status in the appropriate channel for async processing
|
||||
// send it back to the processor for async processing
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsNote,
|
||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||
@ -43,7 +43,8 @@ func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*a
|
||||
if err := p.db.DeleteByID(targetStatus.ID, targetStatus); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err))
|
||||
}
|
||||
|
||||
|
||||
// send it back to the processor for async processing
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsNote,
|
||||
APActivityType: gtsmodel.ActivityStreamsDelete,
|
||||
@ -84,7 +84,7 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err))
|
||||
}
|
||||
|
||||
// send the new fave through the processor channel for federation etc
|
||||
// send it back to the processor for async processing
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsLike,
|
||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||
@ -47,6 +47,6 @@ func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, fromClient
|
||||
config: config,
|
||||
db: db,
|
||||
fromClientAPI: fromClientAPI,
|
||||
log: log,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,7 @@ func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*a
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
|
||||
}
|
||||
|
||||
// send the unfave through the processor channel for federation etc
|
||||
// send it back to the processor for async processing
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsLike,
|
||||
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||
@ -1,189 +0,0 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 timeline
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
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/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
const (
|
||||
preparedPostsMinLength = 80
|
||||
desiredPostIndexLength = 400
|
||||
)
|
||||
|
||||
// Manager abstracts functions for creating timelines for multiple accounts, and adding, removing, and fetching entries from those timelines.
|
||||
//
|
||||
// By the time a status hits the manager interface, it should already have been filtered and it should be established that the status indeed
|
||||
// belongs in the home timeline of the given account ID.
|
||||
//
|
||||
// The manager makes a distinction between *indexed* posts and *prepared* posts.
|
||||
//
|
||||
// Indexed posts consist of just that post's ID (in the database) and the time it was created. An indexed post takes up very little memory, so
|
||||
// it's not a huge priority to keep trimming the indexed posts list.
|
||||
//
|
||||
// Prepared posts consist of the post's database ID, the time it was created, AND the apimodel representation of that post, for quick serialization.
|
||||
// Prepared posts of course take up more memory than indexed posts, so they should be regularly pruned if they're not being actively served.
|
||||
type Manager interface {
|
||||
// Ingest takes one status and indexes it into the timeline for the given account ID.
|
||||
//
|
||||
// It should already be established before calling this function that the status/post actually belongs in the timeline!
|
||||
Ingest(status *gtsmodel.Status, timelineAccountID string) error
|
||||
// IngestAndPrepare takes one status and indexes it into the timeline for the given account ID, and then immediately prepares it for serving.
|
||||
//
|
||||
// It should already be established before calling this function that the status/post actually belongs in the timeline!
|
||||
IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) error
|
||||
// HomeTimeline returns limit n amount of entries from the home timeline of the given account ID, in descending chronological order.
|
||||
// If maxID is provided, it will return entries from that maxID onwards, inclusive.
|
||||
HomeTimeline(timelineAccountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, error)
|
||||
// GetIndexedLength returns the amount of posts/statuses that have been *indexed* for the given account ID.
|
||||
GetIndexedLength(timelineAccountID string) int
|
||||
// GetDesiredIndexLength returns the amount of posts that we, ideally, index for each user.
|
||||
GetDesiredIndexLength() int
|
||||
// GetOldestIndexedID returns the status ID for the oldest post that we have indexed for the given account.
|
||||
GetOldestIndexedID(timelineAccountID string) (string, error)
|
||||
// PrepareXFromTop prepares limit n amount of posts, based on their indexed representations, from the top of the index.
|
||||
PrepareXFromTop(timelineAccountID string, limit int) error
|
||||
}
|
||||
|
||||
// NewManager returns a new timeline manager with the given database, typeconverter, config, and log.
|
||||
func NewManager(db db.DB, tc typeutils.TypeConverter, config *config.Config, log *logrus.Logger) Manager {
|
||||
return &manager{
|
||||
accountTimelines: sync.Map{},
|
||||
db: db,
|
||||
tc: tc,
|
||||
config: config,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
accountTimelines sync.Map
|
||||
db db.DB
|
||||
tc typeutils.TypeConverter
|
||||
config *config.Config
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) error {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "Ingest",
|
||||
"timelineAccountID": timelineAccountID,
|
||||
"statusID": status.ID,
|
||||
})
|
||||
|
||||
t := m.getOrCreateTimeline(timelineAccountID)
|
||||
|
||||
l.Trace("ingesting status")
|
||||
return t.IndexOne(status.CreatedAt, status.ID)
|
||||
}
|
||||
|
||||
func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) error {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "IngestAndPrepare",
|
||||
"timelineAccountID": timelineAccountID,
|
||||
"statusID": status.ID,
|
||||
})
|
||||
|
||||
t := m.getOrCreateTimeline(timelineAccountID)
|
||||
|
||||
l.Trace("ingesting status")
|
||||
return t.IndexAndPrepareOne(status.CreatedAt, status.ID)
|
||||
}
|
||||
|
||||
func (m *manager) Remove(statusID string, timelineAccountID string) error {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "Remove",
|
||||
"timelineAccountID": timelineAccountID,
|
||||
"statusID": statusID,
|
||||
})
|
||||
|
||||
t := m.getOrCreateTimeline(timelineAccountID)
|
||||
|
||||
l.Trace("removing status")
|
||||
return t.Remove(statusID)
|
||||
}
|
||||
|
||||
func (m *manager) HomeTimeline(timelineAccountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, error) {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "HomeTimelineGet",
|
||||
"timelineAccountID": timelineAccountID,
|
||||
})
|
||||
|
||||
t := m.getOrCreateTimeline(timelineAccountID)
|
||||
|
||||
var err error
|
||||
var statuses []*apimodel.Status
|
||||
if maxID != "" {
|
||||
statuses, err = t.GetXFromIDOnwards(limit, maxID)
|
||||
} else if sinceID != "" {
|
||||
statuses, err = t.GetXBeforeID(limit, sinceID)
|
||||
} else {
|
||||
statuses, err = t.GetXFromTop(limit)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
l.Errorf("error getting statuses: %s", err)
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetIndexedLength(timelineAccountID string) int {
|
||||
t := m.getOrCreateTimeline(timelineAccountID)
|
||||
|
||||
return t.PostIndexLength()
|
||||
}
|
||||
|
||||
func (m *manager) GetDesiredIndexLength() int {
|
||||
return desiredPostIndexLength
|
||||
}
|
||||
|
||||
func (m *manager) GetOldestIndexedID(timelineAccountID string) (string, error) {
|
||||
t := m.getOrCreateTimeline(timelineAccountID)
|
||||
|
||||
return t.OldestIndexedPostID()
|
||||
}
|
||||
|
||||
func (m *manager) PrepareXFromTop(timelineAccountID string, limit int) error {
|
||||
t := m.getOrCreateTimeline(timelineAccountID)
|
||||
|
||||
return t.PrepareXFromTop(limit)
|
||||
}
|
||||
|
||||
func (m *manager) getOrCreateTimeline(timelineAccountID string) Timeline {
|
||||
var t Timeline
|
||||
i, ok := m.accountTimelines.Load(timelineAccountID)
|
||||
if !ok {
|
||||
t = NewTimeline(timelineAccountID, m.db, m.tc)
|
||||
m.accountTimelines.Store(timelineAccountID, t)
|
||||
} else {
|
||||
t, ok = i.(Timeline)
|
||||
if !ok {
|
||||
panic("couldn't parse entry as Timeline, this should never happen so panic")
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
package timeline
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type postIndex struct {
|
||||
data *list.List
|
||||
}
|
||||
|
||||
type postIndexEntry struct {
|
||||
createdAt time.Time
|
||||
statusID string
|
||||
}
|
||||
|
||||
func (p *postIndex) insertIndexed(i *postIndexEntry) error {
|
||||
|
||||
if p.data == nil {
|
||||
p.data = &list.List{}
|
||||
}
|
||||
|
||||
// if we have no entries yet, this is both the newest and oldest entry, so just put it in the front
|
||||
if p.data.Len() == 0 {
|
||||
p.data.PushFront(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
// we need to iterate through the index to make sure we put this post in the appropriate place according to when it was created
|
||||
for e := p.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*postIndexEntry)
|
||||
if !ok {
|
||||
return errors.New("Remove: could not parse e as a postIndexEntry")
|
||||
}
|
||||
|
||||
// if the post to index is newer than e, insert it before e in the list
|
||||
if i.createdAt.After(entry.createdAt) {
|
||||
p.data.InsertBefore(i, e)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach this point it's the oldest post we've seen so put it at the back
|
||||
p.data.PushBack(i)
|
||||
return nil
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
package timeline
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
)
|
||||
|
||||
type preparedPosts struct {
|
||||
data *list.List
|
||||
}
|
||||
|
||||
type preparedPostsEntry struct {
|
||||
createdAt time.Time
|
||||
statusID string
|
||||
prepared *apimodel.Status
|
||||
}
|
||||
|
||||
func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
|
||||
if p.data == nil {
|
||||
p.data = &list.List{}
|
||||
}
|
||||
|
||||
// if we have no entries yet, this is both the newest and oldest entry, so just put it in the front
|
||||
if p.data.Len() == 0 {
|
||||
p.data.PushFront(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
// we need to iterate through the index to make sure we put this post in the appropriate place according to when it was created
|
||||
for e := p.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*preparedPostsEntry)
|
||||
if !ok {
|
||||
return errors.New("index: could not parse e as a preparedPostsEntry")
|
||||
}
|
||||
|
||||
// if the post to index is newer than e, insert it before e in the list
|
||||
if i.createdAt.After(entry.createdAt) {
|
||||
p.data.InsertBefore(i, e)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if we reach this point it's the oldest post we've seen so put it at the back
|
||||
p.data.PushBack(i)
|
||||
return nil
|
||||
}
|
||||
@ -1,444 +0,0 @@
|
||||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 timeline
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
const (
|
||||
preparedPostsMaxLength = desiredPostIndexLength
|
||||
)
|
||||
|
||||
// Timeline represents a timeline for one account, and contains indexed and prepared posts.
|
||||
type Timeline interface {
|
||||
/*
|
||||
RETRIEVAL FUNCTIONS
|
||||
*/
|
||||
|
||||
// GetXFromTop returns x amount of posts from the top of the timeline, from newest to oldest.
|
||||
GetXFromTop(amount int) ([]*apimodel.Status, error)
|
||||
// GetXFromIDOnwards returns x amount of posts from the given id onwards, from newest to oldest.
|
||||
// This will include the status with the given ID.
|
||||
GetXFromIDOnwards(amount int, fromID string) ([]*apimodel.Status, error)
|
||||
// GetXBeforeID returns x amount of posts up to the given id, from newest to oldest.
|
||||
// This will NOT include the status with the given ID.
|
||||
GetXBeforeID(amount int, sinceID string) ([]*apimodel.Status, error)
|
||||
|
||||
/*
|
||||
INDEXING FUNCTIONS
|
||||
*/
|
||||
|
||||
// IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property.
|
||||
IndexOne(statusCreatedAt time.Time, statusID string) error
|
||||
// Remove removes a status from the timeline.
|
||||
Remove(statusID string) error
|
||||
// OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong.
|
||||
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
|
||||
OldestIndexedPostID() (string, error)
|
||||
|
||||
/*
|
||||
PREPARATION FUNCTIONS
|
||||
*/
|
||||
|
||||
// PrepareXFromTop instructs the timeline to prepare x amount of posts from the top of the timeline.
|
||||
PrepareXFromTop(amount int) error
|
||||
// PrepareXFromIndex instrucst the timeline to prepare the next amount of entries for serialization, from index onwards.
|
||||
PrepareXFromIndex(amount int, index int) error
|
||||
// IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property,
|
||||
// and then immediately prepares it.
|
||||
IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) error
|
||||
|
||||
/*
|
||||
INFO FUNCTIONS
|
||||
*/
|
||||
|
||||
// ActualPostIndexLength returns the actual length of the post index at this point in time.
|
||||
PostIndexLength() int
|
||||
|
||||
/*
|
||||
UTILITY FUNCTIONS
|
||||
*/
|
||||
|
||||
// Reset instructs the timeline to reset to its base state -- cache only the minimum amount of posts.
|
||||
Reset() error
|
||||
}
|
||||
|
||||
// timeline fulfils the Timeline interface
|
||||
type timeline struct {
|
||||
postIndex *postIndex
|
||||
preparedPosts *preparedPosts
|
||||
accountID string
|
||||
account *gtsmodel.Account
|
||||
db db.DB
|
||||
tc typeutils.TypeConverter
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewTimeline returns a new Timeline for the given account ID
|
||||
func NewTimeline(accountID string, db db.DB, typeConverter typeutils.TypeConverter) Timeline {
|
||||
return &timeline{
|
||||
postIndex: &postIndex{},
|
||||
preparedPosts: &preparedPosts{},
|
||||
accountID: accountID,
|
||||
db: db,
|
||||
tc: typeConverter,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *timeline) PrepareXFromIndex(amount int, index int) error {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
var indexed int
|
||||
var prepared int
|
||||
var preparing bool
|
||||
for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*postIndexEntry)
|
||||
if !ok {
|
||||
return errors.New("PrepareXFromTop: could not parse e as a postIndexEntry")
|
||||
}
|
||||
|
||||
if !preparing {
|
||||
// we haven't hit the index we need to prepare from yet
|
||||
if indexed == index {
|
||||
preparing = true
|
||||
}
|
||||
indexed = indexed + 1
|
||||
continue
|
||||
} else {
|
||||
if err := t.prepare(entry.statusID); err != nil {
|
||||
return fmt.Errorf("PrepareXFromTop: error preparing status with id %s: %s", entry.statusID, err)
|
||||
}
|
||||
prepared = prepared + 1
|
||||
if prepared >= amount {
|
||||
// we're done
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *timeline) PrepareXFromTop(amount int) error {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
t.preparedPosts.data.Init()
|
||||
|
||||
var prepared int
|
||||
for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*postIndexEntry)
|
||||
if !ok {
|
||||
return errors.New("PrepareXFromTop: could not parse e as a postIndexEntry")
|
||||
}
|
||||
|
||||
if err := t.prepare(entry.statusID); err != nil {
|
||||
return fmt.Errorf("PrepareXFromTop: error preparing status with id %s: %s", entry.statusID, err)
|
||||
}
|
||||
|
||||
prepared = prepared + 1
|
||||
if prepared >= amount {
|
||||
// we're done
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *timeline) GetXFromTop(amount int) ([]*apimodel.Status, error) {
|
||||
// make a slice of statuses with the length we need to return
|
||||
statuses := make([]*apimodel.Status, 0, amount)
|
||||
|
||||
// if there are no prepared posts, just return the empty slice
|
||||
if t.preparedPosts.data == nil {
|
||||
t.preparedPosts.data = &list.List{}
|
||||
}
|
||||
|
||||
// make sure we have enough posts prepared to return
|
||||
if t.preparedPosts.data.Len() < amount {
|
||||
if err := t.PrepareXFromTop(amount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// work through the prepared posts from the top and return
|
||||
var served int
|
||||
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*preparedPostsEntry)
|
||||
if !ok {
|
||||
return nil, errors.New("GetXFromTop: could not parse e as a preparedPostsEntry")
|
||||
}
|
||||
statuses = append(statuses, entry.prepared)
|
||||
served = served + 1
|
||||
if served >= amount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (t *timeline) GetXFromIDOnwards(amount int, fromID string) ([]*apimodel.Status, error) {
|
||||
// make a slice of statuses with the length we need to return
|
||||
statuses := make([]*apimodel.Status, 0, amount)
|
||||
|
||||
// if there are no prepared posts, just return the empty slice
|
||||
if t.preparedPosts.data == nil {
|
||||
t.preparedPosts.data = &list.List{}
|
||||
}
|
||||
|
||||
// find the position of id
|
||||
var position int
|
||||
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*preparedPostsEntry)
|
||||
if !ok {
|
||||
return nil, errors.New("GetXBehindID: could not parse e as a preparedPostsEntry")
|
||||
}
|
||||
if entry.statusID == fromID {
|
||||
break
|
||||
}
|
||||
position = position + 1
|
||||
}
|
||||
|
||||
// make sure we have enough posts prepared behind it to return what we're being asked for
|
||||
if t.preparedPosts.data.Len() < amount+position {
|
||||
if err := t.PrepareXFromIndex(amount, position); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through the modified list until we hit the fromID again
|
||||
var serving bool
|
||||
var served int
|
||||
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*preparedPostsEntry)
|
||||
if !ok {
|
||||
return nil, errors.New("GetXBehindID: could not parse e as a preparedPostsEntry")
|
||||
}
|
||||
|
||||
if !serving {
|
||||
// start serving if we've hit the id we're looking for
|
||||
if entry.statusID == fromID {
|
||||
serving = true
|
||||
}
|
||||
}
|
||||
|
||||
if serving {
|
||||
// serve up to the amount requested
|
||||
statuses = append(statuses, entry.prepared)
|
||||
served = served + 1
|
||||
if served >= amount {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (t *timeline) GetXBeforeID(amount int, beforeID string) ([]*apimodel.Status, error) {
|
||||
// make a slice of statuses with the length we need to return
|
||||
statuses := make([]*apimodel.Status, 0, amount)
|
||||
|
||||
// if there are no prepared posts, just return the empty slice
|
||||
if t.preparedPosts.data == nil {
|
||||
t.preparedPosts.data = &list.List{}
|
||||
}
|
||||
|
||||
// iterate through the modified list until we hit the fromID again
|
||||
var served int
|
||||
servloop:
|
||||
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*preparedPostsEntry)
|
||||
if !ok {
|
||||
return nil, errors.New("GetXBeforeID: could not parse e as a preparedPostsEntry")
|
||||
}
|
||||
|
||||
if entry.statusID == beforeID {
|
||||
// we're good
|
||||
break servloop
|
||||
}
|
||||
|
||||
// serve up to the amount requested
|
||||
statuses = append(statuses, entry.prepared)
|
||||
served = served + 1
|
||||
if served >= amount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string) error {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
postIndexEntry := &postIndexEntry{
|
||||
createdAt: statusCreatedAt,
|
||||
statusID: statusID,
|
||||
}
|
||||
|
||||
return t.postIndex.insertIndexed(postIndexEntry)
|
||||
}
|
||||
|
||||
func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) error {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
postIndexEntry := &postIndexEntry{
|
||||
createdAt: statusCreatedAt,
|
||||
statusID: statusID,
|
||||
}
|
||||
|
||||
if err := t.postIndex.insertIndexed(postIndexEntry); err != nil {
|
||||
return fmt.Errorf("IndexAndPrepareOne: error inserting indexed: %s", err)
|
||||
}
|
||||
|
||||
if err := t.prepare(statusID); err != nil {
|
||||
return fmt.Errorf("IndexAndPrepareOne: error preparing: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *timeline) Remove(statusID string) error {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
// remove the entry from the post index
|
||||
for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*postIndexEntry)
|
||||
if !ok {
|
||||
return errors.New("Remove: could not parse e as a postIndexEntry")
|
||||
}
|
||||
if entry.statusID == statusID {
|
||||
t.postIndex.data.Remove(e)
|
||||
break // bail once we found and removed it
|
||||
}
|
||||
}
|
||||
|
||||
// remove the entry from prepared posts
|
||||
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*preparedPostsEntry)
|
||||
if !ok {
|
||||
return errors.New("Remove: could not parse e as a preparedPostsEntry")
|
||||
}
|
||||
if entry.statusID == statusID {
|
||||
t.preparedPosts.data.Remove(e)
|
||||
break // bail once we found and removed it
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *timeline) Reset() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *timeline) PostIndexLength() int {
|
||||
if t.postIndex == nil || t.postIndex.data == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return t.postIndex.data.Len()
|
||||
}
|
||||
|
||||
func (t *timeline) OldestIndexedPostID() (string, error) {
|
||||
var id string
|
||||
if t.postIndex == nil || t.postIndex.data == nil {
|
||||
// return an empty string if postindex hasn't been initialized yet
|
||||
return id, nil
|
||||
}
|
||||
|
||||
e := t.postIndex.data.Back()
|
||||
|
||||
if e == nil {
|
||||
// return an empty string if there's no back entry (ie., the index list hasn't been initialized yet)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
entry, ok := e.Value.(*postIndexEntry)
|
||||
if !ok {
|
||||
return id, errors.New("OldestIndexedPostID: could not parse e as a postIndexEntry")
|
||||
}
|
||||
return entry.statusID, nil
|
||||
}
|
||||
|
||||
func (t *timeline) prepare(statusID string) error {
|
||||
|
||||
// start by getting the status out of the database according to its indexed ID
|
||||
gtsStatus := >smodel.Status{}
|
||||
if err := t.db.GetByID(statusID, gtsStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if the account pointer hasn't been set on this timeline already, set it lazily here
|
||||
if t.account == nil {
|
||||
timelineOwnerAccount := >smodel.Account{}
|
||||
if err := t.db.GetByID(t.accountID, timelineOwnerAccount); err != nil {
|
||||
return err
|
||||
}
|
||||
t.account = timelineOwnerAccount
|
||||
}
|
||||
|
||||
// to convert the status we need relevant accounts from it, so pull them out here
|
||||
relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(gtsStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if this is a boost...
|
||||
var reblogOfStatus *gtsmodel.Status
|
||||
if gtsStatus.BoostOfID != "" {
|
||||
s := >smodel.Status{}
|
||||
if err := t.db.GetByID(gtsStatus.BoostOfID, s); err != nil {
|
||||
return err
|
||||
}
|
||||
reblogOfStatus = s
|
||||
}
|
||||
|
||||
// serialize the status (or, at least, convert it to a form that's ready to be serialized)
|
||||
apiModelStatus, err := t.tc.StatusToMasto(gtsStatus, relevantAccounts.StatusAuthor, t.account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, reblogOfStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// shove it in prepared posts as a prepared posts entry
|
||||
preparedPostsEntry := &preparedPostsEntry{
|
||||
createdAt: gtsStatus.CreatedAt,
|
||||
statusID: statusID,
|
||||
prepared: apiModelStatus,
|
||||
}
|
||||
|
||||
return t.preparedPosts.insertPrepared(preparedPostsEntry)
|
||||
}
|
||||
@ -25,233 +25,11 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func (p *processor) processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
|
||||
// by default all flags are set to true
|
||||
gtsAdvancedVis := >smodel.VisibilityAdvanced{
|
||||
Federated: true,
|
||||
Boostable: true,
|
||||
Replyable: true,
|
||||
Likeable: true,
|
||||
}
|
||||
|
||||
var gtsBasicVis gtsmodel.Visibility
|
||||
// Advanced takes priority if it's set.
|
||||
// If it's not set, take whatever masto visibility is set.
|
||||
// If *that's* not set either, then just take the account default.
|
||||
// If that's also not set, take the default for the whole instance.
|
||||
if form.VisibilityAdvanced != nil {
|
||||
gtsBasicVis = gtsmodel.Visibility(*form.VisibilityAdvanced)
|
||||
} else if form.Visibility != "" {
|
||||
gtsBasicVis = p.tc.MastoVisToVis(form.Visibility)
|
||||
} else if accountDefaultVis != "" {
|
||||
gtsBasicVis = accountDefaultVis
|
||||
} else {
|
||||
gtsBasicVis = gtsmodel.VisibilityDefault
|
||||
}
|
||||
|
||||
switch gtsBasicVis {
|
||||
case gtsmodel.VisibilityPublic:
|
||||
// for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
|
||||
break
|
||||
case gtsmodel.VisibilityUnlocked:
|
||||
// for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
|
||||
if form.Federated != nil {
|
||||
gtsAdvancedVis.Federated = *form.Federated
|
||||
}
|
||||
|
||||
if form.Boostable != nil {
|
||||
gtsAdvancedVis.Boostable = *form.Boostable
|
||||
}
|
||||
|
||||
if form.Replyable != nil {
|
||||
gtsAdvancedVis.Replyable = *form.Replyable
|
||||
}
|
||||
|
||||
if form.Likeable != nil {
|
||||
gtsAdvancedVis.Likeable = *form.Likeable
|
||||
}
|
||||
|
||||
case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
|
||||
// for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
|
||||
gtsAdvancedVis.Boostable = false
|
||||
|
||||
if form.Federated != nil {
|
||||
gtsAdvancedVis.Federated = *form.Federated
|
||||
}
|
||||
|
||||
if form.Replyable != nil {
|
||||
gtsAdvancedVis.Replyable = *form.Replyable
|
||||
}
|
||||
|
||||
if form.Likeable != nil {
|
||||
gtsAdvancedVis.Likeable = *form.Likeable
|
||||
}
|
||||
|
||||
case gtsmodel.VisibilityDirect:
|
||||
// direct is pretty easy: there's only one possible setting so return it
|
||||
gtsAdvancedVis.Federated = true
|
||||
gtsAdvancedVis.Boostable = false
|
||||
gtsAdvancedVis.Federated = true
|
||||
gtsAdvancedVis.Likeable = true
|
||||
}
|
||||
|
||||
status.Visibility = gtsBasicVis
|
||||
status.VisibilityAdvanced = gtsAdvancedVis
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) processReplyToID(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
|
||||
if form.InReplyToID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
|
||||
//
|
||||
// 1. Does the replied status exist in the database?
|
||||
// 2. Is the replied status marked as replyable?
|
||||
// 3. Does a block exist between either the current account or the account that posted the status it's replying to?
|
||||
//
|
||||
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
|
||||
repliedStatus := >smodel.Status{}
|
||||
repliedAccount := >smodel.Account{}
|
||||
// check replied status exists + is replyable
|
||||
if err := p.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
|
||||
}
|
||||
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
||||
}
|
||||
|
||||
if repliedStatus.VisibilityAdvanced != nil {
|
||||
if !repliedStatus.VisibilityAdvanced.Replyable {
|
||||
return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
|
||||
}
|
||||
}
|
||||
|
||||
// check replied account is known to us
|
||||
if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
|
||||
}
|
||||
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
||||
}
|
||||
// check if a block exists
|
||||
if blocked, err := p.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
||||
}
|
||||
} else if blocked {
|
||||
return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
|
||||
}
|
||||
status.InReplyToID = repliedStatus.ID
|
||||
status.InReplyToAccountID = repliedAccount.ID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) processMediaIDs(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
|
||||
if form.MediaIDs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
|
||||
attachments := []string{}
|
||||
for _, mediaID := range form.MediaIDs {
|
||||
// check these attachments exist
|
||||
a := >smodel.MediaAttachment{}
|
||||
if err := p.db.GetByID(mediaID, a); err != nil {
|
||||
return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
|
||||
}
|
||||
// check they belong to the requesting account id
|
||||
if a.AccountID != thisAccountID {
|
||||
return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
|
||||
}
|
||||
// check they're not already used in a status
|
||||
if a.StatusID != "" || a.ScheduledStatusID != "" {
|
||||
return fmt.Errorf("media with id %s is already attached to a status", mediaID)
|
||||
}
|
||||
gtsMediaAttachments = append(gtsMediaAttachments, a)
|
||||
attachments = append(attachments, a.ID)
|
||||
}
|
||||
status.GTSMediaAttachments = gtsMediaAttachments
|
||||
status.Attachments = attachments
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
|
||||
if form.Language != "" {
|
||||
status.Language = form.Language
|
||||
} else {
|
||||
status.Language = accountDefaultLanguage
|
||||
}
|
||||
if status.Language == "" {
|
||||
return errors.New("no language given either in status create form or account default")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
||||
menchies := []string{}
|
||||
gtsMenchies, err := p.db.MentionStringsToMentions(util.DeriveMentionsFromStatus(form.Status), accountID, status.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating mentions from status: %s", err)
|
||||
}
|
||||
for _, menchie := range gtsMenchies {
|
||||
if err := p.db.Put(menchie); err != nil {
|
||||
return fmt.Errorf("error putting mentions in db: %s", err)
|
||||
}
|
||||
menchies = append(menchies, menchie.ID)
|
||||
}
|
||||
// add full populated gts menchies to the status for passing them around conveniently
|
||||
status.GTSMentions = gtsMenchies
|
||||
// add just the ids of the mentioned accounts to the status for putting in the db
|
||||
status.Mentions = menchies
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
||||
tags := []string{}
|
||||
gtsTags, err := p.db.TagStringsToTags(util.DeriveHashtagsFromStatus(form.Status), accountID, status.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating hashtags from status: %s", err)
|
||||
}
|
||||
for _, tag := range gtsTags {
|
||||
if err := p.db.Upsert(tag, "name"); err != nil {
|
||||
return fmt.Errorf("error putting tags in db: %s", err)
|
||||
}
|
||||
tags = append(tags, tag.ID)
|
||||
}
|
||||
// add full populated gts tags to the status for passing them around conveniently
|
||||
status.GTSTags = gtsTags
|
||||
// add just the ids of the used tags to the status for putting in the db
|
||||
status.Tags = tags
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
||||
emojis := []string{}
|
||||
gtsEmojis, err := p.db.EmojiStringsToEmojis(util.DeriveEmojisFromStatus(form.Status), accountID, status.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating emojis from status: %s", err)
|
||||
}
|
||||
for _, e := range gtsEmojis {
|
||||
emojis = append(emojis, e.ID)
|
||||
}
|
||||
// add full populated gts emojis to the status for passing them around conveniently
|
||||
status.GTSEmojis = gtsEmojis
|
||||
// add just the ids of the used emojis to the status for putting in the db
|
||||
status.Emojis = emojis
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
HELPER FUNCTIONS
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user