From f903439cbc504b9a7b676eb8a1c4e1d4c95a61fc Mon Sep 17 00:00:00 2001 From: Benjamin Chausse Date: Fri, 6 Sep 2024 19:59:21 -0400 Subject: Batman --- .gitignore | 1 + assets/songlinkr.jpg | Bin 0 -> 270084 bytes cmd/songlinkr/main.go | 96 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 14 +++++++ go.sum | 20 +++++++++ internal/service/service.go | 66 ++++++++++++++++++++++++++++++ internal/urls/url.go | 36 +++++++++++++++++ 7 files changed, 233 insertions(+) create mode 100644 .gitignore create mode 100644 assets/songlinkr.jpg create mode 100644 cmd/songlinkr/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/service/service.go create mode 100644 internal/urls/url.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce71aab --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +secrets/* diff --git a/assets/songlinkr.jpg b/assets/songlinkr.jpg new file mode 100644 index 0000000..8d85939 Binary files /dev/null and b/assets/songlinkr.jpg differ diff --git a/cmd/songlinkr/main.go b/cmd/songlinkr/main.go new file mode 100644 index 0000000..f068203 --- /dev/null +++ b/cmd/songlinkr/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "path" + "strings" + "syscall" + + services "github.com/ChausseBenjamin/songlinkr/internal/service" + "github.com/ChausseBenjamin/songlinkr/internal/urls" + "github.com/bwmarrin/discordgo" + "github.com/urfave/cli/v2" +) + +func AppAction(ctx *cli.Context) error { + token, err := getSecret(ctx.String("secrets-path"), "token") + if err != nil { + return err + } + + sess, err := discordgo.New("Bot " + token) + if err != nil { + log.Fatal(err) + } + + sess.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { + if m.Author.ID == s.State.User.ID { + return + } + + services := services.GetServices() + + links := urls.Find(m.Content) + for _, link := range links { + for _, srvc := range services { + if srvc.Owns(link) { + + link = srvc.Resolve(link) + src, err := urls.Resolve(link) + if err != nil { + src = link // An error occured, use the less elegant link... + } + + msg := srvc.Name() + " link detected!\n" + msg += "[Here is a universal link for everyone to enjoy](" + src + ")! 🎶\n" + msg += "Beep boop! Have a nice one! 🤖" + s.ChannelMessageSend(m.ChannelID, msg) + break + } + } + } + }) + + sess.Identify.Intents = discordgo.IntentsAllWithoutPrivileged + + err = sess.Open() + if err != nil { + return err + } + defer sess.Close() + + fmt.Println("Bot is now running. Press CTRL-C to exit.") + + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + <-sc + return nil +} + +func getSecret(secretsPath, secretName string) (string, error) { + secretValue, err := os.ReadFile(path.Join(secretsPath, secretName)) + if err != nil { + return "", err + } + return strings.TrimSpace(string(secretValue)), nil +} + +func main() { + app := &cli.App{ + Name: "Songlinkr", + Usage: "A Discord bot that converts song links to Universal Song.link", + Action: AppAction, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "secrets-path", + EnvVars: []string{"SECRETS_DIR"}, + }, + }, + } + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aef2cf3 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/ChausseBenjamin/songlinkr + +go 1.22.6 + +require ( + github.com/bwmarrin/discordgo v0.28.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli/v2 v2.27.4 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b9d36b7 --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= +github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..49647fe --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,66 @@ +package services + +// service defines any Music service recognized by song.link +// Since most services have multiple url version (long, short, etc...), +// it is allowd to have multiple ones. +// This allows for seamless song link detection +type service struct { + name string + patterns []string +} + +func GetServices() []service { + return []service{ + *New( + "Apple Music", + []string{"https://music.apple.com/"}, + ), + *New( + "Spotify", + []string{"https://open.spotify.com/track/", "https://open.spotify.com/album/", "https://open.spotify.com/playlist/"}, + ), + *New( + "Youtube Music", + []string{"https://music.youtube.com/"}, + ), + // *New( + // "Amazon Music", + // []string{"https://music.amazon.com/",}, + // ), + *New( + "Tidal", + []string{"https://tidal.com/"}, + ), + *New( + "Deezer", + []string{"https://www.deezer.com/"}, + ), + *New( + "Pandora", + []string{"https://www.pandora.com/"}, + ), + } +} + +func New(name string, patterns []string) *service { + return &service{name, patterns} +} + +func (s *service) Name() string { + return s.name +} + +func (s *service) Owns(url string) bool { + // If the url begins the same as any of the patterns, it is owned by the service + for _, pattern := range s.patterns { + if len(url) >= len(pattern) && url[:len(pattern)] == pattern { + return true + } + } + return false +} + +// Resolve returns the song.link url for the given service url +func (s *service) Resolve(url string) string { + return "https://song.link/" + url +} diff --git a/internal/urls/url.go b/internal/urls/url.go new file mode 100644 index 0000000..bb76594 --- /dev/null +++ b/internal/urls/url.go @@ -0,0 +1,36 @@ +package urls + +import ( + "net/http" + "regexp" +) + +// Find takes in a string (i.e. a discord message) +// and returns a list of all the urls it has found in this +// string. +func Find(s string) []string { + // Regular expression to match URLs starting with https + re := regexp.MustCompile(`https://[^\s]+`) + + // Find all matching URLs + return re.FindAllString(s, -1) +} + +func Resolve(url string) (string, error) { + client := &http.Client{ + // Automatically follow redirects + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return nil + }, + } + + resp, err := client.Head(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // The final resolved URL after following redirects + finalURL := resp.Request.URL.String() + return finalURL, nil +} -- cgit v1.2.3