From 83d17a5ad6173aeadd3d3438097fcbe3a1fa0cbc Mon Sep 17 00:00:00 2001 From: Hamza Ali Date: Tue, 7 Sep 2021 17:52:27 +0700 Subject: [PATCH] Initial commit --- config.go | 25 +++ doc.go | 516 ++++++++++++++++++++++++++++++++++++++++++++++++++++ doc_test.go | 90 +++++++++ go.mod | 24 +++ go.sum | 63 +++++++ main.go | 128 +++++++++++++ 6 files changed, 846 insertions(+) create mode 100644 config.go create mode 100644 doc.go create mode 100644 doc_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..5cae8e7 --- /dev/null +++ b/config.go @@ -0,0 +1,25 @@ +package main + +import ( + "encoding/json" + "log" + "os" +) + +type Configuration struct { + Prefix string `json:"prefix"` + Token string `json:"token"` +} + +var config Configuration + +func init() { + fileBytes, err := os.ReadFile("config.json") + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(fileBytes, &config) + if err != nil { + log.Fatal(err) + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..e51d7cf --- /dev/null +++ b/doc.go @@ -0,0 +1,516 @@ +package main + +import ( + "context" + "fmt" + "log" + "path" + "strings" + "time" + + "github.com/diamondburned/arikawa/v3/api" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" + "github.com/hhhapz/doc" +) + +var ( + searchErr = "Could not find package with the name of `%s`." + notFound = "Could not find type or function `%s` in package `%s`" + methodNotFound = "Could not find method `%s` for type `%s` in package `%s`" + notOwner = "Only the message sender can do this." + cannotExpand = "You cannot expand this embed." + + privilegedRoles = map[discord.RoleID]struct{}{ + // Gopher Herder ID on discord.gg/golang + 370280974593818644: {}, + } +) + +type interactionData struct { + id string + created time.Time + user *discord.User + query string + full bool +} + +var interactionMap = map[string]*interactionData{} + +func init() { + go func() { + for { + time.Sleep(time.Minute * 5) + now := time.Now() + for _, data := range interactionMap { + if data.created.After(now) { + delete(interactionMap, data.id) + } + } + } + }() +} + +func (b *botState) handleDocs(e *gateway.InteractionCreateEvent) { + data := api.InteractionResponse{Type: api.DeferredMessageInteractionWithSource} + if err := b.state.RespondInteraction(e.ID, e.Token, data); err != nil { + log.Println("failed to send interaction callback:", err) + return + } + + args := map[string]gateway.InteractionOption{} + for _, arg := range e.Data.Options { + args[arg.Name] = arg + } + + query := args["query"].String() + embed := b.onDocs(e, args["query"].String(), false) + var components *[]discord.Component + + if !strings.HasPrefix(embed.Title, "Error") { + interactionMap[e.ID.String()] = &interactionData{ + id: e.ID.String(), + created: time.Now(), + user: e.User, + query: query, + } + + components = &[]discord.Component{ + discord.ActionRowComponent{ + Components: []discord.Component{ + discord.SelectComponent{ + CustomID: e.ID.String(), + Options: selectOptions(false), + Placeholder: "Actions", + }, + }, + }, + } + } + + edit := api.EditInteractionResponseData{ + Embeds: &[]discord.Embed{embed}, + Components: components, + } + + if _, err := b.state.EditInteractionResponse(e.AppID, e.Token, edit); err != nil { + log.Println("failed to send interaction callback:", err) + return + } +} + +func (b *botState) onDocsComponent(e *gateway.InteractionCreateEvent, data *interactionData) { + var embed discord.Embed + var components *[]discord.Component + + var hasRole bool + for _, role := range e.Member.RoleIDs { + if _, ok := privilegedRoles[role]; ok { + hasRole = true + break + } + } + + isAdmin := func() bool { + perms, err := b.state.Permissions(e.ChannelID, e.User.ID) + if err != nil { + return false + } + if !perms.Has(discord.PermissionAdministrator) { + return false + } + return true + } + + action := e.Data.Values[0] + switch action { + case "minimize": + embed, data.full = b.onDocs(e, data.query, false), false + components = &[]discord.Component{ + discord.ActionRowComponent{ + Components: []discord.Component{ + discord.SelectComponent{ + CustomID: data.id, + Options: selectOptions(false), + Placeholder: "Actions", + }, + }, + }, + } + + // admin + priviledged only + // only check admin here to reduce total api calls + case "expand": + embed, data.full = b.onDocs(e, data.query, true), true + components = &[]discord.Component{ + discord.ActionRowComponent{ + Components: []discord.Component{ + discord.SelectComponent{ + CustomID: data.id, + Options: selectOptions(true), + Placeholder: "Actions", + }, + }, + }, + } + + case "hide": + components = &[]discord.Component{} + embed = b.onDocs(e, data.query, data.full) + embed.Description = "" + embed.Footer = nil + delete(interactionMap, data.id) + } + + if e.GuildID != discord.NullGuildID { + // check admin last + if e.User.ID != data.user.ID && !hasRole && !isAdmin() { + embed = failEmbed("Error", notOwner) + } + } + + var resp api.InteractionResponse + if strings.HasPrefix(embed.Title, "Error") { + resp = api.InteractionResponse{ + Type: api.MessageInteractionWithSource, + Data: &api.InteractionResponseData{ + Flags: api.EphemeralResponse, + Embeds: &[]discord.Embed{embed}, + }, + } + } else { + resp = api.InteractionResponse{ + Type: api.UpdateMessage, + Data: &api.InteractionResponseData{ + Embeds: &[]discord.Embed{embed}, + Components: components, + }, + } + } + + if err := b.state.RespondInteraction(e.ID, e.Token, resp); err != nil { + log.Println("failed to send interaction callback:", err) + } +} + +func (b *botState) onDocs(e *gateway.InteractionCreateEvent, query string, full bool) discord.Embed { + module, parts := parseQuery(query) + pkg, err := b.searcher.Search(context.Background(), module) + if err != nil { + log.Printf("Package request by %s failed: %v", e.User.Tag(), err) + return failEmbed("Error", fmt.Sprintf(searchErr, module)) + } + + switch len(parts) { + case 0: + return b.fullPackage(pkg, full) + case 1: + if typ, ok := pkg.Types[parts[0]]; ok { + return b.typ(pkg, typ, full) + } + + if fn, ok := pkg.Functions[parts[0]]; ok { + return b.fn(pkg, fn, full) + } + + return failEmbed("Error: Not Found", fmt.Sprintf(notFound, parts[0], module)) + default: + typ, ok := pkg.Types[parts[0]] + if !ok { + return failEmbed("Error: Not Found", fmt.Sprintf(notFound, parts[0], module)) + } + + method, ok := typ.Methods[parts[1]] + if !ok { + return failEmbed("Error: Not Found", fmt.Sprintf(methodNotFound, parts[1], parts[0], module)) + } + + return b.method(pkg, method, full) + } +} + +const ( + docLimit = 2800 + defLimit = 1000 + + accentColor = 0x007D9C +) + +func (b *botState) fullPackage(pkg doc.Package, full bool) discord.Embed { + return discord.Embed{ + Title: "Package " + pkg.URL, + URL: "https://pkg.go.dev/" + pkg.URL, + Description: fmt.Sprintf("**Types:** %d\n**Functions:** %d\n\n%s", + len(pkg.Types), len(pkg.Functions), format(pkg.Overview, 32, full)), + Color: accentColor, + Footer: &discord.EmbedFooter{ + Text: "https://pkg.go.dev/" + pkg.URL, + }, + } +} + +func (b *botState) typ(pkg doc.Package, typ doc.Type, full bool) discord.Embed { + def := typdef(typ.Signature, full) + return discord.Embed{ + Title: fmt.Sprintf("%s: %s", pkg.URL, typ.Name), + URL: fmt.Sprintf("https://pkg.go.dev/%s#%s", pkg.URL, typ.Name), + Description: fmt.Sprintf("```go\n%s\n```\n%s", def, format(typ.Comment, len(def), full)), + Color: accentColor, + Footer: &discord.EmbedFooter{ + Text: "https://pkg.go.dev/" + pkg.URL, + }, + } +} + +func (b *botState) fn(pkg doc.Package, fn doc.Function, full bool) discord.Embed { + def := typdef(fn.Signature, full) + return discord.Embed{ + Title: fmt.Sprintf("%s: %s", pkg.URL, fn.Name), + URL: fmt.Sprintf("https://pkg.go.dev/%s#%s", pkg.URL, fn.Name), + Description: fmt.Sprintf("```go\n%s\n```\n%s", def, format(fn.Comment, len(def), full)), + Color: accentColor, + Footer: &discord.EmbedFooter{ + Text: "https://pkg.go.dev/" + pkg.URL, + }, + } +} + +func (b *botState) method(pkg doc.Package, method doc.Method, full bool) discord.Embed { + def := typdef(method.Signature, full) + return discord.Embed{ + Title: fmt.Sprintf("%s: %s.%s", pkg.URL, method.For, method.Name), + URL: fmt.Sprintf("https://pkg.go.dev/%s#%s.%s", pkg.URL, method.For, method.Name), + Description: fmt.Sprintf("```go\n%s\n```\n%s", def, format(method.Comment, len(def), full)), + Color: accentColor, + Footer: &discord.EmbedFooter{ + Text: "https://pkg.go.dev/" + pkg.URL, + }, + } +} + +func selectOptions(full bool) []discord.SelectComponentOption { + expand := discord.SelectComponentOption{ + Label: "Expand", + Value: "expand", + Description: "Show more documentation.", + Emoji: &discord.ButtonEmoji{Name: "⬇️"}, + } + if full { + expand = discord.SelectComponentOption{ + Label: "Minimize", + Value: "minimize", + Description: "Show less documentation.", + Emoji: &discord.ButtonEmoji{Name: "⬆️"}, + } + } + + return []discord.SelectComponentOption{ + expand, + { + Label: "Hide", + Value: "hide", + Description: "Hide the message.", + Emoji: &discord.ButtonEmoji{Name: "❌"}, + }, + } +} + +func failEmbed(title, description string) discord.Embed { + return discord.Embed{ + Title: title, + Description: description, + Color: 0xEE0000, + } +} + +func parseQuery(module string) (string, []string) { + module = strings.ReplaceAll(module, " ", ".") + dir, base := path.Split(strings.ToLower(module)) + split := strings.Split(base, ".") + full := dir + split[0] + + if complete, ok := stdlibPackages[full]; ok { + full = complete + } + return full, split[1:] +} + +func typdef(def string, full bool) string { + split := strings.Split(def, "\n") + if !full { + return split[0] + } + + b := strings.Builder{} + b.Grow(len(def)) + + for _, line := range strings.Split(def, "\n") { + b.WriteRune('\n') + + if len(line)+b.Len() > defLimit { + b.WriteString("// full signature omitted") + break + } + b.WriteString(line) + } + return b.String() +} + +func format(c doc.Comment, initial int, full bool) string { + if len(c) == 0 { + return "*No documentation found*" + } + if !full { + md := c[0].Markdown() + if len(md) > 500 { + md = md[:400] + "...\n\n*More documentation omitted*" + } + if len(c) == 1 { + return md + } + return fmt.Sprintf("%s\n\n*More documentation omitted*", md) + } + + var parts doc.Comment + length := initial + for _, note := range c { + l := len(note.Text()) + if l+length > docLimit { + parts = append(parts, doc.Paragraph("*More documentation omitted...*")) + break + } + length += l + parts = append(parts, note) + } + return parts.Markdown() +} + +var stdlibPackages = map[string]string{ + "tar": "archive/tar", + "zip": "archive/zip", + + "bzip2": "compress/bzip2", + "flate": "compress/flate", + "gzip": "compress/gzip", + "lzw": "compress/lzw", + "zlib": "compress/zlib", + + "heap": "container/heap", + "list": "container/list", + "ring": "container/ring", + + "aes": "crypto/aes", + "cipher": "crypto/cipher", + "des": "crypto/des", + "dsa": "crypto/dsa", + "ecdsa": "crypto/ecdsa", + "ed25519": "crypto/ed25519", + "elliptic": "crypto/elliptic", + "hmac": "crypto/hmac", + "md5": "crypto/md5", + "rc4": "crypto/rc4", + "rsa": "crypto/rsa", + "sha1": "crypto/sha1", + "sha256": "crypto/sha256", + "sha512": "crypto/sha512", + "subtle": "crypto/subtle", + "tls": "crypto/tls", + "x509": "crypto/x509", + "pkix": "crypto/x509/pkix", + + "dwarf": "debug/dwarf", + "elf": "debug/elf", + "gosym": "debug/gosym", + "macho": "debug/macho", + "pe": "debug/pe", + "plan9obj": "debug/plan9obj", + + "ascii85": "encoding/ascii85", + "asn1": "encoding/asn1", + "base32": "encoding/base32", + "base64": "encoding/base64", + "binary": "encoding/binary", + "csv": "encoding/csv", + "gob": "encoding/gob", + "hex": "encoding/hex", + "json": "encoding/json", + "pem": "encoding/pem", + "xml": "encoding/xml", + + "ast": "go/ast", + "build": "go/build", + "constraint": "go/build/constraint", + "constant": "go/constant", + "docformat": "go/docformat", + "importer": "go/importer", + "parserprinter": "go/parserprinter", + "scanner": "go/scanner", + "token": "go/token", + "types": "go/types", + + "adler32": "hash/adler32", + "crc32": "hash/crc32", + "crc64": "hash/crc64", + "fnv": "hash/fnv", + "maphash": "hash/maphash", + + "color": "image/color", + "draw": "image/draw", + "gif": "image/gif", + "jpeg": "image/jpeg", + "parsing": "image/parsing", + + "suffixarray": "index/suffixarray", + + "fs": "io/fs", + "ioutil": "io/ioutil", + + "big": "math/big", + "bits": "math/bits", + "cmplx": "math/cmplx", + + "multipart": "mime/multipart", + "quotedprintable": "mime/quotedprintable", + + "http": "net/http", + "cgi": "net/http/cgi", + "cookiejar": "net/http/cookiejar", + "fcgi": "net/http/fcgi", + "httptest": "net/http/httptest", + "httptrace": "net/http/httptrace", + "httputil": "net/http/httputil", + "mail": "net/mail", + "rpc": "net/rpc", + "jsonrpc": "net/rpc/jsonrpc", + "smtp": "net/smtp", + "textproto": "net/textproto", + + "exec": "os/exec", + "signal": "os/signal", + "user": "os/user", + + "filepath": "path/filepath", + + "syntax": "regexp/syntax", + + "cgo": "runtime/cgo", + "metrics": "runtime/metrics", + "msan": "runtime/msan", + "race": "runtime/race", + "trace": "runtime/trace", + + "js": "syscall/js", + + "fstest": "testing/fstest", + "iotest": "testing/iotest", + "quick": "testing/quick", + + "tabwriter": "text/tabwriter", + + "parse": "text/template/parse", + + "tzdata": "time/tzdata", + + "utf16": "unicode/utf16", + "utf8": "unicode/utf8", +} diff --git a/doc_test.go b/doc_test.go new file mode 100644 index 0000000..87abd8f --- /dev/null +++ b/doc_test.go @@ -0,0 +1,90 @@ +package main + +import "testing" + +func TestParseQuery(t *testing.T) { + cases := []struct { + name string + query string + module string + parts []string + }{ + { + name: "stdlib basic", + query: "strings", + module: "strings", + parts: nil, + }, + { + name: "stdlib type", + query: "strings.Split", + module: "strings", + parts: []string{"split"}, + }, + { + name: "stdlib method", + query: "strings.Builder.Grow", + module: "strings", + parts: []string{"builder", "grow"}, + }, + { + name: "stdlib redirect basic", + query: "json", + module: "encoding/json", + parts: nil, + }, + { + name: "stdlib redirect type", + query: "json.Unmarshal", + module: "encoding/json", + parts: []string{"unmarshal"}, + }, + { + name: "stdlib redirect method", + query: "json.NewDecoder.Decode", + module: "encoding/json", + parts: []string{"newdecoder", "decode"}, + }, + { + name: "custom basic", + query: "github.com/golang/go", + module: "github.com/golang/go", + parts: nil, + }, + { + name: "custom type", + query: "github.com/bwmarrin/discordgo.Session", + module: "github.com/bwmarrin/discordgo", + parts: []string{"session"}, + }, + { + name: "custom method", + query: "github.com/bwmarrin/discordgo.Session.AddHandler", + module: "github.com/bwmarrin/discordgo", + parts: []string{"session", "addhandler"}, + }, + { + name: "custom method with space", + query: "github.com/bwmarrin/discordgo Session AddHandler", + module: "github.com/bwmarrin/discordgo", + parts: []string{"session", "addhandler"}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + module, parts := parseQuery(c.query) + if module != c.module { + t.Errorf("INVALID MODULE:\nGOT:%s\nEXPECTED:%s", module, c.module) + } + if len(parts) != len(c.parts) { + t.Errorf("INVALID PARTS:\nGOT:%v\nEXPECTED:%v", parts, c.parts) + } + for i, part := range parts { + if part != c.parts[i] { + t.Errorf("INVALID PARTS(%d):\nGOT:%v\nEXPECTED:%v", i, part, c.parts[i]) + } + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..caae91a --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module github.com/hhhapz/discodoc + +go 1.17 + +require ( + github.com/diamondburned/arikawa/v3 v3.0.0-rc.1 + github.com/hhhapz/doc v0.3.1 + github.com/k0kubun/pp v3.0.1+incompatible + github.com/pkg/errors v0.9.1 +) + +require ( + github.com/PuerkitoBio/goquery v1.7.1 // indirect + github.com/andybalholm/cascadia v1.2.0 // indirect + github.com/gorilla/schema v1.2.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.13 // indirect + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect + golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect + golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c2be601 --- /dev/null +++ b/go.sum @@ -0,0 +1,63 @@ +github.com/PuerkitoBio/goquery v1.7.1 h1:oE+T06D+1T7LNrn91B4aERsRIeCLJ/oPSa6xB9FPnz4= +github.com/PuerkitoBio/goquery v1.7.1/go.mod h1:XY0pP4kfraEmmV1O7Uf6XyjoslwsneBbgeDjLYuN8xY= +github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE= +github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= +github.com/diamondburned/arikawa/v3 v3.0.0-20210824182349-f334491deed4 h1:Lj1nG3DiPyY4Mu7N4Wk0wtKtQsFlX9/8oT4g1EjENIo= +github.com/diamondburned/arikawa/v3 v3.0.0-20210824182349-f334491deed4/go.mod h1:sNqM/iGXuH87wEH1rpQBEY1PR0AAkRKJuUhJGOdo7To= +github.com/diamondburned/arikawa/v3 v3.0.0-rc.1 h1:1RHtYaVstlEJ5v8b3PNUIpa7D0cfg3qT7n7WbWjb5ZQ= +github.com/diamondburned/arikawa/v3 v3.0.0-rc.1/go.mod h1:sNqM/iGXuH87wEH1rpQBEY1PR0AAkRKJuUhJGOdo7To= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +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/hhhapz/doc v0.1.0 h1:Saowq412pkjVwzbbaj26E4695MsAh2YvRAy7ai2gE9Y= +github.com/hhhapz/doc v0.1.0/go.mod h1:EmQF3Mq8Y4MacCdVLDORczoxh1AD4WS6VpvHILXNbDE= +github.com/hhhapz/doc v0.1.2 h1:WqVVy/1Pa0LvPd5hhi7Z6SDRNBZ1CJ30cb/vEOww0iQ= +github.com/hhhapz/doc v0.1.2/go.mod h1:veI3TCLBx/7jTkNVShg1omZOx+XducAFqRQN3yG/xyM= +github.com/hhhapz/doc v0.1.3 h1:GOwFE+ewsLE+fIqQyBfJixuYfK3hpzVJss/gIVwu/4o= +github.com/hhhapz/doc v0.1.3/go.mod h1:RzUhRa6guPMbXtNfNm4jW6aYCu+JBapioQEgrzoHSMo= +github.com/hhhapz/doc v0.1.4 h1:xspKqlE36MmkuKhcHAzNUWlz6iyL4EWPh0Ky2Y/eaZQ= +github.com/hhhapz/doc v0.1.4/go.mod h1:RzUhRa6guPMbXtNfNm4jW6aYCu+JBapioQEgrzoHSMo= +github.com/hhhapz/doc v0.2.0 h1:vUCk8kQ7s+SPrjF/+ID2+qa1Jvo0YQXmco/n4vfLG2k= +github.com/hhhapz/doc v0.2.0/go.mod h1:RzUhRa6guPMbXtNfNm4jW6aYCu+JBapioQEgrzoHSMo= +github.com/hhhapz/doc v0.3.0 h1:SuUTYmZu83tTXTQ0wyOWYCcVeSSxfzvnSXEsE6jr4VI= +github.com/hhhapz/doc v0.3.0/go.mod h1:RzUhRa6guPMbXtNfNm4jW6aYCu+JBapioQEgrzoHSMo= +github.com/hhhapz/doc v0.3.1 h1:TKlytW6nDcJydOIDffYF7OINF6z3x6AUhEvlnfXk9FU= +github.com/hhhapz/doc v0.3.1/go.mod h1:RzUhRa6guPMbXtNfNm4jW6aYCu+JBapioQEgrzoHSMo= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a496def --- /dev/null +++ b/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "flag" + "log" + "net/http" + + "github.com/diamondburned/arikawa/v3/api" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" + "github.com/diamondburned/arikawa/v3/state" + "github.com/hhhapz/doc" + "github.com/hhhapz/doc/godocs" + "github.com/pkg/errors" +) + +type botState struct { + appID discord.AppID + searcher *doc.CachedSearcher + state *state.State +} + +func (b *botState) OnCommand(e *gateway.InteractionCreateEvent) { + if e.GuildID != discord.NullGuildID { + e.User = &e.Member.User + } + + if e.Data.Name == "docs" { + b.handleDocs(e) + return + } + if data, ok := interactionMap[e.Data.CustomID]; ok { + b.onDocsComponent(e, data) + return + } +} + +var update bool + +func main() { + updateVar := flag.Bool("update", false, "update all commands, regardless of if they are present or not") + flag.Parse() + update = *updateVar + + if config.Token == "" { + log.Fatalln("no token provided") + } + + s, err := state.New("Bot " + config.Token) + if err != nil { + log.Fatalln("session failed:", err) + } + + searcher := doc.New(http.DefaultClient, godocs.Parser) + cs := doc.WithCache(searcher) + b := botState{ + searcher: cs, + state: s, + } + s.AddHandler(b.OnCommand) + s.AddIntents(gateway.IntentGuildMessageReactions) + + if err := s.Open(context.Background()); err != nil { + log.Fatalln("failed to open:", err) + } + defer s.Close() + + log.Println("Gateway connection established.") + me, err := s.Me() + if err != nil { + log.Println("Could not get me:", err) + return + } + b.appID = discord.AppID(me.ID) + + log.Println("Logged in as ", me.Tag()) + + if err := loadCommands(s, me.ID); err != nil { + log.Println("Could not load commands:", err) + return + } + + select {} +} + +func loadCommands(s *state.State, me discord.UserID) error { + appID := discord.AppID(me) + registered, err := s.Commands(appID) + if err != nil { + return err + } + + registeredMap := map[string]bool{} + if !update { + for _, c := range registered { + registeredMap[c.Name] = true + log.Println("Registered command:", c.Name) + } + } + + for _, c := range commands { + if registeredMap[c.Name] { + continue + } + if _, err := s.CreateCommand(appID, c); err != nil { + return errors.Wrap(err, "could not register "+c.Name) + } + log.Println("Created command:", c.Name) + } + + return nil +} + +var commands = []api.CreateCommandData{ + { + Name: "docs", + Description: "Base command", + Options: []discord.CommandOption{ + { + Name: "query", + Description: "Search query", + Type: discord.StringOption, + Required: true, + }, + }, + }, +}