├── .env.example ├── .gitignore ├── README.md ├── go.mod ├── go.sum ├── handlers └── index.go ├── lib ├── multi.go ├── single.go ├── test.go └── validate.go ├── main.go └── tlds.json /.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_TOKEN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | domainbot -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Domainbot 2 | 3 | A bot that lets you check domain availability while brainstorming names for an organization or event on Discord. 4 | 5 | ## Adding to your server 6 | 7 | Go to [this](https://discord.com/api/oauth2/authorize?client_id=709320758475751495&permissions=18432&scope=bot) link to add the bot to your server. You need to have `manage` permissions on your Discord server to be able to add it. 8 | 9 | ### Usage 10 | 11 | Simply type `domain example.com` in a channel with domainbot in it to check if it's available! If it isn't available, it'll let you know which registrar it was registered at. 12 | 13 | ## Self-hosting 14 | 15 | This bot is currently hosted on AWS, but if you want to host it youself, make sure you've installed the `whois` package on your linux machine with `sudo apt install whois`. 16 | 17 | Copy the `.env.example` file to `.env` and populate the environment variables. 18 | 19 | After that, simply run the following commands to get it up and running: 20 | 21 | ```console 22 | pip install -U discord.py whois python-dotenv validators 23 | python3 bot.py 24 | ``` 25 | 26 | Issues and Pull Requests welcome :) 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/someshkar/domainbot 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/bwmarrin/discordgo v0.20.3 7 | github.com/joho/godotenv v1.3.0 8 | github.com/likexian/whois-go v1.7.1 9 | github.com/likexian/whois-parser-go v1.14.5 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= 2 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= 3 | github.com/bwmarrin/discordgo v0.20.3 h1:AxjcHGbyBFSC0a3Zx5nDQwbOjU7xai5dXjRnZ0YB7nU= 4 | github.com/bwmarrin/discordgo v0.20.3/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= 5 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 6 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 7 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 8 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 9 | github.com/likexian/gokit v0.23.3 h1:1klD04/osK9b16Q9sNEChOOim6oDa14fEuv1JL8yzU4= 10 | github.com/likexian/gokit v0.23.3/go.mod h1:/asXq96N3H5gVxyfyNuQO7HFoSorzcU+ZMEImyBGZB8= 11 | github.com/likexian/whois-go v1.7.1 h1:P8gjFh6F5gZ2shft2CfO9yqu6JGtVk05eUVja9IYDPQ= 12 | github.com/likexian/whois-go v1.7.1/go.mod h1:lnblFx9bGrnTxchHUDr2An3EmSb+aujU48SA0ekPbRw= 13 | github.com/likexian/whois-parser-go v1.14.3/go.mod h1:nhh8bZ0mHgLu3p0mUV2kh9DgUJ6BXHb5elPgt7CL0VY= 14 | github.com/likexian/whois-parser-go v1.14.5 h1:zyPnpTcuweEDa9sEDeKZhNurdG764+mZFr6fVK5cDLA= 15 | github.com/likexian/whois-parser-go v1.14.5/go.mod h1:nhh8bZ0mHgLu3p0mUV2kh9DgUJ6BXHb5elPgt7CL0VY= 16 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= 17 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 18 | -------------------------------------------------------------------------------- /handlers/index.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strings" 5 | 6 | d "github.com/bwmarrin/discordgo" 7 | "github.com/someshkar/domainbot/lib" 8 | ) 9 | 10 | // MainHandler handles single domain lookups 11 | func MainHandler(s *d.Session, m *d.MessageCreate) { 12 | // Ignore messages created by the bot itself 13 | if m.Author.ID == s.State.User.ID { 14 | return 15 | } 16 | 17 | if strings.HasPrefix(m.Content, "domain ") { 18 | mSlice := strings.Fields(m.Content) 19 | 20 | if mSlice[1] == "all" { 21 | // Handle multiple domains 22 | s.ChannelMessageSend(m.ChannelID, lib.AllDomainRes(mSlice[2], m)) 23 | return 24 | } 25 | 26 | // Check if it's a valid domain 27 | if !lib.IsDomain(mSlice[1]) { 28 | s.ChannelMessageSend(m.ChannelID, "Please enter a valid domain!") 29 | return 30 | } 31 | 32 | // Handle single domain 33 | s.ChannelMessageSend(m.ChannelID, lib.SingleDomainRes(mSlice[1], m)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/multi.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "sync" 8 | 9 | d "github.com/bwmarrin/discordgo" 10 | "github.com/likexian/whois-go" 11 | whoisparser "github.com/likexian/whois-parser-go" 12 | ) 13 | 14 | // AllDomainRes returns the Domainbot 15 | // response for multiple popular domains 16 | func AllDomainRes(s string, m *d.MessageCreate) string { 17 | domains := CheckDomains(s) 18 | 19 | if len(domains) == 1 { 20 | res := fmt.Sprintf("%s %s may be available!", 21 | m.Author.Mention(), domains[0]) 22 | log.Printf("'all %s' returned '%s'", s, res) 23 | return res 24 | } 25 | 26 | if len(domains) > 0 { 27 | res := fmt.Sprintf("%s %s and %s may be available!", 28 | m.Author.Mention(), 29 | strings.Join(domains[:len(domains)-1], ", "), 30 | domains[len(domains)-1]) 31 | log.Printf("'all %s' returned '%s'", s, res) 32 | return res 33 | } 34 | 35 | res := fmt.Sprintf("%s none of the common TLDs are available for '%s'.", 36 | m.Author.Mention(), s) 37 | log.Printf("'all %s' returned '%s'", s, res) 38 | return res 39 | } 40 | 41 | // CheckDomains checks which popular domains 42 | // are avaiable for a particular string 43 | func CheckDomains(s string) (available []string) { 44 | tlds := []string{"com", "org", "net", "co", "io", "dev", "xyz", "tech"} 45 | 46 | var wg sync.WaitGroup 47 | wg.Add(len(tlds)) 48 | 49 | var availableDomains []string 50 | 51 | for _, tld := range tlds { 52 | go func(tld string) { 53 | domainAvailable(s+"."+tld, &availableDomains) 54 | wg.Done() 55 | }(tld) 56 | } 57 | 58 | wg.Wait() 59 | 60 | return availableDomains 61 | } 62 | 63 | // domainAvailable checks if a domain is available 64 | // and appends it to a slice if it is 65 | func domainAvailable(domain string, availableDomains *[]string) { 66 | raw, err := whois.Whois(domain) 67 | if err != nil { 68 | log.Println(err) 69 | } 70 | 71 | _, err = whoisparser.Parse(raw) 72 | if err != nil { 73 | if err == whoisparser.ErrDomainNotFound { 74 | *availableDomains = append(*availableDomains, domain) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/single.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "time" 8 | 9 | d "github.com/bwmarrin/discordgo" 10 | "github.com/likexian/whois-go" 11 | whoisparser "github.com/likexian/whois-parser-go" 12 | ) 13 | 14 | // SingleDomainRes generates the Domainbot 15 | // response for a single domain 16 | func SingleDomainRes(domain string, m *d.MessageCreate) string { 17 | isRegistered, tldSupported, registrar, expiryStr := checkDomain(domain) 18 | 19 | if !tldSupported { 20 | tld := strings.Split(domain, ".")[1] 21 | res := fmt.Sprintf("%s the .%s TLD isn't currently supported.", m.Author.Mention(), tld) 22 | log.Printf("'%s' returned '%s'", domain, res) 23 | return res 24 | } 25 | 26 | if isRegistered { 27 | t, _ := time.Parse(time.RFC3339, expiryStr) 28 | expiry := t.Format("January 2, 2006") 29 | 30 | // res := domain + " is registered at " + registrar + " and will expire on " + expiry + "." 31 | res := fmt.Sprintf("%s %s is registered at %s and will expire on %s.", 32 | m.Author.Mention(), domain, registrar, expiry) 33 | log.Printf("'%s' returned '%s'", domain, res) 34 | return res 35 | } 36 | 37 | // res := domain + " may be available!" 38 | res := fmt.Sprintf("%s %s may be available!", m.Author.Mention(), domain) 39 | log.Printf("'%s' returned '%s'", domain, res) 40 | return res 41 | } 42 | 43 | // checkDomain checks if a domain is available and returns relevant info if it is 44 | func checkDomain(domain string) (taken bool, tldSupported bool, registrar string, expiryDate string) { 45 | raw, err := whois.Whois(domain) 46 | if err != nil { 47 | log.Println(err) 48 | } 49 | 50 | result, err := whoisparser.Parse(raw) 51 | if err != nil { 52 | if err == whoisparser.ErrDomainNotFound { 53 | return false, true, "", "" 54 | } 55 | 56 | if err == whoisparser.ErrDomainInvalidData || err == whoisparser.ErrDomainLimitExceed { 57 | return false, false, "", "" 58 | } 59 | 60 | log.Println(err) 61 | } 62 | 63 | return true, true, result.Registrar.Name, result.Domain.ExpirationDate 64 | } 65 | -------------------------------------------------------------------------------- /lib/test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | d "github.com/bwmarrin/discordgo" 9 | ) 10 | 11 | func TestRes(s string, m *d.MessageCreate) *d.MessageEmbed { 12 | domains := []string{"someshkar.com", "someshkar.net"} 13 | 14 | embed := &d.MessageEmbed{ 15 | Author: &d.MessageEmbedAuthor{ 16 | Name: m.Author.Username, 17 | IconURL: m.Author.AvatarURL("32"), 18 | }, 19 | Title: "Lookup for ***google***", 20 | Description: "Looks like a few common TLDs are available for ***google***", 21 | Color: 0x2977f5, 22 | Fields: []*d.MessageEmbedField{ 23 | &d.MessageEmbedField{ 24 | Name: "Available Domains", 25 | Value: fmt.Sprintf("%s\n\n", strings.Join(domains, "\n")), 26 | }, 27 | }, 28 | Timestamp: time.Now().Format(time.RFC3339), 29 | } 30 | 31 | return embed 32 | } 33 | 34 | // // TestRes is a test handler 35 | // func TestRes(s string, m *d.MessageCreate) *d.MessageEmbed { 36 | // embed := &d.MessageEmbed{ 37 | // Author: &d.MessageEmbedAuthor{ 38 | // Name: m.Author.Username, 39 | // IconURL: m.Author.AvatarURL("32"), 40 | // }, 41 | // Title: "Lookup for google.com", 42 | // URL: "https://www.google.com", 43 | // Description: "Seems like google.com isn't available :(", 44 | // Color: 0x2977f5, 45 | // Fields: []*d.MessageEmbedField{ 46 | // &d.MessageEmbedField{ 47 | // Name: "Registrar", 48 | // Value: "Namecheap Inc.", 49 | // Inline: true, 50 | // }, 51 | // &d.MessageEmbedField{ 52 | // Name: "Expiry", 53 | // Value: "January 26, 2029", 54 | // Inline: true, 55 | // }, 56 | // }, 57 | // Timestamp: time.Now().Format(time.RFC3339), 58 | // } 59 | 60 | // return embed 61 | // } 62 | -------------------------------------------------------------------------------- /lib/validate.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import "regexp" 4 | 5 | // IsDomain checks if a string is a valid domain 6 | func IsDomain(toTest string) bool { 7 | // rxPat := regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`) 8 | rxPat := regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.[a-zA-Z]{2,}$`) 9 | 10 | // TODO: Load tlds.json and check is tld is supported 11 | return rxPat.MatchString(toTest) 12 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | "github.com/joho/godotenv" 12 | "github.com/someshkar/domainbot/handlers" 13 | ) 14 | 15 | func main() { 16 | // Load env variables from .env 17 | err := godotenv.Load() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | discordToken := os.Getenv("DISCORD_TOKEN") 23 | 24 | // Create a new Discord session using the provided bot token 25 | dg, err := discordgo.New("Bot " + discordToken) 26 | if err != nil { 27 | log.Fatalln(err) 28 | return 29 | } 30 | 31 | // Add handlers 32 | dg.AddHandler(handlers.MainHandler) 33 | 34 | // Open a websocket connection to Discord and start listening 35 | err = dg.Open() 36 | if err != nil { 37 | log.Fatalln("Error opening connection", err) 38 | return 39 | } 40 | 41 | // Wait here until Ctrl-C or some other term signal is received 42 | fmt.Println("Domainbot is running. Press CTRL-C to exit.") 43 | sc := make(chan os.Signal, 1) 44 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 45 | <-sc 46 | 47 | // Cleanly close the Discord session 48 | dg.Close() 49 | } 50 | -------------------------------------------------------------------------------- /tlds.json: -------------------------------------------------------------------------------- 1 | { 2 | "featured": ["com", "org", "net", "co", "io", "xyz", "tech"], 3 | "supported": [ 4 | "com", 5 | "uk", 6 | "ac.uk", 7 | "ar", 8 | "at", 9 | "pl", 10 | "be", 11 | "biz", 12 | "br", 13 | "ca", 14 | "cc", 15 | "cl", 16 | "club", 17 | "cn", 18 | "co", 19 | "jp", 20 | "co.jp", 21 | "cz", 22 | "de", 23 | "store", 24 | "download", 25 | "edu", 26 | "education", 27 | "eu", 28 | "fi", 29 | "fr", 30 | "id", 31 | "in", 32 | "info", 33 | "io", 34 | "ir", 35 | "is.is", 36 | "it", 37 | "kr", 38 | "kz", 39 | "lt", 40 | "ru", 41 | "lv", 42 | "me", 43 | "mobi", 44 | "mx", 45 | "name", 46 | "net", 47 | "ninja", 48 | "se", 49 | "nu", 50 | "nyc", 51 | "nz", 52 | "online", 53 | "org", 54 | "pharmacy", 55 | "press", 56 | "pw", 57 | "rest", 58 | "ru.rf", 59 | "security", 60 | "sh", 61 | "site", 62 | "space", 63 | "tech", 64 | "tel", 65 | "theatre", 66 | "tickets", 67 | "tv", 68 | "us", 69 | "uz", 70 | "video", 71 | "website", 72 | "wiki", 73 | "xyz" 74 | ] 75 | } 76 | --------------------------------------------------------------------------------