Update chatbot program
* New object macros: dm, takeback, report * Bugfixes
This commit is contained in:
parent
9c05af2c2e
commit
9c8ff88f6e
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
settings.toml
|
settings.toml
|
||||||
|
chatbot/
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -4,4 +4,5 @@ run:
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build:
|
build:
|
||||||
go build -o BareRTC cmd/BareRTC/main.go
|
go build -o BareRTC cmd/BareRTC/main.go
|
||||||
|
go build -o BareBot cmd/BareBot/main.go
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
> topic PublicChannel
|
> topic PublicChannel
|
||||||
|
|
||||||
// Users saying hello = react with a wave emoji.
|
// Users saying hello = react with a wave emoji.
|
||||||
+ [*] (hello|hi|hey|howdy|hola|hai|yo) [*]
|
+ [*] (hello|hi|hey|howdy|hola|hai|yo|greetings) [*]
|
||||||
- <call>react <get messageID> 👋</call>
|
- <call>react <get messageID> 👋</call>
|
||||||
^ <noreply>
|
^ <noreply>
|
||||||
|
|
||||||
|
|
|
@ -73,10 +73,16 @@ func (c *Client) Run() error {
|
||||||
c.jwt = token
|
c.jwt = token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the WebSocket URL.
|
||||||
|
wss, err := WebSocketURL(c.url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't get WebSocket URL from %s: %s", c.url, err)
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
c.ctx = ctx
|
c.ctx = ctx
|
||||||
|
|
||||||
conn, _, err := websocket.Dial(ctx, c.url, nil)
|
conn, _, err := websocket.Dial(ctx, wss, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("dialing websocket URL (%s): %s", c.url, err)
|
return fmt.Errorf("dialing websocket URL (%s): %s", c.url, err)
|
||||||
}
|
}
|
||||||
|
@ -94,13 +100,6 @@ func (c *Client) Run() error {
|
||||||
return fmt.Errorf("sending login message: %s", err)
|
return fmt.Errorf("sending login message: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Testing!
|
|
||||||
c.Send(messages.Message{
|
|
||||||
Action: messages.ActionMessage,
|
|
||||||
Channel: "lobby",
|
|
||||||
Message: "Hello, world! BareBot client connected!",
|
|
||||||
})
|
|
||||||
|
|
||||||
// Enter the Read Loop
|
// Enter the Read Loop
|
||||||
for {
|
for {
|
||||||
var msg messages.Message
|
var msg messages.Message
|
||||||
|
|
|
@ -12,6 +12,9 @@ import (
|
||||||
"github.com/aichaos/rivescript-go"
|
"github.com/aichaos/rivescript-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Number of recent chat messages to hold onto.
|
||||||
|
const ScrollbackBuffer = 500
|
||||||
|
|
||||||
// BotHandlers holds onto a set of handler functions for the BareBot.
|
// BotHandlers holds onto a set of handler functions for the BareBot.
|
||||||
type BotHandlers struct {
|
type BotHandlers struct {
|
||||||
rs *rivescript.RiveScript
|
rs *rivescript.RiveScript
|
||||||
|
@ -22,8 +25,15 @@ type BotHandlers struct {
|
||||||
whoMu sync.RWMutex
|
whoMu sync.RWMutex
|
||||||
|
|
||||||
// Auto-greeter cooldowns
|
// Auto-greeter cooldowns
|
||||||
autoGreet map[string]time.Time
|
autoGreet map[string]time.Time
|
||||||
autoGreetMu sync.RWMutex
|
autoGreetCooldown time.Time // global cooldown between auto-greets
|
||||||
|
autoGreetMu sync.RWMutex
|
||||||
|
|
||||||
|
// MessageID history. Keep a buffer of recent messages sent in
|
||||||
|
// case the robot needs to report one (which should generally
|
||||||
|
// happen immediately, if it does).
|
||||||
|
messageBuf []messages.Message
|
||||||
|
messageBufMu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupChatbot configures a sensible set of default handlers for the BareBot application.
|
// SetupChatbot configures a sensible set of default handlers for the BareBot application.
|
||||||
|
@ -37,7 +47,8 @@ func (c *Client) SetupChatbot() error {
|
||||||
rs: rivescript.New(&rivescript.Config{
|
rs: rivescript.New(&rivescript.Config{
|
||||||
UTF8: true,
|
UTF8: true,
|
||||||
}),
|
}),
|
||||||
autoGreet: map[string]time.Time{},
|
autoGreet: map[string]time.Time{},
|
||||||
|
messageBuf: []messages.Message{},
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Initializing RiveScript brain")
|
log.Info("Initializing RiveScript brain")
|
||||||
|
@ -85,6 +96,31 @@ func (h *BotHandlers) OnMe(msg messages.Message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffer a message seen on chat for a while.
|
||||||
|
func (h *BotHandlers) cacheMessage(msg messages.Message) {
|
||||||
|
h.messageBufMu.Lock()
|
||||||
|
defer h.messageBufMu.Unlock()
|
||||||
|
|
||||||
|
h.messageBuf = append(h.messageBuf, msg)
|
||||||
|
|
||||||
|
if len(h.messageBuf) > ScrollbackBuffer {
|
||||||
|
h.messageBuf = h.messageBuf[len(h.messageBuf)-ScrollbackBuffer:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a message by ID from the recent message buffer.
|
||||||
|
func (h *BotHandlers) getMessageByID(msgID int) (messages.Message, bool) {
|
||||||
|
h.messageBufMu.RLock()
|
||||||
|
defer h.messageBufMu.RUnlock()
|
||||||
|
for _, msg := range h.messageBuf {
|
||||||
|
if msg.MessageID == msgID {
|
||||||
|
return msg, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.Message{}, false
|
||||||
|
}
|
||||||
|
|
||||||
// OnMessage handles Who List updates in chat.
|
// OnMessage handles Who List updates in chat.
|
||||||
func (h *BotHandlers) OnMessage(msg messages.Message) {
|
func (h *BotHandlers) OnMessage(msg messages.Message) {
|
||||||
// Strip HTML.
|
// Strip HTML.
|
||||||
|
@ -95,6 +131,9 @@ func (h *BotHandlers) OnMessage(msg messages.Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache it in our message buffer.
|
||||||
|
h.cacheMessage(msg)
|
||||||
|
|
||||||
// Do we send a reply to this?
|
// Do we send a reply to this?
|
||||||
var (
|
var (
|
||||||
sendReply bool
|
sendReply bool
|
||||||
|
@ -210,6 +249,12 @@ func (h *BotHandlers) OnPresence(msg messages.Message) {
|
||||||
|
|
||||||
// A join message?
|
// A join message?
|
||||||
if strings.Contains(msg.Message, "has joined the room") {
|
if strings.Contains(msg.Message, "has joined the room") {
|
||||||
|
// Global auto-greet cooldown.
|
||||||
|
if time.Now().Before(h.autoGreetCooldown) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.autoGreetCooldown = time.Now().Add(15 * time.Minute)
|
||||||
|
|
||||||
// Don't greet the same user too often in case of bouncing.
|
// Don't greet the same user too often in case of bouncing.
|
||||||
h.autoGreetMu.Lock()
|
h.autoGreetMu.Lock()
|
||||||
if timeout, ok := h.autoGreet[msg.Username]; ok {
|
if timeout, ok := h.autoGreet[msg.Username]; ok {
|
||||||
|
|
|
@ -3,8 +3,10 @@ package client
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
"git.kirsle.net/apps/barertc/pkg/messages"
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
"github.com/aichaos/rivescript-go"
|
"github.com/aichaos/rivescript-go"
|
||||||
)
|
)
|
||||||
|
@ -47,8 +49,74 @@ func (h *BotHandlers) setObjectMacros() {
|
||||||
} else {
|
} else {
|
||||||
return fmt.Sprintf("[react: %s]", err)
|
return fmt.Sprintf("[react: %s]", err)
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "[react: invalid number of parameters]"
|
||||||
|
})
|
||||||
|
|
||||||
|
// Takeback a message (admin action especially)
|
||||||
|
h.rs.SetSubroutine("takeback", func(rs *rivescript.RiveScript, args []string) string {
|
||||||
|
if len(args) >= 1 {
|
||||||
|
if msgID, err := strconv.Atoi(args[0]); err == nil {
|
||||||
|
// Take it back.
|
||||||
|
h.client.Send(messages.Message{
|
||||||
|
Action: messages.ActionTakeback,
|
||||||
|
MessageID: msgID,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("[takeback: %s]", err)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "[takeback: invalid number of parameters]"
|
||||||
|
})
|
||||||
|
|
||||||
|
// Flag (report) a message on chat.
|
||||||
|
h.rs.SetSubroutine("report", func(rs *rivescript.RiveScript, args []string) string {
|
||||||
|
if len(args) >= 2 {
|
||||||
|
if msgID, err := strconv.Atoi(args[0]); err == nil {
|
||||||
|
var comment = strings.Join(args[1:], " ")
|
||||||
|
|
||||||
|
// Look up this message.
|
||||||
|
if msg, ok := h.getMessageByID(msgID); ok {
|
||||||
|
// Report it with the custom comment.
|
||||||
|
h.client.Send(messages.Message{
|
||||||
|
Action: messages.ActionReport,
|
||||||
|
Channel: msg.Channel,
|
||||||
|
Username: msg.Username,
|
||||||
|
Timestamp: "not recorded",
|
||||||
|
Reason: "Automated chatbot flag",
|
||||||
|
Message: msg.Message,
|
||||||
|
Comment: comment,
|
||||||
|
})
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "[msgID not found]"
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("[report: %s]", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "[report: invalid number of parameters]"
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send a user a Direct Message.
|
||||||
|
h.rs.SetSubroutine("dm", func(rs *rivescript.RiveScript, args []string) string {
|
||||||
|
if len(args) >= 2 {
|
||||||
|
var (
|
||||||
|
username = args[0]
|
||||||
|
message = strings.Join(args[1:], " ")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Slide into their DMs.
|
||||||
|
log.Error("Send DM to [%s]: %s", username, message)
|
||||||
|
h.client.Send(messages.Message{
|
||||||
|
Action: messages.ActionMessage,
|
||||||
|
Channel: "@" + username,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
return "[react: invalid number of parameters]"
|
return "[dm: invalid number of parameters]"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -17,6 +19,25 @@ func StripHTML(s string) string {
|
||||||
return strings.TrimSpace(reHTML.ReplaceAllString(s, ""))
|
return strings.TrimSpace(reHTML.ReplaceAllString(s, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebSocketURL converts the BareRTC base (https) URL into the WebSocket link.
|
||||||
|
func WebSocketURL(baseURL string) (string, error) {
|
||||||
|
url, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch url.Scheme {
|
||||||
|
case "https":
|
||||||
|
return fmt.Sprintf("wss://%s/ws", url.Host), nil
|
||||||
|
case "http":
|
||||||
|
return fmt.Sprintf("ws://%s/ws", url.Host), nil
|
||||||
|
case "ws", "wss":
|
||||||
|
return fmt.Sprintf("%s//%s/ws", url.Scheme, url.Host), nil
|
||||||
|
default:
|
||||||
|
return "", errors.New("unsupported URL scheme")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AtMentioned checks if somebody has "at mentioned" your username (having your
|
// AtMentioned checks if somebody has "at mentioned" your username (having your
|
||||||
// name at the beginning or end of their message). Returns whether the at mention
|
// name at the beginning or end of their message). Returns whether the at mention
|
||||||
// was detected, along with the modified message without the at mention name on the
|
// was detected, along with the modified message without the at mention name on the
|
||||||
|
|
|
@ -54,7 +54,7 @@ func init() {
|
||||||
|
|
||||||
// Get the JWT auth token.
|
// Get the JWT auth token.
|
||||||
log.Info("Authenticating with BareRTC (getting JWT token)")
|
log.Info("Authenticating with BareRTC (getting JWT token)")
|
||||||
client, err := client.NewClient("ws://localhost:9000/ws", jwt.Claims{
|
client, err := client.NewClient(config.Current.BareRTC.URL, jwt.Claims{
|
||||||
IsAdmin: config.Current.Profile.IsAdmin,
|
IsAdmin: config.Current.Profile.IsAdmin,
|
||||||
Avatar: config.Current.Profile.AvatarURL,
|
Avatar: config.Current.Profile.AvatarURL,
|
||||||
ProfileURL: config.Current.Profile.ProfileURL,
|
ProfileURL: config.Current.Profile.ProfileURL,
|
||||||
|
|
|
@ -122,6 +122,8 @@ You can invoke these by using the `<call>` tag in your RiveScript responses -- s
|
||||||
|
|
||||||
This command can reload the chatbot's RiveScript sources from disk, making it easy to iterate on your robot without rebooting the whole program.
|
This command can reload the chatbot's RiveScript sources from disk, making it easy to iterate on your robot without rebooting the whole program.
|
||||||
|
|
||||||
|
Usage: `reload`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```rivescript
|
```rivescript
|
||||||
|
@ -136,6 +138,8 @@ It returns a message like "The RiveScript brain has been reloaded!"
|
||||||
|
|
||||||
You can send an emoji reaction to a message ID. The current message ID is available in the `<get messageID>` tag of a RiveScript reply.
|
You can send an emoji reaction to a message ID. The current message ID is available in the `<get messageID>` tag of a RiveScript reply.
|
||||||
|
|
||||||
|
Usage: `react <int MessageID> <string Emoji>`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```rivescript
|
```rivescript
|
||||||
|
@ -148,3 +152,62 @@ Example:
|
||||||
Note: the `react` command returns no text (except on error). Couple it with a `<noreply>` if you want to ensure the bot does not send a reply to the message in chat, but simply applies the reaction emoji.
|
Note: the `react` command returns no text (except on error). Couple it with a `<noreply>` if you want to ensure the bot does not send a reply to the message in chat, but simply applies the reaction emoji.
|
||||||
|
|
||||||
The reaction is delayed about 2.5 seconds.
|
The reaction is delayed about 2.5 seconds.
|
||||||
|
|
||||||
|
## DM
|
||||||
|
|
||||||
|
Slide into a user's DMs and send them a Direct Message no matter what channel you saw their message in.
|
||||||
|
|
||||||
|
Usage: `dm <username> <message to send>`
|
||||||
|
|
||||||
|
Example: say you have a global keyword trigger on public rooms and want to DM a user if they match one.
|
||||||
|
|
||||||
|
```rivescript
|
||||||
|
> topic PublicChannel
|
||||||
|
|
||||||
|
+ [*] sensitive keywords [*]
|
||||||
|
- <call>dm <id> I saw you say something in the public channel!</call>
|
||||||
|
^ Please don't say that stuff on here! @<id>
|
||||||
|
|
||||||
|
< topic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Takeback
|
||||||
|
|
||||||
|
Take back a message by its ID. This may be useful if you have a global moderator trigger set up so you can remove a user's message.
|
||||||
|
|
||||||
|
Usage: `takeback <int MessageID>`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```rivescript
|
||||||
|
> topic PublicChannel
|
||||||
|
|
||||||
|
+ [*] sensitive keywords [*]
|
||||||
|
- <call>takeback <get messageID></call>
|
||||||
|
^ Please don't say that stuff on here! @<id>
|
||||||
|
|
||||||
|
< topic
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the `takeback` command returns no text (except on error).
|
||||||
|
|
||||||
|
## Report
|
||||||
|
|
||||||
|
Send a BareRTC `report` action about a message ID. If your chat server is configured with a webhook URL to report messages to your website, those reports can be sent automatically by your bot.
|
||||||
|
|
||||||
|
Usage: `report <int MessageID> <custom comment to attach>`
|
||||||
|
|
||||||
|
The message ID must have been recently sent: the chatbot holds a buffer of recently seen message IDs (last 500 or so). This should work OK since most likely you would report the current message automatically based on a keyword trigger.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```rivescript
|
||||||
|
> topic PublicChannel
|
||||||
|
|
||||||
|
+ [*] (sensitive|keywords) [*]
|
||||||
|
- <call>report <get messageID> User has said the keyword '<star>'</call>
|
||||||
|
|
||||||
|
< topic
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the `report` command returns no text (except on error).
|
||||||
|
|
Loading…
Reference in New Issue
Block a user