Initial Commit

This commit is contained in:
Daniel Mason 2025-01-16 00:06:17 +13:00
parent f278ffc00c
commit a8b2007f05
Signed by: idanoo
GPG key ID: 387387CDBC02F132
13 changed files with 386 additions and 0 deletions

1
.env.example Normal file
View file

@ -0,0 +1 @@
DISCORD_TOKEN=""

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.env
src/build/*
src/tmp/*

View file

@ -1,2 +1,4 @@
# GoDiscMoji # GoDiscMoji
WIP Discord bot to measure emoji usage WIP Discord bot to measure emoji usage
Very rough proof of concept..

10
air/Dockerfile Normal file
View file

@ -0,0 +1,10 @@
FROM public.ecr.aws/docker/library/golang:1.23.4
WORKDIR /app
RUN go install github.com/air-verse/air@latest
COPY src/go.mod src/go.sum ./
RUN go mod download
CMD ["air", "-c", ".air.toml"]

12
docker-compose.yml Normal file
View file

@ -0,0 +1,12 @@
services:
godiscmoji:
container_name: GoDiscMoji
build:
context: .
dockerfile: air/Dockerfile
working_dir: /app
volumes:
- ./src:/app
- ./src/build:/data
restart: always
env_file: ".env"

3
src/.air.toml Normal file
View file

@ -0,0 +1,3 @@
[build]
bin = "/bin/sh -c './build/bot'"
cmd = "/bin/sh -c 'go build -o ./build/bot ./cmd/bot/main.go'"

28
src/cmd/bot/main.go Normal file
View file

@ -0,0 +1,28 @@
package main
import (
"log/slog"
"os"
"github.com/idanoo/GoDiscMoji/internal/bot"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
// Get required vars
discordToken := os.Getenv("DISCORD_TOKEN")
if discordToken == "" {
slog.Error("DISCORD_TOKEN env var is required")
os.Exit(1)
}
// Start the bot
bot := bot.New(discordToken)
err := bot.Start()
if err != nil {
slog.Error("Error starting bot", "err", err)
os.Exit(1)
}
}

12
src/go.mod Normal file
View file

@ -0,0 +1,12 @@
module github.com/idanoo/GoDiscMoji
go 1.23.4
require (
github.com/bwmarrin/discordgo v0.28.1 // indirect
github.com/golang-migrate/migrate v3.5.4+incompatible // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
)

16
src/go.sum Normal file
View file

@ -0,0 +1,16 @@
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View file

@ -0,0 +1,75 @@
package bot
import (
"log/slog"
"strings"
"github.com/bwmarrin/discordgo"
)
var (
commands = []*discordgo.ApplicationCommand{
{
Name: "show-top-emojis",
Description: "Show top emojis",
},
{
Name: "show-top-users",
Description: "Show top users",
},
}
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"show-top-emojis": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
top, err := b.Db.GetTopEmojisForGuild(i.GuildID, 5)
if err != nil {
slog.Error("Error getting top emojis", "err", err)
return
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Top Emojis:\n" + strings.Join(top, "\n"),
},
})
},
"show-top-users": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
top, err := b.Db.GetTopUsersForGuild(i.GuildID, 5)
if err != nil {
slog.Error("Error getting top users", "err", err)
return
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Top Users:\n" + strings.Join(top, "\n"),
},
})
},
}
)
// RegisterCommands
func (bot *Bot) RegisterCommands() {
bot.registeredCommands = make([]*discordgo.ApplicationCommand, len(commands))
for i, v := range commands {
cmd, err := bot.DiscordSession.ApplicationCommandCreate(bot.DiscordSession.State.User.ID, "", v)
if err != nil {
slog.Error("Error creating command", "err", err)
}
bot.registeredCommands[i] = cmd
}
}
// DeregisterCommands - Deregister all commands
func (bot *Bot) DeregisterCommands() {
for _, v := range bot.registeredCommands {
err := bot.DiscordSession.ApplicationCommandDelete(bot.DiscordSession.State.User.ID, "", v.ID)
if err != nil {
slog.Error("Error deleting command", "err", err)
}
}
}

108
src/internal/bot/main.go Normal file
View file

@ -0,0 +1,108 @@
package bot
import (
"log/slog"
"os"
"os/signal"
"github.com/bwmarrin/discordgo"
"github.com/idanoo/GoDiscMoji/internal/db"
)
var b *Bot
type Bot struct {
DiscordSession *discordgo.Session
Token string
registeredCommands []*discordgo.ApplicationCommand
Db *db.Database
}
// New - Return new instance of *Bot
func New(token string) *Bot {
return &Bot{
Token: token,
registeredCommands: make([]*discordgo.ApplicationCommand, len(commands)),
}
}
// Start - Boots the bot!
func (bot *Bot) Start() error {
// Boot db
db, err := db.InitDb()
if err != nil {
return err
}
defer db.CloseDbConn()
bot.Db = db
// Boot discord
discord, err := discordgo.New("Bot " + bot.Token)
if err != nil {
return err
}
bot.DiscordSession = discord
// Add command handler
bot.DiscordSession.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
})
// Add handlers
// bot.DiscordSession.AddHandler(bot.HandleMessage)
bot.DiscordSession.AddHandler(bot.HandleReaction)
// Load session
err = discord.Open()
if err != nil {
return err
}
defer discord.Close()
// Register commands
bot.RegisterCommands()
b = bot
// Keep running untill there is NO os interruption (ctrl + C)
slog.Info("Bot is now running. Press CTRL-C to exit.")
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
// Deregister any commands we created
bot.DeregisterCommands()
return nil
}
// func (bot *Bot) HandleMessage(discord *discordgo.Session, message *discordgo.MessageCreate) {
// // Don't reply to self
// if message.Author.ID == discord.State.User.ID {
// return
// }
// slog.Info("Message received", "message", message.Content)
// // respond to user message if it contains `!help` or `!bye`
// switch {
// case strings.HasPrefix(message.Content, "!help"):
// _, err := discord.ChannelMessageSend(message.ChannelID, "Hello World😃")
// if err != nil {
// slog.Error("Failed to send message", "err", err)
// }
// case strings.Contains(message.Content, "!bye"):
// discord.ChannelMessageSend(message.ChannelID, "Good Bye👋")
// // add more cases if required
// }
// }
// HandleReaction - Simply log it
func (bot *Bot) HandleReaction(discord *discordgo.Session, reaction *discordgo.MessageReactionAdd) {
err := bot.Db.LogEmojiUsage(reaction.GuildID, reaction.ChannelID, reaction.UserID, reaction.Emoji.Name)
if err != nil {
slog.Error("Failed to log emoji usage", "err", err)
}
}

View file

@ -0,0 +1,55 @@
package db
import (
"database/sql"
_ "github.com/golang-migrate/migrate/source/file"
_ "github.com/mattn/go-sqlite3"
)
type Database struct {
db *sql.DB
}
// InitDb - Initialize DB connection
func InitDb() (*Database, error) {
ddb := Database{}
db, err := sql.Open("sqlite3", "file:/data/db.sqlite?loc=auto")
if err != nil {
return &ddb, err
}
db.SetConnMaxLifetime(0)
db.SetMaxOpenConns(5)
db.SetMaxIdleConns(2)
err = db.Ping()
if err != nil {
return &ddb, err
}
ddb.db = db
return ddb.runMigrations()
}
// runMigrations - Run migrations for connection
func (db *Database) runMigrations() (*Database, error) {
// Hacked up af - Rerunnable
_, err := db.db.Exec("CREATE TABLE IF NOT EXISTS `emoji_usage` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
"`guild_id` TEXT, " +
"`channel_id` TEXT, " +
"`user_id` TEXT, " +
"`emoji_id` TEXT, " +
"`timestamp` DATETIME, `viewed` INT DEFAULT 0" +
")")
return db, err
}
// CloseDbConn - Closes DB connection
func (db *Database) CloseDbConn() {
db.db.Close()
}

View file

@ -0,0 +1,61 @@
package db
import "fmt"
// LogEmojiUsage - Log usage
func (db *Database) LogEmojiUsage(guildID, channelID, userID, emojiID string) error {
_, err := db.db.Exec(
"INSERT INTO `emoji_usage` (`guild_id`, `channel_id`, `user_id`, `emoji_id`, `timestamp`) VALUES (?,?,?,?, datetime())",
guildID, channelID, userID, emojiID,
)
return err
}
// GetTopUsersForGuild - Report usage
func (db *Database) GetTopUsersForGuild(guildID string, num int) ([]string, error) {
var data []string
row, err := db.db.Query(
"SELECT user_id, count(*) FROM `emoji_usage` WHERE `guild_id` = ? GROUP BY user_id ORDER BY count(*) DESC LIMIT ?",
guildID,
num,
)
if err != nil {
return data, err
}
defer row.Close()
for row.Next() {
var user string
var count int64
row.Scan(&user, &count)
data = append(data, fmt.Sprintf("<@%s>: %d", user, count))
}
return data, nil
}
// GetTopEmojisForGuild - Report usage
func (db *Database) GetTopEmojisForGuild(guildID string, num int) ([]string, error) {
var data []string
row, err := db.db.Query(
"SELECT emoji_id, count(*) FROM `emoji_usage` WHERE `guild_id` = ? GROUP BY emoji_id ORDER BY count(*) DESC LIMIT ?",
guildID,
num,
)
if err != nil {
return data, err
}
defer row.Close()
for row.Next() {
var user string
var count int64
row.Scan(&user, &count)
data = append(data, fmt.Sprintf("%s: %d", user, count))
}
return data, nil
}