mirror of
https://github.com/idanoo/GoDiscMoji
synced 2025-07-01 10:22:14 +00:00
Initial Commit
This commit is contained in:
parent
f278ffc00c
commit
a8b2007f05
13 changed files with 386 additions and 0 deletions
1
.env.example
Normal file
1
.env.example
Normal file
|
@ -0,0 +1 @@
|
|||
DISCORD_TOKEN=""
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.env
|
||||
src/build/*
|
||||
src/tmp/*
|
|
@ -1,2 +1,4 @@
|
|||
# GoDiscMoji
|
||||
WIP Discord bot to measure emoji usage
|
||||
|
||||
Very rough proof of concept..
|
10
air/Dockerfile
Normal file
10
air/Dockerfile
Normal 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
12
docker-compose.yml
Normal 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
3
src/.air.toml
Normal 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
28
src/cmd/bot/main.go
Normal 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
12
src/go.mod
Normal 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
16
src/go.sum
Normal 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=
|
75
src/internal/bot/commands.go
Normal file
75
src/internal/bot/commands.go
Normal 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
108
src/internal/bot/main.go
Normal 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)
|
||||
}
|
||||
}
|
55
src/internal/db/database.go
Normal file
55
src/internal/db/database.go
Normal 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()
|
||||
}
|
61
src/internal/db/emoji_usage.go
Normal file
61
src/internal/db/emoji_usage.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue