diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1dddaae..f1e25a7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,46 +17,34 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - name: Get Version id: get_version uses: battila7/get-version-action@v2.0.0 - - name: Build, tag, and push image to Amazon ECR - id: build-image + - name: Build container image env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: geeksbot IMAGE_TAG: ${{ steps.get_version.outputs.version-without-v }} - run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest - docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest - echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: "geeksbot" - image: ${{ steps.build-image.outputs.image }} + run: docker build -t registry.digitalocean.com/djpianalto/geeksbot:$IMAGE_TAG . - - name: Deploy Amazon ECS task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + - name: Install doctl + uses: digitalocean/action-doctl@v2 with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - service: "geeksbot" - cluster: "discord-bots" - wait-for-service-stability: true + token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + + - name: Login to DigitalOcean Container Registry with short-lived credentials + run: doctl registry login --expiry-seconds 600 + + - name: Push image to DigitalOcean Container Registry + run: docker push registry.digitalocean.com/djpianalto/geeksbot + + - name: Update deployment file + run: TAG=${{ steps.get_version.outputs.version-without-v }} && sed -i 's||registry.digitalocean.com/djpianalto/geeksbot:'${TAG}'|' $GITHUB_WORKSPACE/deployment.yml + + - name: Save DigitalOcean kubeconfig with short-lived credentials + run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 discord-bots + + - name: Deploy to DigitalOcean Kubernetes + run: kubectl apply -f $GITHUB_WORKSPACE/deployment.yml + + - name: Verify deployment + run: kubectl rollout status deployment/goff diff --git a/cmd/geeksbot/main.go b/cmd/geeksbot/main.go index e69de29..9a4746d 100644 --- a/cmd/geeksbot/main.go +++ b/cmd/geeksbot/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + "github.com/bwmarrin/discordgo" + "github.com/dustinpianalto/disgoman" +) + +func main() { + Token := os.Getenv("DISCORDGO_TOKEN") + dg, err := discordgo.New("Bot " + Token) + if err != nil { + log.Println("There was an error when creating the Discord Session, ", err) + return + } + dg.State.MaxMessageCount = 100 + dg.StateEnabled = true + + dg.Identify = discordgo.Identify{ + Intents: discordgo.MakeIntent(discordgo.IntentsAll), + } + + //postgres.ConnectDatabase(os.Getenv("DATABASE_URL")) + //postgres.InitializeDatabase() + //utils.LoadTestData() + + //us := &postgres.UserService{DB: postgres.DB} + //gs := &postgres.GuildService{DB: postgres.DB} + + owners := []string{ + "351794468870946827", + } + + manager := 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(&manager) + //services.InitalizeServices(us, gs) + + //if _, ok := handler.Commands["help"]; !ok { + // handler.AddDefaultHelpCommand() + //} + + dg.AddHandler(manager.OnMessage) + dg.AddHandler(manager.StatusManager.OnReady) + //dg.AddHandler(guild_management.OnMessageUpdate) + //dg.AddHandler(guild_management.OnMessageDelete) + //dg.AddHandler(user_management.OnGuildMemberAddLogging) + //dg.AddHandler(user_management.OnGuildMemberRemoveLogging) + + err = dg.Open() + if err != nil { + log.Println("There was an error opening the connection, ", err) + return + } + + // Start the Error handler in a goroutine + go ErrorHandler(manager.ErrorChannel) + + // Start the Logging handler in a goroutine + //go logging.LoggingHandler(logging.LoggingChannel) + + log.Println("The Bot is now running.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + log.Println("Shutting Down...") + err = dg.Close() + if err != nil { + log.Println(err) + } +} + +func getPrefixes(guildID string) []string { + return []string{"G.", "g."} +} + +func ErrorHandler(ErrorChan chan disgoman.CommandError) { + for ce := range ErrorChan { + msg := ce.Message + if msg == "" { + msg = ce.Error.Error() + } + _, _ = ce.Context.Send(msg) + log.Println(ce.Error) + } +} diff --git a/deployment.yml b/deployment.yml new file mode 100644 index 0000000..317b862 --- /dev/null +++ b/deployment.yml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: geeksbot + namespace: default + labels: + app: geeksbot +spec: + replicas: 1 + selector: + matchLabels: + app: geeksbot + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + minReadySeconds: 120 + template: + metadata: + labels: + app: geeksbot + spec: + containers: + - name: geeksbot + image: + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: geeksbot + key: database_url + - name: DISCORD_TOKEN + valueFrom: + secretKeyRef: + name: geeksbot + key: discord_token diff --git a/go.mod b/go.mod index 5e51d7a..6b9bef5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module github.com/dustinpianalto/geeksbot go 1.14 + +require ( + github.com/bwmarrin/discordgo v0.22.1 + github.com/dustinpianalto/disgoman v0.0.15 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a1d06cb --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/bwmarrin/discordgo v0.20.2/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/bwmarrin/discordgo v0.22.1 h1:254fNYyfqJWKbPzO5g8j/nUvRgj4dNlI19EB8rnkpt8= +github.com/bwmarrin/discordgo v0.22.1/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/dustinpianalto/disgoman v0.0.15 h1:kdIw6jhC82WBut7+4BarqxBw06dozU+Hu47LQzkkoGM= +github.com/dustinpianalto/disgoman v0.0.15/go.mod h1:v3FM6n+4dH9XlvO+IDx6MN3DUnGq6YVDBvy1A1k202g= +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/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +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= diff --git a/internal/exts/init.go b/internal/exts/init.go new file mode 100644 index 0000000..081f07a --- /dev/null +++ b/internal/exts/init.go @@ -0,0 +1,56 @@ +package exts + +import ( + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/goff/internal/exts/fun" + "github.com/dustinpianalto/goff/internal/exts/guild_management" + "github.com/dustinpianalto/goff/internal/exts/roles" + "github.com/dustinpianalto/goff/internal/exts/tags" + "github.com/dustinpianalto/goff/internal/exts/tasks" + "github.com/dustinpianalto/goff/internal/exts/user_management" + "github.com/dustinpianalto/goff/internal/exts/utils" + + "github.com/dustinpianalto/goff/internal/exts/p_interpreter" +) + +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(utils.UserCommand) + _ = h.AddCommand(utils.SayCommand) + _ = h.AddCommand(utils.GitCommand) + _ = h.AddCommand(utils.InviteCommand) + _ = h.AddCommand(utils.PingCommand) + _ = h.AddCommand(tasks.AddReminderCommand) + _ = h.AddCommand(tags.AddTagCommand) + _ = h.AddCommand(tags.TagCommand) + _ = h.AddCommand(roles.MakeRoleSelfAssignableCommand) + _ = h.AddCommand(roles.RemoveSelfAssignableCommand) + _ = h.AddCommand(roles.SelfAssignRoleCommand) + _ = h.AddCommand(roles.UnAssignRoleCommand) + _ = h.AddCommand(p_interpreter.PCommand) + _ = h.AddCommand(fun.InterleaveCommand) + _ = h.AddCommand(fun.DeinterleaveCommand) + _ = h.AddCommand(fun.GenerateRPNCommand) + _ = h.AddCommand(fun.ParseRPNCommand) + _ = h.AddCommand(fun.SolveCommand) + _ = h.AddCommand(user_management.KickUserCommand) + _ = h.AddCommand(user_management.BanUserCommand) + _ = h.AddCommand(user_management.UnbanUserCommand) + _ = h.AddCommand(guild_management.SetLoggingChannelCommand) + _ = h.AddCommand(guild_management.GetLoggingChannelCommand) + _ = h.AddCommand(guild_management.SetWelcomeChannelCommand) + _ = h.AddCommand(guild_management.GetWelcomeChannelCommand) + _ = h.AddCommand(guild_management.AddGuildCommand) + _ = h.AddCommand(guild_management.SetPuzzleChannelCommand) + _ = h.AddCommand(guild_management.GetPuzzleChannelCommand) + _ = h.AddCommand(guild_management.SetPuzzleRoleCommand) + _ = h.AddCommand(guild_management.GetPuzzleRoleCommand) + +} diff --git a/internal/exts/utils/utils.go b/internal/exts/utils/utils.go new file mode 100644 index 0000000..215e6a7 --- /dev/null +++ b/internal/exts/utils/utils.go @@ -0,0 +1,248 @@ +package utils + +import ( + "fmt" + "sort" + "strconv" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/goff/internal/discord_utils" +) + +var PingCommand = &disgoman.Command{ + Name: "ping", + Aliases: []string{" "}, + Description: "Check the bot's ping", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: pingCommandFunc, +} + +func pingCommandFunc(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.CommandManager.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Ping Failed", + Error: err, + } + } +} + +var InviteCommand = &disgoman.Command{ + Name: "invite", + Aliases: nil, + Description: "Get the invite link for this bot or others", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: inviteCommandFunc, +} + +func inviteCommandFunc(ctx disgoman.Context, args []string) { + var ids []string + if len(args) == 0 && len(ctx.Message.Mentions) == 0 { + ids = []string{ctx.Session.State.User.ID} + } else { + if len(ctx.Message.Mentions) > 0 { + for _, user := range ctx.Message.Mentions { + member, err := ctx.Session.GuildMember(ctx.Guild.ID, user.ID) + if err != nil { + ctx.CommandManager.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Could not find member " + user.Username, + Error: err, + } + continue + } + ids = append(ids, member.User.ID) + } + } + if len(args) > 0 { + for _, id := range args { + member, err := ctx.Session.GuildMember(ctx.Guild.ID, id) + if err != nil { + ctx.CommandManager.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Could not find member " + id, + Error: err, + } + continue + } + ids = append(ids, member.User.ID) + } + } + } + if len(ids) == 0 { + return + } + for _, id := range ids { + url := fmt.Sprintf("", id) + _, err := ctx.Send(url) + if err != nil { + ctx.CommandManager.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't send the invite link.", + Error: err, + } + } + } +} + +var GitCommand = &disgoman.Command{ + Name: "git", + Aliases: nil, + Description: "Show my github link", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: gitCommandFunc, +} + +func gitCommandFunc(ctx disgoman.Context, _ []string) { + embed := &discordgo.MessageEmbed{ + Title: "Hi there, My code is on Github", + Color: 0, + URL: "https://github.com/dustinpianalto/Geeksbot", + } + _, err := ctx.Session.ChannelMessageSendEmbed(ctx.Channel.ID, embed) + if err != nil { + ctx.CommandManager.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Git failed", + Error: err, + } + } +} + +var SayCommand = &disgoman.Command{ + Name: "say", + Aliases: nil, + Description: "Repeat a message", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + SanitizeEveryone: true, + Invoke: sayCommandFunc, +} + +func sayCommandFunc(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.CommandManager.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Say Failed", + Error: err, + } + } +} + +var UserCommand = &disgoman.Command{ + Name: "user", + Aliases: nil, + Description: "Get user info", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: userCommandFunc, +} + +func userCommandFunc(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.CommandManager.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: discord_utils.ParseDateString(guildJoinTime), + Inline: false, + } + + int64ID, _ := strconv.ParseInt(member.User.ID, 10, 64) + s := discord_utils.ParseSnowflake(int64ID) + discordJoinedField := &discordgo.MessageEmbedField{ + Name: "Joined Discord:", + Value: discord_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.CommandManager.ErrorChannel <- disgoman.CommandError{ + Context: ctx, + Message: "Couldn't send the user embed", + Error: err, + } + } +} diff --git a/task-definition.json b/task-definition.json deleted file mode 100644 index f4ad8f0..0000000 --- a/task-definition.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "ipcMode": null, - "executionRoleArn": "arn:aws:iam::005692590034:role/goff_ecs_rds+cw+ecr", - "containerDefinitions": [ - { - "dnsSearchDomains": null, - "environmentFiles": null, - "logConfiguration": { - "logDriver": "awslogs", - "secretOptions": null, - "options": { - "awslogs-group": "/ecs/geeksbot", - "awslogs-region": "us-east-1", - "awslogs-stream-prefix": "ecs" - } - }, - "entryPoint": null, - "portMappings": [], - "command": null, - "linuxParameters": null, - "cpu": 0, - "environment": [], - "resourceRequirements": null, - "ulimits": null, - "dnsServers": null, - "mountPoints": [], - "workingDirectory": null, - "secrets": [ - { - "valueFrom": "geeksbot_database_uri", - "name": "DATABASE_URL" - }, - { - "valueFrom": "geeksbot_discord_token", - "name": "DISCORDGO_TOKEN" - } - ], - "dockerSecurityOptions": null, - "memory": 512, - "memoryReservation": null, - "volumesFrom": [], - "stopTimeout": null, - "image": "005692590034.dkr.ecr.us-east-1.amazonaws.com/geeksbot:latest", - "startTimeout": null, - "firelensConfiguration": null, - "dependsOn": null, - "disableNetworking": null, - "interactive": null, - "healthCheck": null, - "essential": true, - "links": null, - "hostname": null, - "extraHosts": null, - "pseudoTerminal": null, - "user": null, - "readonlyRootFilesystem": null, - "dockerLabels": null, - "systemControls": null, - "privileged": null, - "name": "geeksbot" - } - ], - "placementConstraints": [], - "memory": "512", - "taskRoleArn": "arn:aws:iam::005692590034:role/goff_ecs_rds+cw+ecr", - "compatibilities": [ - "EC2" - ], - "taskDefinitionArn": "arn:aws:ecs:us-east-1:005692590034:task-definition/geeksbot:1", - "family": "geeksbot", - "requiresAttributes": [ - { - "targetId": null, - "targetType": null, - "value": null, - "name": "com.amazonaws.ecs.capability.logging-driver.awslogs" - }, - { - "targetId": null, - "targetType": null, - "value": null, - "name": "ecs.capability.execution-role-awslogs" - }, - { - "targetId": null, - "targetType": null, - "value": null, - "name": "com.amazonaws.ecs.capability.ecr-auth" - }, - { - "targetId": null, - "targetType": null, - "value": null, - "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19" - }, - { - "targetId": null, - "targetType": null, - "value": null, - "name": "com.amazonaws.ecs.capability.task-iam-role" - }, - { - "targetId": null, - "targetType": null, - "value": null, - "name": "ecs.capability.execution-role-ecr-pull" - }, - { - "targetId": null, - "targetType": null, - "value": null, - "name": "ecs.capability.secrets.ssm.environment-variables" - } - ], - "pidMode": null, - "requiresCompatibilities": [ - "EC2" - ], - "networkMode": null, - "cpu": "1024", - "revision": 2, - "status": "ACTIVE", - "inferenceAccelerators": null, - "proxyConfiguration": null, - "volumes": [] -}