diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..455f67d --- /dev/null +++ b/.env.sample @@ -0,0 +1,2 @@ +DISCORD_BOT_TOKEN= +DISCORD_CHANNEL_ID= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f10862a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..835238e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Build Stage +FROM golang:1.22-alpine AS builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o ip-monitor . + +# Run Stage +FROM alpine:latest +RUN apk --no-cache add ca-certificates tzdata +WORKDIR /root/ +COPY --from=builder /app/ip-monitor . +CMD ["./ip-monitor"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..0454a9c --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# IP Monitor Discord Bot + +This Go application monitors your external IP address and updates a pinned message in a specified Discord channel whenever the IP changes. It also sends a broadcast message notifying about the IP change. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Setup Instructions](#setup-instructions) + - [1. Clone the Repository](#1-clone-the-repository) + - [2. Set Up Environment Variables](#2-set-up-environment-variables) + - [3. Build the Docker Image](#3-build-the-docker-image) + - [4. Run the Docker Container](#4-run-the-docker-container) +- [Environment Variables](#environment-variables) +- [Stopping the Bot](#stopping-the-bot) +- [Updating the Bot](#updating-the-bot) +- [Notes](#notes) +- [Troubleshooting](#troubleshooting) +- [License](#license) + +## Prerequisites + +- **Docker**: Ensure Docker is installed on your system. +- **Discord Bot**: You need a Discord bot token and the ID of the channel where the bot will operate. + +## Setup Instructions + +### 1. Clone the Repository + +```bash +git clone https://github.com/yourusername/ip-monitor.git +cd ip-monitor +``` + +### 2. Create a Discord Bot + +- Go to the [Discord Developer Portal](https://discord.com/developers/applications). +- Create a new application and add a bot to it. +- Copy the bot's token; you'll need it for authentication. +- Invite the bot to your server with appropriate permissions (send messages, manage messages). \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..67bd069 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3' + +services: + ip-monitor: + build: . + container_name: ip-monitor + restart: unless-stopped + environment: + - DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN} + - DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d851b03 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module ip-monitor + +go 1.22 + +require ( + github.com/bwmarrin/discordgo v0.28.1 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/gorilla/websocket v1.5.3 // indirect + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..f7eefe6 --- /dev/null +++ b/main.go @@ -0,0 +1,138 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/joho/godotenv" +) + +const ( + ipCheckURL = "https://api.ipify.org" // Service to get external IP + checkInterval = 1 * time.Hour // Interval between IP checks +) + +var ( + previousIP string +) + +func main() { + // Load environment variables from .env file + err := godotenv.Load() + if err != nil { + log.Println("No .env file found or error reading .env file") + } + + // Now you can use os.Getenv to get the variables + botToken := os.Getenv("DISCORD_BOT_TOKEN") + channelID := os.Getenv("DISCORD_CHANNEL_ID") + + if botToken == "" || channelID == "" { + log.Fatal("Bot token or channel ID not set. Please set DISCORD_BOT_TOKEN and DISCORD_CHANNEL_ID.") + } + + // Create a new Discord session + dg, err := discordgo.New("Bot " + botToken) + if err != nil { + log.Fatalf("Error creating Discord session: %v", err) + } + + // Open a WebSocket connection to Discord + err = dg.Open() + if err != nil { + log.Fatalf("Error opening connection to Discord: %v", err) + } + defer dg.Close() + + log.Println("Bot is now running. Press CTRL+C to exit.") + + // Handle graceful shutdown + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + + // Start the IP monitoring in a goroutine + go func() { + for { + currentIP, err := getExternalIP() + if err != nil { + log.Printf("Error fetching external IP: %v", err) + time.Sleep(checkInterval) + continue + } + + if currentIP != previousIP { + log.Printf("IP changed from %s to %s", previousIP, currentIP) + err = updateDiscordMessage(dg, channelID, currentIP) + if err != nil { + log.Printf("Error updating Discord message: %v", err) + } + previousIP = currentIP + } + + time.Sleep(checkInterval) + } + }() + + // Wait for a termination signal + <-stop + log.Println("Shutting down bot.") +} + +func getExternalIP() (string, error) { + resp, err := http.Get(ipCheckURL) + if err != nil { + return "", err + } + defer resp.Body.Close() + + ip, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(ip), nil +} + +func updateDiscordMessage(dg *discordgo.Session, channelID, currentIP string) error { + // Fetch pinned messages + pinnedMessages, err := dg.ChannelMessagesPinned(channelID) + if err != nil { + return fmt.Errorf("error fetching pinned messages: %w", err) + } + + var pinnedMessageID string + if len(pinnedMessages) > 0 { + // Assume the first pinned message is the one to update + pinnedMessageID = pinnedMessages[0].ID + _, err = dg.ChannelMessageEdit(channelID, pinnedMessageID, fmt.Sprintf("Current IP Address: `%s`", currentIP)) + if err != nil { + return fmt.Errorf("error editing pinned message: %w", err) + } + } else { + // Send a new message and pin it + msg, err := dg.ChannelMessageSend(channelID, fmt.Sprintf("Current IP Address: `%s`", currentIP)) + if err != nil { + return fmt.Errorf("error sending message: %w", err) + } + + err = dg.ChannelMessagePin(channelID, msg.ID) + if err != nil { + return fmt.Errorf("error pinning message: %w", err) + } + } + + // Send a broadcast message + _, err = dg.ChannelMessageSend(channelID, fmt.Sprintf("IP Address has changed to `%s`", currentIP)) + if err != nil { + return fmt.Errorf("error sending broadcast message: %w", err) + } + + return nil +}