From 28d78bdb8c9d9e46ef8a263448a86c28e429c036 Mon Sep 17 00:00:00 2001 From: Dustin Pianalto Date: Sun, 30 Aug 2020 00:08:40 -0800 Subject: [PATCH] Reorganize and update Dockerfile --- Dockerfile | 19 ++- events/member_events.go | 135 ++++++++++++++++++ events/message_events.go | 97 +++++++++++++ exts/P_interpreter.go | 84 ++++++++++++ exts/fun.go | 84 ++++++++++++ exts/guild.go | 290 +++++++++++++++++++++++++++++++++++++++ exts/init.go | 234 +++++++++++++++++++++++++++++++ exts/tags.go | 137 ++++++++++++++++++ exts/tasks.go | 56 ++++++++ exts/user_management.go | 256 ++++++++++++++++++++++++++++++++++ exts/utils.go | 169 +++++++++++++++++++++++ go.mod | 4 +- goff.go | 136 ++++++++++++++++++ utils/database.go | 154 +++++++++++++++++++++ utils/date_strings.go | 64 +++++++++ utils/email.go | 132 ++++++++++++++++++ utils/logging.go | 34 +++++ utils/postfixes.go | 77 +++++++++++ utils/puzzles.go | 93 +++++++++++++ utils/rpn.go | 156 +++++++++++++++++++++ utils/rpnParser.go | 95 +++++++++++++ utils/snowflake.go | 32 +++++ utils/tasks.go | 132 ++++++++++++++++++ utils/types.go | 10 ++ 24 files changed, 2672 insertions(+), 8 deletions(-) create mode 100644 events/member_events.go create mode 100644 events/message_events.go create mode 100644 exts/P_interpreter.go create mode 100644 exts/fun.go create mode 100644 exts/guild.go create mode 100644 exts/init.go create mode 100644 exts/tags.go create mode 100644 exts/tasks.go create mode 100644 exts/user_management.go create mode 100644 exts/utils.go create mode 100644 goff.go create mode 100644 utils/database.go create mode 100644 utils/date_strings.go create mode 100644 utils/email.go create mode 100644 utils/logging.go create mode 100644 utils/postfixes.go create mode 100644 utils/puzzles.go create mode 100644 utils/rpn.go create mode 100644 utils/rpnParser.go create mode 100644 utils/snowflake.go create mode 100644 utils/tasks.go create mode 100644 utils/types.go diff --git a/Dockerfile b/Dockerfile index ac585ff..e5c5618 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,20 @@ -FROM golang:1.14-alpine +FROM golang:1.14-alpine as dev WORKDIR /go/src/Goff COPY ./go.mod . +COPY ./go.sum . -RUN apk add --no-cache git - -RUN go get -d -v ./... +RUN go mod download COPY . . -RUN go install -v ./... +RUN go install github.com/dustinpianalto/goff + +CMD [ "go", "run", "goff.go"] + +from alpine + +WORKDIR /bin + +COPY --from=dev /go/bin/goff ./goff -ENTRYPOINT /go/bin/goff +CMD [ "goff" ] diff --git a/events/member_events.go b/events/member_events.go new file mode 100644 index 0000000..ebc0755 --- /dev/null +++ b/events/member_events.go @@ -0,0 +1,135 @@ +package events + +import ( + "fmt" + "log" + "strconv" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/dustinpianalto/goff/utils" +) + +func OnGuildMemberAddLogging(s *discordgo.Session, member *discordgo.GuildMemberAdd) { + defer func() { + if r := recover(); r != nil { + log.Println("Recovered from panic in OnGuildMemberAddLogging", r) + } + }() + var channelID string + row := utils.Database.QueryRow("SELECT logging_channel FROM guilds where id=$1", member.GuildID) + err := row.Scan(&channelID) + if err != nil || channelID == "" { + return + } + guild, err := s.State.Guild(member.GuildID) + if err != nil { + log.Println(err) + return + } + + var title string + if member.User.Bot { + title = "Bot Joined" + } else { + title = "Member Joined" + } + + thumb := &discordgo.MessageEmbedThumbnail{ + URL: member.User.AvatarURL(""), + } + + int64ID, _ := strconv.ParseInt(member.User.ID, 10, 64) + snow := utils.ParseSnowflake(int64ID) + + field := &discordgo.MessageEmbedField{ + Name: "User was created:", + Value: utils.ParseDateString(snow.CreationTime), + Inline: false, + } + + joinTime, _ := member.JoinedAt.Parse() + + embed := &discordgo.MessageEmbed{ + Title: title, + Description: fmt.Sprintf("%v (%v) Has Joined the Server", member.User.Mention(), member.User.ID), + Color: 0x0cc56a, + Thumbnail: thumb, + Footer: &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Current Member Count: %v", guild.MemberCount), + IconURL: guild.IconURL(), + }, + Timestamp: joinTime.Format(time.RFC3339), + Fields: []*discordgo.MessageEmbedField{field}, + } + s.ChannelMessageSendEmbed(channelID, embed) +} + +func OnGuildMemberRemoveLogging(s *discordgo.Session, member *discordgo.GuildMemberRemove) { + defer func() { + if r := recover(); r != nil { + log.Println("Recovered from panic in OnGuildMemberAddLogging", r) + } + }() + timeNow := time.Now() + var channelID string + row := utils.Database.QueryRow("SELECT logging_channel FROM guilds where id=$1", member.GuildID) + err := row.Scan(&channelID) + if err != nil || channelID == "" { + return + } + guild, err := s.State.Guild(member.GuildID) + if err != nil { + log.Println(err) + return + } + + var title string + if member.User.Bot { + title = "Bot Left" + } else { + title = "Member Left" + } + + thumb := &discordgo.MessageEmbedThumbnail{ + URL: member.User.AvatarURL(""), + } + + desc := "" + al, err := s.GuildAuditLog(member.GuildID, "", "", 20, 1) + if err != nil { + log.Println(err) + } else { + for _, log := range al.AuditLogEntries { + if log.TargetID == member.User.ID { + int64ID, _ := strconv.ParseInt(log.ID, 10, 64) + logSnow := utils.ParseSnowflake(int64ID) + if timeNow.Sub(logSnow.CreationTime).Seconds() <= 10 { + user, err := s.User(log.UserID) + if err == nil { + desc = fmt.Sprintf("%v (%v) was Kicked by: %v\nReason: %v", member.User.String(), member.User.ID, user.String(), log.Reason) + } else { + desc = fmt.Sprintf("%v (%v) was Kicked by: %v\nReason: %v", member.User.String(), member.User.ID, log.UserID, log.Reason) + } + break + } + } + } + } + if desc == "" { + desc = fmt.Sprintf("%v (%v) Has Left the Server", member.User.String(), member.User.ID) + } + + embed := &discordgo.MessageEmbed{ + Title: title, + Description: desc, + Color: 0xff9431, + Thumbnail: thumb, + Footer: &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Current Member Count: %v", guild.MemberCount), + IconURL: guild.IconURL(), + }, + Timestamp: timeNow.Format(time.RFC3339), + } + s.ChannelMessageSendEmbed(channelID, embed) +} diff --git a/events/message_events.go b/events/message_events.go new file mode 100644 index 0000000..5167a82 --- /dev/null +++ b/events/message_events.go @@ -0,0 +1,97 @@ +package events + +import ( + "fmt" + "log" + + "github.com/bwmarrin/discordgo" + "github.com/dustinpianalto/goff/utils" +) + +func OnMessageUpdate(session *discordgo.Session, m *discordgo.MessageUpdate) { + defer func() { + if r := recover(); r != nil { + log.Println("Recovered from panic in OnMessageUpdate", r) + } + }() + msg := m.BeforeUpdate + if msg.Author.Bot { + return + } + var channelID string + row := utils.Database.QueryRow("SELECT logging_channel FROM guilds where id=$1", msg.GuildID) + err := row.Scan(&channelID) + if err != nil || channelID == "" { + return + } + channel, err := session.State.Channel(msg.ChannelID) + if err != nil { + log.Println(err) + return + } + embed := &discordgo.MessageEmbed{ + Title: fmt.Sprintf("Message Edited: %v", msg.ID), + Description: fmt.Sprintf("**Before:** %v\n**After:** %v\nIn Channel: %v", msg.Content, m.Content, channel.Mention()), + Color: session.State.UserColor(msg.Author.ID, channelID), + Footer: &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Author: %v", msg.Author.String()), + IconURL: msg.Author.AvatarURL(""), + }, + } + session.ChannelMessageSendEmbed(channelID, embed) +} + +func OnMessageDelete(session *discordgo.Session, m *discordgo.MessageDelete) { + defer func() { + if r := recover(); r != nil { + log.Println("Recovered from panic in OnMessageDelete", r) + } + }() + msg := m.BeforeDelete + if msg == nil { + log.Printf("Message Deleted but the original message was not in my cache so we are ignoring it.\nMessage ID: %v\nGuild ID: %v\nChannel ID: %v\n", m.ID, m.GuildID, m.ChannelID) + return + } + if msg.Author.Bot { + return + } + var channelID string + row := utils.Database.QueryRow("SELECT logging_channel FROM guilds where id=$1", msg.GuildID) + err := row.Scan(&channelID) + if err != nil || channelID == "" { + return + } + channel, err := session.State.Channel(msg.ChannelID) + if err != nil { + log.Println(err) + return + } + desc := "" + al, err := session.GuildAuditLog(msg.GuildID, "", "", 72, 1) + if err != nil { + log.Println(err) + } else { + for _, log := range al.AuditLogEntries { + if log.TargetID == msg.Author.ID && log.Options.ChannelID == msg.ChannelID { + user, err := session.User(log.UserID) + if err == nil { + desc = fmt.Sprintf("**Content:** %v\nIn Channel: %v\nDeleted By: %v", msg.Content, channel.Mention(), user.Mention()) + } + break + } + } + } + if desc == "" { + desc = fmt.Sprintf("**Content:** %v\nIn Channel: %v", msg.Content, channel.Mention()) + } + embed := &discordgo.MessageEmbed{ + Title: fmt.Sprintf("Message Deleted: %v", msg.ID), + Description: desc, + Color: session.State.UserColor(msg.Author.ID, channelID), + Footer: &discordgo.MessageEmbedFooter{ + Text: fmt.Sprintf("Author: %v", msg.Author.String()), + IconURL: msg.Author.AvatarURL(""), + }, + } + session.ChannelMessageSendEmbed(channelID, embed) +} diff --git a/exts/P_interpreter.go b/exts/P_interpreter.go new file mode 100644 index 0000000..57f4b77 --- /dev/null +++ b/exts/P_interpreter.go @@ -0,0 +1,84 @@ +package exts + +import ( + "errors" + "fmt" + "github.com/dustinpianalto/disgoman" + "strings" +) + +func pCommand(ctx disgoman.Context, args []string) { + input := strings.Join(args, "") + const LENGTH = 1999 + var mem [LENGTH]byte + pointer := 0 + l := 0 + for i := 0; i < len(input); i++ { + if input[i] == 'L' { + if pointer == 0 { + pointer = LENGTH - 1 + } else { + pointer-- + } + } else if input[i] == 'R' { + if pointer == LENGTH-1 { + pointer = 0 + } else { + pointer++ + } + } else if input[i] == '+' { + mem[pointer]++ + } else if input[i] == '-' { + mem[pointer]-- + } else if input[i] == '(' { + if mem[pointer] == 0 { + i++ + for l > 0 || input[i] != ')' { + if input[i] == '(' { + l++ + } + if input[i] == ')' { + l-- + } + i++ + } + } + } else if input[i] == ')' { + if mem[pointer] != 0 { + i-- + for l > 0 || input[i] != '(' { + if input[i] == ')' { + l++ + } + if input[i] == '(' { + l-- + } + i-- + } + } + } else { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: fmt.Sprintf("Invalid Character: %v", input[i]), + Error: errors.New("invalid character"), + } + return + } + } + var out []byte + for _, i := range mem { + if i != 0 { + out = append(out, i) + } + } + _, err := ctx.Send(string(out)) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't send results", + Error: err, + } + return + } + return +} diff --git a/exts/fun.go b/exts/fun.go new file mode 100644 index 0000000..5084128 --- /dev/null +++ b/exts/fun.go @@ -0,0 +1,84 @@ +package exts + +import ( + "fmt" + "strconv" + "strings" + + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/rpnparse" +) + +func interleave(ctx disgoman.Context, args []string) { + if len(args) == 2 { + x, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return + } + y, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return + } + var z = int64(0) + for i := 0; i < 64; i++ { + x_masked_i := x & (1 << i) + y_masked_i := y & (1 << i) + + z |= x_masked_i << i + z |= y_masked_i << (i + 1) + } + ctx.Send(fmt.Sprintf("%v", z)) + } +} + +func deinterleave(ctx disgoman.Context, args []string) { + if len(args) == 1 { + z, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return + } + var x = int64(0) + var y = int64(0) + i := 0 + for z > 0 { + x |= (z & 1) << i + z >>= 1 + y |= (z & 1) << i + z >>= 1 + i++ + } + ctx.Send(fmt.Sprintf("(%v, %v)", x, y)) + } +} + +func generateRPNCommand(ctx disgoman.Context, args []string) { + rpn, err := rpnparse.GenerateRPN(args) + if err != nil { + ctx.Send(err.Error()) + return + } + ctx.Send(rpn) +} + +func parseRPNCommand(ctx disgoman.Context, args []string) { + res, err := rpnparse.ParseRPN(args) + if err != nil { + ctx.Send(err.Error()) + return + } + ctx.Send(fmt.Sprintf("The result is: %v", res)) +} + +func solveCommand(ctx disgoman.Context, args []string) { + rpn, err := rpnparse.GenerateRPN(args) + if err != nil { + ctx.Send(err.Error()) + return + } + res, err := rpnparse.ParseRPN(strings.Split(rpn, " ")) + if err != nil { + ctx.Send(err.Error()) + return + } + ctx.Send(fmt.Sprintf("The result is: %v", res)) +} diff --git a/exts/guild.go b/exts/guild.go new file mode 100644 index 0000000..b5da4bc --- /dev/null +++ b/exts/guild.go @@ -0,0 +1,290 @@ +package exts + +import ( + "fmt" + "strings" + + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/goff/utils" +) + +// Guild management commands + +func loggingChannel(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 logging_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("Logging 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 logging_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("Logging Channel Updated.") +} + +func getLoggingChannel(ctx disgoman.Context, _ []string) { + var channelID string + row := utils.Database.QueryRow("SELECT logging_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 logging 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 logging channel is currently %s", channel.Mention())) + return +} + +func welcomeChannel(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 welcome_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("Welcomer Disabled.") + 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 welcome_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("Welcome Channel Updated.") + return +} + +func getWelcomeChannel(ctx disgoman.Context, _ []string) { + var channelID string + row := utils.Database.QueryRow("SELECT welcome_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 welcomer is disabled.") + 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 welcome channel is currently %s", channel.Mention())) +} + +func addGuildCommand(ctx disgoman.Context, args []string) { + var guildID string + row := utils.Database.QueryRow("SELECT id FROM guilds where id=$1", ctx.Guild.ID) + err := row.Scan(&guildID) + if err == nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "This guild is already in my database", + Error: err, + } + return + } + + _, err = utils.Database.Query("INSERT INTO guilds (id) VALUES ($1)", ctx.Guild.ID) + if err != nil { + fmt.Println(err) + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "There was a problem inserting this guild into the database", + Error: err, + } + return + } + _, _ = 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/exts/init.go b/exts/init.go new file mode 100644 index 0000000..94d70ee --- /dev/null +++ b/exts/init.go @@ -0,0 +1,234 @@ +package exts + +import ( + "github.com/dustinpianalto/disgoman" +) + +func AddCommandHandlers(h *disgoman.CommandManager) { + // Arguments: + // name - command name - string + // desc - command description - string + // owneronly - only allow owners to run - bool + // hidden - hide command from non-owners - bool + // perms - permissisions required - anpan.Permission (int) + // type - command type, sets where the command is available + // run - function to run - func(anpan.Context, []string) / CommandRunFunc + _ = h.AddCommand(&disgoman.Command{ + Name: "ping", + Aliases: nil, + Description: "Check the bot's ping", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: pingCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "say", + Aliases: nil, + Description: "Repeat a message", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + SanitizeEveryone: true, + Invoke: sayCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "user", + Aliases: nil, + Description: "Get user info", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: userCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "git", + Aliases: nil, + Description: "Show my github link", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: gitCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "tag", + Aliases: nil, + Description: "Get a tag", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: tagCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "addtag", + Aliases: nil, + Description: "Add a tag", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + SanitizeEveryone: true, + Invoke: addTagCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "invite", + Aliases: nil, + Description: "Get the invite link for this bot or others", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: inviteCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "P", + Aliases: nil, + Description: "Interpret a P\" program and return the results", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: pCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "set-logging-channel", + Aliases: []string{"slc"}, + Description: "Set the channel logging messages will be sent to.", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionManageServer, + Invoke: loggingChannel, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "get-logging-channel", + Aliases: []string{"glc"}, + Description: "Gets the channel logging messages will be sent to.", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionManageServer, + Invoke: getLoggingChannel, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "set-welcome-channel", + Aliases: []string{"swc"}, + Description: "Set the channel welcome messages will be sent to.", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionManageServer, + Invoke: welcomeChannel, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "get-welcome-channel", + Aliases: []string{"gwc"}, + Description: "Gets the channel welcome messages will be sent to.", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionManageServer, + Invoke: getWelcomeChannel, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "kick", + Aliases: nil, + Description: "Kicks the given user with the given reason", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionKickMembers, + Invoke: kickUserCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "addGuild", + Aliases: nil, + Description: "Adds the current guild to the database", + OwnerOnly: true, + Hidden: false, + RequiredPermissions: disgoman.PermissionManageServer, + Invoke: addGuildCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "ban", + Aliases: []string{"ban-no-delete"}, + Description: "Bans the given user with the given reason", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionBanMembers, + Invoke: banUserCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "unban", + Aliases: nil, + Description: "Unbans the given user", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: disgoman.PermissionBanMembers, + Invoke: unbanUserCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "remind", + Aliases: nil, + Description: "Remind me at a later time", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: addReminderCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "encode", + Aliases: []string{"e"}, + Description: "Encode 2 numbers", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: interleave, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "decode", + Aliases: []string{"d"}, + Description: "Decode 1 number into 2", + OwnerOnly: false, + Hidden: false, + 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, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "RPN", + Aliases: []string{"rpn"}, + Description: "Convert infix to rpn", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: generateRPNCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "ParseRPN", + Aliases: []string{"PRPN", "prpn"}, + Description: "Parse RPN string and return the result", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: parseRPNCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "solve", + Aliases: []string{"math", "infix"}, + Description: "Solve infix equation and return the result", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: solveCommand, + }) +} diff --git a/exts/tags.go b/exts/tags.go new file mode 100644 index 0000000..6839a7e --- /dev/null +++ b/exts/tags.go @@ -0,0 +1,137 @@ +package exts + +import ( + "errors" + "fmt" + "log" + "strings" + + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/goff/utils" + "github.com/kballard/go-shellquote" +) + +func addTagCommand(ctx disgoman.Context, input []string) { + if len(input) >= 1 { + args, err := shellquote.Split(strings.Join(input, " ")) + if err != nil { + if strings.Contains(err.Error(), "Unterminated") { + args = strings.SplitN(strings.Join(args, " "), " ", 2) + } else { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "", + Error: err, + } + return + } + } + queryString := `SELECT tags.id, tags.tag, tags.content from tags + WHERE tags.guild_id = $1 + AND tags.tag = $2;` + row := utils.Database.QueryRow(queryString, ctx.Guild.ID, args[0]) + var dest string + if err := row.Scan(&dest); err != nil { + tag := args[0] + if tag == "" { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "That is not a valid tag name", + Error: err, + } + return + } + if len(args) <= 1 { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "I got a name but no value", + Error: err, + } + return + } + value := args[1] + if value == "" { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "You have to include a content for the tag", + Error: err, + } + return + } + queryString = `INSERT INTO tags (tag, content, creator, guild_id) VALUES ($1, $2, $3, $4);` + _, err := utils.Database.Exec(queryString, tag, value, ctx.Message.Author.ID, ctx.Guild.ID) + if err != nil { + ctx.Send(err.Error()) + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "", + Error: err, + } + return + } + ctx.Send(fmt.Sprintf("Tag %v added successfully.", tag)) + return + } else { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "That tag already exists", + Error: err, + } + return + } + } else { + ctx.Send("You need to tell me what tag you want to add...") + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "You need to tell me what tag you want to add...", + Error: errors.New("nothing to do"), + } + return + } +} + +func tagCommand(ctx disgoman.Context, args []string) { + if len(args) >= 1 { + tagString := strings.Join(args, " ") + queryString := `SELECT tags.id, tags.tag, tags.content from tags + WHERE tags.guild_id = $1;` + rows, err := utils.Database.Query(queryString, ctx.Guild.ID) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "", + Error: err, + } + return + } else { + for rows.Next() { + var ( + id int + tag string + content string + ) + if err := rows.Scan(&id, &tag, &content); err != nil { + log.Fatal(err) + } + if tagString == tag { + ctx.Send(content) + return + } + } + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: fmt.Sprintf("Tag %v not found", args[0]), + Error: err, + } + return + } + } else { + ctx.Send("I need a tag to fetch...") + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "I need a tag to fetch...", + Error: errors.New("nothing to do"), + } + return + } +} diff --git a/exts/tasks.go b/exts/tasks.go new file mode 100644 index 0000000..fa54327 --- /dev/null +++ b/exts/tasks.go @@ -0,0 +1,56 @@ +package exts + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/goff/utils" + "github.com/olebedev/when" + "github.com/olebedev/when/rules/common" + "github.com/olebedev/when/rules/en" +) + +func addReminderCommand(ctx disgoman.Context, args []string) { + w := when.New(nil) + w.Add(en.All...) + w.Add(common.All...) + + text := strings.Join(args, " ") + r, err := w.Parse(text, time.Now()) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Error parsing time", + Error: err, + } + return + } + if r == nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "You need to include a valid time", + Error: errors.New("no time found"), + } + return + } + content := strings.Replace(text, r.Text+" ", "", 1) + query := "INSERT INTO tasks (type, content, guild_id, channel_id, user_id, trigger_time) " + + "VALUES ('Reminder', $1, $2, $3, $4, $5)" + _, err = utils.Database.Exec(query, content, ctx.Guild.ID, ctx.Channel.ID, ctx.User.ID, r.Time) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Error adding task to database", + Error: err, + } + return + } + _ = ctx.Session.MessageReactionAdd(ctx.Channel.ID, ctx.Message.ID, "✅") + _, _ = ctx.Session.ChannelMessageSend( + ctx.Channel.ID, + fmt.Sprintf("I will remind you at %v, with `%v`", r.Time.Format(time.RFC1123), content), + ) +} diff --git a/exts/user_management.go b/exts/user_management.go new file mode 100644 index 0000000..08c5bc3 --- /dev/null +++ b/exts/user_management.go @@ -0,0 +1,256 @@ +package exts + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/goff/utils" +) + +func kickUserCommand(ctx disgoman.Context, args []string) { + var member *discordgo.Member + var err error + if len(ctx.Message.Mentions) > 0 { + member, err = ctx.Session.GuildMember(ctx.Guild.ID, ctx.Message.Mentions[0].ID) + } else if len(args) >= 1 { + idString := args[0] + if strings.HasPrefix(idString, "<@!") && strings.HasSuffix(idString, ">") { + idString = idString[3 : len(idString)-1] + } + member, err = ctx.Session.GuildMember(ctx.Guild.ID, idString) + } else { + err = errors.New("that is not a valid id") + } + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't get that member", + Error: err, + } + return + } + + if higher, _ := disgoman.HasHigherRole(ctx.Session, ctx.Guild.ID, ctx.Message.Author.ID, member.User.ID); !higher { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "You must have a higher role than the person you are trying to kick", + Error: errors.New("need higher role"), + } + return + } + + if higher, _ := disgoman.HasHigherRole(ctx.Session, ctx.Guild.ID, ctx.Session.State.User.ID, member.User.ID); !higher { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "I don't have a high enough role to kick that person", + Error: errors.New("need higher role"), + } + return + } + + var reason string + if len(args) > 1 { + reason = strings.Join(args[1:], " ") + } else { + reason = "No Reason Given" + } + auditReason := fmt.Sprintf("%v#%v: %v", ctx.User.Username, ctx.User.Discriminator, reason) + err = ctx.Session.GuildMemberDeleteWithReason(ctx.Guild.ID, member.User.ID, auditReason) + + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: fmt.Sprintf("Something went wrong kicking %v", member.User.Username), + Error: err, + } + return + } + + event := &utils.LogEvent{ + Embed: discordgo.MessageEmbed{ + Title: "User Kicked", + Description: fmt.Sprintf( + "User %v#%v was kicked by %v.\nReason: %v", + member.User.Username, + member.User.Discriminator, + ctx.Message.Author.Username, + reason), + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + Color: 0xff8c00, + }, + GuildID: ctx.Guild.ID, + Session: ctx.Session, + } + utils.LoggingChannel <- event + _, _ = ctx.Send(fmt.Sprintf("User %v#%v has been kicked.", member.User.Username, member.User.Discriminator)) +} + +func banUserCommand(ctx disgoman.Context, args []string) { + var user *discordgo.User + var err error + if len(ctx.Message.Mentions) > 0 { + user, err = ctx.Session.User(ctx.Message.Mentions[0].ID) + } else if len(args) >= 1 { + idString := args[0] + if strings.HasPrefix(idString, "<@!") && strings.HasSuffix(idString, ">") { + idString = idString[3 : len(idString)-1] + } + user, err = ctx.Session.User(idString) + } else { + err = errors.New("that is not a valid id") + } + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't get that user", + Error: err, + } + return + } + + if higher, err := disgoman.HasHigherRole(ctx.Session, ctx.Guild.ID, ctx.Message.Author.ID, user.ID); err != nil { + if err.Error() == "can't find caller member" { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Who are you?", + Error: err, + } + return + } + } else if !higher { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "You must have a higher role than the person you are trying to ban", + Error: errors.New("need higher role"), + } + return + } + + if higher, err := disgoman.HasHigherRole(ctx.Session, ctx.Guild.ID, ctx.Session.State.User.ID, user.ID); err != nil { + if err.Error() == "can't find caller member" { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Who am I?", + Error: err, + } + return + } + } else if !higher { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "I don't have a high enough role to ban that person", + Error: errors.New("need higher role"), + } + return + } + + var reason string + if len(args) > 1 { + reason = strings.Join(args[1:], " ") + } else { + reason = "No Reason Given" + } + auditReason := fmt.Sprintf("%v#%v: %v", ctx.User.Username, ctx.User.Discriminator, reason) + days := 7 + if ctx.Invoked == "ban-no-delete" { + days = 0 + } + err = ctx.Session.GuildBanCreateWithReason(ctx.Guild.ID, user.ID, auditReason, days) + + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: fmt.Sprintf("Something went wrong banning %v", user.Username), + Error: err, + } + return + } + + event := &utils.LogEvent{ + Embed: discordgo.MessageEmbed{ + Title: "User Banned", + Description: fmt.Sprintf( + "User %v#%v was banned by %v.\nReason: %v", + user.Username, + user.Discriminator, + ctx.Message.Author.Username, + reason), + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + Color: 0xff0000, + }, + GuildID: ctx.Guild.ID, + Session: ctx.Session, + } + utils.LoggingChannel <- event + _, _ = ctx.Send(fmt.Sprintf("User %v#%v has been banned.", user.Username, user.Discriminator)) +} + +func unbanUserCommand(ctx disgoman.Context, args []string) { + var user *discordgo.User + var err error + if len(ctx.Message.Mentions) > 0 { + user, err = ctx.Session.User(ctx.Message.Mentions[0].ID) + } else if len(args) >= 1 { + idString := args[0] + if strings.HasPrefix(idString, "<@!") && strings.HasSuffix(idString, ">") { + idString = idString[3 : len(idString)-1] + } + user, err = ctx.Session.User(idString) + } else { + err = errors.New("that is not a valid id") + } + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't get that user", + Error: err, + } + return + } + + bans, err := ctx.Session.GuildBans(ctx.Guild.ID) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Error processing current bans", + Error: err, + } + return + } + for _, ban := range bans { + if ban.User.ID == user.ID { + err = ctx.Session.GuildBanDelete(ctx.Guild.ID, user.ID) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: fmt.Sprintf("Something went wrong unbanning %v", user.Username), + Error: err, + } + return + } + event := &utils.LogEvent{ + Embed: discordgo.MessageEmbed{ + Title: "User Banned", + Description: fmt.Sprintf( + "User %v#%v was unbanned by %v.\nOrignal Ban Reason: %v", + user.Username, + user.Discriminator, + ctx.Message.Author.Username, + ban.Reason), + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + Color: 0x00ff00, + }, + GuildID: ctx.Guild.ID, + Session: ctx.Session, + } + utils.LoggingChannel <- event + _, _ = ctx.Send(fmt.Sprintf("User %v#%v has been unbanned.", user.Username, user.Discriminator)) + return + } + } + _, _ = ctx.Send(fmt.Sprintf("%v#%v is not banned in this guild.", user.Username, user.Discriminator)) +} diff --git a/exts/utils.go b/exts/utils.go new file mode 100644 index 0000000..4a0e3f6 --- /dev/null +++ b/exts/utils.go @@ -0,0 +1,169 @@ +package exts + +import ( + "fmt" + "sort" + "strconv" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/goff/utils" +) + +func pingCommand(ctx disgoman.Context, _ []string) { + timeBefore := time.Now() + msg, _ := ctx.Send("Pong!") + took := time.Now().Sub(timeBefore) + _, err := ctx.Session.ChannelMessageEdit(ctx.Message.ChannelID, msg.ID, fmt.Sprintf("Pong!\nPing Took **%s**", took.String())) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Ping Failed", + Error: err, + } + } +} + +func inviteCommand(ctx disgoman.Context, args []string) { + var ids []string + if len(args) == 0 { + ids = []string{ctx.Session.State.User.ID} + } else { + for _, id := range args { + ids = append(ids, id) + } + } + for _, id := range ids { + url := fmt.Sprintf("", id) + _, err := ctx.Send(url) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't send the invite link.", + Error: err, + } + } + } +} + +func gitCommand(ctx disgoman.Context, _ []string) { + embed := &discordgo.MessageEmbed{ + Title: "Hi there, My code is on Github", + Color: 0, + URL: "https://github.com/dustinpianalto/Goff", + } + _, err := ctx.Session.ChannelMessageSendEmbed(ctx.Channel.ID, embed) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Git failed", + Error: err, + } + } +} + +func sayCommand(ctx disgoman.Context, args []string) { + resp := strings.Join(args, " ") + resp = strings.ReplaceAll(resp, "@everyone", "@\ufff0everyone") + resp = strings.ReplaceAll(resp, "@here", "@\ufff0here") + _, err := ctx.Session.ChannelMessageSend(ctx.Message.ChannelID, resp) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Say Failed", + Error: err, + } + } +} + +func userCommand(ctx disgoman.Context, args []string) { + var member *discordgo.Member + if len(args) == 0 { + member, _ = ctx.Session.GuildMember(ctx.Guild.ID, ctx.Message.Author.ID) + } else { + var err error + if len(ctx.Message.Mentions) > 0 { + member, err = ctx.Session.GuildMember(ctx.Guild.ID, ctx.Message.Mentions[0].ID) + } else { + member, err = ctx.Session.GuildMember(ctx.Guild.ID, args[0]) + } + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't get that member", + Error: err, + } + return + } + } + thumb := &discordgo.MessageEmbedThumbnail{ + URL: member.User.AvatarURL(""), + } + + var botString string + if member.User.Bot { + botString = "BOT" + } else { + botString = "" + } + + var roles []*discordgo.Role + for _, roleID := range member.Roles { + role, _ := ctx.Session.State.Role(ctx.Guild.ID, roleID) + roles = append(roles, role) + } + sort.Slice(roles, func(i, j int) bool { return roles[i].Position > roles[j].Position }) + var roleMentions []string + for _, role := range roles { + roleMentions = append(roleMentions, role.Mention()) + } + var rolesString string + if len(roleMentions) > 0 { + rolesString = strings.Join(roleMentions, " ") + } else { + rolesString = "None" + } + + rolesField := &discordgo.MessageEmbedField{ + Name: "Roles:", + Value: rolesString, + Inline: false, + } + + guildJoinTime, _ := member.JoinedAt.Parse() + guildJoinedField := &discordgo.MessageEmbedField{ + Name: "Joined Guild:", + Value: utils.ParseDateString(guildJoinTime), + Inline: false, + } + + int64ID, _ := strconv.ParseInt(member.User.ID, 10, 64) + s := utils.ParseSnowflake(int64ID) + discordJoinedField := &discordgo.MessageEmbedField{ + Name: "Joined Discord:", + Value: utils.ParseDateString(s.CreationTime), + Inline: false, + } + + embed := &discordgo.MessageEmbed{ + Title: fmt.Sprintf("%v#%v %v", member.User.Username, member.User.Discriminator, botString), + Description: fmt.Sprintf("**%v** (%v)", member.Nick, member.User.ID), + Color: ctx.Session.State.UserColor(member.User.ID, ctx.Channel.ID), + Thumbnail: thumb, + Fields: []*discordgo.MessageEmbedField{ + guildJoinedField, + discordJoinedField, + rolesField, + }, + } + _, err := ctx.Session.ChannelMessageSendEmbed(ctx.Channel.ID, embed) + if err != nil { + ctx.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't send the user embed", + Error: err, + } + } +} diff --git a/go.mod b/go.mod index 29d07d6..92010e8 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ -module djpianalto.com/goff +module github.com/dustinpianalto/goff go 1.14 require ( github.com/bwmarrin/discordgo v0.20.3-0.20200525154655-ca64123b05de github.com/dustinpianalto/disgoman v0.0.10 + github.com/dustinpianalto/rpnparse v1.0.1 github.com/emersion/go-imap v1.0.5 github.com/emersion/go-message v0.12.0 - github.com/dustinpianalto/rpnparse v1.0.1 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/goff.go b/goff.go new file mode 100644 index 0000000..08a4124 --- /dev/null +++ b/goff.go @@ -0,0 +1,136 @@ +package main + +import ( + "fmt" + "log" + + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/goff/events" + "github.com/dustinpianalto/goff/exts" + "github.com/dustinpianalto/goff/utils" + + //"github.com/MikeModder/anpan" + "os" + "os/signal" + "syscall" + + "github.com/bwmarrin/discordgo" +) + +var ( + Token string +) + +//func init() { +// flag.StringVar(&Token, "t", "", "Bot Token") +// flag.Parse() +//} + +func main() { + Token = os.Getenv("DISCORDGO_TOKEN") + dg, err := discordgo.New("Bot " + Token) + if err != nil { + fmt.Println("There was an error when creating the Discord Session, ", err) + return + } + dg.State.MaxMessageCount = 100 + + utils.ConnectDatabase(os.Getenv("DATABASE_URL")) + utils.InitializeDatabase() + //utils.LoadTestData() + + //prefixes := []string{ + // "Go.", + //} + owners := []string{ + "351794468870946827", + } + + // Arguments are: + // prefixes - []string + // owner ids - []string + // ignore bots - bool + // check perms - bool + handler := disgoman.CommandManager{ + Prefixes: getPrefixes, + Owners: owners, + StatusManager: disgoman.GetDefaultStatusManager(), + ErrorChannel: make(chan disgoman.CommandError, 10), + Commands: make(map[string]*disgoman.Command), + IgnoreBots: true, + CheckPermissions: false, + } + + // Add Command Handlers + exts.AddCommandHandlers(&handler) + + //if _, ok := handler.Commands["help"]; !ok { + // handler.AddDefaultHelpCommand() + //} + + dg.AddHandler(handler.OnMessage) + dg.AddHandler(handler.StatusManager.OnReady) + dg.AddHandler(events.OnMessageUpdate) + dg.AddHandler(events.OnMessageDelete) + dg.AddHandler(events.OnGuildMemberAddLogging) + dg.AddHandler(events.OnGuildMemberRemoveLogging) + + err = dg.Open() + if err != nil { + fmt.Println("There was an error opening the connection, ", err) + return + } + + // Start the Error handler in a goroutine + go ErrorHandler(handler.ErrorChannel) + + // Start the Logging handler in a goroutine + go utils.LoggingHandler(utils.LoggingChannel) + + // Start the task handler in a goroutine + go utils.ProcessTasks(dg, 1) + + go utils.RecieveEmail(dg) + + fmt.Println("The Bot is now running.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + fmt.Println("Shutting Down...") + err = dg.Close() + if err != nil { + fmt.Println(err) + } +} + +func getPrefixes(guildID string) []string { + queryString := "Select prefix from prefixes p, x_guilds_prefixes xgp where xgp.guild_id = $1 and xgp.prefix_id = p.id" + rows, err := utils.Database.Query(queryString, guildID) + if err != nil { + log.Println(err) + return []string{"Go.", "go."} + } + var prefixes []string + for rows.Next() { + var prefix string + err = rows.Scan(&prefix) + if err != nil { + log.Println(err) + return []string{"Go.", "go."} + } + prefixes = append(prefixes, prefix) + } + return prefixes +} + +func ErrorHandler(ErrorChan chan disgoman.CommandError) { + for ce := range ErrorChan { + msg := ce.Message + if msg == "" { + msg = ce.Error.Error() + } + _, _ = ce.Context.Send(msg) + fmt.Println(ce.Error) + } +} diff --git a/utils/database.go b/utils/database.go new file mode 100644 index 0000000..88d0aed --- /dev/null +++ b/utils/database.go @@ -0,0 +1,154 @@ +package utils + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/lib/pq" +) + +var ( + Database *sql.DB +) + +func ConnectDatabase(dbConnString string) { + db, err := sql.Open("postgres", dbConnString) + if err != nil { + panic(fmt.Sprintf("Can't connect to the database. %v", err)) + } else { + fmt.Println("Database Connected.") + } + Database = db +} + +func InitializeDatabase() { + _, err := Database.Query("CREATE TABLE IF NOT EXISTS users(" + + "id varchar(30) primary key," + + "banned bool not null default false," + + "logging bool not null default true," + + "steam_id varchar(30) NOT NULL DEFAULT ''," + + "is_active bool not null default true," + + "is_staff bool not null default false," + + "is_admin bool not null default false" + + ")") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("CREATE TABLE IF NOT EXISTS guilds(" + + "id varchar(30) primary key," + + "welcome_message varchar(1000) NOT NULL DEFAULT ''," + + "goodbye_message varchar(1000) NOT NULL DEFAULT ''," + + "logging_channel varchar(30) NOT NULL DEFAULT ''," + + "welcome_channel varchar(30) NOT NULL DEFAULT ''" + + ")") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("CREATE TABLE IF NOT EXISTS prefixes(" + + "id serial primary key," + + "prefix varchar(10) not null unique default 'Go.'" + + ")") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("CREATE TABLE IF NOT EXISTS tags(" + + "id serial primary key," + + "tag varchar(100) not null unique," + + "content varchar(1000) not null," + + "creator varchar(30) not null references users(id)," + + "creation_time timestamp not null default NOW()," + + "guild_id varchar(30) not null references guilds(id)" + + ")") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("CREATE TABLE IF NOT EXISTS x_users_guilds(" + + "guild_id varchar(30) not null references guilds(id)," + + "user_id varchar(30) not null references users(id)" + + ")") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("CREATE TABLE IF NOT EXISTS x_guilds_prefixes(" + + "guild_id varchar(30) not null references guilds(id)," + + "prefix_id int not null references prefixes(id)" + + ")") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("CREATE TABLE IF NOT EXISTS tasks(" + + "id serial primary key," + + "type varchar(10) not null," + + "content text not null," + + "guild_id varchar(30) not null references guilds(id)," + + "channel_id varchar(30) not null," + + "user_id varchar(30) not null," + + "creation_time timestamp not null default NOW()," + + "trigger_time timestamp not null," + + "completed bool not null default false," + + "processing bool default false)") + 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() { + _, err := Database.Query("INSERT INTO users (id, banned, logging, steam_id, is_active, is_staff, is_admin) values " + + "('351794468870946827', false, true, '76561198024193239', true, true, true)," + + "('692908139506434065', false, true, '', true, false, false)," + + "('396588996706304010', false, true, '', true, true, false)") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("INSERT INTO guilds (id, welcome_message, goodbye_message) VALUES " + + "('265828729970753537', 'Hey there is someone new here.', 'Well fine then... Just leave without saying goodbye')") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("INSERT INTO prefixes (prefix) VALUES ('Godev.'), ('godev.'), ('godev,')") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("INSERT INTO x_users_guilds (guild_id, user_id) VALUES " + + "('265828729970753537', '351794468870946827')," + + "('265828729970753537', '692908139506434065')," + + "('265828729970753537', '396588996706304010')") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("INSERT INTO x_guilds_prefixes (guild_id, prefix_id) VALUES " + + "('265828729970753537', 1)," + + "('265828729970753537', 2)," + + "('265828729970753537', 3)") + if err != nil { + fmt.Println(err) + } + _, err = Database.Query("INSERT INTO tags (tag, content, creator, guild_id) VALUES " + + "('test', 'This is a test of the tag system', '351794468870946827', '265828729970753537')") + if err != nil { + fmt.Println(err) + } +} diff --git a/utils/date_strings.go b/utils/date_strings.go new file mode 100644 index 0000000..510288d --- /dev/null +++ b/utils/date_strings.go @@ -0,0 +1,64 @@ +package utils + +import ( + "fmt" + "time" +) + +func ParseDateString(inTime time.Time) string { + d := time.Now().Sub(inTime) + s := int64(d.Seconds()) + days := s / 86400 + s = s - (days * 86400) + hours := s / 3600 + s = s - (hours * 3600) + minutes := s / 60 + seconds := s - (minutes * 60) + dateString := "" + if days != 0 { + dateString += fmt.Sprintf("%v days ", days) + } + if hours != 0 { + dateString += fmt.Sprintf("%v hours ", hours) + } + if minutes != 0 { + dateString += fmt.Sprintf("%v minutes ", minutes) + } + if seconds != 0 { + dateString += fmt.Sprintf("%v seconds ", seconds) + } + if dateString != "" { + dateString += " ago." + } else { + dateString = "Now" + } + stamp := inTime.Format("2006-01-02 15:04:05") + return fmt.Sprintf("%v\n%v", dateString, stamp) +} + +func ParseDurationString(inDur time.Duration) string { + s := int64(inDur.Seconds()) + days := s / 86400 + s = s - (days * 86400) + hours := s / 3600 + s = s - (hours * 3600) + minutes := s / 60 + seconds := s - (minutes * 60) + durString := "" + if days != 0 { + durString += fmt.Sprintf("%v days ", days) + } + if hours != 0 { + durString += fmt.Sprintf("%v hours ", hours) + } + if minutes != 0 { + durString += fmt.Sprintf("%v minutes ", minutes) + } + if seconds != 0 { + durString += fmt.Sprintf("%v seconds ", seconds) + } + if durString == "" { + durString = "0 seconds" + } + return fmt.Sprintf("%v", durString) +} diff --git a/utils/email.go b/utils/email.go new file mode 100644 index 0000000..305ff6a --- /dev/null +++ b/utils/email.go @@ -0,0 +1,132 @@ +package utils + +import ( + "io" + "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(dg *discordgo.Session) { + for { + log.Println("Connecting to Email server.") + + EmailClient, err := client.DialTLS("mail.djpianalto.com:993", nil) + if err != nil { + log.Println(err) + return + } + if err = EmailClient.Login(emailUsername, emailPassword); err != nil { + log.Println(err) + return + } + log.Println("Connected to Email server.") + + mbox, err := EmailClient.Select("INBOX", false) + if err != nil { + log.Println(err) + return + } + + if mbox.Messages == 0 { + log.Println("No Messages in Mailbox") + } + + criteria := imap.NewSearchCriteria() + criteria.WithoutFlags = []string{"\\Seen"} + uids, err := EmailClient.Search(criteria) + if err != nil { + log.Println(err) + } + if len(uids) > 0 { + seqset := new(imap.SeqSet) + seqset.AddNum(uids...) + section := &imap.BodySectionName{} + items := []imap.FetchItem{section.FetchItem()} + messages := make(chan *imap.Message, 10) + go func() { + if err = EmailClient.Fetch(seqset, items, messages); err != nil { + log.Println(err) + return + } + }() + + var wg sync.WaitGroup + + for msg := range messages { + if msg == nil { + log.Println("No New Messages") + continue + } + r := msg.GetBody(section) + if r == nil { + log.Println("Server didn't send a message body") + continue + } + 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/utils/logging.go b/utils/logging.go new file mode 100644 index 0000000..c3415c4 --- /dev/null +++ b/utils/logging.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "github.com/bwmarrin/discordgo" +) + +var LoggingChannel = make(chan *LogEvent, 10) + +type LogEvent struct { + // Embed with log message + Embed discordgo.MessageEmbed + // Guild to log event in + GuildID string + // Discordgo Session. Needed for sending messages + Session *discordgo.Session +} + +func LoggingHandler(lc chan *LogEvent) { + for event := range lc { + var channelID string + row := Database.QueryRow("SELECT logging_channel FROM guilds where id=$1", event.GuildID) + err := row.Scan(&channelID) + if err != nil { + fmt.Println(err) + return + } + if channelID == "" { + return + } + + _, _ = event.Session.ChannelMessageSendEmbed(channelID, &event.Embed) + } +} diff --git a/utils/postfixes.go b/utils/postfixes.go new file mode 100644 index 0000000..12d297a --- /dev/null +++ b/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/utils/puzzles.go b/utils/puzzles.go new file mode 100644 index 0000000..6edd5ad --- /dev/null +++ b/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/utils/rpn.go b/utils/rpn.go new file mode 100644 index 0000000..db39f5d --- /dev/null +++ b/utils/rpn.go @@ -0,0 +1,156 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" +) + +type Operator struct { + Token string + Precedence int + Association string +} + +func (o Operator) HasHigherPrecedence(t Operator) bool { + return o.Precedence < t.Precedence // lower number is higher precedence +} + +func (o Operator) HasEqualPrecedence(t Operator) bool { + return o.Precedence == t.Precedence +} + +func (o Operator) IsLeftAssociative() bool { + return o.Association == "left" +} + +var operators = map[string]Operator{ + "+": Operator{ + Token: "+", + Precedence: 4, + Association: "left", + }, + "-": Operator{ + Token: "-", + Precedence: 4, + Association: "left", + }, + "*": Operator{ + Token: "*", + Precedence: 3, + Association: "left", + }, + "/": Operator{ + Token: "/", + Precedence: 3, + Association: "left", + }, + "%": Operator{ + Token: "%", + Precedence: 3, + Association: "left", + }, + "(": Operator{ + Token: "(", + Precedence: 1, + Association: "left", + }, + ")": Operator{ + Token: ")", + Precedence: 1, + Association: "left", + }, +} + +type Stack []Operator + +func (s *Stack) IsEmpty() bool { + return len(*s) == 0 +} + +func (s *Stack) Push(op Operator) { + *s = append(*s, op) +} + +func (s *Stack) Pop() (Operator, bool) { + if s.IsEmpty() { + return Operator{}, false + } + index := len(*s) - 1 + element := (*s)[index] + *s = (*s)[:index] + return element, true +} + +func (s *Stack) Top() Operator { + if s.IsEmpty() { + return Operator{} + } + return (*s)[len(*s)-1] +} + +func GenerateRPN(tokens []string) (string, error) { + output := "" + s := Stack{} + for _, token := range tokens { + err := processToken(token, &s, &output) + if err != nil { + return "", err + } + } + for !s.IsEmpty() { + ele, _ := s.Pop() + output += " " + ele.Token + } + + return strings.TrimSpace(output), nil +} + +func processToken(t string, s *Stack, o *string) error { + if _, err := strconv.Atoi(t); err == nil { + *o += " " + t + return nil + } else if op, ok := operators[t]; ok { + if op.Token == "(" { + s.Push(op) + } else if op.Token == ")" { + if s.IsEmpty() { + return fmt.Errorf("mismatched parentheses") + } + for s.Top().Token != "(" { + if ele, ok := s.Pop(); ok { + *o += " " + ele.Token + } else { + return fmt.Errorf("mismatched parentheses") + } + if s.IsEmpty() { + break + } + } + s.Pop() // Pop and discard the ( + } else if !s.IsEmpty() { + for { + if (s.Top().HasHigherPrecedence(op) || + (s.Top().HasEqualPrecedence(op) && + op.IsLeftAssociative())) && + s.Top().Token != "(" { + if ele, ok := s.Pop(); ok { + *o += " " + ele.Token + if s.IsEmpty() { + break + } + continue + } else { + break + } + } + break + } + s.Push(op) + } else { + s.Push(op) + } + return nil + } + return fmt.Errorf("invalid character %s", t) +} diff --git a/utils/rpnParser.go b/utils/rpnParser.go new file mode 100644 index 0000000..7f9dff9 --- /dev/null +++ b/utils/rpnParser.go @@ -0,0 +1,95 @@ +package utils + +import ( + "errors" + "fmt" + "math" + "strconv" +) + +type FStack []float64 + +func (s *FStack) IsEmpty() bool { + return len(*s) == 0 +} + +func (s *FStack) Push(op float64) { + *s = append(*s, op) +} + +func (s *FStack) Pop() (float64, bool) { + if s.IsEmpty() { + return 0, false + } + index := len(*s) - 1 + element := (*s)[index] + *s = (*s)[:index] + return element, true +} + +func (s *FStack) PopTwo() (float64, float64, bool) { + if s.IsEmpty() || len(*s) < 2 { + return 0, 0, false + } + index := len(*s) - 1 + b := (*s)[index] + a := (*s)[index-1] + *s = (*s)[:index-1] + return a, b, true + +} + +func (s *FStack) Top() float64 { + if s.IsEmpty() { + return 0 + } + return (*s)[len(*s)-1] +} + +func ParseRPN(args []string) (float64, error) { + s := FStack{} + for _, token := range args { + switch token { + case "+": + if a, b, ok := s.PopTwo(); ok { + s.Push(a + b) + } else { + return 0, fmt.Errorf("not enough operands on stack for +: %v", s) + } + case "-": + if a, b, ok := s.PopTwo(); ok { + s.Push(a - b) + } else { + return 0, fmt.Errorf("not enough operands on stack for -: %v", s) + } + case "*": + if a, b, ok := s.PopTwo(); ok { + s.Push(a * b) + } else { + return 0, fmt.Errorf("not enough operands on stack for *: %v", s) + } + case "/": + if a, b, ok := s.PopTwo(); ok { + s.Push(a / b) + } else { + return 0, fmt.Errorf("not enough operands on stack for /: %v", s) + } + case "%": + if a, b, ok := s.PopTwo(); ok { + s.Push(math.Mod(a, b)) + } else { + return 0, fmt.Errorf("not enough operands on stack for %: %v", s) + } + default: + f, err := strconv.ParseFloat(token, 64) + if err != nil { + return 0, err + } + s.Push(f) + } + } + if res, ok := s.Pop(); ok { + return res, nil + } + return 0, errors.New("no result") +} diff --git a/utils/snowflake.go b/utils/snowflake.go new file mode 100644 index 0000000..f26978c --- /dev/null +++ b/utils/snowflake.go @@ -0,0 +1,32 @@ +package utils + +import "time" + +type Snowflake struct { + CreationTime time.Time + WorkerID int8 + ProcessID int8 + Increment int16 +} + +func ParseSnowflake(s int64) Snowflake { + const ( + DISCORD_EPOCH = 1420070400000 + TIME_BITS_LOC = 22 + WORKER_ID_LOC = 17 + WORKER_ID_MASK = 0x3E0000 + PROCESS_ID_LOC = 12 + PROCESS_ID_MASK = 0x1F000 + INCREMENT_MASK = 0xFFF + ) + creationTime := time.Unix(((s>>TIME_BITS_LOC)+DISCORD_EPOCH)/1000.0, 0) + workerID := (s & WORKER_ID_MASK) >> WORKER_ID_LOC + processID := (s & PROCESS_ID_MASK) >> PROCESS_ID_LOC + increment := s & INCREMENT_MASK + return Snowflake{ + CreationTime: creationTime, + WorkerID: int8(workerID), + ProcessID: int8(processID), + Increment: int16(increment), + } +} diff --git a/utils/tasks.go b/utils/tasks.go new file mode 100644 index 0000000..3af8494 --- /dev/null +++ b/utils/tasks.go @@ -0,0 +1,132 @@ +package utils + +import ( + "fmt" + "github.com/bwmarrin/discordgo" + "log" + "time" +) + +type Task struct { + ID int64 + Type string + Content string + GuildID string + ChannelID string + UserID string + CreationTime time.Time + TriggerTime time.Time +} + +func processTask(task *Task, s *discordgo.Session) { + query := "SELECT completed, processing from tasks where id = $1" + res, err := Database.Query(query, task.ID) + if err != nil { + log.Println(err) + return + } + var completed bool + var processing bool + res.Next() + err = res.Scan(&completed, &processing) + if err != nil { + log.Println(err) + return + } + if completed || processing { + return + } + closeQuery := "Update tasks set completed = true where id = $1" + processQuery := "UPDATE tasks SET processing = true WHERE id = $1" + defer Database.Exec(closeQuery, task.ID) + _, err = Database.Exec(processQuery, task.ID) + if err != nil { + log.Println(err) + return + } + log.Println(fmt.Sprintf("Processing task %v", task.ID)) + guild, err := s.Guild(task.GuildID) + if err != nil { + log.Print(fmt.Sprintf("Can't find guild with ID %v. Canceling task %v.", task.GuildID, task.ID)) + return + } + channel, err := s.Channel(task.ChannelID) + if err != nil { + log.Print(fmt.Sprintf("Can't find channel with ID %v. Canceling task %v.", task.ChannelID, task.ID)) + return + } + if channel.GuildID != guild.ID { + log.Print(fmt.Sprintf("The channel %v is not in guild %v. Canceling task %v.", channel.Name, guild.Name, task.ID)) + return + } + member, err := s.GuildMember(guild.ID, task.UserID) + if err != nil { + log.Print(fmt.Sprintf("Can't find user with ID %v in guild %v. Canceling task %v.", task.UserID, guild.Name, task.ID)) + return + } + if task.Type == "Reminder" { + color := s.State.UserColor(member.User.ID, channel.ID) + e := discordgo.MessageEmbed{ + Title: "REMINDER", + Description: task.Content, + Timestamp: task.CreationTime.Format(time.RFC3339), + Color: color, + Footer: &discordgo.MessageEmbedFooter{ + Text: "Created: ", + }, + } + msg := discordgo.MessageSend{ + Content: member.Mention(), + Embed: &e, + } + _, err = s.ChannelMessageSendComplex(channel.ID, &msg) + if err != nil { + log.Println(err) + } + } + processQuery = "UPDATE tasks SET processing = false WHERE id = $1" + _, err = Database.Exec(processQuery, task.ID) + if err != nil { + log.Println(err) + } + +} + +func getTasksToRun() []Task { + query := "SELECT id, type, content, guild_id, channel_id, user_id, creation_time, trigger_time " + + "from tasks where completed is false and processing is false and trigger_time < $1" + res, err := Database.Query(query, time.Now()) + if err != nil { + log.Println(err) + } + var tasks []Task + for res.Next() { + var t Task + err = res.Scan(&t.ID, &t.Type, &t.Content, &t.GuildID, &t.ChannelID, &t.UserID, &t.CreationTime, &t.TriggerTime) + if err != nil { + log.Println(err) + } + for _, task := range tasks { + if task.ID == t.ID { + continue + } + } + tasks = append(tasks, t) + } + + return tasks +} + +func ProcessTasks(s *discordgo.Session, interval int) { + for { + time.Sleep(time.Duration(interval * 1e9)) + + tasks := getTasksToRun() + + if len(tasks) > 0 { + for _, t := range tasks { + go processTask(&t, s) + } + } + } +} diff --git a/utils/types.go b/utils/types.go new file mode 100644 index 0000000..0fd92b9 --- /dev/null +++ b/utils/types.go @@ -0,0 +1,10 @@ +package utils + +type Guild struct { + ID string + WelcomeMessage string + GoodbyeMessage string + LoggingChannel string + WelcomeChannel string + PuzzleChannel string +}