Update chatbot program

* New object macros: dm, takeback, report
* Bugfixes
ipad-testing
Noah 2023-08-13 20:45:53 -07:00
parent 9c05af2c2e
commit 9c8ff88f6e
9 changed files with 213 additions and 15 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
settings.toml
chatbot/

View File

@ -4,4 +4,5 @@ run:
.PHONY: 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

View File

@ -9,7 +9,7 @@
> topic PublicChannel
// 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>
^ <noreply>

View File

@ -73,10 +73,16 @@ func (c *Client) Run() error {
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()
c.ctx = ctx
conn, _, err := websocket.Dial(ctx, c.url, nil)
conn, _, err := websocket.Dial(ctx, wss, nil)
if err != nil {
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)
}
// Testing!
c.Send(messages.Message{
Action: messages.ActionMessage,
Channel: "lobby",
Message: "Hello, world! BareBot client connected!",
})
// Enter the Read Loop
for {
var msg messages.Message

View File

@ -12,6 +12,9 @@ import (
"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.
type BotHandlers struct {
rs *rivescript.RiveScript
@ -22,8 +25,15 @@ type BotHandlers struct {
whoMu sync.RWMutex
// Auto-greeter cooldowns
autoGreet map[string]time.Time
autoGreetMu sync.RWMutex
autoGreet map[string]time.Time
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.
@ -37,7 +47,8 @@ func (c *Client) SetupChatbot() error {
rs: rivescript.New(&rivescript.Config{
UTF8: true,
}),
autoGreet: map[string]time.Time{},
autoGreet: map[string]time.Time{},
messageBuf: []messages.Message{},
}
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.
func (h *BotHandlers) OnMessage(msg messages.Message) {
// Strip HTML.
@ -95,6 +131,9 @@ func (h *BotHandlers) OnMessage(msg messages.Message) {
return
}
// Cache it in our message buffer.
h.cacheMessage(msg)
// Do we send a reply to this?
var (
sendReply bool
@ -210,6 +249,12 @@ func (h *BotHandlers) OnPresence(msg messages.Message) {
// A join message?
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.
h.autoGreetMu.Lock()
if timeout, ok := h.autoGreet[msg.Username]; ok {

View File

@ -3,8 +3,10 @@ package client
import (
"fmt"
"strconv"
"strings"
"time"
"git.kirsle.net/apps/barertc/pkg/log"
"git.kirsle.net/apps/barertc/pkg/messages"
"github.com/aichaos/rivescript-go"
)
@ -47,8 +49,74 @@ func (h *BotHandlers) setObjectMacros() {
} else {
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 {
return "[react: invalid number of parameters]"
return "[dm: invalid number of parameters]"
}
return ""
})

View File

@ -1,7 +1,9 @@
package client
import (
"errors"
"fmt"
"net/url"
"regexp"
"strings"
)
@ -17,6 +19,25 @@ func StripHTML(s string) string {
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
// 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

View File

@ -54,7 +54,7 @@ func init() {
// Get the JWT auth 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,
Avatar: config.Current.Profile.AvatarURL,
ProfileURL: config.Current.Profile.ProfileURL,

View File

@ -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.
Usage: `reload`
Example:
```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.
Usage: `react <int MessageID> <string Emoji>`
Example:
```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.
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).