Initial commit
commit
83d17a5ad6
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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",
|
||||||
|
}
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -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=
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
Loading…
Reference in New Issue