Compare commits
35 Commits
atridadl-p
...
atridadl-p
Author | SHA1 | Date | |
---|---|---|---|
a33c3185db | |||
13bf11abce
|
|||
0130ec538c
|
|||
26e931f44f
|
|||
7328a0139e
|
|||
b00028d8e3
|
|||
999605c2fc
|
|||
d2f94dec79
|
|||
8c2d7189c7
|
|||
0406e5cbf7
|
|||
44fc4e8dd1
|
|||
e76a886d5b
|
|||
18aa6846e9
|
|||
12e3414833
|
|||
e18d1c9aeb
|
|||
a615512ed7
|
|||
9385a31364 | |||
ba144dafa1 | |||
299cbcfcee | |||
b9e72fa537 | |||
605e40f16c | |||
5fa146dff9 | |||
d9d0c84135 | |||
a1fd9aec29 | |||
d3badf183c | |||
9ad6aa0c59 | |||
b317ebed86 | |||
3f908c4153 | |||
014dd57d93 | |||
db7b4e8e45 | |||
e89fdb4ac9 | |||
2038fa6df5 | |||
655409ea1a | |||
8a944a1e31 | |||
4eeeb39506 |
10
.dockerignore
Normal file
10
.dockerignore
Normal 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
|
@ -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
18
.github/workflows/fly-deploy.yml
vendored
Normal 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 }}
|
15
.github/workflows/fly.yml
vendored
15
.github/workflows/fly.yml
vendored
@ -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 }}
|
18
Dockerfile
18
Dockerfile
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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{},
|
||||
}
|
||||
}
|
@ -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
115
command/markov.go
Normal 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, " ")
|
||||
}
|
@ -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},
|
||||
}
|
||||
}
|
@ -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
13
docker-compose.dev.yml
Normal 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
|
18
fly.toml
18
fly.toml
@ -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
17
go.mod
@ -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
88
go.sum
@ -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
43
lib/command.go
Normal 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
66
lib/cooldown.go
Normal 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
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString(content),
|
||||
Flags: discord.EphemeralMessage,
|
||||
AllowedMentions: &api.AllowedMentions{},
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func ThrowWithError(command, message string) error {
|
||||
return fmt.Errorf("error in command '%s': %s", command, message)
|
||||
}
|
||||
|
@ -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
47
lib/member.go
Normal 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 ""
|
||||
}
|
143
lib/replicate.go
143
lib/replicate.go
@ -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
|
||||
}
|
55
lib/s3.go
55
lib/s3.go
@ -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: ®ion,
|
||||
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
|
||||
}
|
72
lib/timer.go
72
lib/timer.go
@ -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)
|
||||
}
|
133
main.go
133
main.go
@ -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 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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user