diff --git a/bot/handlers/url.go b/bot/handlers/url.go index e61c1ac..d3512ec 100644 --- a/bot/handlers/url.go +++ b/bot/handlers/url.go @@ -55,11 +55,9 @@ func URLHandler(bot *gotgbot.Bot, ctx *ext.Context) error { } func URLFilter(msg *gotgbot.Message) bool { - return message.Text(msg) && !message.Command(msg) && containsURL(msg) -} - -func containsURL(msg *gotgbot.Message) bool { - return message.Entity("url")(msg) + return message.Text(msg) && + !message.Command(msg) && + message.Entity("url")(msg) } func getMessageURL(msg *gotgbot.Message) string { diff --git a/ext/main.go b/ext/main.go index 5f5bb7c..8fd5231 100644 --- a/ext/main.go +++ b/ext/main.go @@ -5,6 +5,7 @@ import ( "govd/ext/ninegag" "govd/ext/pinterest" "govd/ext/reddit" + "govd/ext/redgifs" "govd/ext/tiktok" "govd/ext/twitter" "govd/models" @@ -23,5 +24,6 @@ var List = []*models.Extractor{ reddit.Extractor, reddit.ShortExtractor, ninegag.Extractor, + redgifs.Extractor, // todo: add every ext lol } diff --git a/ext/redgifs/main.go b/ext/redgifs/main.go new file mode 100644 index 0000000..72a7dc9 --- /dev/null +++ b/ext/redgifs/main.go @@ -0,0 +1,150 @@ +package redgifs + +import ( + "fmt" + "govd/enums" + "govd/models" + "govd/util" + "net/http" + "regexp" + + "github.com/bytedance/sonic" +) + +const ( + baseAPI = "https://api.redgifs.com/v2/" + tokenEndpoint = baseAPI + "auth/temporary" + videoEndpoint = baseAPI + "gifs/" +) + +var ( + session = util.GetHTTPSession() + + baseApiHeaders = map[string]string{ + "referer": "https://www.redgifs.com/", + "origin": "https://www.redgifs.com", + "content-type": "application/json", + } +) + +var Extractor = &models.Extractor{ + Name: "RedGifs", + CodeName: "redgifs", + Type: enums.ExtractorTypeSingle, + Category: enums.ExtractorCategorySocial, + URLPattern: regexp.MustCompile(`https?://(?:(?:www\.)?redgifs\.com/(?:watch|ifr)/|thumbs2\.redgifs\.com/)(?P[^-/?#\.]+)`), + Host: []string{ + "redgifs.com", + "thumbs2.redgifs.com", + }, + + Run: func(ctx *models.DownloadContext) (*models.ExtractorResponse, error) { + mediaList, err := MediaListFromAPI(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get media: %w", err) + } + return &models.ExtractorResponse{ + MediaList: mediaList, + }, nil + }, +} + +func MediaListFromAPI(ctx *models.DownloadContext) ([]*models.Media, error) { + var mediaList []*models.Media + + response, err := GetVideo(ctx.MatchedContentID) + if err != nil { + return nil, fmt.Errorf("failed to get from api: %w", err) + } + gif := response.Gif + media := ctx.Extractor.NewMedia( + ctx.MatchedContentID, + ctx.MatchedContentURL, + ) + + if gif.Description != "" { + media.SetCaption(gif.Description) + } + media.NSFW = true // always nsfw + + if gif.Urls.Sd != "" { + media.AddFormat(&models.MediaFormat{ + FormatID: "sd", + Type: enums.MediaTypeVideo, + URL: []string{gif.Urls.Sd}, + VideoCodec: enums.MediaCodecAVC, + Width: int64(gif.Width / 2), + Height: int64(gif.Height / 2), + }) + } + + if gif.Urls.Hd != "" { + media.AddFormat(&models.MediaFormat{ + FormatID: "hd", + Type: enums.MediaTypeVideo, + URL: []string{gif.Urls.Hd}, + VideoCodec: enums.MediaCodecAVC, + Width: int64(gif.Width), + Height: int64(gif.Height), + }) + } + + if gif.Urls.Poster != "" { + thumbnails := []string{gif.Urls.Poster} + if gif.Urls.Thumbnail != "" { + thumbnails = append(thumbnails, gif.Urls.Thumbnail) + } + + for _, format := range media.Formats { + format.Thumbnail = thumbnails + format.Duration = int64(gif.Duration) + } + } + + if gif.HasAudio { + for _, format := range media.Formats { + format.AudioCodec = enums.MediaCodecAAC + } + } + + if len(media.Formats) > 0 { + mediaList = append(mediaList, media) + } + + return mediaList, nil +} + +func GetVideo(videoID string) (*Response, error) { + url := videoEndpoint + videoID + "?views=true" + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + token, err := GetAccessToken() + if err != nil { + return nil, fmt.Errorf("failed to get access token: %w", err) + } + req.Header.Set("authorization", "Bearer "+token.AccessToken) + req.Header.Set("user-agent", token.Agent) + req.Header.Set("x-customheader", "https://www.redgifs.com/watch/"+videoID) + for k, v := range baseApiHeaders { + req.Header.Set(k, v) + } + res, err := session.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get video: %s", res.Status) + } + var response Response + err = sonic.ConfigFastest.NewDecoder(res.Body).Decode(&response) + if err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + if response.Gif == nil { + return nil, fmt.Errorf("failed to get video: %s", res.Status) + } + return &response, nil +} diff --git a/ext/redgifs/models.go b/ext/redgifs/models.go new file mode 100644 index 0000000..efb276c --- /dev/null +++ b/ext/redgifs/models.go @@ -0,0 +1,44 @@ +package redgifs + +type Response struct { + Gif *Gif `json:"gif"` +} + +type Token struct { + AccessToken string `json:"token"` + Agent string `json:"agent"` + ExpiresIn int64 `json:"expires_in"` +} + +type Urls struct { + Silent string `json:"silent"` + Sd string `json:"sd"` + Hd string `json:"hd"` + Thumbnail string `json:"thumbnail"` + HTML string `json:"html"` + Poster string `json:"poster"` +} + +type Gif struct { + AvgColor string `json:"avgColor"` + CreateDate int `json:"createDate"` + Description string `json:"description"` + Duration float64 `json:"duration"` + HasAudio bool `json:"hasAudio"` + Height int `json:"height"` + HideHome bool `json:"hideHome"` + HideTrending bool `json:"hideTrending"` + Hls bool `json:"hls"` + ID string `json:"id"` + Likes int `json:"likes"` + Niches []string `json:"niches"` + Published bool `json:"published"` + Type int `json:"type"` + Sexuality []string `json:"sexuality"` + Tags []string `json:"tags"` + Urls Urls `json:"urls"` + UserName string `json:"userName"` + Verified bool `json:"verified"` + Views int `json:"views"` + Width int `json:"width"` +} diff --git a/ext/redgifs/util.go b/ext/redgifs/util.go new file mode 100644 index 0000000..a065e8f --- /dev/null +++ b/ext/redgifs/util.go @@ -0,0 +1,45 @@ +package redgifs + +import ( + "fmt" + "govd/util" + "net/http" + "time" + + "github.com/bytedance/sonic" +) + +var accessToken *Token + +func GetAccessToken() (*Token, error) { + if accessToken == nil || time.Now().Unix() >= accessToken.ExpiresIn { + if err := RefreshAccessToken(); err != nil { + return nil, err + } + } + return accessToken, nil +} + +func RefreshAccessToken() error { + req, err := http.NewRequest(http.MethodGet, tokenEndpoint, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("User-Agent", util.ChromeUA) + res, err := session.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return fmt.Errorf("failed to get access token: %s", res.Status) + } + var token Token + err = sonic.ConfigFastest.NewDecoder(res.Body).Decode(&token) + if err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + token.ExpiresIn = time.Now().Add(23 * time.Hour).Unix() + accessToken = &token + return nil +}