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 settings.toml
chatbot/

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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 {

View File

@ -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 ""
}) })

View File

@ -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

View File

@ -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,

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. 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).