| 
									
										
										
										
											2023-03-12 16:00:57 +01:00
										 |  |  | // 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/>. | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | package account | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/gorilla/feeds" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/config" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | const ( | 
					
						
							|  |  |  | 	rssFeedLength = 20 | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type GetRSSFeed func() (string, gtserror.WithCode) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // GetRSSFeedForUsername returns a function to return the RSS feed of a local account | 
					
						
							|  |  |  | // with the given username, and the last-modified time (time that the account last | 
					
						
							|  |  |  | // posted a status eligible to be included in the rss feed). | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // To save db calls, callers to this function should only call the returned GetRSSFeed | 
					
						
							|  |  |  | // func if the last-modified time is newer than the last-modified time they have cached. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // If the account has not yet posted an RSS-eligible status, the returned last-modified | 
					
						
							|  |  |  | // time will be zero, and the GetRSSFeed func will return a valid RSS xml with no items. | 
					
						
							|  |  |  | func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string) (GetRSSFeed, time.Time, gtserror.WithCode) { | 
					
						
							|  |  |  | 	var ( | 
					
						
							|  |  |  | 		never = time.Time{} | 
					
						
							|  |  |  | 	) | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-01 18:26:53 +00:00
										 |  |  | 	account, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "") | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 		if errors.Is(err, db.ErrNoEntries) { | 
					
						
							|  |  |  | 			// Simply no account with this username. | 
					
						
							|  |  |  | 			err = gtserror.New("account not found") | 
					
						
							|  |  |  | 			return nil, never, gtserror.NewErrorNotFound(err) | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Real db error. | 
					
						
							|  |  |  | 		err = gtserror.Newf("db error getting account %s: %w", username, err) | 
					
						
							|  |  |  | 		return nil, never, gtserror.NewErrorInternalError(err) | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 	// Ensure account has rss feed enabled. | 
					
						
							| 
									
										
										
										
											2024-03-22 14:03:46 +01:00
										 |  |  | 	if !*account.Settings.EnableRSS { | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 		err = gtserror.New("account RSS feed not enabled") | 
					
						
							|  |  |  | 		return nil, never, gtserror.NewErrorNotFound(err) | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-16 13:10:13 +02:00
										 |  |  | 	// Ensure account stats populated. | 
					
						
							| 
									
										
										
										
											2024-08-02 12:15:11 +00:00
										 |  |  | 	if err := p.state.DB.PopulateAccountStats(ctx, account); err != nil { | 
					
						
							|  |  |  | 		err = gtserror.Newf("db error getting account stats %s: %w", username, err) | 
					
						
							|  |  |  | 		return nil, never, gtserror.NewErrorInternalError(err) | 
					
						
							| 
									
										
										
										
											2024-04-16 13:10:13 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 	// LastModified time is needed by callers to check freshness for cacheing. | 
					
						
							|  |  |  | 	// This might be a zero time.Time if account has never posted a status that's | 
					
						
							|  |  |  | 	// eligible to appear in the RSS feed; that's fine. | 
					
						
							| 
									
										
										
										
											2024-04-16 13:10:13 +02:00
										 |  |  | 	lastPostAt := account.Stats.LastStatusAt | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return func() (string, gtserror.WithCode) { | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 		// Assemble author namestring once only. | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 		author := "@" + account.Username + "@" + config.GetAccountDomain() | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Derive image/thumbnail for this account (may be nil). | 
					
						
							|  |  |  | 		image, errWithCode := p.rssImageForAccount(ctx, account, author) | 
					
						
							|  |  |  | 		if errWithCode != nil { | 
					
						
							|  |  |  | 			return "", errWithCode | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		feed := &feeds.Feed{ | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 			Title:       "Posts from " + author, | 
					
						
							|  |  |  | 			Description: "Posts from " + author, | 
					
						
							|  |  |  | 			Link:        &feeds.Link{Href: account.URL}, | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 			Image:       image, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 		// If the account has never posted anything, just use | 
					
						
							|  |  |  | 		// account creation time as Updated value for the feed; | 
					
						
							|  |  |  | 		// we could use time.Now() here but this would likely | 
					
						
							|  |  |  | 		// mess up cacheing; we want something determinate. | 
					
						
							|  |  |  | 		// | 
					
						
							|  |  |  | 		// We can also return early rather than wasting a db call, | 
					
						
							|  |  |  | 		// since we already know there's no eligible statuses. | 
					
						
							|  |  |  | 		if lastPostAt.IsZero() { | 
					
						
							|  |  |  | 			feed.Updated = account.CreatedAt | 
					
						
							|  |  |  | 			return stringifyFeed(feed) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Account has posted at least one status that's | 
					
						
							|  |  |  | 		// eligible to appear in the RSS feed. | 
					
						
							|  |  |  | 		// | 
					
						
							|  |  |  | 		// Reuse the lastPostAt value for feed.Updated. | 
					
						
							|  |  |  | 		feed.Updated = lastPostAt | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 		// Retrieve latest statuses as they'd be shown on the web view of the account profile. | 
					
						
							| 
									
										
										
										
											2024-09-09 18:07:25 +02:00
										 |  |  | 		statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, rssFeedLength, "") | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 		if err != nil && !errors.Is(err, db.ErrNoEntries) { | 
					
						
							|  |  |  | 			err = fmt.Errorf("db error getting account web statuses: %w", err) | 
					
						
							|  |  |  | 			return "", gtserror.NewErrorInternalError(err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Add each status to the rss feed. | 
					
						
							|  |  |  | 		for _, status := range statuses { | 
					
						
							| 
									
										
										
										
											2023-09-23 17:44:11 +01:00
										 |  |  | 			item, err := p.converter.StatusToRSSItem(ctx, status) | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 			if err != nil { | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 				err = gtserror.Newf("error converting status to feed item: %w", err) | 
					
						
							|  |  |  | 				return "", gtserror.NewErrorInternalError(err) | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			feed.Add(item) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 		return stringifyFeed(feed) | 
					
						
							|  |  |  | 	}, lastPostAt, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (p *Processor) rssImageForAccount(ctx context.Context, account *gtsmodel.Account, author string) (*feeds.Image, gtserror.WithCode) { | 
					
						
							|  |  |  | 	if account.AvatarMediaAttachmentID == "" { | 
					
						
							|  |  |  | 		// No image, no problem! | 
					
						
							|  |  |  | 		return nil, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Ensure account avatar attachment populated. | 
					
						
							|  |  |  | 	if account.AvatarMediaAttachment == nil { | 
					
						
							|  |  |  | 		var err error | 
					
						
							|  |  |  | 		account.AvatarMediaAttachment, err = p.state.DB.GetAttachmentByID(ctx, account.AvatarMediaAttachmentID) | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 		if err != nil { | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 			if errors.Is(err, db.ErrNoEntries) { | 
					
						
							|  |  |  | 				// No attachment found with this ID (race condition?). | 
					
						
							|  |  |  | 				return nil, nil | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Real db error. | 
					
						
							|  |  |  | 			err = gtserror.Newf("db error fetching avatar media attachment: %w", err) | 
					
						
							|  |  |  | 			return nil, gtserror.NewErrorInternalError(err) | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return &feeds.Image{ | 
					
						
							|  |  |  | 		Url:   account.AvatarMediaAttachment.Thumbnail.URL, | 
					
						
							|  |  |  | 		Title: "Avatar for " + author, | 
					
						
							|  |  |  | 		Link:  account.URL, | 
					
						
							|  |  |  | 	}, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func stringifyFeed(feed *feeds.Feed) (string, gtserror.WithCode) { | 
					
						
							|  |  |  | 	// Stringify the feed. Even with no statuses, | 
					
						
							|  |  |  | 	// this will still produce valid rss xml. | 
					
						
							|  |  |  | 	rss, err := feed.ToRss() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		err := gtserror.Newf("error converting feed to rss string: %w", err) | 
					
						
							|  |  |  | 		return "", gtserror.NewErrorInternalError(err) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-10 17:05:59 +02:00
										 |  |  | 	return rss, nil | 
					
						
							| 
									
										
										
										
											2022-10-08 14:00:39 +02:00
										 |  |  | } |