diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7b6a2f2..d9e3aa5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -53,4 +53,23 @@ dealsbot: stage: build variables: GO_CMD: dealsbot - <<: *docker_build \ No newline at end of file + <<: *docker_build + +domaincheckbot: + stage: build + variables: + GO_CMD: domaincheckbot + <<: *docker_build + +pages: + stage: build + script: + - mkdir public + - cp cmd/domaincheckbot/config/domains.json public/ + artifacts: + paths: + - public + rules: + - if: $CI_COMMIT_BRANCH + changes: + - cmd/domaincheckbot/config/domains.json \ No newline at end of file diff --git a/cmd/domaincheckadd/main.go b/cmd/domaincheckadd/main.go new file mode 100644 index 0000000..747e76f --- /dev/null +++ b/cmd/domaincheckadd/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "grow.rievo.dev/discordBots/cmd/domaincheckbot/config" + "log" + "os" + "sort" + "strings" +) + +func main() { + log.Println("add domains to domains.json") + + scanner := bufio.NewScanner(os.Stdin) + + var newDomains []string + + fmt.Print("domains: ") + if scanner.Scan() { + + input := scanner.Text() + + values := strings.Split(input, ",") + + // trim space + for i := range values { + values[i] = strings.TrimSpace(values[i]) + } + + // remove empty + values = removeEmpty(values) + + // sort + sort.SliceStable(values, func(i, j int) bool { + return values[i] < values[j] + }) + + newDomains = values + } + log.Printf("domains to add: %v\n", newDomains) + + fmt.Print("add to domains.json? [y|N]: ") + if scanner.Scan() { + input := scanner.Text() + if input == "y" || input == "Y" { + err := storeDomains(newDomains) + if err != nil { + log.Fatal("failed storing domains.json") + } + fmt.Println("domains.json updated") + } else { + fmt.Println("canceled!") + } + } + +} + +func storeDomains(domains []string) error { + for _, domain := range domains { + config.AddDomain(domain) + } + file, _ := json.MarshalIndent(config.Domains, "", " ") + err := os.WriteFile("./cmd/domaincheckbot/config/domain.json", file, 0644) + return err +} + +func removeEmpty(sliceList []string) []string { + var list []string + for _, str := range sliceList { + if str != "" { + list = append(list, str) + } + } + return list +} diff --git a/cmd/domaincheckbot/config/domain.go b/cmd/domaincheckbot/config/domain.go new file mode 100644 index 0000000..c34a269 --- /dev/null +++ b/cmd/domaincheckbot/config/domain.go @@ -0,0 +1,40 @@ +package config + +import ( + _ "embed" + "encoding/json" + "github.com/disgoorg/log" + "sort" +) + +//go:embed domain.json +var domainsFiles []byte + +var Domains []string + +func init() { + err := json.Unmarshal(domainsFiles, &Domains) + if err != nil { + log.Fatal(err) + } +} + +func AddDomain(domain string) { + domains := append(Domains, domain) + sort.SliceStable(domains, func(i, j int) bool { + return domains[i] < domains[j] + }) + Domains = removeDuplicate(domains) +} + +func removeDuplicate[T string](sliceList []T) []T { + allKeys := make(map[T]bool) + var list []T + for _, item := range sliceList { + if _, value := allKeys[item]; !value { + allKeys[item] = true + list = append(list, item) + } + } + return list +} diff --git a/cmd/domaincheckbot/config/domain.json b/cmd/domaincheckbot/config/domain.json new file mode 100644 index 0000000..2e49160 --- /dev/null +++ b/cmd/domaincheckbot/config/domain.json @@ -0,0 +1,105 @@ +[ + "1488.ch", + "ascii.tools", + "astonish.ch", + "astonishing.ch", + "attribution.ch", + "blizan.ch", + "buehnenbande.ch", + "ccc.ch", + "cori.us", + "dropdrop.dev", + "dropdrop.fun", + "dropdrop.pro", + "dropdrop.quest", + "dropdrop.wiki", + "example.com", + "familie.st", + "family.st", + "felizian.ch", + "felizian.st", + "fst.ch", + "g8.co", + "g8.com", + "g8.io", + "g8.is", + "g8.net", + "g8.nz", + "g8.re", + "gr8.click", + "gwydi.me", + "incline.ch", + "mojang.studio", + "nilu.ch", + "nqa.ch", + "phynecs.com", + "poisoned.app", + "poisonedapple.ch", + "rievo.app", + "rievo.cc", + "rievo.ch", + "rievo.co", + "rievo.co.uk", + "rievo.com", + "rievo.cz", + "rievo.de", + "rievo.dev", + "rievo.eu", + "rievo.eu.org", + "rievo.group", + "rievo.host", + "rievo.info", + "rievo.io", + "rievo.mx", + "rievo.net", + "rievo.org", + "rievo.page", + "rievo.swiss", + "rievo.systems", + "rievo.us", + "rievo.xyz", + "rv.gy", + "rvo.co", + "rvo.com", + "rvo.net", + "rvo.one", + "rvo.re", + "schuer.ch", + "schuerch.ch", + "schuerch.co", + "schuerch.com", + "schuerch.dev", + "schuerch.id", + "schuerch.net", + "schuerch.xyz", + "schur.ch", + "schurch.ch", + "schurch.com", + "schurch.dev", + "schurch.net", + "schwabe.ch", + "scrib.li", + "seraphimstrub.com", + "signage.ch", + "sos-esport.com", + "sos-esports.com", + "sst.ch", + "sst.place", + "strub.cc", + "strub.ch", + "strub.co", + "strub.com", + "strub.consulting", + "strub.info", + "strub.net", + "strub.one", + "strub.org", + "strub.st", + "strub.swiss", + "strub.xyz", + "swizer.land", + "thehat.ch", + "unlogis.ch", + "xii.st", + "xn--schrch-5ya.ch" +] \ No newline at end of file diff --git a/cmd/domaincheckbot/dns/domain.go b/cmd/domaincheckbot/dns/domain.go new file mode 100644 index 0000000..51b5374 --- /dev/null +++ b/cmd/domaincheckbot/dns/domain.go @@ -0,0 +1,35 @@ +package dns + +import ( + "grow.rievo.dev/discordBots/cmd/domaincheckbot/repository" + "net" + "sort" +) + +func CheckDomain(domain string) repository.Domain { + nameservers, err := net.LookupNS(domain) + + if len(nameservers) > 0 && err == nil { + return repository.Domain{ + Name: domain, + NS: nsToArray(nameservers), + } + } + return repository.Domain{ + Name: domain, + NS: []string{}, + } +} + +func nsToArray(nameservers []*net.NS) []string { + var nsArray []string + for _, nameserver := range nameservers { + nsArray = append(nsArray, nameserver.Host) + } + + sort.SliceStable(nsArray, func(i, j int) bool { + return nsArray[i] < nsArray[j] + }) + + return nsArray +} diff --git a/cmd/domaincheckbot/main.go b/cmd/domaincheckbot/main.go new file mode 100644 index 0000000..54af159 --- /dev/null +++ b/cmd/domaincheckbot/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "context" + "errors" + "fmt" + "github.com/dgraph-io/badger/v4" + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/rest" + "github.com/disgoorg/disgo/webhook" + "github.com/disgoorg/log" + "github.com/disgoorg/snowflake/v2" + "grow.rievo.dev/discordBots/cmd/domaincheckbot/config" + "grow.rievo.dev/discordBots/cmd/domaincheckbot/dns" + "grow.rievo.dev/discordBots/cmd/domaincheckbot/repository" + "os" + "os/signal" + "reflect" + "syscall" + "time" +) + +var ( + webhookID = snowflake.GetEnv("webhook_id") + webhookToken = os.Getenv("webhook_token") +) + +// TODO: clear db from domains removed from json + +func main() { + log.SetLevel(log.LevelInfo) + log.Info("starting domainCheck...") + log.Info("disgo version: ", disgo.Version) + + client := webhook.New(webhookID, webhookToken) + defer client.Close(context.TODO()) + + repo := repository.InitDb() + defer repo.Close() + + ticker := time.NewTicker(10 * time.Minute) + tickerGC := time.NewTicker(15 * time.Minute) + quit := make(chan struct{}) + + go func() { + for { + select { + case <-ticker.C: + for _, d := range config.Domains { + domain := dns.CheckDomain(d) + retrievedDomain, _ := repo.GetValue(d) + if reflect.DeepEqual(domain, retrievedDomain) { + log.Debugf(" %v: did not change", d) + } else { + log.Infof("!%v: changed", d) + go sendWebhook(client, domain, retrievedDomain) + repo.SetValue(domain) + } + } + + case <-tickerGC.C: + err := repo.RunGC() + if err != nil && !errors.Is(err, badger.ErrNoRewrite) { + log.Errorf("error with GC: %v", err) + } else { + log.Debug("GC successful") + } + + case <-quit: + ticker.Stop() + tickerGC.Stop() + return + } + } + }() + + log.Infof("domainCheck is now running. Press CTRL-C to exit.") + s := make(chan os.Signal, 1) + signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + <-s +} + +func sendWebhook(client webhook.Client, domain repository.Domain, oldDomain repository.Domain) { + var status string + + status = fmt.Sprintf("```md\n# %v", domain.Name) + status = fmt.Sprintf("%v\n - %v", status, oldDomain.NS) + status = fmt.Sprintf("%v\n + %v", status, domain.NS) + status = fmt.Sprintf("%v```\n", status) + + if _, err := client.CreateMessage(discord.NewWebhookMessageCreateBuilder(). + SetContent(status).Build(), + rest.WithDelay(2*time.Second), + ); err != nil { + log.Errorf("error sending message %v", err.Error()) + } +} diff --git a/cmd/domaincheckbot/repository/repository.go b/cmd/domaincheckbot/repository/repository.go new file mode 100644 index 0000000..cdadc66 --- /dev/null +++ b/cmd/domaincheckbot/repository/repository.go @@ -0,0 +1,108 @@ +package repository + +import ( + "encoding/json" + "github.com/dgraph-io/badger/v4" + "github.com/disgoorg/log" +) + +type Domain struct { + Name string + NS []string +} + +type Repository interface { + GetAll() ([]Domain, error) + GetValue(domainName string) Domain + SetValue(domain Domain) error + DeleteValue(domainName string) error + Close() error +} + +type DomainRepository struct { + db *badger.DB +} + +func InitDb() *DomainRepository { + opts := badger.DefaultOptions("./badger") + opts.Logger = nil + db, err := badger.Open(opts) + if err != nil { + log.Fatal(err) + } + return &DomainRepository{db} +} + +func (d *DomainRepository) Close() error { + return d.db.Close() +} + +func (d *DomainRepository) RunGC() error { + return d.db.RunValueLogGC(0.7) +} + +func (d *DomainRepository) GetAll() ([]Domain, error) { + var domains []Domain + err := d.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchSize = 10 + it := txn.NewIterator(opts) + defer it.Close() + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + err := item.Value(func(val []byte) error { + retrievedDomain := Domain{} + err := json.Unmarshal(val, &retrievedDomain) + domains = append(domains, retrievedDomain) + return err + }) + if err != nil { + return err + } + } + return nil + }) + return domains, err +} + +func (d *DomainRepository) GetValue(domainName string) (Domain, error) { + retrievedDomain := Domain{} + err := d.db.View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte(domainName)) + if err != nil { + return err + } + err = item.Value(func(val []byte) error { + err = json.Unmarshal(val, &retrievedDomain) + return err + }) + return err + }) + if err != nil { + return Domain{}, err + } + return retrievedDomain, nil +} + +func (d *DomainRepository) SetValue(domain Domain) error { + jsonBytes, err := json.Marshal(domain) + if err != nil { + return err + } + err = d.db.Update(func(txn *badger.Txn) error { + err := txn.Set([]byte(domain.Name), jsonBytes) + return err + }) + if err != nil { + return err + } + return nil +} + +func (d *DomainRepository) DeleteValue(domainName string) error { + err := d.db.Update(func(txn *badger.Txn) error { + err := txn.Delete([]byte(domainName)) + return err + }) + return err +}