mirror of
https://github.com/mmahdium/TGSS.git
synced 2026-08-12 08:32:48 +03:30
feat: add ImageHandler and ImageService for image retrieval from channels
This commit is contained in:
@@ -21,6 +21,7 @@ func main() {
|
||||
|
||||
handlers.NewChannelHandler,
|
||||
handlers.NewAuthHandler,
|
||||
handlers.NewImageHandler,
|
||||
|
||||
rss.NewRSSGenerator,
|
||||
rss.NewFeedCache,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"tgss/internal/config"
|
||||
"tgss/internal/telegram"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ImageHandler struct {
|
||||
logger *zap.Logger
|
||||
imageService *telegram.ImageService
|
||||
}
|
||||
|
||||
func NewImageHandler(
|
||||
imageService *telegram.ImageService,
|
||||
logger *zap.Logger,
|
||||
) *ImageHandler {
|
||||
return &ImageHandler{
|
||||
logger: logger,
|
||||
imageService: imageService,
|
||||
}
|
||||
}
|
||||
|
||||
func (ih *ImageHandler) GetImage(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), config.AuthStatusTimeout)
|
||||
defer cancel()
|
||||
|
||||
channelId := c.Param("channelId")
|
||||
if channelId == "" {
|
||||
c.JSON(400, gin.H{"error": "invalid channel ID or message ID"})
|
||||
return
|
||||
}
|
||||
|
||||
msgIdStr := c.Param("messageId")
|
||||
msgId, err := strconv.Atoi(msgIdStr)
|
||||
if err != nil {
|
||||
ih.logger.Error("message Id is invalid", zap.Error(err))
|
||||
c.JSON(400, gin.H{"error": "message Id is invalid"})
|
||||
return
|
||||
}
|
||||
|
||||
reader, err := ih.imageService.GetChannelsMessageImageById(ctx, channelId, msgId)
|
||||
if err != nil {
|
||||
ih.logger.Error("failed to get image", zap.Error(err))
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer reader.(io.ReadCloser).Close()
|
||||
|
||||
if closer, ok := reader.(io.ReadCloser); ok {
|
||||
defer closer.Close()
|
||||
}
|
||||
|
||||
extraHeaders := map[string]string{
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"Expires": time.Now().Add(1 * 365 * 24 * time.Hour).Format(http.TimeFormat),
|
||||
}
|
||||
|
||||
c.DataFromReader(http.StatusOK, -1, "image/jpeg", reader, extraHeaders)
|
||||
}
|
||||
@@ -123,3 +123,5 @@ func (r *RSSGenerator) messageToItem(msg tg.MessageClass, channelId string) (*RS
|
||||
Guid: messageURL.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: add a hasPhoto helper to add the rss tag accordingly
|
||||
@@ -25,6 +25,7 @@ type RouterParams struct {
|
||||
|
||||
ChannelHandler *handlers.ChannelHandler
|
||||
AuthHandler *handlers.AuthHandler
|
||||
ImageHandler *handlers.ImageHandler
|
||||
}
|
||||
|
||||
func NewGin() *gin.Engine {
|
||||
@@ -47,12 +48,13 @@ func RegisterRoutes(params RouterParams) {
|
||||
})
|
||||
})
|
||||
|
||||
// params.GinEngine.GET("/feed/:id/json", params.ChannelHandler.GetMessagesJson)
|
||||
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)
|
||||
|
||||
// TODO: improve accurecy in rss channel fields
|
||||
// TODO: add ui with templates under /setup with fetch
|
||||
// TODO: image endpoint + hash and expiry
|
||||
// TODO: image endpoint + HMAC
|
||||
authStatCtx, cancel := context.WithTimeout(context.Background(), config.AuthStatusTimeout)
|
||||
authStat, err := params.TgService.AuthStatus(authStatCtx)
|
||||
cancel()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// TODO: another limiter rate for images
|
||||
type ClientLimiter struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/gotd/td/telegram"
|
||||
"github.com/gotd/td/telegram/downloader"
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ImageService struct {
|
||||
client *telegram.Client
|
||||
logger *zap.Logger
|
||||
|
||||
downloader *downloader.Downloader
|
||||
}
|
||||
|
||||
func NewImageService(client *telegram.Client, logger *zap.Logger) *ImageService {
|
||||
return &ImageService{
|
||||
client: client,
|
||||
logger: logger,
|
||||
downloader: downloader.NewDownloader(),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ImageService) GetChannelsMessageImageById(ctx context.Context, channelId string, messageId int) (io.Reader, error) {
|
||||
api := i.client.API()
|
||||
|
||||
resolvedChats, err := api.ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{
|
||||
Username: channelId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resolvedChats.Chats) == 0 {
|
||||
return nil, errors.New("channel not found")
|
||||
}
|
||||
|
||||
channel, ok := resolvedChats.Chats[0].(*tg.Channel)
|
||||
if !ok {
|
||||
return nil, errors.New("resolved peer is not a channel")
|
||||
}
|
||||
|
||||
messages, err := api.ChannelsGetMessages(ctx, &tg.ChannelsGetMessagesRequest{
|
||||
Channel: channel.AsInput(),
|
||||
ID: []tg.InputMessageClass{
|
||||
&tg.InputMessageID{ID: messageId},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to fetch messages from channel")
|
||||
}
|
||||
|
||||
msgs, ok := messages.(*tg.MessagesChannelMessages)
|
||||
if !ok {
|
||||
return nil, errors.New("messages fetched do not represent a channel message")
|
||||
}
|
||||
|
||||
// Count will always be 1, because we are giving one ID and even if its not found, an empty message is returned
|
||||
message, ok := msgs.Messages[0].(*tg.Message)
|
||||
if !ok {
|
||||
return nil, errors.New("no message found")
|
||||
}
|
||||
|
||||
media, ok := message.Media.(*tg.MessageMediaPhoto)
|
||||
if !ok {
|
||||
return nil, errors.New("the message does not contain a photo media object")
|
||||
}
|
||||
|
||||
photo, ok := media.Photo.(*tg.Photo)
|
||||
if !ok {
|
||||
return nil, errors.New("the message media has no photo payload")
|
||||
}
|
||||
|
||||
var largestSize *tg.PhotoSize
|
||||
for _, size := range photo.Sizes {
|
||||
if ps, ok := size.(*tg.PhotoSize); ok {
|
||||
if largestSize == nil || ps.Size > largestSize.Size {
|
||||
largestSize = size.(*tg.PhotoSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
if largestSize == nil {
|
||||
return nil, errors.New("no valid photo size found")
|
||||
}
|
||||
photoLocation := &tg.InputPhotoFileLocation{
|
||||
ID: photo.ID,
|
||||
AccessHash: photo.AccessHash,
|
||||
FileReference: photo.FileReference,
|
||||
|
||||
ThumbSize: largestSize.Type,
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
_, err := i.downloader.Download(api, photoLocation).Stream(ctx, pw)
|
||||
if err != nil {
|
||||
pw.CloseWithError(errors.New("unable to download photo"))
|
||||
}
|
||||
}()
|
||||
|
||||
return pr, nil
|
||||
}
|
||||
@@ -90,6 +90,7 @@ var Module = fx.Options(
|
||||
fx.Provide(
|
||||
NewTelegramClient,
|
||||
NewService,
|
||||
NewImageService,
|
||||
),
|
||||
fx.Invoke(RunClient),
|
||||
)
|
||||
|
||||
@@ -127,16 +127,10 @@ func (s *Service) LastMessages(ctx context.Context, username string, limit int)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var msgs []tg.MessageClass
|
||||
|
||||
switch h := history.(type) {
|
||||
case *tg.MessagesMessages:
|
||||
msgs = h.Messages
|
||||
case *tg.MessagesMessagesSlice:
|
||||
msgs = h.Messages
|
||||
case *tg.MessagesChannelMessages:
|
||||
msgs = h.Messages
|
||||
msgs, ok := history.(*tg.MessagesChannelMessages)
|
||||
if !ok {
|
||||
return nil, errors.New("resolved messages are not from a channel")
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
return msgs.Messages, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user