feat: add download commands (yt, ig, tt, fb) and middleware support

- Add core Middleware support via `core.Default.Use()`
- Implement download command handlers for YouTube, Instagram, TikTok, and Facebook
- Add regex-based URL validation for all download commands
- Add API client for fetching download links
- Add helpers for uploading and sending media to WhatsApp

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Fd
2026-07-15 10:33:58 +07:00
co-authored by Claude Opus 4.7
parent faeda0b21d
commit e783284881
11 changed files with 701 additions and 7 deletions
+63
View File
@@ -0,0 +1,63 @@
package download
import (
"fmt"
"regexp"
"neo/api"
hc "neo/context"
"neo/core"
"neo/helper"
)
type facebookResult struct {
HD string `json:"hd"`
SD string `json:"sd"`
Thumbnail string `json:"thumbnail"`
}
func init() { core.Default.Register(Facebook) }
var Facebook = &core.Command{
Name: "facebook",
Aliases: []string{"fb"},
Category: "download",
Description: "Download Facebook video",
Run: func(ctx *hc.Ctx) {
url := ctx.Arg(0)
if url == "" {
ctx.Reply("Usage: !fb <url>")
return
}
fbRegex := regexp.MustCompile(`(?i)(?:facebook\.com|fb\.com|fb\.watch)\/(?:share\/[pr]\/|reel\/|watch\/?\?v=|.*\/videos\/)?([a-zA-Z0-9_\-]+)`)
if !fbRegex.MatchString(url) {
ctx.Reply("Invalid Facebook URL")
return
}
ctx.Send("Downloading...")
var res facebookResult
if err := api.Default().Get("/fb", map[string]string{"url": url}, &res); err != nil {
ctx.Reply(fmt.Sprintf("Failed to fetch: %v", err))
return
}
downloadURL := res.HD
if downloadURL == "" {
downloadURL = res.SD
}
if downloadURL == "" {
ctx.Reply("No video found")
return
}
data, mime, err := api.Download(downloadURL)
if err != nil {
ctx.Reply(fmt.Sprintf("Download failed: %v", err))
return
}
helper.SendMedia(ctx, data, mime, "")
},
}
+69
View File
@@ -0,0 +1,69 @@
package download
import (
"fmt"
"regexp"
"neo/api"
hc "neo/context"
"neo/core"
"neo/helper"
)
type instagramVideo struct {
Src string `json:"src"`
Thumbnail string `json:"thumbnail"`
}
type instagramResult struct {
Images []string `json:"images"`
Videos []instagramVideo `json:"videos"`
}
func init() { core.Default.Register(Instagram) }
var Instagram = &core.Command{
Name: "instagram",
Aliases: []string{"ig"},
Category: "download",
Description: "Download Instagram media",
Run: func(ctx *hc.Ctx) {
url := ctx.Arg(0)
if url == "" {
ctx.Reply("Usage: !ig <url>")
return
}
igRegex := regexp.MustCompile(`(?i)(?:instagram\.com|instagr\.am)\/(?:p|reel|tv)\/([a-zA-Z0-9_\-]+)`)
if !igRegex.MatchString(url) {
ctx.Reply("Invalid Instagram URL")
return
}
ctx.Send("Downloading...")
var res instagramResult
if err := api.Default().Get("/ig", map[string]string{"url": url}, &res); err != nil {
ctx.Reply(fmt.Sprintf("Failed to fetch: %v", err))
return
}
mediaURL := ""
if len(res.Videos) > 0 {
mediaURL = res.Videos[0].Src
} else if len(res.Images) > 0 {
mediaURL = res.Images[0]
}
if mediaURL == "" {
ctx.Reply("No media found")
return
}
data, mime, err := api.Download(mediaURL)
if err != nil {
ctx.Reply(fmt.Sprintf("Download failed: %v", err))
return
}
helper.SendMedia(ctx, data, mime, "")
},
}
+67
View File
@@ -0,0 +1,67 @@
package download
import (
"fmt"
"regexp"
"neo/api"
hc "neo/context"
"neo/core"
"neo/helper"
)
type tiktokResult struct {
Description string `json:"description"`
ImageURL []string `json:"image_url"`
Username string `json:"username"`
VideoURL []string `json:"video_url"`
}
func init() { core.Default.Register(TikTok) }
var TikTok = &core.Command{
Name: "tiktok",
Aliases: []string{"tt", "tk"},
Category: "download",
Description: "Download TikTok video",
Run: func(ctx *hc.Ctx) {
url := ctx.Arg(0)
if url == "" {
ctx.Reply("Usage: !tiktok <url>")
return
}
ttRegex := regexp.MustCompile(`(?i)(?:tiktok\.com)\/(?:@[\w.-]+\/video\/|v\/|t\/)([0-9]+)`)
if !ttRegex.MatchString(url) {
ctx.Reply("Invalid TikTok URL")
return
}
ctx.Send("Downloading...")
var res tiktokResult
if err := api.Default().Get("/tiktok", map[string]string{"url": url}, &res); err != nil {
ctx.Reply(fmt.Sprintf("Failed to fetch: %v", err))
return
}
mediaURL := ""
if len(res.VideoURL) > 0 {
mediaURL = res.VideoURL[0]
} else if len(res.ImageURL) > 0 {
mediaURL = res.ImageURL[0]
}
if mediaURL == "" {
ctx.Reply("No media found")
return
}
data, mime, err := api.Download(mediaURL)
if err != nil {
ctx.Reply(fmt.Sprintf("Download failed: %v", err))
return
}
caption := fmt.Sprintf("@%s\n%s", res.Username, res.Description)
helper.SendMedia(ctx, data, mime, caption)
},
}
+156
View File
@@ -0,0 +1,156 @@
package download
import (
"context"
"fmt"
"regexp"
"strings"
"neo/api"
hc "neo/context"
"neo/core"
"neo/helper"
waE2E "go.mau.fi/whatsmeow/proto/waE2E"
)
func init() {
core.Default.Register(YouTube)
core.Default.Use(handleYTReply)
}
func handleYTReply(ctx *hc.Ctx) bool {
if ctx.Cmd() != "" {
return false
}
quotedID := hc.GetQuotedStanzaID(ctx.Message())
if quotedID == "" {
return false
}
result, ok := api.GetYtCache(quotedID)
if !ok {
return false
}
formatID := strings.TrimSpace(hc.ParseMessageText(ctx.Event()))
if formatID == "" || !api.HasFormatID(result, formatID) {
return false
}
ctx.Send("Downloading...")
downloadByFormatID(ctx, result, formatID)
return true
}
var YouTube = &core.Command{
Name: "youtube",
Aliases: []string{"yt", "ytdl"},
Category: "download",
Description: "Download YouTube video",
Run: func(ctx *hc.Ctx) {
url := ctx.Arg(0)
if url == "" {
ctx.Reply("Usage: !yt <url>")
return
}
ytRegex := regexp.MustCompile(`(?i)(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/|youtube\.com\/shorts\/)([^"&?\/\s]{11})`)
if !ytRegex.MatchString(url) {
ctx.Reply("Invalid YouTube URL")
return
}
ctx.Send("Processing...")
var res api.YtDownloadResult
if err := api.Default().Get("/yt/query", map[string]string{"url": url}, &res); err != nil {
ctx.Reply(fmt.Sprintf("Error: %v", err))
return
}
showFormatPicker(ctx, &res)
},
}
func showFormatPicker(ctx *hc.Ctx, res *api.YtDownloadResult) {
thumb, _, err := api.Download(res.Thumbnail)
if err != nil {
ctx.Send(formatCaption(res))
return
}
img, err := ctx.UploadImage(thumb, formatCaption(res))
if err != nil {
ctx.Reply("Failed to upload thumbnail: " + err.Error())
return
}
resp, err := ctx.Client().SendMessage(context.Background(), ctx.ChatJID(), &waE2E.Message{ImageMessage: img})
if err != nil {
ctx.Reply("Failed to send: " + err.Error())
return
}
api.SetYtCache(resp.ID, res)
}
func formatCaption(res *api.YtDownloadResult) string {
var b strings.Builder
b.WriteString("Title: ")
b.WriteString(res.Title)
b.WriteString("\n\n── Video ──\n")
for _, f := range res.VideoFormats {
if f.URL == "" {
continue
}
fmt.Fprintf(&b, "%s · %s · %s · %s\n", f.FormatID, f.Resolution, f.Ext, formatSize(f.Filesize))
}
b.WriteString("\n── Audio ──\n")
for _, f := range res.AudioFormats {
if f.URL == "" {
continue
}
fmt.Fprintf(&b, "%s · %s · %s\n", f.FormatID, f.Ext, formatSize(f.Filesize))
}
b.WriteString("\nReply with formatId")
return b.String()
}
func formatSize(bytes int) string {
if bytes <= 0 {
return "?"
}
if bytes < 1024*1024 {
return fmt.Sprintf("%dKB", bytes/1024)
}
return fmt.Sprintf("%.1fMB", float64(bytes)/(1024*1024))
}
func downloadByFormatID(ctx *hc.Ctx, res *api.YtDownloadResult, fid string) {
// Look for video
for _, f := range res.VideoFormats {
if f.FormatID == fid && f.URL != "" {
data, mime, err := api.Download(f.URL)
if err != nil {
ctx.Reply(fmt.Sprintf("Download failed: %v", err))
return
}
helper.SendMedia(ctx, data, mime, res.Title) // using existing sendMedia handling
return
}
}
// Look for audio
for _, f := range res.AudioFormats {
if f.FormatID == fid && f.URL != "" {
data, mime, err := api.Download(f.URL)
if err != nil {
ctx.Reply(fmt.Sprintf("Download failed: %v", err))
return
}
helper.SendMedia(ctx, data, mime, res.Title)
return
}
}
ctx.Reply("Format " + fid + " not found")
}