35 Commits

Author SHA1 Message Date
a33c3185db Update README.md 2024-10-22 21:55:24 -06:00
13bf11abce Better command abstraction 2024-10-22 17:07:53 -06:00
0130ec538c Better error handling 2024-10-22 16:19:31 -06:00
26e931f44f Re-write the libs with discordgo 2024-10-22 12:24:02 -06:00
7328a0139e Made errors send ephemeral messages 2024-10-22 11:49:33 -06:00
b00028d8e3 Fixed go version in go mod 2024-10-22 10:05:33 -06:00
999605c2fc Fixed 2024-10-22 10:02:41 -06:00
d2f94dec79 Added an optional param 2024-10-22 09:49:52 -06:00
8c2d7189c7 Markov meme 2024-10-22 00:10:38 -06:00
0406e5cbf7 Markov implementation 2024-10-22 00:01:19 -06:00
44fc4e8dd1 Moved to discordgo 2024-10-21 23:50:17 -06:00
e76a886d5b Welp guess I need CA certs 2024-10-21 22:12:57 -06:00
18aa6846e9 Fly issue 2024-10-21 21:27:22 -06:00
12e3414833 Optimized Dockerfile 2024-10-21 21:20:10 -06:00
e18d1c9aeb Dont scale down 2024-10-20 23:08:22 -06:00
a615512ed7 Fly guys 2024-10-20 22:45:16 -06:00
9385a31364 D: 2024-06-08 23:18:21 -06:00
ba144dafa1 docks 2024-06-08 23:15:31 -06:00
299cbcfcee Bump golang 2024-06-03 17:11:20 -06:00
b9e72fa537 nvm 2024-05-15 10:15:14 -06:00
605e40f16c ??? 2024-05-15 10:14:02 -06:00
5fa146dff9 ??? 2024-05-15 10:10:51 -06:00
d9d0c84135 One more time! 2024-05-15 10:05:16 -06:00
a1fd9aec29 Dockerify 2024-05-15 10:02:42 -06:00
d3badf183c fuck ai again 2024-04-20 12:10:02 -06:00
9ad6aa0c59 Fuck AI 2024-04-20 12:06:49 -06:00
b317ebed86 Ok better images JK 2024-04-07 23:56:35 -06:00
3f908c4153 Revert guidance scale 2024-04-07 23:29:40 -06:00
014dd57d93 ? 2024-04-07 23:27:18 -06:00
db7b4e8e45 Lock down config 2024-04-07 23:19:50 -06:00
e89fdb4ac9 Fix refiner 2024-04-07 23:16:43 -06:00
2038fa6df5 Cheaper pics 2024-04-07 22:24:42 -06:00
655409ea1a Updating to Mixtral 2024-04-07 22:08:38 -06:00
8a944a1e31 Switch to Nixpacks 2024-04-02 18:47:53 -06:00
4eeeb39506 Delete .github directory 2024-04-01 02:20:52 -06:00
25 changed files with 479 additions and 731 deletions

BIN
.DS_Store vendored

Binary file not shown.

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
# flyctl launch added from .gitignore
# Environment variables
**/.env
**/.env.local
**/.env.development.local
**/.env.test.local
**/.env.production.local
**/.env
**/himbot
fly.toml

View File

@ -1,11 +1,2 @@
# Tokens
DISCORD_TOKEN=""
REPLICATE_API_TOKEN=""
# Comma separated
COOLDOWN_ALLOW_LIST=""
# S3
BUCKET_NAME=
AWS_ENDPOINT_URL_S3=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=

18
.github/workflows/fly-deploy.yml vendored Normal file
View File

@ -0,0 +1,18 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
concurrency: deploy-group # optional: ensure only one action runs at a time
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

View File

@ -1,15 +0,0 @@
name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

View File

@ -1,14 +1,20 @@
FROM golang:1.22.0 as build
FROM golang:1.23.2-alpine AS build
WORKDIR /app
COPY . .
COPY go.mod go.sum ./
RUN go mod download
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /go/bin/app
FROM gcr.io/distroless/base-debian12
COPY . .
COPY --from=build /go/bin/app /
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /go/bin/app
CMD [ "/app" ]
FROM gcr.io/distroless/static-debian11
COPY --from=alpine:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /go/bin/app /app
# Set the entrypoint
ENTRYPOINT ["/app"]

View File

@ -4,14 +4,13 @@ A discord bot written in Go.
## It's dangerous to go alone! Take this!
- Install Go 1.21.5 or higher (required)
- Install Go 1.23.2 or higher (required)
## Running Locally
- Copy .env.example and rename to .env
- Create a Discord Bot with all gateway permissions enabled
- Generate a token for this discord bot and paste it in the .env for DISCORD_TOKEN
- Generate and provide an Replicate token and paste it in the .env for REPLICATE_API_TOKEN
- Run `go run main.go` to run locally
## Adding the bot to a server

View File

@ -1,64 +0,0 @@
package command
import (
"bytes"
"context"
"errors"
"fmt"
"himbot/lib"
"time"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/diamondburned/arikawa/v3/utils/sendpart"
)
func Ask(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
// Cooldown Logic
allowed, cooldownString := lib.CooldownHandler(*data.Event, "ask", time.Minute)
if !allowed {
return lib.ErrorResponse(errors.New(cooldownString))
}
// Command Logic
var options struct {
Prompt string `discord:"prompt"`
}
if err := data.Options.Unmarshal(&options); err != nil {
lib.CancelTimer(data.Event.Member.User.ID.String(), "ask")
return lib.ErrorResponse(err)
}
respString, err := lib.ReplicateTextGeneration(options.Prompt)
if err != nil {
fmt.Printf("ChatCompletion error: %v\n", err)
lib.CancelTimer(data.Event.Member.User.ID.String(), "ask")
return &api.InteractionResponseData{
Content: option.NewNullableString("ChatCompletion Error!"),
AllowedMentions: &api.AllowedMentions{},
}
}
if len(respString) > 1800 {
textFile := bytes.NewBuffer([]byte(respString))
file := sendpart.File{
Name: "himbot_response.md",
Reader: textFile,
}
return &api.InteractionResponseData{
Content: option.NewNullableString("Prompt: " + options.Prompt + "\n"),
AllowedMentions: &api.AllowedMentions{},
Files: []sendpart.File{file},
}
}
return &api.InteractionResponseData{
Content: option.NewNullableString("Prompt: " + options.Prompt + "\n--------------------\n" + respString),
AllowedMentions: &api.AllowedMentions{},
}
}

View File

@ -1,26 +1,25 @@
package command
import (
"context"
"fmt"
"himbot/lib"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/bwmarrin/discordgo"
)
func HS(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
var options struct {
Arg string `discord:"nickname"`
func HsCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
options := i.ApplicationCommandData().Options
if len(options) == 0 || options[0].Type != discordgo.ApplicationCommandOptionString {
return "", fmt.Errorf("please provide a nickname")
}
nickname := options[0].StringValue()
user, err := lib.GetUser(i)
if err != nil {
return "", fmt.Errorf("error processing command: %w", err)
}
if err := data.Options.Unmarshal(&options); err != nil {
return lib.ErrorResponse(err)
}
response := fmt.Sprintf("%s was %s's nickname in high school!", nickname, user.Username)
user := lib.GetUserObject(*data.Event)
return &api.InteractionResponseData{
Content: option.NewNullableString(options.Arg + " was " + user.DisplayName() + "'s nickname in highschool!"),
}
return response, nil
}

115
command/markov.go Normal file
View File

@ -0,0 +1,115 @@
package command
import (
"math/rand"
"strings"
"github.com/bwmarrin/discordgo"
)
func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
channelID := i.ChannelID
numMessages := 100 // Default value
if len(i.ApplicationCommandData().Options) > 0 {
if i.ApplicationCommandData().Options[0].Name == "messages" {
numMessages = int(i.ApplicationCommandData().Options[0].IntValue())
if numMessages <= 0 {
numMessages = 100
} else if numMessages > 1000 {
numMessages = 1000 // Limit to 1000 messages max
}
}
}
// Fetch messages
allMessages, err := fetchMessages(s, channelID, numMessages)
if err != nil {
return "", err
}
// Build the Markov chain from the fetched messages
chain := buildMarkovChain(allMessages)
// Generate a new message using the Markov chain
newMessage := generateMessage(chain)
// Check if the generated message is empty and provide a fallback message
if newMessage == "" {
newMessage = "I couldn't generate a message. The channel might be empty or contain no usable text."
}
return newMessage, nil
}
func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*discordgo.Message, error) {
var allMessages []*discordgo.Message
var lastMessageID string
for len(allMessages) < numMessages {
batchSize := 100
if numMessages-len(allMessages) < 100 {
batchSize = numMessages - len(allMessages)
}
batch, err := s.ChannelMessages(channelID, batchSize, lastMessageID, "", "")
if err != nil {
return nil, err
}
if len(batch) == 0 {
break // No more messages to fetch
}
allMessages = append(allMessages, batch...)
lastMessageID = batch[len(batch)-1].ID
if len(batch) < 100 {
break // Less than 100 messages returned, we've reached the end
}
}
return allMessages, nil
}
// buildMarkovChain creates a Markov chain from a list of messages
func buildMarkovChain(messages []*discordgo.Message) map[string][]string {
chain := make(map[string][]string)
for _, msg := range messages {
words := strings.Fields(msg.Content)
// Build the chain by associating each word with the word that follows it
for i := 0; i < len(words)-1; i++ {
chain[words[i]] = append(chain[words[i]], words[i+1])
}
}
return chain
}
// generateMessage creates a new message using the Markov chain
func generateMessage(chain map[string][]string) string {
if len(chain) == 0 {
return ""
}
words := []string{}
var currentWord string
// Start with a random word from the chain
for word := range chain {
currentWord = word
break
}
// Generate up to 20 words
for i := 0; i < 20; i++ {
words = append(words, currentWord)
if nextWords, ok := chain[currentWord]; ok && len(nextWords) > 0 {
// Randomly select the next word from the possible follow-ups
currentWord = nextWords[rand.Intn(len(nextWords))]
} else {
break
}
}
return strings.Join(words, " ")
}

View File

@ -1,56 +0,0 @@
package command
import (
"context"
"errors"
"himbot/lib"
"strconv"
"time"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/diamondburned/arikawa/v3/utils/sendpart"
)
func Pic(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
// Cooldown Logic
allowed, cooldownString := lib.CooldownHandler(*data.Event, "pic", time.Minute*5)
if !allowed {
return lib.ErrorResponse(errors.New(cooldownString))
}
// Command Logic
var options struct {
Prompt string `discord:"prompt"`
}
if err := data.Options.Unmarshal(&options); err != nil {
lib.CancelTimer(data.Event.Member.User.ID.String(), "pic")
return lib.ErrorResponse(err)
}
// Get current epoch timestamp
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
// Concatenate clean username and timestamp to form filename
filename := data.Event.Sender().Username + "_" + timestamp + ".jpg"
imageFile, err := lib.ReplicateImageGeneration(options.Prompt, filename)
if err != nil {
lib.CancelTimer(data.Event.Member.User.ID.String(), "pic")
return lib.ErrorResponse(err)
}
file := sendpart.File{
Name: filename,
Reader: imageFile,
}
return &api.InteractionResponseData{
Content: option.NewNullableString("Prompt: " + options.Prompt),
Files: []sendpart.File{file},
}
}

View File

@ -1,16 +1,12 @@
package command
import (
"context"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/bwmarrin/discordgo"
)
func Ping(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
// Command Logic
return &api.InteractionResponseData{
Content: option.NewNullableString("Pong!"),
}
func PingCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
// Customize the response based on whether it's a guild or DM
responseContent := "Pong!"
return responseContent, nil
}

13
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,13 @@
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
image: your-app-image:latest
command: ["/app"]
pull_policy: build
environment:
- DISCORD_TOKEN=$DISCORD_TOKEN
- COOLDOWN_ALLOW_LIST=$COOLDOWN_ALLOW_LIST

View File

@ -1,15 +1,17 @@
# fly.toml app configuration file generated for himbot on 2023-10-19T18:34:44-03:00
# fly.toml app configuration file generated for himbot on 2024-10-20T22:41:20-06:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "himbot"
primary_region = "sea"
swap_size_mb = 512
app = 'himbot'
primary_region = 'ord'
[http_service]
[build]
[[services]]
internal_port = 3000
auto_stop_machines = true
auto_start_machines = true
protocol = "tcp"
min_machines_running = 1
processes = ["app"]
[[vm]]
size = 'shared-cpu-1x'

17
go.mod
View File

@ -1,20 +1,17 @@
module himbot
go 1.22.0
go 1.23
require github.com/diamondburned/arikawa/v3 v3.3.5
require github.com/diamondburned/arikawa/v3 v3.4.0
require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/sys v0.26.0 // indirect
)
require (
github.com/aws/aws-sdk-go v1.51.8
github.com/gorilla/schema v1.3.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/bwmarrin/discordgo v0.28.1
github.com/gorilla/schema v1.4.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/joho/godotenv v1.5.1
github.com/replicate/replicate-go v0.18.1
golang.org/x/time v0.5.0 // indirect
)

88
go.sum
View File

@ -1,83 +1,21 @@
github.com/aws/aws-sdk-go v1.50.37 h1:gnAf6eYPSTb4QpVwugtWFqD07QXOoX7LewRrtLUx3lI=
github.com/aws/aws-sdk-go v1.50.37/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.51.3 h1:OqSyEXcJwf/XhZNVpMRgKlLA9nmbo5X8dwbll4RWxq8=
github.com/aws/aws-sdk-go v1.51.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.51.8 h1:tD7gQq5XKuKdhA6UMEH26ZNQH0s+HbL95rzv/ACz5TQ=
github.com/aws/aws-sdk-go v1.51.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/diamondburned/arikawa/v3 v3.3.5 h1:Z6BwetBMzPxTBLY2Ixxic2kdJJe0JhNvVrdbJ0gRcWg=
github.com/diamondburned/arikawa/v3 v3.3.5/go.mod h1:KPkkWr40xmEithhd15XD2dbkVY8A5+MCmZO0gRXk3qc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM=
github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/schema v1.3.0 h1:rbciOzXAx3IB8stEFnfTwO3sYa6EWlQk79XdyustPDA=
github.com/gorilla/schema v1.3.0/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/diamondburned/arikawa/v3 v3.4.0 h1:wI3Qv8h2E2dkeddF1I35nv4T6OQ3RtA21rbghW/fnd0=
github.com/diamondburned/arikawa/v3 v3.4.0/go.mod h1:WVkbdenUfsCCkptIlqSglF4eo2/HSXv74eCqGnOZaYY=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/replicate/replicate-go v0.18.1 h1:4zduLVJxdQAoyl7zKj1e2nxwJVMcT6O/sXe6/eUEtns=
github.com/replicate/replicate-go v0.18.1/go.mod h1:D2x8SztjeUKcaYnSgVu3H2DechufLJWZJB4+TLA3Rag=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

43
lib/command.go Normal file
View File

@ -0,0 +1,43 @@
package lib
import (
"time"
"github.com/bwmarrin/discordgo"
)
type CommandFunc func(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error)
func HandleCommand(commandName string, cooldownDuration time.Duration, handler CommandFunc) func(s *discordgo.Session, i *discordgo.InteractionCreate) {
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !CheckAndApplyCooldown(s, i, commandName, cooldownDuration) {
return
}
// Acknowledge the interaction immediately
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
if err != nil {
ThrowWithError(commandName, "Error deferring response: "+err.Error())
return
}
// Execute the command handler
response, err := handler(s, i)
if err != nil {
RespondWithError(s, i, "Error processing command: "+err.Error())
return
}
// Send the follow-up message with the response
_, err = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: response,
})
if err != nil {
ThrowWithError(commandName, "Error sending follow-up message: "+err.Error())
}
}
}

66
lib/cooldown.go Normal file
View File

@ -0,0 +1,66 @@
package lib
import (
"fmt"
"sync"
"time"
"github.com/bwmarrin/discordgo"
)
var (
once sync.Once
instance *CooldownManager
)
type CooldownManager struct {
cooldowns map[string]time.Time
mu sync.Mutex
}
func GetCooldownManager() *CooldownManager {
once.Do(func() {
instance = &CooldownManager{
cooldowns: make(map[string]time.Time),
}
})
return instance
}
func (cm *CooldownManager) SetCooldown(userID, commandName string, duration time.Duration) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.cooldowns[userID+":"+commandName] = time.Now().Add(duration)
}
func (cm *CooldownManager) CheckCooldown(userID, commandName string) (bool, time.Duration) {
cm.mu.Lock()
defer cm.mu.Unlock()
key := userID + ":" + commandName
if cooldownEnd, exists := cm.cooldowns[key]; exists {
if time.Now().Before(cooldownEnd) {
return false, time.Until(cooldownEnd)
}
delete(cm.cooldowns, key)
}
return true, 0
}
func CheckAndApplyCooldown(s *discordgo.Session, i *discordgo.InteractionCreate, commandName string, duration time.Duration) bool {
cooldownManager := GetCooldownManager()
user, err := GetUser(i)
if err != nil {
RespondWithError(s, i, "Error processing command: "+err.Error())
return false
}
canUse, remaining := cooldownManager.CheckCooldown(user.ID, commandName)
if !canUse {
RespondWithError(s, i, fmt.Sprintf("You can use this command again in %v", remaining.Round(time.Second)))
return false
}
cooldownManager.SetCooldown(user.ID, commandName, duration)
return true
}

View File

@ -1,28 +1,27 @@
package lib
import (
"net"
"os"
"fmt"
"log"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/bwmarrin/discordgo"
)
func ErrorResponse(err error) *api.InteractionResponseData {
var content string
switch e := err.(type) {
case *net.OpError:
content = "**Network Error:** " + e.Error()
case *os.PathError:
content = "**File Error:** " + e.Error()
default:
content = "**Error:** " + err.Error()
// respondWithError sends an error message as a response to the interaction
func RespondWithError(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
log.Printf("Responding with error: %s", message)
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: message,
Flags: discordgo.MessageFlagsEphemeral,
},
})
if err != nil {
log.Printf("Error sending error response: %v", err)
}
}
return &api.InteractionResponseData{
Content: option.NewNullableString(content),
Flags: discord.EphemeralMessage,
AllowedMentions: &api.AllowedMentions{},
}
func ThrowWithError(command, message string) error {
return fmt.Errorf("error in command '%s': %s", command, message)
}

View File

@ -1,89 +0,0 @@
package lib
import (
"fmt"
"os"
"strings"
"time"
"github.com/diamondburned/arikawa/v3/discord"
)
var manager = NewTimerManager()
// Userish is an interface that captures the common methods you may want to call
// on either a discord.Member or discord.User, including a display name.
type Userish interface {
ID() discord.UserID
Username() string
DisplayName() string
}
// memberUser adapts a discord.Member to the Userish interface.
type memberUser struct {
*discord.Member
}
func (mu memberUser) ID() discord.UserID {
return mu.User.ID
}
func (mu memberUser) Username() string {
return mu.User.Username
}
func (mu memberUser) DisplayName() string {
// If Nick is set, return it as the display name, otherwise return Username
if mu.Member.Nick != "" {
return mu.Member.Nick
}
return mu.User.Username
}
// directUser adapts a discord.User to the Userish interface.
type directUser struct {
*discord.User
}
func (du directUser) ID() discord.UserID {
return du.User.ID
}
func (du directUser) Username() string {
return du.User.Username
}
func (du directUser) DisplayName() string {
// For a direct user, the display name is just the username since no nickname is available.
return du.User.Username
}
// GetUserObject takes an interaction event and returns a Userish, which may be
// either a discord.Member or a discord.User, but exposes it through a consistent interface.
func GetUserObject(event discord.InteractionEvent) Userish {
if event.Member != nil {
return memberUser{event.Member}
} else {
return directUser{event.User}
}
}
func CooldownHandler(event discord.InteractionEvent, key string, duration time.Duration) (bool, string) {
user := GetUserObject(event)
allowList := strings.Split(os.Getenv("COOLDOWN_ALLOW_LIST"), ",")
// Check if the user ID is in the allowList
for _, id := range allowList {
if id == user.ID().String() {
return true, ""
}
}
isOnCooldown, remaining := manager.TimerRunning(user.ID().String(), key)
if isOnCooldown {
return false, fmt.Sprintf("You are on cooldown. Please wait for %v", remaining)
}
manager.StartTimer(user.ID().String(), key, duration)
return true, ""
}

47
lib/member.go Normal file
View File

@ -0,0 +1,47 @@
package lib
import (
"github.com/bwmarrin/discordgo"
)
// InteractionUser represents a user from an interaction, abstracting away the differences
// between guild members and DM users.
type InteractionUser struct {
ID string
Username string
Bot bool
}
// GetUser extracts user information from an interaction, handling both guild and DM cases.
func GetUser(i *discordgo.InteractionCreate) (*InteractionUser, error) {
if i.Member != nil && i.Member.User != nil {
// Guild interaction
return &InteractionUser{
ID: i.Member.User.ID,
Username: i.Member.User.Username,
Bot: i.Member.User.Bot,
}, nil
} else if i.User != nil {
// DM interaction
return &InteractionUser{
ID: i.User.ID,
Username: i.User.Username,
Bot: i.User.Bot,
}, nil
}
return nil, ThrowWithError("GetUser", "Unable to extract user information from interaction")
}
// IsInGuild checks if the interaction occurred in a guild.
func IsInGuild(i *discordgo.InteractionCreate) bool {
return i.Member != nil
}
// GetGuildID safely retrieves the guild ID if the interaction is from a guild.
func GetGuildID(i *discordgo.InteractionCreate) string {
if i.GuildID != "" {
return i.GuildID
}
return ""
}

View File

@ -1,143 +0,0 @@
package lib
import (
"bytes"
"context"
"errors"
"io"
"log"
"net/http"
"os"
"strings"
"github.com/replicate/replicate-go"
)
var ReplicatePromptPrefix = "Your designation is Himbot. You are an assistant bot designed to provide helpful responses with a touch of wit and sarcasm. Your responses should be natural and engaging, reflecting your unique personality. Avoid clichéd or overused expressions of sarcasm. Instead, focus on delivering information in a clever and subtly humorous way. If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information."
func ReplicateTextGeneration(prompt string) (string, error) {
client, clientError := replicate.NewClient(replicate.WithTokenFromEnv())
if clientError != nil {
return "", clientError
}
input := replicate.PredictionInput{
"prompt": prompt,
"system_prompt": ReplicatePromptPrefix,
"max_new_tokens": 4096,
}
webhook := replicate.Webhook{
URL: "https://example.com/webhook",
Events: []replicate.WebhookEventType{"start", "completed"},
}
prediction, predictionError := client.Run(context.Background(), "meta/llama-2-70b-chat:2d19859030ff705a87c746f7e96eea03aefb71f166725aee39692f1476566d48", input, &webhook)
if predictionError != nil {
return "", predictionError
}
if prediction == nil {
return "", errors.New("there was an error generating a response based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
test, ok := prediction.([]interface{})
if !ok {
return "", errors.New("there was an error generating a response based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
strs := make([]string, len(test))
for i, v := range test {
str, ok := v.(string)
if !ok {
return "", errors.New("element is not a string")
}
strs[i] = str
}
result := strings.Join(strs, "")
return result, nil
}
func ReplicateImageGeneration(prompt string, filename string) (*bytes.Buffer, error) {
client, clientError := replicate.NewClient(replicate.WithTokenFromEnv())
if clientError != nil {
return nil, clientError
}
input := replicate.PredictionInput{
"width": 1024,
"height": 1024,
"prompt": prompt,
"refine": "expert_ensemble_refiner",
"negative_prompt": "worst quality, normal quality, low quality, low res, blurry, text, watermark, logo, banner, extra digits, cropped, jpeg artifacts, signature, username, error, sketch ,duplicate, ugly, monochrome, horror, geometry, mutation, disgusting",
"num_outputs": 1,
"disable_safety_checker": true,
}
webhook := replicate.Webhook{
URL: "https://example.com/webhook",
Events: []replicate.WebhookEventType{"start", "completed"},
}
prediction, predictionError := client.Run(context.Background(), "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b", input, &webhook)
if predictionError != nil {
return nil, predictionError
}
if prediction == nil {
return nil, errors.New("there was an error generating the image based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
test, ok := prediction.([]interface{})
if !ok {
return nil, errors.New("there was an error generating the image based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
imgUrl, ok := test[0].(string)
if !ok {
return nil, errors.New("there was an error generating the image based on this prompt... please reach out to @himbothyswaggins to fix this issue")
}
imageRes, imageGetErr := http.Get(imgUrl)
if imageGetErr != nil {
return nil, imageGetErr
}
defer imageRes.Body.Close()
imageBytes, imgReadErr := io.ReadAll(imageRes.Body)
if imgReadErr != nil {
return nil, imgReadErr
}
// Save image to a temporary file
tmpfile, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write(imageBytes); err != nil {
log.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
// Upload the image to S3
_, uploadErr := UploadToS3(tmpfile.Name())
if uploadErr != nil {
log.Printf("Failed to upload image to S3: %v", uploadErr)
}
imageFile := bytes.NewBuffer(imageBytes)
return imageFile, nil
}

View File

@ -1,55 +0,0 @@
package lib
import (
"fmt"
"os"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
)
func UploadToS3(filePath string) (*s3manager.UploadOutput, error) {
bucket := os.Getenv("BUCKET_NAME")
if bucket == "" {
fmt.Println("No S3 bucket specified, skipping upload.")
return nil, nil
}
endpoint := os.Getenv("AWS_ENDPOINT_URL_S3")
accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
region := os.Getenv("AWS_REGION")
sess, err := session.NewSession(&aws.Config{
Region: &region,
Credentials: credentials.NewStaticCredentials(
accessKeyID,
secretAccessKey,
"",
),
Endpoint: aws.String(endpoint),
})
if err != nil {
return nil, fmt.Errorf("failed to create session, %v", err)
}
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file, %v", err)
}
defer file.Close()
uploader := s3manager.NewUploader(sess)
result, err := uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String(bucket),
Key: aws.String(filePath),
Body: file,
})
if err != nil {
return nil, fmt.Errorf("failed to upload file, %v", err)
}
return result, nil
}

View File

@ -1,72 +0,0 @@
package lib
import (
"sync"
"time"
)
var (
mu sync.Mutex
instance *TimerManager
)
type TimerManager struct {
timers map[string]time.Time
mu sync.Mutex
}
func NewTimerManager() *TimerManager {
return &TimerManager{
timers: make(map[string]time.Time),
}
}
func GetInstance() *TimerManager {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &TimerManager{
timers: make(map[string]time.Time),
}
}
return instance
}
func (m *TimerManager) StartTimer(userID string, key string, duration time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
m.timers[userID+":"+key] = time.Now().Add(duration)
}
func (m *TimerManager) TimerRunning(userID string, key string) (bool, time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
timerEnd, exists := m.timers[userID+":"+key]
if !exists {
return false, 0
}
if time.Now().After(timerEnd) {
delete(m.timers, userID+":"+key)
return false, 0
}
return true, time.Until(timerEnd)
}
func CancelTimer(userID string, key string) {
manager := GetInstance()
// Handle non-existent keys gracefully
if _, exists := manager.timers[userID+":"+key]; !exists {
return
}
manager.mu.Lock()
defer manager.mu.Unlock()
delete(manager.timers, userID+":"+key)
}

129
main.go
View File

@ -1,106 +1,109 @@
package main
import (
"context"
"himbot/command"
"himbot/lib"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/api/cmdroute"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/gateway"
"github.com/diamondburned/arikawa/v3/state"
"github.com/bwmarrin/discordgo"
"github.com/joho/godotenv"
)
var commands = []api.CreateCommandData{
var (
commands = []*discordgo.ApplicationCommand{
{
Name: "ping",
Description: "ping pong!",
},
{
Name: "ask",
Description: "Ask Himbot! Cooldown: 1 Minute.",
Options: []discord.CommandOption{
&discord.StringOption{
OptionName: "prompt",
Description: "The prompt to send to Himbot.",
Required: true,
},
},
},
{
Name: "pic",
Description: "Generate an image! Cooldown: 5 Minutes.",
Options: []discord.CommandOption{
&discord.StringOption{
OptionName: "prompt",
Description: "The prompt for the image generation.",
Required: true,
},
},
},
{
Name: "hs",
Description: "This command was your nickname in highschool!",
Options: []discord.CommandOption{
&discord.StringOption{
OptionName: "nickname",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "nickname",
Description: "Your nickname in highschool.",
Required: true,
},
},
},
{
Name: "markov",
Description: "Why did the Markov chain break up with its partner? Because it couldn't handle the past!",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "messages",
Description: "Number of messages to use (default: 100, max: 1000)",
Required: false,
},
},
},
}
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"ping": lib.HandleCommand("ping", 5*time.Second, command.PingCommand),
"hs": lib.HandleCommand("hs", 10*time.Second, command.HsCommand),
"markov": lib.HandleCommand("markov", 30*time.Second, command.MarkovCommand),
}
)
func main() {
godotenv.Load(".env")
token := os.Getenv("DISCORD_TOKEN")
if token == "" {
godotenv.Load(".env")
if token == "" {
log.Fatalln("No $DISCORD_TOKEN given.")
}
dg, err := discordgo.New("Bot " + token)
if err != nil {
log.Fatalf("Error creating Discord session: %v", err)
}
h := newHandler(state.New("Bot " + token))
h.s.AddInteractionHandler(h)
h.s.AddIntents(gateway.IntentGuilds)
h.s.AddHandler(func(*gateway.ReadyEvent) {
me, _ := h.s.Me()
log.Println("connected to the gateway as", me.Tag())
})
dg.AddHandler(ready)
dg.AddHandler(interactionCreate)
if err := cmdroute.OverwriteCommands(h.s, commands); err != nil {
log.Fatalln("cannot update commands:", err)
dg.Identify.Intents = discordgo.IntentsGuilds
err = dg.Open()
if err != nil {
log.Fatalf("Error opening connection: %v", err)
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
log.Println("Bot is now running. Press CTRL-C to exit.")
registerCommands(dg)
if err := h.s.Connect(ctx); err != nil {
log.Fatalln("cannot connect:", err)
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-sc
dg.Close()
}
func ready(s *discordgo.Session, event *discordgo.Ready) {
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
}
func interactionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
}
type handler struct {
*cmdroute.Router
s *state.State
func registerCommands(s *discordgo.Session) {
log.Println("Registering commands...")
registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
for i, v := range commands {
cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", v)
if err != nil {
log.Panicf("Cannot create '%v' command: %v", v.Name, err)
}
registeredCommands[i] = cmd
}
func newHandler(s *state.State) *handler {
h := &handler{s: s}
h.Router = cmdroute.NewRouter()
// Automatically defer handles if they're slow.
h.Use(cmdroute.Deferrable(s, cmdroute.DeferOpts{}))
h.AddFunc("ping", command.Ping)
h.AddFunc("ask", command.Ask)
h.AddFunc("pic", command.Pic)
h.AddFunc("hs", command.HS)
return h
}