Skip to content

Instantly share code, notes, and snippets.

@miyoyo
Last active July 8, 2020 18:03
Show Gist options
  • Save miyoyo/8641057636892863791ca7c41a1fab97 to your computer and use it in GitHub Desktop.
Save miyoyo/8641057636892863791ca7c41a1fab97 to your computer and use it in GitHub Desktop.
Flutter doc explorer bot (CC-By) v3
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 = ""
}
}
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
)
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",
},
})
}
}
}
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"`
}
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()
}
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