diff --git a/djpianalto.com/goff/exts/guild.go b/djpianalto.com/goff/exts/guild.go index cd1556f..960934b 100644 --- a/djpianalto.com/goff/exts/guild.go +++ b/djpianalto.com/goff/exts/guild.go @@ -1,10 +1,11 @@ package exts import ( - "djpianalto.com/goff/djpianalto.com/goff/utils" "fmt" - "github.com/dustinpianalto/disgoman" "strings" + + "djpianalto.com/goff/djpianalto.com/goff/utils" + "github.com/dustinpianalto/disgoman" ) // Guild management commands @@ -203,3 +204,87 @@ func addGuildCommand(ctx disgoman.Context, args []string) { _, _ = ctx.Send("This guild has been added.") } + +func puzzleChannel(ctx disgoman.Context, args []string) { + var idString string + if len(args) > 0 { + idString = args[0] + if strings.HasPrefix(idString, "<#") && strings.HasSuffix(idString, ">") { + idString = idString[2 : len(idString)-1] + } + } else { + idString = "" + } + fmt.Println(idString) + if idString == "" { + _, err := utils.Database.Exec("UPDATE guilds SET puzzle_channel='' WHERE id=$1;", ctx.Guild.ID) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Error Updating Database", + Error: err, + } + return + } + _, _ = ctx.Send("Puzzle Channel Updated.") + return + } + channel, err := ctx.Session.State.Channel(idString) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Can't find that channel.", + Error: err, + } + return + } + if channel.GuildID != ctx.Guild.ID { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "The channel passed is not in this guild.", + Error: err, + } + return + } + _, err = utils.Database.Exec("UPDATE guilds SET puzzle_channel=$1 WHERE id=$2;", idString, ctx.Guild.ID) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Error Updating Database", + Error: err, + } + return + } + _, _ = ctx.Send("Puzzle Channel Updated.") +} + +func getPuzzleChannel(ctx disgoman.Context, _ []string) { + var channelID string + row := utils.Database.QueryRow("SELECT puzzle_channel FROM guilds where id=$1", ctx.Guild.ID) + err := row.Scan(&channelID) + if err != nil { + fmt.Println(err) + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Error getting data from the database.", + Error: err, + } + return + } + if channelID == "" { + _, _ = ctx.Send("The puzzle channel is not set.") + return + } + channel, err := ctx.Session.State.GuildChannel(ctx.Guild.ID, channelID) + if err != nil { + fmt.Println(err) + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "I got the channel ID but it does not appear to be a valid channel in this guild.", + Error: err, + } + return + } + _, _ = ctx.Send(fmt.Sprintf("The puzzle channel is currently %s", channel.Mention())) + return +} diff --git a/djpianalto.com/goff/exts/init.go b/djpianalto.com/goff/exts/init.go index 9d8bacd..9f7471f 100644 --- a/djpianalto.com/goff/exts/init.go +++ b/djpianalto.com/goff/exts/init.go @@ -186,4 +186,22 @@ func AddCommandHandlers(h *disgoman.CommandManager) { RequiredPermissions: 0, Invoke: deinterleave, }) + _ = h.AddCommand(&disgoman.Command{ + Name: "set-puzzle-channel", + Aliases: []string{"spc"}, + Description: "Set the channel puzzle messages will be sent to.", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionManageServer, + Invoke: puzzleChannel, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "get-puzzle-channel", + Aliases: []string{"gpc"}, + Description: "Gets the channel puzzle messages will be sent to.", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionManageServer, + Invoke: getPuzzleChannel, + }) } diff --git a/djpianalto.com/goff/goff.go b/djpianalto.com/goff/goff.go index 2e5af8e..4bf65cc 100644 --- a/djpianalto.com/goff/goff.go +++ b/djpianalto.com/goff/goff.go @@ -90,7 +90,7 @@ func main() { // Start the task handler in a goroutine go utils.ProcessTasks(dg, 1) - go utils.RecieveEmail() + go utils.RecieveEmail(dg) fmt.Println("The Bot is now running.") sc := make(chan os.Signal, 1) diff --git a/djpianalto.com/goff/utils/database.go b/djpianalto.com/goff/utils/database.go index 6ca6296..88d0aed 100644 --- a/djpianalto.com/goff/utils/database.go +++ b/djpianalto.com/goff/utils/database.go @@ -3,6 +3,7 @@ package utils import ( "database/sql" "fmt" + "log" _ "github.com/lib/pq" ) @@ -90,6 +91,28 @@ func InitializeDatabase() { if err != nil { fmt.Println(err) } + _, err = Database.Query(`CREATE TABLE IF NOT EXISTS postfixes( + id serial primary key, + name varchar(100) not null, + time timestamp not null default NOW())`) + if err != nil { + log.Println(err) + } + _, err = Database.Exec(`CREATE TABLE IF NOT EXISTS puzzles( + id serial primary key, + text text not null, + time timestamp not null + )`) + if err != nil { + log.Println(err) + } + _, err = Database.Exec(`CREATE TABLE IF NOT EXISTS x_guilds_puzzles( + id serial primary key, + guild_id varchar(30) not null references guilds(id), + puzzle_id int not null references puzzles(id), + message_id varchar(30) not null + )`) + RunPostfixes() } func LoadTestData() { diff --git a/djpianalto.com/goff/utils/email.go b/djpianalto.com/goff/utils/email.go index 2208c1d..305ff6a 100644 --- a/djpianalto.com/goff/utils/email.go +++ b/djpianalto.com/goff/utils/email.go @@ -2,24 +2,32 @@ package utils import ( "io" - "io/ioutil" "log" "os" + "strings" + "sync" "time" + "github.com/bwmarrin/discordgo" imap "github.com/emersion/go-imap" "github.com/emersion/go-imap/client" "github.com/emersion/go-message/mail" ) +const () + var ( emailUsername = os.Getenv("GOFF_EMAIL_USERNAME") emailPassword = os.Getenv("GOFF_EMAIL_PASSWORD") + puzzleAddress = mail.Address{ + Name: "Daily Coding Problem", + Address: "founders@dailycodingproblem.com", + } ) var EmailClient client.Client -func RecieveEmail() { +func RecieveEmail(dg *discordgo.Session) { for { log.Println("Connecting to Email server.") @@ -33,7 +41,6 @@ func RecieveEmail() { return } log.Println("Connected to Email server.") - defer EmailClient.Logout() mbox, err := EmailClient.Select("INBOX", false) if err != nil { @@ -64,6 +71,8 @@ func RecieveEmail() { } }() + var wg sync.WaitGroup + for msg := range messages { if msg == nil { log.Println("No New Messages") @@ -74,46 +83,50 @@ func RecieveEmail() { log.Println("Server didn't send a message body") continue } - mr, err := mail.CreateReader(r) - if err != nil { - log.Println(err) - continue - } - header := mr.Header - if date, err := header.Date(); err == nil { - log.Println("Date:", date) - } - if from, err := header.AddressList("From"); err == nil { - log.Println("From:", from) - } - if to, err := header.AddressList("To"); err == nil { - log.Println("To:", to) - } - if subject, err := header.Subject(); err == nil { - log.Println("Subject:", subject) - } - for { - p, err := mr.NextPart() - if err == io.EOF { - break - } else if err != nil { - log.Println(err) - break - } - - switch h := p.Header.(type) { - case *mail.InlineHeader: - // This is the message's text (can be plain-text or HTML) - b, _ := ioutil.ReadAll(p.Body) - log.Printf("Got text: %v\n", string(b)) - case *mail.AttachmentHeader: - // This is an attachment - filename, _ := h.Filename() - log.Printf("Got attachment: %v\n", filename) - } - } + wg.Add(1) + go processEmail(r, dg, &wg) } + wg.Wait() } + + EmailClient.Logout() time.Sleep(300 * time.Second) } } + +func processEmail(r io.Reader, dg *discordgo.Session, wg *sync.WaitGroup) { + defer wg.Done() + mr, err := mail.CreateReader(r) + if err != nil { + log.Println(err) + return + } + header := mr.Header + from, err := header.AddressList("From") + if err != nil { + log.Println(err) + return + } + subject, err := header.Subject() + if err != nil { + log.Println(err) + return + } + log.Println(from) + log.Println(subject) + if addressIn(from, puzzleAddress) && + strings.Contains(subject, "Daily Coding Problem:") { + log.Println("Processing Puzzle") + ProcessPuzzleEmail(mr, dg) + } + +} + +func addressIn(s []*mail.Address, a mail.Address) bool { + for _, item := range s { + if item.String() == a.String() { + return true + } + } + return false +} diff --git a/djpianalto.com/goff/utils/postfixes.go b/djpianalto.com/goff/utils/postfixes.go new file mode 100644 index 0000000..12d297a --- /dev/null +++ b/djpianalto.com/goff/utils/postfixes.go @@ -0,0 +1,77 @@ +package utils + +import "log" + +type postfix struct { + Name string + Invoke func(bool) error +} + +var postfixes = []postfix{ + postfix{ + Name: "1_Update_Guild_for_Puzzle", + Invoke: updateGuildForPuzzle, + }, + postfix{ + Name: "1_Update_X_Guild_Prefixes_to_add_ID", + Invoke: updateXGuildPrefixesToAddID, + }, +} + +func RunPostfixes() { + for _, postfix := range postfixes { + queryString := "SELECT * from postfixes where name = $1" + rows, err := Database.Query(queryString, postfix.Name) + if err != nil { + log.Println(err) + continue + } + if rows.Next() { + continue + } else { + err := postfix.Invoke(false) + if err != nil { + continue + } + _, err = Database.Exec("INSERT INTO postfixes (name) VALUES ($1)", postfix.Name) + if err != nil { + log.Println(err) + continue + } + } + } +} + +func updateGuildForPuzzle(revert bool) error { + var queryString string + if !revert { + queryString = `ALTER TABLE guilds + ADD COLUMN puzzle_channel varchar(30) not null default ''` + } else { + queryString = `ALTER TABLE guilds + DROP COLUMN puzzleChat` + } + _, err := Database.Exec(queryString) + if err != nil { + log.Println(err) + return err + } + return nil +} + +func updateXGuildPrefixesToAddID(revert bool) error { + var queryString string + if !revert { + queryString = `ALTER TABLE x_guilds_prefixes + ADD COLUMN id serial primary key` + } else { + queryString = `ALTER TABLE x_guilds_prefixes + DROP COLUMN id` + } + _, err := Database.Exec(queryString) + if err != nil { + log.Println(err) + return err + } + return nil +} diff --git a/djpianalto.com/goff/utils/puzzles.go b/djpianalto.com/goff/utils/puzzles.go new file mode 100644 index 0000000..6edd5ad --- /dev/null +++ b/djpianalto.com/goff/utils/puzzles.go @@ -0,0 +1,93 @@ +package utils + +import ( + "io" + "io/ioutil" + "log" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/emersion/go-message/mail" +) + +func ProcessPuzzleEmail(mr *mail.Reader, dg *discordgo.Session) { + var body []byte + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } else if err != nil { + log.Println(err) + break + } + + switch h := p.Header.(type) { + case *mail.InlineHeader: + // This is the message's text (can be plain-text or HTML) + if t, _, _ := h.ContentType(); t == "text/plain" { + body, _ = ioutil.ReadAll(p.Body) + break + } + } + } + if len(body) > 0 { + s := string(body) + puzzle := strings.Split(s, "----------")[0] + date, err := mr.Header.Date() + if err != nil { + log.Println(err) + return + } + e := discordgo.MessageEmbed{ + Title: "Daily Coding Problem", + URL: "https://dailycodingproblem.com/", + Description: "```" + puzzle + "```", + Timestamp: date.Format(time.RFC3339), + Footer: &discordgo.MessageEmbedFooter{ + Text: "Daily Coding Problem", + }, + } + var guilds []Guild + queryString := `SELECT id, puzzle_channel from guilds` + rows, err := Database.Query(queryString) + if err != nil { + log.Println(err) + } + for rows.Next() { + var guild Guild + err := rows.Scan(&guild.ID, &guild.PuzzleChannel) + if err != nil { + log.Println(err) + continue + } + guilds = append(guilds, guild) + } + var puzzleID int64 + queryString = "INSERT INTO puzzles (text, time) VALUES ($1, $2) RETURNING id" + err = Database.QueryRow(queryString, puzzle, date).Scan(&puzzleID) + if err != nil { + log.Println(err) + return + } + for _, g := range guilds { + if g.PuzzleChannel == "" { + continue + } + msg := discordgo.MessageSend{ + Embed: &e, + } + m, err := dg.ChannelMessageSendComplex(g.PuzzleChannel, &msg) + if err != nil { + log.Println(err) + } + queryString = "INSERT INTO x_guilds_puzzles (guild_id, puzzle_id, message_id) VALUES ($1, $2, $3)" + _, err = Database.Exec(queryString, g.ID, puzzleID, m.ID) + if err != nil { + log.Println(err) + continue + } + } + } + +} diff --git a/djpianalto.com/goff/utils/types.go b/djpianalto.com/goff/utils/types.go new file mode 100644 index 0000000..0fd92b9 --- /dev/null +++ b/djpianalto.com/goff/utils/types.go @@ -0,0 +1,10 @@ +package utils + +type Guild struct { + ID string + WelcomeMessage string + GoodbyeMessage string + LoggingChannel string + WelcomeChannel string + PuzzleChannel string +} diff --git a/go.mod b/go.mod index 05b3072..22156f7 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.14 require ( github.com/bwmarrin/discordgo v0.20.3-0.20200525154655-ca64123b05de github.com/dustinpianalto/disgoman v0.0.10 + github.com/emersion/go-imap v1.0.5 + github.com/emersion/go-message v0.12.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/lib/pq v1.3.0 github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254 diff --git a/go.sum b/go.sum index 8c9466e..1b163dc 100644 --- a/go.sum +++ b/go.sum @@ -9,12 +9,23 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustinpianalto/disgoman v0.0.10 h1:UzmvMpOi4peF59tXGaNfVU+ePHs1hILa6gQbjxjWQ9g= github.com/dustinpianalto/disgoman v0.0.10/go.mod h1:v3FM6n+4dH9XlvO+IDx6MN3DUnGq6YVDBvy1A1k202g= +github.com/emersion/go-imap v1.0.5 h1:8xg/d2wo2BBP3AEP5AOaM/6i8887RGyVW2st/IVHWUw= +github.com/emersion/go-imap v1.0.5/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= +github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= +github.com/emersion/go-message v0.12.0 h1:mZnv35eZ6lB6EftTQBgYXspOH0FQdhpFhSUhA9i6/Zg= +github.com/emersion/go-message v0.12.0/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= +github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs= +github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= +github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg= +github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A= +github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254 h1:JYoQR67E1vv1WGoeW8DkdFs7vrIEe/5wP+qJItd5tUE= github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -26,3 +37,6 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=