diff --git a/.env-sample b/.env-sample new file mode 100644 index 0000000..9dc84f2 --- /dev/null +++ b/.env-sample @@ -0,0 +1,14 @@ +# Discord bot credentials +DISCORD_BOT_TOKEN=your-bot-token-here +DISCORD_CHANNEL_ID=your-channel-id-here + +# Timing configuration +SEND_MESSAGE_INTERVAL_MIN=5 +SEND_MESSAGE_INTERVAL_MAX=10 +SEND_MESSAGE_UNIT=second # "second" | "seconds" | "minute" | "minutes" | "millisecond" | "milliseconds" + +# Delay before deleting a conspiracy message (eg "3s", "500ms", "1m") +DELETE_CONSPIRACY_DELAY=3s + +# Probability of sending a conspiracy message (0.0 to 1.0) +CONSPIRACY_PROBABILITY=0.4 diff --git a/.github/workflows/publish-latest.yml b/.github/workflows/publish-latest.yml new file mode 100644 index 0000000..852148d --- /dev/null +++ b/.github/workflows/publish-latest.yml @@ -0,0 +1,33 @@ +name: publish-latest +on: + push: + branches: [main] +concurrency: + group: ${{ github.event.workflow_run.head_repository.full_name }}::${{ github.event.workflow_run.head_branch }}::${{ github.workflow }} + cancel-in-progress: true +jobs: + publish-latest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - name: Get image repository + run: echo IMAGE_REPOSITORY=$(echo ghcr.io/the-programmers-hangout/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV + - name: Login to registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: the-programmers-hangout + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64 + pull: true + push: true + tags: ${{ env.IMAGE_REPOSITORY }}:latest diff --git a/.gitignore b/.gitignore index 915d6c8..95ea213 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .env bin +vendor +.idea diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8741a99 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:alpine AS builder +RUN apk --update add ca-certificates +WORKDIR /app +COPY . ./ +RUN go mod tidy +ENV DISCORD_BOT_TOKEN="" +ENV GIT_COMMIT="" +ENV BUILD_DATE="" +RUN go build -ldflags="-s -w -X main.CommitHash=$GIT_COMMIT -X main.BuildDate=$BUILD_DATE" -o bin/swiftspiracybot cmd/bot/main.go +ENTRYPOINT ["bin/swiftspiracybot"] diff --git a/README.md b/README.md index 505bc41..ca7fa59 100644 --- a/README.md +++ b/README.md @@ -1 +1,28 @@ # swiftspiracy + +A playful Discord bot that sends random praises and (occasionally) a conspiracy theory — and cleans up after itself. + +## Setup + +### Configuration + +Create a `.env` file in the root of your project with the following keys: + +- `DISCORD_BOT_TOKEN`: Your bot token from the Discord developer portal. +- `DISCORD_CHANNEL_ID`: The ID of the channel where the bot should send messages. + +- `SEND_MESSAGE_INTERVAL_MIN`: Minimum interval between messages (as an integer). +- `SEND_MESSAGE_INTERVAL_MAX`: Maximum interval between messages (as an integer). +- `SEND_MESSAGE_UNIT`: Time unit for the interval. Acceptable values: "second", "seconds", "minute", "minutes", + "millisecond", "milliseconds". + +- `DELETE_CONSPIRACY_DELAY`: Time to wait before deleting a conspiracy message. Use Go duration format (eg "3s", + "500ms", "1m"). +- `CONSPIRACY_PROBABILITY`: Probability (from 0.0 to 1.0) that a conspiracy message is sent after a praise. Eg, 0.4 = + 40% chance. + +You can view the [.env-sample](./.env-sample) as an example. + +## Notes + +- The bot gracefully shuts down on CTRL+C. diff --git a/cmd/bot/main.go b/cmd/bot/main.go index 3a90826..e0db7ae 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -8,6 +8,7 @@ import ( "math/rand" "os" "os/signal" + "strconv" "syscall" "time" @@ -31,29 +32,71 @@ var ( conspiracyIndex int ) -const ( - // TODO: update value after debugging - SendMessageIntervalMin = 5 - // TODO: update value after debugging - SendMessageIntervalMax = 10 - // TODO: update value after debugging - SendMessageUnit = time.Second - // TODO: update value after debugging - DeleteConspiracyDelay = 3 * time.Second - // TODO: update value after debugging - ConspiracyProbability = 0.4 +var ( + botToken string + channelID string ) -func main() { - // Load environment variables +var ( + SendMessageIntervalMin int + SendMessageIntervalMax int + SendMessageUnit time.Duration + DeleteConspiracyDelay time.Duration + ConspiracyProbability float64 +) + +func loadEnvConfig() error { if err := godotenv.Load(); err != nil { log.Println("[!] No .env file found, using system environment variables.") } - // Retrieve token and channel ID - botToken, channelID := os.Getenv("DISCORD_BOT_TOKEN"), os.Getenv("DISCORD_CHANNEL_ID") - if botToken == "" || channelID == "" { - log.Fatal("[x] Missing DISCORD_BOT_TOKEN or DISCORD_CHANNEL_ID in .env") + botToken = os.Getenv("DISCORD_BOT_TOKEN") + if botToken == "" { + return fmt.Errorf("Missing DISCORD_BOT_TOKEN") + } + channelID = os.Getenv("DISCORD_CHANNEL_ID") + if botToken == "" { + return fmt.Errorf("Missing DISCORD_CHANNEL_ID") + } + + var err error + + SendMessageIntervalMin, err = strconv.Atoi(os.Getenv("SEND_MESSAGE_INTERVAL_MIN")) + if err != nil { + return fmt.Errorf("SEND_MESSAGE_INTERVAL_MIN: %w", err) + } + SendMessageIntervalMax, err = strconv.Atoi(os.Getenv("SEND_MESSAGE_INTERVAL_MAX")) + if err != nil { + return fmt.Errorf("SEND_MESSAGE_INTERVAL_MAX: %w", err) + } + + switch os.Getenv("SEND_MESSAGE_UNIT") { + case "second", "seconds": + SendMessageUnit = time.Second + case "minute", "minutes": + SendMessageUnit = time.Minute + case "millisecond", "milliseconds": + SendMessageUnit = time.Millisecond + default: + return fmt.Errorf("unsupported SEND_MESSAGE_UNIT: %s", os.Getenv("SEND_MESSAGE_UNIT")) + } + + DeleteConspiracyDelay, err = time.ParseDuration(os.Getenv("DELETE_CONSPIRACY_DELAY")) + if err != nil { + return fmt.Errorf("DELETE_CONSPIRACY_DELAY: %w", err) + } + + ConspiracyProbability, err = strconv.ParseFloat(os.Getenv("CONSPIRACY_PROBABILITY"), 64) + if err != nil { + return fmt.Errorf("CONSPIRACY_PROBABILITY: %w", err) + } + + return nil +} + +func main() { + if err := loadEnvConfig(); err != nil { + log.Fatalf("[x] Failed to load configuration: %v", err) } // Load messages at build time @@ -127,7 +170,7 @@ func startScheduler(s *discordgo.Session, channelID string) { praiseIndex++ // Chance to send a conspiracy theory - if rand.Float32() < ConspiracyProbability { + if rand.Float64() < ConspiracyProbability { discordMessage := sendMessage(conspiracies[conspiracyIndex%len(conspiracies)], s, channelID) conspiracyIndex++ diff --git a/go.mod b/go.mod index f5cb850..5e741e2 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module github.com/khinshankhan/swiftspiracy go 1.23.4 require ( - github.com/bwmarrin/discordgo v0.28.1 // indirect + github.com/bwmarrin/discordgo v0.28.1 + github.com/joho/godotenv v1.5.1 +) + +require ( github.com/gorilla/websocket v1.4.2 // indirect - github.com/joho/godotenv v1.5.1 // indirect golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect )