From 77d41bb945b0b3c01a0c99b07d8eeca5b62e3100 Mon Sep 17 00:00:00 2001
From: Layla <layla@layla.gg>
Date: Sat, 17 Jun 2023 22:27:14 +0000
Subject: [PATCH] Recieving messages via the external link

---
 app/bot.go                            | 30 ++++++++++++++---
 app/component_loader.go               |  2 +-
 common/chat_sync.go                   |  6 ----
 common/external_chat.go               | 11 +++++++
 common/module.go                      |  2 +-
 discord/discord.go                    | 18 ++++++++++-
 discord/message.go                    | 45 ++++++++++++++++++++++++++
 main.go                               |  2 +-
 persistence/database.go               |  8 +++++
 persistence/sql/20230617-webhooks.sql |  9 ++++++
 persistence/sqlite3.go                | 46 +++++++++++++++++++++++++++
 11 files changed, 164 insertions(+), 15 deletions(-)
 delete mode 100644 common/chat_sync.go
 create mode 100644 common/external_chat.go
 create mode 100644 discord/message.go
 create mode 100644 persistence/sql/20230617-webhooks.sql

diff --git a/app/bot.go b/app/bot.go
index 1f76d81..445df2a 100644
--- a/app/bot.go
+++ b/app/bot.go
@@ -8,6 +8,7 @@ import (
 	"github.com/yeslayla/birdbot/core"
 	"github.com/yeslayla/birdbot/discord"
 	"github.com/yeslayla/birdbot/mastodon"
+	"github.com/yeslayla/birdbot/persistence"
 )
 
 var Version string
@@ -17,6 +18,8 @@ type Bot struct {
 	Session  *discord.Discord
 	Mastodon *mastodon.Mastodon
 
+	Database persistence.Database
+
 	// Discord Objects
 	guildID               string
 	eventCategoryID       string
@@ -31,7 +34,7 @@ type Bot struct {
 	onEventUpdatedHandlers   [](func(common.Event) error)
 	onEventCompletedHandlers [](func(common.Event) error)
 
-	gameModules []common.ChatSyncModule
+	channelChats map[string][]common.ExternalChatModule
 }
 
 // Initalize creates the discord session and registers handlers
@@ -57,7 +60,7 @@ func (app *Bot) Initialize(cfg *core.Config) error {
 			cfg.Mastodon.Username, cfg.Mastodon.Password)
 	}
 
-	app.Session = discord.New(cfg.Discord.ApplicationID, app.guildID, cfg.Discord.Token)
+	app.Session = discord.New(cfg.Discord.ApplicationID, app.guildID, cfg.Discord.Token, app.Database)
 
 	// Register Event Handlers
 	app.Session.OnReady(app.onReady)
@@ -65,6 +68,10 @@ func (app *Bot) Initialize(cfg *core.Config) error {
 	app.Session.OnEventDelete(app.onEventDelete)
 	app.Session.OnEventUpdate(app.onEventUpdate)
 
+	if len(app.channelChats) > 0 {
+		app.Session.OnMessageRecieved(app.onMessageRecieved)
+	}
+
 	return nil
 }
 
@@ -160,7 +167,20 @@ func (app *Bot) onEventComplete(d *discord.Discord, event common.Event) {
 
 }
 
-// NewBot creates a new bot instance
-func NewBot() *Bot {
-	return &Bot{}
+func (app *Bot) onMessageRecieved(d *discord.Discord, channel string, user common.User, message string) {
+	chats, ok := app.channelChats[channel]
+	if !ok {
+		return
+	}
+
+	for _, chat := range chats {
+		chat.RecieveMessage(user, message)
+	}
+}
+
+// NewBot creates a new bot instance
+func NewBot(db persistence.Database) *Bot {
+	return &Bot{
+		Database: db,
+	}
 }
diff --git a/app/component_loader.go b/app/component_loader.go
index 476ef1e..3c77cc7 100644
--- a/app/component_loader.go
+++ b/app/component_loader.go
@@ -50,7 +50,7 @@ func (loader *ComponentLoader) OnEventComplete(handler func(common.Event) error)
 	return nil
 }
 
-func (loader *ComponentLoader) RegisterChatSyncModule(ID string, plugin common.ChatSyncModule) error {
+func (loader *ComponentLoader) RegisterExternalChat(ID string, plugin common.ExternalChatModule) error {
 	return fmt.Errorf("unimplemented")
 }
 
diff --git a/common/chat_sync.go b/common/chat_sync.go
deleted file mode 100644
index e51f897..0000000
--- a/common/chat_sync.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package common
-
-type ChatSyncModule interface {
-	SendMessage(user string, message string)
-	RecieveMessage(user User, message string)
-}
diff --git a/common/external_chat.go b/common/external_chat.go
new file mode 100644
index 0000000..e191107
--- /dev/null
+++ b/common/external_chat.go
@@ -0,0 +1,11 @@
+package common
+
+type ExternalChatManager interface {
+	SendMessage(user string, message string)
+}
+
+type ExternalChatModule interface {
+	Initialize(ExternalChatManager)
+
+	RecieveMessage(user User, message string)
+}
diff --git a/common/module.go b/common/module.go
index caed7a8..bb95b54 100644
--- a/common/module.go
+++ b/common/module.go
@@ -24,5 +24,5 @@ type ModuleManager interface {
 	// Commands
 	RegisterCommand(string, ChatCommandConfiguration, func(User, map[string]any) string)
 
-	RegisterChatSyncModule(ID string, plugin ChatSyncModule) error
+	RegisterExternalChat(ID string, chat ExternalChatModule) error
 }
diff --git a/discord/discord.go b/discord/discord.go
index 7025fad..3b292a3 100644
--- a/discord/discord.go
+++ b/discord/discord.go
@@ -9,6 +9,7 @@ import (
 	"github.com/bwmarrin/discordgo"
 	"github.com/stretchr/testify/mock"
 	"github.com/yeslayla/birdbot/common"
+	"github.com/yeslayla/birdbot/persistence"
 )
 
 type Discord struct {
@@ -21,12 +22,14 @@ type Discord struct {
 	commands        map[string]*discordgo.ApplicationCommand
 	commandHandlers map[string]func(session *discordgo.Session, i *discordgo.InteractionCreate)
 
+	db persistence.Database
+
 	// Signal for shutdown
 	stop chan os.Signal
 }
 
 // New creates a new Discord session
-func New(applicationID string, guildID string, token string) *Discord {
+func New(applicationID string, guildID string, token string, db persistence.Database) *Discord {
 
 	// Create Discord Session
 	session, err := discordgo.New(fmt.Sprint("Bot ", token))
@@ -35,6 +38,8 @@ func New(applicationID string, guildID string, token string) *Discord {
 	}
 	session.ShouldReconnectOnError = true
 	return &Discord{
+		db: db,
+
 		session:         session,
 		applicationID:   applicationID,
 		guildID:         guildID,
@@ -117,6 +122,17 @@ func (discord *Discord) OnEventUpdate(handler func(*Discord, common.Event)) {
 	})
 }
 
+// OnMessageRecieved registers a handler when a message is recieved
+func (discord *Discord) OnMessageRecieved(handler func(*Discord, string, common.User, string)) {
+	discord.session.AddHandler(func(s *discordgo.Session, r *discordgo.MessageCreate) {
+		if r.GuildID != discord.guildID {
+			return
+		}
+
+		handler(discord, r.ChannelID, NewUser(r.Author), r.Content)
+	})
+}
+
 func (discord *Discord) SetStatus(status string) {
 	if err := discord.session.UpdateGameStatus(0, status); err != nil {
 		log.Fatal("Failed to update status: ", err)
diff --git a/discord/message.go b/discord/message.go
new file mode 100644
index 0000000..5f54f2a
--- /dev/null
+++ b/discord/message.go
@@ -0,0 +1,45 @@
+package discord
+
+import (
+	"log"
+
+	"github.com/bwmarrin/discordgo"
+	"github.com/yeslayla/birdbot/core"
+	"github.com/yeslayla/birdbot/persistence"
+)
+
+func (discord *Discord) WebhookSendMessage(channel *core.Channel, displayName string, message string) {
+
+	webhookData, err := discord.db.GetDiscordWebhook(channel.ID)
+	if err != nil {
+		log.Printf("Error getting webhook from DB: %s", err)
+		return
+	}
+
+	if webhookData == nil {
+		webhook, err := discord.session.WebhookCreate(channel.ID, "BirdBot", "")
+		if err != nil {
+			log.Printf("Error creating webhook: %s", err)
+			return
+		}
+
+		webhookData = &persistence.DBDiscordWebhook{
+			ID:    webhook.ID,
+			Token: webhook.Token,
+		}
+
+		if err := discord.db.SetDiscordWebhook(channel.ID, webhookData); err != nil {
+			log.Fatalf("Error failed to store webhook in DB: %s", err)
+			return
+		}
+
+	}
+
+	if _, err = discord.session.WebhookExecute(webhookData.ID, webhookData.Token, false, &discordgo.WebhookParams{
+		Content:  message,
+		Username: displayName,
+	}); err != nil {
+		log.Printf("Failed to send message over webhook: %s", err)
+	}
+
+}
diff --git a/main.go b/main.go
index 98c59dc..1794121 100644
--- a/main.go
+++ b/main.go
@@ -59,7 +59,7 @@ func main() {
 		log.Fatal("Failed to migrate db: ", err)
 	}
 
-	bot := app.NewBot()
+	bot := app.NewBot(db)
 
 	if err := bot.Initialize(cfg); err != nil {
 		log.Fatal("Failed to initialize: ", err)
diff --git a/persistence/database.go b/persistence/database.go
index df6aabf..0ac111c 100644
--- a/persistence/database.go
+++ b/persistence/database.go
@@ -4,4 +4,12 @@ package persistence
 type Database interface {
 	GetDiscordMessage(id string) (string, error)
 	SetDiscordMessage(id string, messageID string) error
+
+	GetDiscordWebhook(id string) (*DBDiscordWebhook, error)
+	SetDiscordWebhook(id string, data *DBDiscordWebhook) error
+}
+
+type DBDiscordWebhook struct {
+	ID    string
+	Token string
 }
diff --git a/persistence/sql/20230617-webhooks.sql b/persistence/sql/20230617-webhooks.sql
new file mode 100644
index 0000000..1095992
--- /dev/null
+++ b/persistence/sql/20230617-webhooks.sql
@@ -0,0 +1,9 @@
+-- +migrate Up
+CREATE TABLE IF NOT EXISTS discord_webhooks (
+    id TEXT NOT NULL PRIMARY KEY,
+    webhook_id TEXT NOT NULL,
+    webhook_token TEXT NOT NULL
+);
+
+-- +migrate Down
+DROP TABLE discord_webhooks;
\ No newline at end of file
diff --git a/persistence/sqlite3.go b/persistence/sqlite3.go
index 3b4db49..de70444 100644
--- a/persistence/sqlite3.go
+++ b/persistence/sqlite3.go
@@ -120,3 +120,49 @@ func (db *Sqlite3Database) SetDiscordMessage(id string, messageID string) error
 
 	return nil
 }
+
+// GetDiscordWebhook finds a discord webhook based on a given local id
+func (db *Sqlite3Database) GetDiscordWebhook(id string) (*DBDiscordWebhook, error) {
+
+	var data DBDiscordWebhook = DBDiscordWebhook{}
+	row := db.db.QueryRow("SELECT webhook_id, webhook_token FROM discord_webhooks WHERE id = $1", id)
+
+	if err := row.Scan(&data.ID, &data.Token); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("failed to get discord webhook from sqlite3: %s", err)
+	}
+
+	return &data, nil
+}
+
+// SetDiscordWebhook stores a discord webhook based on a given local id
+func (db *Sqlite3Database) SetDiscordWebhook(id string, data *DBDiscordWebhook) error {
+
+	statement, err := db.db.Prepare("INSERT OR IGNORE INTO discord_webhooks (id, webhook_id, webhook_token) VALUES (?, ?)")
+	if err != nil {
+		return err
+	}
+
+	result, err := statement.Exec(id, data.ID, data.Token)
+	if err != nil {
+		return err
+	}
+
+	n, _ := result.RowsAffected()
+
+	if n == 0 {
+		statement, err := db.db.Prepare("UPDATE discord_webhooks SET webhook_id = (?), webhook_token = (?) WHERE id = (?)")
+		if err != nil {
+			return err
+		}
+
+		if _, err := statement.Exec(data.ID, data.Token, id); err != nil {
+			return err
+		}
+
+	}
+
+	return nil
+}