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
|
||||
chatbot/
|
||||
|
|
3
Makefile
3
Makefile
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ""
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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).
|
||||
|
|
Loading…
Reference in New Issue
Block a user