mirror of
https://github.com/mmahdium/TGSS.git
synced 2026-08-12 08:32:48 +03:30
feat: add BaseURL config, RSS image enclosure, and separate rate limiters
- Load and validate BASE_URL, store in Config - RSSGenerator now receives Config; adds <enclosure> for photo messages using BaseURL - RateLimiter tracks feed and image limiters; provide FeedRateLimit and ImageRateLimit middleware - Update routes to use the new middlewares and inject Config into RSSGenerator
This commit is contained in:
@@ -20,6 +20,7 @@ type Config struct {
|
||||
ProxyURL string
|
||||
AppHost string
|
||||
AppPort int
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func Load(logger *zap.Logger) *Config {
|
||||
@@ -74,13 +75,22 @@ func Load(logger *zap.Logger) *Config {
|
||||
|
||||
proxyURL := os.Getenv("TG_PROXY_URL")
|
||||
|
||||
baseURL := os.Getenv("BASE_URL")
|
||||
if baseURL == "" {
|
||||
logger.Fatal("BASE_URL cant be empty")
|
||||
}
|
||||
if baseURL[len(baseURL)-1] == '/' {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
|
||||
return &Config{
|
||||
TgAppId: func() int { i, _ := strconv.Atoi(appId); return i }(),
|
||||
TgAppHash: strings.TrimSpace(appHash),
|
||||
TgAppId: func() int { i, _ := strconv.Atoi(appId); return i }(),
|
||||
TgAppHash: strings.TrimSpace(appHash),
|
||||
|
||||
SessionPath: sessionFile,
|
||||
ProxyURL: strings.TrimSpace(proxyURL),
|
||||
AppHost: appHost,
|
||||
AppPort: appPort,
|
||||
BaseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
+45
-10
@@ -2,9 +2,11 @@ package rss
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"tgss/internal/config"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
@@ -12,11 +14,18 @@ import (
|
||||
)
|
||||
|
||||
type RSSItem struct {
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
PubDate string `xml:"pubDate,omitempty"`
|
||||
Description string `xml:"description"`
|
||||
Guid string `xml:"guid,omitempty"`
|
||||
Title string `xml:"title"`
|
||||
Link string `xml:"link"`
|
||||
Enclosure *RSSEnclosure `xml:"enclosure,omitempty"`
|
||||
PubDate string `xml:"pubDate,omitempty"`
|
||||
Description string `xml:"description"`
|
||||
Guid string `xml:"guid,omitempty"`
|
||||
}
|
||||
|
||||
type RSSEnclosure struct {
|
||||
URL string `xml:"url,attr"`
|
||||
Length string `xml:"lenghth,omitempty,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
}
|
||||
|
||||
type RSSChannel struct {
|
||||
@@ -40,10 +49,11 @@ type RSSFeed struct {
|
||||
|
||||
type RSSGenerator struct {
|
||||
logger *zap.Logger
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewRSSGenerator(logger *zap.Logger) *RSSGenerator {
|
||||
return &RSSGenerator{logger: logger}
|
||||
func NewRSSGenerator(logger *zap.Logger, config *config.Config) *RSSGenerator {
|
||||
return &RSSGenerator{logger: logger, config: config}
|
||||
}
|
||||
|
||||
func (r *RSSGenerator) GenerateFeed(items []tg.MessageClass, channelId string) *RSSFeed {
|
||||
@@ -85,6 +95,7 @@ func (r *RSSGenerator) GenerateFeed(items []tg.MessageClass, channelId string) *
|
||||
rssChannel.Items = append(rssChannel.Items, *item)
|
||||
}
|
||||
|
||||
// Logging is done by AI
|
||||
if errorCount > 0 {
|
||||
r.logger.Warn("feed generation completed with errors",
|
||||
zap.String("channel", channelId),
|
||||
@@ -115,13 +126,37 @@ func (r *RSSGenerator) messageToItem(msg tg.MessageClass, channelId string) (*RS
|
||||
description = "No content"
|
||||
}
|
||||
|
||||
return &RSSItem{
|
||||
rssItem := &RSSItem{
|
||||
Title: "Post by @" + channelId + " on Telegram",
|
||||
Link: messageURL.String(),
|
||||
PubDate: time.Unix(int64(message.Date), 0).Format("Mon, 02 Jan 2006 15:04 MST"),
|
||||
Description: description,
|
||||
Guid: messageURL.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err = r.messageHasPhoto(message); err == nil {
|
||||
if enclosureURL, err := url.ParseRequestURI(r.config.BaseURL + "/image/" + channelId + "/" + strconv.Itoa(msg.GetID())); err == nil {
|
||||
rssItem.Enclosure = &RSSEnclosure{
|
||||
URL: enclosureURL.String(),
|
||||
Length: "0",
|
||||
Type: "image/jpeg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rssItem, nil
|
||||
}
|
||||
|
||||
// TODO: add a hasPhoto helper to add the rss tag accordingly
|
||||
func (r *RSSGenerator) messageHasPhoto(message tg.MessageClass) error {
|
||||
media, ok := message.(*tg.Message).Media.(*tg.MessageMediaPhoto)
|
||||
if !ok {
|
||||
return errors.New("the message does not contain a photo media object")
|
||||
}
|
||||
|
||||
_, ok = media.Photo.(*tg.Photo)
|
||||
if !ok {
|
||||
return errors.New("the message media has no photo payload")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ func RegisterRoutes(params RouterParams) {
|
||||
})
|
||||
|
||||
params.GinEngine.GET("/feed/:id/json", params.ChannelHandler.GetMessagesJson)
|
||||
params.GinEngine.GET("/feed/:id", params.RateLimiter.RateLimit(), params.ChannelHandler.GetMessagesRSS)
|
||||
params.GinEngine.GET("/image/:channelId/:messageId", params.ImageHandler.GetImage)
|
||||
params.GinEngine.GET("/feed/:id", params.RateLimiter.FeedRateLimit(), params.ChannelHandler.GetMessagesRSS)
|
||||
params.GinEngine.GET("/image/:channelId/:messageId", params.RateLimiter.ImageRateLimit(), params.ImageHandler.GetImage)
|
||||
|
||||
// TODO: improve accurecy in rss channel fields
|
||||
// TODO: add ui with templates under /setup with fetch
|
||||
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
|
||||
// TODO: another limiter rate for images
|
||||
type ClientLimiter struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
feedLimiter *rate.Limiter
|
||||
imageLimiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
type RateLimiter struct {
|
||||
@@ -27,19 +28,30 @@ func NewRateLimiter(logger *zap.Logger) *RateLimiter {
|
||||
return &RateLimiter{logger: logger, mu: sync.Mutex{}, clients: map[string]*ClientLimiter{}}
|
||||
}
|
||||
|
||||
func (r *RateLimiter) getLimiter(ip string) *rate.Limiter {
|
||||
func (r *RateLimiter) getLimiter(ip string, kind string) *rate.Limiter {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if c, ok := r.clients[ip]; ok {
|
||||
c.lastSeen = time.Now()
|
||||
return c.limiter
|
||||
if kind == "image" {
|
||||
return c.imageLimiter
|
||||
}
|
||||
return c.feedLimiter
|
||||
}
|
||||
|
||||
limiter := rate.NewLimiter(rate.Every(time.Second*3), 5) // TODO: get from config
|
||||
r.clients[ip] = &ClientLimiter{limiter: limiter, lastSeen: time.Now()}
|
||||
feedLim := rate.NewLimiter(rate.Every(3*time.Second), 5)
|
||||
imageLim := rate.NewLimiter(rate.Every(5*time.Second), 4)
|
||||
|
||||
return limiter
|
||||
r.clients[ip] = &ClientLimiter{
|
||||
feedLimiter: feedLim,
|
||||
imageLimiter: imageLim,
|
||||
lastSeen: time.Now(),
|
||||
}
|
||||
if kind == "image" {
|
||||
return imageLim
|
||||
}
|
||||
return feedLim
|
||||
}
|
||||
|
||||
func (r *RateLimiter) CleanupRateLimiter() {
|
||||
@@ -91,16 +103,26 @@ func RegisterRateLimiterCleanup(lc fx.Lifecycle, logger *zap.Logger, r *RateLimi
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RateLimiter) RateLimit() gin.HandlerFunc {
|
||||
func (r *RateLimiter) FeedRateLimit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ip := c.ClientIP()
|
||||
|
||||
limiter := r.getLimiter(ip)
|
||||
|
||||
if !limiter.Allow() {
|
||||
lim := r.getLimiter(ip, "feed")
|
||||
if !lim.Allow() {
|
||||
c.AbortWithStatusJSON(429, gin.H{"error": "Slow down pls"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RateLimiter) ImageRateLimit() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ip := c.ClientIP()
|
||||
lim := r.getLimiter(ip, "image")
|
||||
if !lim.Allow() {
|
||||
c.AbortWithStatusJSON(429, gin.H{"error": "Slow down pls, these arent your mothers nudes"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user