265 lines
7.6 KiB
Go
265 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"io"
|
|
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
type ScanState struct {
|
|
ValidInviteCount int `json:"valid_invite_count"`
|
|
ScannedInviteCount int `json:"scanned_invite_count"`
|
|
ErrorCount int `json:"error_count"`
|
|
StartTime time.Time `json:"-"`
|
|
ValidServers []ValidServerInfo `json:"valid_servers"`
|
|
}
|
|
|
|
type ValidServerInfo struct {
|
|
InviteURL string `json:"invite_url"`
|
|
ServerName string `json:"server_name"`
|
|
Members int `json:"members"`
|
|
}
|
|
|
|
var (
|
|
server = "irc.supernets.org"
|
|
port = "6667"
|
|
channel = "#superbowl"
|
|
outputChannel = "#guttertarts"
|
|
nickname = "x"
|
|
nickservPassword = "x"
|
|
|
|
httpClient = &http.Client{Timeout: 10 * time.Second}
|
|
state ScanState
|
|
stateMutex sync.Mutex
|
|
inviteLog *os.File
|
|
)
|
|
|
|
func handleIRCConnection(conn net.Conn, connected chan bool) {
|
|
scanner := bufio.NewScanner(conn)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.Contains(line, ":End of /MOTD command.") {
|
|
connected <- true
|
|
}
|
|
if strings.HasPrefix(line, "PING") {
|
|
pongResponse := strings.Split(line, " ")[1]
|
|
ircSend(conn, fmt.Sprintf("PONG %s", pongResponse))
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
fmt.Printf("Connection error: %v\n", err)
|
|
}
|
|
}
|
|
|
|
func checkInvite(url string, conn net.Conn) {
|
|
stateMutex.Lock()
|
|
state.ScannedInviteCount++
|
|
stateMutex.Unlock()
|
|
|
|
resp, err := httpClient.Get(url)
|
|
if err != nil {
|
|
stateMutex.Lock()
|
|
state.ErrorCount++
|
|
stateMutex.Unlock()
|
|
fmt.Printf("Error fetching invite: %v\n", err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == 200 {
|
|
serverName, members, isValid := parseDiscordHTML(resp.Body)
|
|
if isValid {
|
|
stateMutex.Lock()
|
|
state.ValidInviteCount++
|
|
state.ValidServers = append(state.ValidServers, ValidServerInfo{
|
|
InviteURL: url,
|
|
ServerName: serverName,
|
|
Members: members,
|
|
})
|
|
stateMutex.Unlock()
|
|
|
|
logMessage := fmt.Sprintf("Valid invite: %s | Server: %s | Members: %d\n",
|
|
url, serverName, members)
|
|
ircMessage := fmt.Sprintf("PRIVMSG %s :Valid invite: %s | Server: %s | Members: %d",
|
|
outputChannel, url, serverName, members)
|
|
|
|
fmt.Fprint(inviteLog, logMessage)
|
|
ircSend(conn, ircMessage)
|
|
} else {
|
|
fmt.Printf("Invalid invite (no 'Join the' phrase): %s\n", url)
|
|
}
|
|
} else {
|
|
fmt.Printf("Invalid invite: %s\n", url)
|
|
}
|
|
}
|
|
|
|
func parseDiscordHTML(bodyReader io.Reader) (string, int, bool) {
|
|
doc, err := html.Parse(bodyReader)
|
|
if err != nil {
|
|
fmt.Printf("Error parsing HTML: %v\n", err)
|
|
return "", 0, false
|
|
}
|
|
|
|
var serverName string
|
|
var members int
|
|
var hasJoinThe bool
|
|
|
|
var parse func(*html.Node)
|
|
parse = func(n *html.Node) {
|
|
if n.Type == html.ElementNode && n.Data == "meta" {
|
|
var property, name, content string
|
|
for _, attr := range n.Attr {
|
|
if attr.Key == "property" {
|
|
property = attr.Val
|
|
} else if attr.Key == "name" {
|
|
name = attr.Val
|
|
} else if attr.Key == "content" {
|
|
content = attr.Val
|
|
}
|
|
}
|
|
// Check for server name in og:title
|
|
if property == "og:title" {
|
|
serverName = strings.TrimSpace(strings.Replace(content, "Join the", "", 1))
|
|
serverName = strings.Replace(serverName, "Discord Server!", "", 1)
|
|
serverName = strings.TrimSpace(serverName)
|
|
}
|
|
// Check for member count in description
|
|
if name == "description" {
|
|
if strings.Contains(content, "members") {
|
|
parts := strings.Split(content, "|")
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if strings.HasSuffix(part, "members") {
|
|
fmt.Sscanf(part, "%d members", &members)
|
|
break
|
|
}
|
|
}
|
|
} else if strings.Contains(content, "other members") {
|
|
fmt.Sscanf(content, "%d other members", &members)
|
|
}
|
|
}
|
|
// Check for "Join the" phrase
|
|
if strings.Contains(content, "Join the") {
|
|
hasJoinThe = true
|
|
}
|
|
}
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
parse(c)
|
|
}
|
|
}
|
|
|
|
parse(doc)
|
|
return serverName, members, hasJoinThe
|
|
}
|
|
|
|
func ircSend(conn net.Conn, message string) {
|
|
fmt.Fprintf(conn, "%s\r\n", message)
|
|
fmt.Printf("[IRC Command Sent]: %s\n", message)
|
|
}
|
|
|
|
func generateRandomCode() string {
|
|
length := rand.Intn(9) + 2
|
|
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
|
code := make([]rune, length)
|
|
for i := range code {
|
|
code[i] = letters[rand.Intn(len(letters))]
|
|
}
|
|
return string(code)
|
|
}
|
|
|
|
func startWebDashboard() {
|
|
http.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
|
|
stateMutex.Lock()
|
|
defer stateMutex.Unlock()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(state)
|
|
})
|
|
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
stateMutex.Lock()
|
|
defer stateMutex.Unlock()
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintf(w, "<html><head><title>Invite Scanner</title></head><body>")
|
|
fmt.Fprintf(w, "<h1>Discord Invite Scanner Dashboard</h1>")
|
|
fmt.Fprintf(w, "<ul>")
|
|
fmt.Fprintf(w, "<li><strong>Valid Invites:</strong> %d</li>", state.ValidInviteCount)
|
|
fmt.Fprintf(w, "<li><strong>Scanned Invites:</strong> %d</li>", state.ScannedInviteCount)
|
|
fmt.Fprintf(w, "<li><strong>Error Count:</strong> %d</li>", state.ErrorCount)
|
|
fmt.Fprintf(w, "<li><strong>Scanning Duration:</strong> %s</li>", time.Since(state.StartTime).Truncate(time.Second))
|
|
fmt.Fprintf(w, "<li><strong>Valid Servers:</strong></li>")
|
|
fmt.Fprintf(w, "<ul>")
|
|
for _, server := range state.ValidServers {
|
|
fmt.Fprintf(w, "<li><strong>Invite:</strong> <a href='%s'>%s</a> | <strong>Server Name:</strong> %s | <strong>Members:</strong> %d</li>", server.InviteURL, server.InviteURL, server.ServerName, server.Members)
|
|
}
|
|
fmt.Fprintf(w, "</ul>")
|
|
fmt.Fprintf(w, "</ul>")
|
|
fmt.Fprintf(w, "</body></html>")
|
|
})
|
|
|
|
fmt.Println("Web dashboard running on http://localhost:55555")
|
|
http.ListenAndServe(":55555", nil)
|
|
}
|
|
|
|
func main() {
|
|
var err error
|
|
inviteLog, err = os.OpenFile("valid_invites.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
fmt.Printf("Error opening log file: %v\n", err)
|
|
return
|
|
}
|
|
defer inviteLog.Close()
|
|
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", server, port))
|
|
if err != nil {
|
|
fmt.Printf("Error connecting to IRC server: %v\n", err)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
connected := make(chan bool)
|
|
go handleIRCConnection(conn, connected)
|
|
|
|
ircSend(conn, fmt.Sprintf("NICK %s", nickname))
|
|
ircSend(conn, fmt.Sprintf("USER %s 0 * :%s", nickname, nickname))
|
|
<-connected
|
|
|
|
ircSend(conn, fmt.Sprintf("PRIVMSG NickServ :IDENTIFY %s", nickservPassword))
|
|
time.Sleep(5 * time.Second)
|
|
ircSend(conn, fmt.Sprintf("JOIN %s", channel))
|
|
time.Sleep(5 * time.Second)
|
|
ircSend(conn, fmt.Sprintf("JOIN %s", outputChannel))
|
|
|
|
state.StartTime = time.Now()
|
|
|
|
go startWebDashboard()
|
|
|
|
ticker := time.NewTicker(10 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
duration := time.Since(state.StartTime)
|
|
statusMessage := fmt.Sprintf(
|
|
"Status: Time Scanning: %s | Invites Scanned: %d | Valid Invites: %d",
|
|
duration.Truncate(time.Second), state.ScannedInviteCount, state.ValidInviteCount,
|
|
)
|
|
ircSend(conn, fmt.Sprintf("PRIVMSG %s :%s", channel, statusMessage))
|
|
case <-time.After(time.Duration(rand.Intn(30)+1) * time.Second):
|
|
randomCode := generateRandomCode()
|
|
inviteURL := fmt.Sprintf("https://discord.com/invite/%s", randomCode)
|
|
checkInvite(inviteURL, conn)
|
|
}
|
|
}
|
|
}
|