Last active
July 8, 2020 18:03
-
-
Save miyoyo/8641057636892863791ca7c41a1fab97 to your computer and use it in GitHub Desktop.
Flutter doc explorer bot (CC-By) v3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"github.com/bwmarrin/discordgo" | |
) | |
var messageCache = map[string][]*discordgo.Message{} | |
var metaChannel = "421444762956988418" | |
var rulesChannel = "421444488205041665" | |
// DeDupe messages sent on the server by caching them into a map and comparing them as they come in | |
func DeDupe(s *discordgo.Session, m *discordgo.MessageCreate) { | |
if m.GuildID == "" { | |
// DMs do not count | |
return | |
} | |
if m.Author.Bot { | |
return | |
} | |
if len(messageCache) == 0 { | |
// On start, drill all channels once | |
// Cache 5 messages deep | |
for _, v := range s.State.Guilds[0].Channels { | |
fmt.Println(v.Name) | |
messageCache[v.ID], _ = s.ChannelMessages(v.ID, 5, "", "", "") | |
} | |
return | |
} | |
if len(m.ContentWithMentionsReplaced()) >= 30 { | |
channelLoop: | |
for k, c := range messageCache { | |
if k == m.ChannelID { | |
continue | |
} | |
for _, v := range c { | |
if m.Content == v.Content && m.Author.ID == v.Author.ID { | |
s.ChannelMessageSend(metaChannel, "Hey, "+m.Author.Mention()+", please take a second to read the "+fmt.Sprintf("<#%s>", rulesChannel)+",\nspecifically, the section about not duplicating your messages across channels.\nIf you want to move a message, copy it, delete it, **then** paste it in another channel.\n\nThanks!") | |
break channelLoop | |
} | |
} | |
} | |
} | |
if len(messageCache[m.ChannelID]) == 5 { | |
messageCache[m.ChannelID] = messageCache[m.ChannelID][:4] | |
} | |
messageCache[m.ChannelID] = append([]*discordgo.Message{m.Message}, messageCache[m.ChannelID]...) | |
} | |
// DeleteDeDupe or rather mask deleted messages | |
func DeleteDeDupe(s *discordgo.Session, m *discordgo.MessageDelete) { | |
for _, v := range messageCache[m.ChannelID] { | |
v.Author = s.State.User | |
v.Content = "" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module github.com/miyoyo/flutterdoc | |
go 1.13 | |
require ( | |
github.com/antoan-angelov/go-fuzzy v0.0.0-20160220022448-4c77dcd0046a | |
github.com/bwmarrin/discordgo v0.20.3 | |
github.com/jasonlvhit/gocron v0.0.0-20200323211822-1a413f9a41a2 | |
robpike.io/filter v0.0.0-20150108201509-2984852a2183 | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "github.com/bwmarrin/discordgo" | |
// Help users when they mention the bot | |
func Help(s *discordgo.Session, h *discordgo.MessageCreate) { | |
for _, user := range h.Mentions { | |
if user.ID == s.State.User.ID { | |
s.ChannelMessageSendEmbed(h.ChannelID, &discordgo.MessageEmbed{ | |
Title: "ℹ️ Help", | |
Description: "⚠️ These commands can be within a message, and there can be multiple per messages", | |
Fields: []*discordgo.MessageEmbedField{ | |
{ | |
Name: "![Object] or ![Object.property] or ![package/Object] or ![package/Object.property]", | |
Value: "Gives a direct link to the closest match from the flutter documentation", | |
}, | |
{ | |
Name: "?[Object] or ?[Object.property] or ?[package/Object] or ?[package/Object.property]", | |
Value: "Shows the 10 first search results from the flutter documentation", | |
}, | |
{ | |
Name: "&[package]", | |
Value: "Shows up to 10 search results about 'package' on Pub", | |
}, | |
}, | |
Footer: &discordgo.MessageEmbedFooter{ | |
Text: "Source: https://gist.github.com/miyoyo/8641057636892863791ca7c41a1fab97", | |
}, | |
}) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "encoding/json" | |
func unmarshalSearchStruct(data []byte) ([]interface{}, error) { | |
var a []SearchStructElement | |
err := json.Unmarshal(data, &a) | |
b := make([]interface{}, len(a)) | |
for i := range a { | |
b[i] = a[i] | |
} | |
return b, err | |
} | |
// SearchStructElement is one element in the search structure generated by DartDoc | |
type SearchStructElement struct { | |
Name string `json:"name"` | |
QualifiedName string `json:"qualifiedName"` | |
Href string `json:"href"` | |
Type string `json:"type"` | |
OverriddenDepth int64 `json:"overriddenDepth"` | |
EnclosedBy *struct { | |
Name string `json:"name"` | |
Type string `json:"type"` | |
} `json:"enclosedBy,omitempty"` | |
} | |
func unmarshalPubSearch(data []byte) (PubSearch, error) { | |
var r PubSearch | |
err := json.Unmarshal(data, &r) | |
return r, err | |
} | |
// PubSearch are the search results from pub.dev | |
type PubSearch struct { | |
Packages []struct { | |
Package string `json:"package"` | |
} `json:"packages"` | |
Next string `json:"next"` | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"os" | |
"os/signal" | |
"syscall" | |
"github.com/bwmarrin/discordgo" | |
"github.com/jasonlvhit/gocron" | |
) | |
func main() { | |
fmt.Print("Loading search structure...") | |
updateCache() | |
fmt.Println(" Done") | |
bot, err := discordgo.New("Bot TOKEN") | |
if err != nil { | |
panic("Could not create bot: " + err.Error()) | |
} | |
bot.AddHandler(Help) | |
bot.AddHandler(Search) | |
bot.AddHandler(DeDupe) | |
bot.AddHandler(DeleteDeDupe) | |
if bot.Open() != nil { | |
panic("Could not open bot: " + err.Error()) | |
} | |
go func() { | |
gocron.Every(1).Day().Do(func() { | |
bot.UpdateStatusComplex(discordgo.UpdateStatusData{ | |
Status: "idle", | |
AFK: true, | |
Game: &discordgo.Game{ | |
Name: "updating search...", | |
}, | |
}) | |
updateCache() | |
bot.UpdateStatus(0, "mention me for commands.") | |
}) | |
gocron.Every(30).Minutes().Do(func() { bot.UpdateStatus(0, "mention me for commands.") }) | |
}() | |
fmt.Println("FlutterDoc running. CTRL+C to exit") | |
sc := make(chan os.Signal, 1) | |
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) | |
<-sc | |
bot.Close() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"io/ioutil" | |
"net/http" | |
"regexp" | |
"strings" | |
"github.com/antoan-angelov/go-fuzzy" | |
"github.com/bwmarrin/discordgo" | |
"robpike.io/filter" | |
) | |
var ( | |
topFuzz *fuzzy.Fuzzy | |
libTopFuzz *fuzzy.Fuzzy | |
lopPropFuzz *fuzzy.Fuzzy | |
libTopPropFuzz *fuzzy.Fuzzy | |
questionMatch *regexp.Regexp | |
prefixMatch *regexp.Regexp | |
topMatch *regexp.Regexp | |
libTopMatch *regexp.Regexp | |
topPropMatch *regexp.Regexp | |
libTopPropMatch *regexp.Regexp | |
) | |
func init() { | |
questionMatch = regexp.MustCompile(`([?!&])\[(.*?)\]`) | |
prefixMatch = regexp.MustCompile(`^([A-Za-z$_][A-Za-z0-9$_:.]*?\.)([A-Za-z$_A-Za-z0-9$_]*?)\.([A-Za-z$_<+|[>\/^~&*%=\-][A-Za-z0-9$_\]=\/<>\-]*?)$`) | |
topMatch = regexp.MustCompile(`^([A-Za-z$_A-Za-z0-9$_]*?)$`) | |
libTopMatch = regexp.MustCompile(`^([A-Za-z$_][A-Za-z0-9$_:.]*?)\/([A-Za-z$_A-Za-z0-9$_]*?)$`) | |
topPropMatch = regexp.MustCompile(`^([A-Za-z$_A-Za-z0-9$_]*?)\.([A-Za-z$_<+|[>\/^~&*%=\-][A-Za-z0-9$_\]=\/<>\-]*?)$`) | |
libTopPropMatch = regexp.MustCompile(`^([A-Za-z$_][A-Za-z0-9$_:.]*?)\/([A-Za-z$_A-Za-z0-9$_]*?)\.([A-Za-z$_<+|[>\/^~&*%=\-][A-Za-z0-9$_\]=\/<>\-]*?)$`) | |
} | |
// Search for an element in the documentation or on pub | |
func Search(session *discordgo.Session, message *discordgo.MessageCreate) { | |
if message.Author.Bot { | |
return | |
} | |
if len(message.Content) < 4 { | |
return | |
} | |
query := message.Content | |
matches := questionMatch.FindAllStringSubmatch(query, -1) | |
if len(matches) == 0 { | |
return | |
} | |
for _, match := range matches { | |
switch match[1] { | |
case `!`: | |
fallthrough | |
case `?`: | |
out := []interface{}{} | |
var err error | |
if topMatch.FindString(match[2]) != "" { | |
out, err = topFuzz.Search(match[2]) | |
} else if topPropMatch.FindString(match[2]) != "" { | |
out, err = lopPropFuzz.Search(match[2]) | |
} else if libTopMatch.FindString(match[2]) != "" { | |
out, err = libTopFuzz.Search(strings.Replace(match[2], "/", ".", 1)) | |
} else if libTopPropMatch.FindString(match[2]) != "" { | |
out, err = libTopPropFuzz.Search(strings.Replace(match[2], "/", ".", 1)) | |
} | |
if err != nil || len(out) == 0 { | |
notFound(session, message.ChannelID, match[2]) | |
return | |
} | |
if match[1] == "!" { | |
session.ChannelMessageSend( | |
message.ChannelID, | |
"https://api.flutter.dev/flutter/"+out[0].(SearchStructElement).Href, | |
) | |
} else { | |
fields := []*discordgo.MessageEmbedField{} | |
for _, result := range out[0:min(10, len(out))] { | |
if result.(SearchStructElement).EnclosedBy != nil { | |
fields = append(fields, &discordgo.MessageEmbedField{ | |
Name: result.(SearchStructElement).Type + " " + result.(SearchStructElement).Name + " - " + result.(SearchStructElement).EnclosedBy.Name, | |
Value: "https://api.flutter.dev/flutter/" + result.(SearchStructElement).Href, | |
}) | |
} else { | |
fields = append(fields, &discordgo.MessageEmbedField{ | |
Name: result.(SearchStructElement).Type + " " + result.(SearchStructElement).Name, | |
Value: "https://api.flutter.dev/flutter/" + result.(SearchStructElement).Href, | |
}) | |
} | |
} | |
session.ChannelMessageSendEmbed( | |
message.ChannelID, | |
&discordgo.MessageEmbed{ | |
Title: "Pub Search Results - " + match[2], | |
Fields: fields, | |
}, | |
) | |
} | |
case `&`: | |
r, err := http.Get("https://pub.dev/api/search?q=" + match[2]) | |
if err != nil { | |
return | |
} | |
b, err := ioutil.ReadAll(r.Body) | |
if err != nil { | |
return | |
} | |
s, err := unmarshalPubSearch(b) | |
if err != nil { | |
return | |
} | |
if len(s.Packages) == 0 { | |
notFound(session, message.ChannelID, match[2]) | |
return | |
} | |
fields := []*discordgo.MessageEmbedField{} | |
for _, result := range s.Packages[0:min(10, len(s.Packages))] { | |
fields = append(fields, &discordgo.MessageEmbedField{ | |
Name: result.Package, | |
Value: "https://pub.dev/package/" + result.Package, | |
}) | |
} | |
session.ChannelMessageSendEmbed( | |
message.ChannelID, | |
&discordgo.MessageEmbed{ | |
Title: "Pub Search Results - " + match[2], | |
Fields: fields, | |
}, | |
) | |
} | |
} | |
} | |
func notFound(s *discordgo.Session, channel string, message string) { | |
s.ChannelMessageSendEmbed( | |
channel, | |
&discordgo.MessageEmbed{ | |
Title: "Not Found", | |
Color: 0xDD2222, | |
Description: message, | |
}, | |
) | |
} | |
func min(a, b int) int { | |
if a < b { | |
return a | |
} | |
return b | |
} | |
func updateCache() { | |
r, err := http.Get("https://api.flutter.dev/flutter/index.json") | |
if err != nil { | |
panic(err) | |
} | |
b, err := ioutil.ReadAll(r.Body) | |
if err != nil { | |
panic(err) | |
} | |
s, err := unmarshalSearchStruct(b) | |
if err != nil { | |
panic(err) | |
} | |
top := filter.Choose(s, func(value interface{}) bool { | |
return (value.(SearchStructElement).EnclosedBy != nil && value.(SearchStructElement).EnclosedBy.Type == "library") | |
}).([]interface{}) | |
libTopProp := filter.Choose(s, func(value interface{}) bool { | |
return (value.(SearchStructElement).EnclosedBy != nil && value.(SearchStructElement).EnclosedBy.Type != "library") | |
}).([]interface{}) | |
topProp := filter.Apply(libTopProp, func(value interface{}) interface{} { | |
test := prefixMatch.FindStringSubmatch(value.(SearchStructElement).QualifiedName) | |
if len(test) == 0 { | |
fmt.Printf("Did not find this %v\n", value) | |
panic("a") | |
} | |
return SearchStructElement{ | |
EnclosedBy: value.(SearchStructElement).EnclosedBy, | |
QualifiedName: strings.TrimPrefix(value.(SearchStructElement).QualifiedName, test[1]), | |
Href: value.(SearchStructElement).Href, | |
Name: value.(SearchStructElement).Name, | |
OverriddenDepth: value.(SearchStructElement).OverriddenDepth, | |
Type: value.(SearchStructElement).Type, | |
} | |
}).([]interface{}) | |
topFuzz = toFuzz(&top, "Name") | |
libTopFuzz = toFuzz(&top, "QualifiedName") | |
lopPropFuzz = toFuzz(&topProp, "QualifiedName") | |
libTopPropFuzz = toFuzz(&libTopProp, "QualifiedName") | |
} | |
func toFuzz(elements *[]interface{}, key string) (f *fuzzy.Fuzzy) { | |
f = fuzzy.NewFuzzy() | |
f.Set(elements) | |
f.Options.SetThreshold(5) | |
f.SetKeys([]string{key}) | |
return | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment