Extended the scripting interface. Fixes
The following new callbacks were added:
* onMessageReceive
* onMessageDelete
The following callable functions were added:
* getCurrentChannel
* getCurrentGuild
* printLineToConsole
* printToConsole
* triggerNotification
Detailed documentation is gonna follow soon.
MODIFY
scripting/engine.go
scripting/engine.go
package scripting
-import "io"
+import (
+ "github.com/Bios-Marcel/discordgo"
+ "io"
+)
// Engine describes a type that is capable of handling events from the main
// application and allows mutation of data.
type Engine interface {
// LoadScripts loads scripts from a directory into the VM
LoadScripts(string) error
- // OnMessageSend handles the client sending new messages
- OnMessageSend(string) string
// SetErrorOutput sets the io.Writer that the errors are piped into.
SetErrorOutput(errorOutput io.Writer)
+
+ // OnMessageSend handles the client sending new messages, allowing scripts
+ // to manipulate what's sent. The order of script execution is undefined
+ // and should therefore be expected to be random.
+ OnMessageSend(string) string
+ // OnMessageReceive gets called every time a message is received, no matter
+ // in which channel or guild.
+ OnMessageReceive(*discordgo.Message)
+ // OnMessageDelete gets called every time a message gets deleted, no matter
+ // in which channel or guild.
+ OnMessageDelete(*discordgo.Message)
+
+ SetTriggerNotificationFunction(func(string, string))
+ SetPrintToConsoleFunction(func(string))
+ SetPrintLineToConsoleFunction(func(string))
+
+ SetGetCurrentGuildFunction(func() string)
+ SetGetCurrentChannelFunction(func() string)
}

MODIFY
scripting/js/javascript.go
scripting/js/javascript.go
@@ -2,8 +2,10 @@ package js
import (
"fmt"
+ "github.com/Bios-Marcel/discordgo"
"io"
"io/ioutil"
+ "log"
"os"
"path/filepath"
"strings"
@@ -13,7 +15,8 @@ import (
"github.com/robertkrimen/otto"
)
-//FIXME Remove global state
+// This decleration makes sure that JavaScriptEngine complies with the
+// Engine interface.
var _ scripting.Engine = &JavaScriptEngine{}
// JavaScriptEngine stores scripting engine state
@@ -23,12 +26,8 @@ type JavaScriptEngine struct {
}
// New instantiates a new scripting engine
-func New() (engine *JavaScriptEngine) {
- engine = &JavaScriptEngine{
- vms: make([]*otto.Otto, 0),
- }
-
- return
+func New() *JavaScriptEngine {
+ return &JavaScriptEngine{}
}
// LoadScripts implements Engine
@@ -89,14 +88,22 @@ func (engine *JavaScriptEngine) SetErrorOutput(errorOutput io.Writer) {
engine.errorOutput = errorOutput
}
+func escapeNewlines(parameter string) string {
+ return strings.NewReplacer(
+ "\\", "\\\\",
+ "\n", "\\n",
+ "\"", "\\\"").
+ Replace(parameter)
+}
+
// OnMessageSend implements Engine
func (engine *JavaScriptEngine) OnMessageSend(oldText string) (newText string) {
newText = oldText
for _, vm := range engine.vms {
jsValue, jsError := vm.Run(fmt.Sprintf("onMessageSend(\"%s\")", escapeNewlines(newText)))
if jsError != nil {
- if engine.errorOutput != nil {
- fmt.Fprintf(engine.errorOutput, "Error occurred during execution of javascript: %s", jsError.Error())
+ if engine.errorOutput != nil && !jsValue.IsUndefined() {
+ fmt.Fprintf(engine.errorOutput, "Error occurred during execution of javascript: %s\n", jsError.Error())
}
//This script failed, go to next one
continue
@@ -107,10 +114,134 @@ func (engine *JavaScriptEngine) OnMessageSend(oldText string) (newText string) {
return
}
-func escapeNewlines(parameter string) string {
- return strings.NewReplacer(
- "\\", "\\\\",
- "\n", "\\n",
- "\"", "\\\"").
- Replace(parameter)
+// OnMessageRead implements Engine
+func (engine *JavaScriptEngine) OnMessageReceive(message *discordgo.Message) {
+ for _, vm := range engine.vms {
+ onMessageReceiveJS, resolveError := vm.Get("onMessageReceive")
+ if onMessageReceiveJS.IsUndefined() {
+ continue
+ }
+ if resolveError != nil {
+ log.Printf("Error resolving function onMessageReceive: %s\n", resolveError)
+ continue
+ }
+
+ messageToJS, toValueError := vm.ToValue(*message)
+ if toValueError != nil {
+ log.Printf("Error converting message to Otto value: %s\n", toValueError)
+ } else {
+ _, callError := onMessageReceiveJS.Call(otto.NullValue(), messageToJS)
+ if callError != nil {
+ log.Printf("Error calling onMessageReceive: %s\n", callError)
+ }
+ }
+ }
+}
+
+// OnMessageDelete implements Engine
+func (engine *JavaScriptEngine) OnMessageDelete(message *discordgo.Message) {
+ for _, vm := range engine.vms {
+ onMessageDeleteJS, resolveError := vm.Get("onMessageDelete")
+ if onMessageDeleteJS.IsUndefined() {
+ continue
+ }
+ if resolveError != nil {
+ log.Printf("Error resolving function onMessageDelete: %s\n", resolveError)
+ continue
+ }
+
+ messageToJS, toValueError := vm.ToValue(*message)
+ if toValueError != nil {
+ log.Printf("Error converting message to Otto value: %s\n", toValueError)
+ } else {
+ _, callError := onMessageDeleteJS.Call(otto.NullValue(), messageToJS)
+ if callError != nil {
+ log.Printf("Error calling onMessageDelete: %s\n", callError)
+ }
+ }
+ }
+}
+
+// SetTriggerNotificationFunction implements Engine
+func (engine *JavaScriptEngine) SetTriggerNotificationFunction(function func(title, text string)) {
+ triggerNotification := func(call otto.FunctionCall) otto.Value {
+ title, argError := call.Argument(0).ToString()
+ if argError != nil {
+ log.Printf("Error invoking triggerNotification in JS engine: %s\n", argError)
+ return otto.NullValue()
+ }
+ text, argError := call.Argument(1).ToString()
+ if argError != nil {
+ log.Printf("Error invoking triggerNotification in JS engine: %s\n", argError)
+ return otto.NullValue()
+ }
+ function(title, text)
+ return otto.NullValue()
+ }
+ engine.setFunctionOnVMs("triggerNotification", triggerNotification)
+}
+
+// SetPrintToConsoleFunction implements Engine
+func (engine *JavaScriptEngine) SetPrintToConsoleFunction(function func(text string)) {
+ printToConsole := func(call otto.FunctionCall) otto.Value {
+ text, argError := call.Argument(0).ToString()
+ if argError != nil {
+ log.Printf("Error invoking printToConsole in JS engine: %s\n", argError)
+ } else {
+ function(text)
+ }
+ return otto.UndefinedValue()
+ }
+ engine.setFunctionOnVMs("printToConsole", printToConsole)
+}
+
+// SetPrintLineToConsoleFunction implements Engine
+func (engine *JavaScriptEngine) SetPrintLineToConsoleFunction(function func(text string)) {
+ printLineToConsole := func(call otto.FunctionCall) otto.Value {
+ text, argError := call.Argument(0).ToString()
+ if argError != nil {
+ log.Printf("Error invoking printLineToConsole in JS engine: %s\n", argError)
+ } else {
+ function(text)
+ }
+ return otto.UndefinedValue()
+ }
+ engine.setFunctionOnVMs("printLineToConsole", printLineToConsole)
+}
+
+// SetGetCurrentGuildFunction implements Engine
+func (engine *JavaScriptEngine) SetGetCurrentGuildFunction(function func() string) {
+ getCurrentGuild := func(call otto.FunctionCall) otto.Value {
+ guildID, callError := call.Otto.ToValue(function())
+ if callError != nil {
+ log.Printf("Error calling getCurrentGuild: %s\n", callError)
+ return otto.NullValue()
+ } else {
+ return guildID
+ }
+ }
+ engine.setFunctionOnVMs("getCurrentGuild", getCurrentGuild)
+}
+
+// SetGetCurrentChannelFunction implements Engine
+func (engine *JavaScriptEngine) SetGetCurrentChannelFunction(function func() string) {
+ getCurrentChannel := func(call otto.FunctionCall) otto.Value {
+ guildID, callError := call.Otto.ToValue(function())
+ if callError != nil {
+ log.Printf("Error calling getCurrentChannel: %s\n", callError)
+ return otto.NullValue()
+ } else {
+ return guildID
+ }
+ }
+ engine.setFunctionOnVMs("getCurrentChannel", getCurrentChannel)
+}
+
+func (engine *JavaScriptEngine) setFunctionOnVMs(name string, function func(call otto.FunctionCall) otto.Value) {
+ for _, vm := range engine.vms {
+ setError := vm.Set(name, function)
+ if setError != nil {
+ log.Printf("Error setting function %s: %s", name, setError)
+ }
+ }
}

MODIFY
ui/window.go
ui/window.go
@@ -113,10 +113,9 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
window.commandView = NewCommandView(window.ExecuteCommand)
log.SetOutput(window.commandView)
-
- window.jsEngine.SetErrorOutput(window.commandView.commandOutput)
- if err := window.jsEngine.LoadScripts(config.GetScriptDirectory()); err != nil {
- return nil, err
+ initError := window.initJSEngine()
+ if initError != nil {
+ return nil, initError
}
guilds := readyEvent.Guilds
@@ -916,6 +915,44 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
return window, nil
}
+func (window *Window) initJSEngine() error {
+ window.jsEngine.SetErrorOutput(window.commandView.commandOutput)
+ if err := window.jsEngine.LoadScripts(config.GetScriptDirectory()); err != nil {
+ return err
+ }
+
+ window.jsEngine.SetTriggerNotificationFunction(func(title, text string) {
+ notifyError := beeep.Notify("Cordless - "+title, text, "assets/information.png")
+ if notifyError != nil {
+ log.Printf("["+tviewutil.ColorToHex(config.GetTheme().ErrorColor)+"]Error sending notification:\n\t[%s]%s\n", tviewutil.ColorToHex(config.GetTheme().ErrorColor), notifyError)
+ }
+ })
+
+ window.jsEngine.SetGetCurrentGuildFunction(func() string {
+ if window.selectedGuild != nil {
+ return window.selectedGuild.ID
+ }
+ return ""
+ })
+
+ window.jsEngine.SetGetCurrentChannelFunction(func() string {
+ if window.selectedChannel != nil {
+ return window.selectedChannel.ID
+ }
+ return ""
+ })
+
+ window.jsEngine.SetPrintToConsoleFunction(func(text string) {
+ log.Print(text)
+ })
+
+ window.jsEngine.SetPrintLineToConsoleFunction(func(text string) {
+ log.Println(text)
+ })
+
+ return nil
+}
+
func (window *Window) loadPrivateChannel(channel *discordgo.Channel) {
window.LoadChannel(channel)
window.RefreshLayout()
@@ -1479,6 +1516,7 @@ func (window *Window) startMessageHandlerRoutines(input, edit, delete chan *disc
for tempMessage := range input {
message := tempMessage
window.session.State.MessageAdd(message)
+ go window.jsEngine.OnMessageReceive(message)
channel, stateError := window.session.State.Channel(message.ChannelID)
if stateError != nil {
@@ -1595,6 +1633,7 @@ func (window *Window) startMessageHandlerRoutines(input, edit, delete chan *disc
for messageDeleted := range delete {
tempMessageDeleted := messageDeleted
window.session.State.MessageRemove(tempMessageDeleted)
+ go window.jsEngine.OnMessageDelete(tempMessageDeleted)
window.chatView.Lock()
if window.selectedChannel != nil && window.selectedChannel.ID == tempMessageDeleted.ChannelID {
window.QueueUpdateDrawSynchronized(func() {

Extended the scripting interface. Fixes #153

The following new callbacks were added:
* onMessageReceive
* onMessageDelete

The following callable functions were added:
* getCurrentChannel
* getCurrentGuild
* printLineToConsole
* printToConsole
* triggerNotification

Detailed documentation is gonna follow soon.