gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

rss.go (4834B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package web
     19 
     20 import (
     21 	"bytes"
     22 	"errors"
     23 	"fmt"
     24 	"net/http"
     25 	"strings"
     26 	"time"
     27 
     28 	"github.com/gin-gonic/gin"
     29 	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
     30 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     31 	"github.com/superseriousbusiness/gotosocial/internal/log"
     32 )
     33 
     34 const appRSSUTF8 = string(apiutil.AppRSSXML + "; charset=utf-8")
     35 
     36 func (m *Module) GetRSSETag(urlPath string, lastModified time.Time, getRSSFeed func() (string, gtserror.WithCode)) (string, error) {
     37 	if cachedETag, ok := m.eTagCache.Get(urlPath); ok && !lastModified.After(cachedETag.lastModified) {
     38 		// only return our cached etag if the file wasn't
     39 		// modified since last time, otherwise generate a
     40 		// new one; eat fresh!
     41 		return cachedETag.eTag, nil
     42 	}
     43 
     44 	rssFeed, errWithCode := getRSSFeed()
     45 	if errWithCode != nil {
     46 		return "", fmt.Errorf("error getting rss feed: %s", errWithCode)
     47 	}
     48 
     49 	eTag, err := generateEtag(bytes.NewReader([]byte(rssFeed)))
     50 	if err != nil {
     51 		return "", fmt.Errorf("error generating etag: %s", err)
     52 	}
     53 
     54 	// put new entry in cache before we return
     55 	m.eTagCache.Set(urlPath, eTagCacheEntry{
     56 		eTag:         eTag,
     57 		lastModified: lastModified,
     58 	})
     59 
     60 	return eTag, nil
     61 }
     62 
     63 func extractIfModifiedSince(r *http.Request) time.Time {
     64 	hdr := r.Header.Get(ifModifiedSinceHeader)
     65 
     66 	if hdr == "" {
     67 		return time.Time{}
     68 	}
     69 
     70 	t, err := http.ParseTime(hdr)
     71 	if err != nil {
     72 		log.Errorf(r.Context(), "couldn't parse if-modified-since %s: %s", hdr, err)
     73 		return time.Time{}
     74 	}
     75 
     76 	return t
     77 }
     78 
     79 func (m *Module) rssFeedGETHandler(c *gin.Context) {
     80 	// set this Cache-Control header to instruct clients to validate the response with us
     81 	// before each reuse (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
     82 	c.Header(cacheControlHeader, cacheControlNoCache)
     83 	ctx := c.Request.Context()
     84 
     85 	if _, err := apiutil.NegotiateAccept(c, apiutil.AppRSSXML); err != nil {
     86 		apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
     87 		return
     88 	}
     89 
     90 	// usernames on our instance will always be lowercase
     91 	username := strings.ToLower(c.Param(usernameKey))
     92 	if username == "" {
     93 		err := errors.New("no account username specified")
     94 		apiutil.WebErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
     95 		return
     96 	}
     97 
     98 	ifNoneMatch := c.Request.Header.Get(ifNoneMatchHeader)
     99 	ifModifiedSince := extractIfModifiedSince(c.Request)
    100 
    101 	getRssFeed, accountLastPostedPublic, errWithCode := m.processor.Account().GetRSSFeedForUsername(ctx, username)
    102 	if errWithCode != nil {
    103 		apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    104 		return
    105 	}
    106 
    107 	var rssFeed string
    108 	cacheKey := c.Request.URL.Path
    109 	cacheEntry, ok := m.eTagCache.Get(cacheKey)
    110 
    111 	if !ok || cacheEntry.lastModified.Before(accountLastPostedPublic) {
    112 		// we either have no cache entry for this, or we have an expired cache entry; generate a new one
    113 		rssFeed, errWithCode = getRssFeed()
    114 		if errWithCode != nil {
    115 			apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    116 			return
    117 		}
    118 
    119 		eTag, err := generateEtag(bytes.NewBufferString(rssFeed))
    120 		if err != nil {
    121 			apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
    122 			return
    123 		}
    124 
    125 		cacheEntry.lastModified = accountLastPostedPublic
    126 		cacheEntry.eTag = eTag
    127 		m.eTagCache.Set(cacheKey, cacheEntry)
    128 	}
    129 
    130 	c.Header(eTagHeader, cacheEntry.eTag)
    131 	c.Header(lastModifiedHeader, accountLastPostedPublic.Format(http.TimeFormat))
    132 
    133 	if ifNoneMatch == cacheEntry.eTag {
    134 		c.AbortWithStatus(http.StatusNotModified)
    135 		return
    136 	}
    137 
    138 	lmUnix := cacheEntry.lastModified.Unix()
    139 	imsUnix := ifModifiedSince.Unix()
    140 	if lmUnix <= imsUnix {
    141 		c.AbortWithStatus(http.StatusNotModified)
    142 		return
    143 	}
    144 
    145 	if rssFeed == "" {
    146 		// we had a cache entry already so we didn't call to get the rss feed yet
    147 		rssFeed, errWithCode = getRssFeed()
    148 		if errWithCode != nil {
    149 			apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    150 			return
    151 		}
    152 	}
    153 
    154 	c.Data(http.StatusOK, appRSSUTF8, []byte(rssFeed))
    155 }