summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--assets/songlinkr.jpgbin0 -> 270084 bytes
-rw-r--r--cmd/songlinkr/main.go96
-rw-r--r--go.mod14
-rw-r--r--go.sum20
-rw-r--r--internal/service/service.go66
-rw-r--r--internal/urls/url.go36
7 files changed, 233 insertions, 0 deletions
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
--- /dev/null
+++ b/assets/songlinkr.jpg
Binary files 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
+}