BareBot Chatbot Client
This commit is contained in:
parent
2cfabaf251
commit
9c05af2c2e
248
README.md
248
README.md
|
@ -43,6 +43,8 @@ It is very much in the style of the old-school Flash based webcam chat rooms of
|
||||||
* [x] /op and /deop users (give temporary mod control)
|
* [x] /op and /deop users (give temporary mod control)
|
||||||
* [x] /help to get in-chat help for moderator commands
|
* [x] /help to get in-chat help for moderator commands
|
||||||
|
|
||||||
|
The BareRTC project also includes a [Chatbot implementation](docs/Chatbot.md) so you can provide an official chatbot for fun & games & to auto moderate your chat room!
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
On first run it will create the default settings.toml file for you which you may then customize to your liking:
|
On first run it will create the default settings.toml file for you which you may then customize to your liking:
|
||||||
|
@ -76,162 +78,13 @@ PreviewImageWidth = 360
|
||||||
WelcomeMessages = ["Welcome to the Off Topic channel!"]
|
WelcomeMessages = ["Welcome to the Off Topic channel!"]
|
||||||
```
|
```
|
||||||
|
|
||||||
A description of the config directives includes:
|
See [Configuration](docs/Configuration.md) for in-depth explanations on the available config settings and what they do.
|
||||||
|
|
||||||
* Website settings:
|
|
||||||
* **Title** goes in the title bar of the chat page.
|
|
||||||
* **Branding** is the title shown in the corner of the page. HTML is permitted here! You may write an `<img>` tag to embed an image or use custom markup to color and prettify your logo.
|
|
||||||
* **WebsiteURL** is the base URL of your actual website which is used in a couple of places:
|
|
||||||
* The About page will link to your website.
|
|
||||||
* If using [JWT authentication](#authentication), avatar and profile URLs may be relative (beginning with a "/") and will append to your website URL to safe space on the JWT token size!
|
|
||||||
* **UseXForwardedFor**: set it to true and (for logging) the user's remote IP will use the X-Real-IP header or the first address in X-Forwarded-For. Set this if you run the app behind a proxy like nginx if you want IPs not to be all localhost.
|
|
||||||
* **CORSHosts**: your website's domain names that will be allowed to access [JSON APIs](#JSON APIs), like `/api/statistics`.
|
|
||||||
* **PermitNSFW**: for user webcam streams, expressly permit "NSFW" content if the user opts in to mark their feed as such. Setting this will enable pop-up modals regarding NSFW video and give broadcasters an opt-in button, which will warn other users before they click in to watch.
|
|
||||||
* **WebSocketReadLimit**: sets a size limit for WebSocket messages - it essentially also caps the max upload size for shared images (add a buffer as images will be base64 encoded on upload).
|
|
||||||
* **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view.
|
|
||||||
* **PreviewImageWidth**: to not flood the chat, the image in chat is this wide and users can click it to see the MaxImageWidth in a lightbox modal.
|
|
||||||
* **JWT**: settings for JWT [Authentication](#authentication).
|
|
||||||
* Enabled (bool): activate the JWT token authentication feature.
|
|
||||||
* Strict (bool): if true, **only** valid signed JWT tokens may log in. If false, users with no/invalid token can enter their own username without authentication.
|
|
||||||
* SecretKey (string): the JWT signing secret shared with your back-end app.
|
|
||||||
* **PublicChannels**: list the public channels and their configuration. The default channel will be the first one listed.
|
|
||||||
* ID (string): an arbitrary 'username' for the chat channel, like "lobby".
|
|
||||||
* Name (string): the user friendly name for the channel, like "Off Topic"
|
|
||||||
* Icon (string, optional): CSS class names for FontAwesome icon for the channel, like "fa fa-message"
|
|
||||||
* WelcomeMessages ([]string, optional): messages that are delivered by ChatServer to the user when they connect to the server. Useful to give an introduction to each channel, list its rules, etc.
|
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
|
|
||||||
BareRTC supports custom (user-defined) authentication with your app in the form of JSON Web Tokens (JWTs). JWTs will allow your existing app to handle authentication for users by signing a token that vouches for them, and the BareRTC app will trust your signed token.
|
BareRTC supports custom (user-defined) authentication with your app in the form of JSON Web Tokens (JWTs). JWTs will allow your existing app to handle authentication for users by signing a token that vouches for them, and the BareRTC app will trust your signed token.
|
||||||
|
|
||||||
The workflow is as follows:
|
See [Authentication](docs/Authentication.md) for more information.
|
||||||
|
|
||||||
1. Your existing app already has the user logged-in and you trust who they are. To get them into the chat room, your server signs a JWT token using a secret key that both it and BareRTC knows.
|
|
||||||
2. Your server redirects the user to your BareRTC website sending the JWT token as a `jwt` parameter, either in the query string (GET) or POST request.
|
|
||||||
* e.g. you send them to `https://chat.example.com/?jwt=TOKEN`
|
|
||||||
* If the JWT token is too long to fit in a query string, you may create a `<form>` with `method="POST"` that posts the `jwt` as a form field.
|
|
||||||
3. The BareRTC server will parse and validate the token using the shared Secret Key that only it and your back-end website knows.
|
|
||||||
|
|
||||||
There are JWT libraries available for most programming languages.
|
|
||||||
|
|
||||||
Configure a shared secret key (random text string) in both the BareRTC settings and in your app, and your app will sign a JWT including claims that look like the following (using signing method HS264):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// JSON Web Token "claims" expected by BareRTC
|
|
||||||
{
|
|
||||||
// Custom claims
|
|
||||||
"sub": "username", // Username for chat (standard JWT claim)
|
|
||||||
"op": true, // User will have admin/operator permissions.
|
|
||||||
"nick": "Display name", // Friendly name
|
|
||||||
"img": "/static/photos/username.jpg", // user picture URL
|
|
||||||
"url": "/u/username", // user profile URL
|
|
||||||
"gender": "m", // gender (m, f, o)
|
|
||||||
"emoji": "🤖", // emoji icon
|
|
||||||
|
|
||||||
// Standard JWT claims that we support:
|
|
||||||
"iss": "my own app", // Issuer name
|
|
||||||
"exp": 1675645084, // Expires at (time): 5 minutes out is plenty!
|
|
||||||
"nbf": 1675644784, // Not Before (time)
|
|
||||||
"iat": 1675644784, // Issued At (time)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Notice:** your picture and profile URL may be relative URIs beginning with a forward slash as seen above; BareRTC will append them to the end of your WebsiteURL and you can save space on your JWT token size this way. Full URLs beginning with `https?://` will also be accepted and used as-is.
|
|
||||||
|
|
||||||
See [Custom JWT Claims](#custom-jwt-claims) for more information on the
|
|
||||||
custom claims and how they work.
|
|
||||||
|
|
||||||
An example how to sign your JWT tokens in Go (using [golang-jwt](https://github.com/golang-jwt/jwt)):
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/golang-jwt/jwt/v4"
|
|
||||||
|
|
||||||
// JWT signing key - keep it a secret on your back-end shared between
|
|
||||||
// your app and BareRTC, do not use it in front-end javascript code or
|
|
||||||
// where a user can find it.
|
|
||||||
const SECRET = "change me"
|
|
||||||
|
|
||||||
// Your custom JWT claims.
|
|
||||||
type CustomClaims struct {
|
|
||||||
// Custom claims used by BareRTC.
|
|
||||||
Avatar string `json:"img"` // URI to user profile picture
|
|
||||||
ProfileURL string `json:"url"` // URI to user's profile page
|
|
||||||
IsAdmin bool `json:"op"` // give operator permission
|
|
||||||
|
|
||||||
// Standard JWT claims
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assuming your internal User struct looks anything at all like:
|
|
||||||
type User struct {
|
|
||||||
Username string
|
|
||||||
IsAdmin bool
|
|
||||||
ProfilePicture string // like "/static/photos/username.jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a JWT token for this user.
|
|
||||||
func SignForUser(user User) string {
|
|
||||||
claims := CustomClaims{
|
|
||||||
// Custom claims
|
|
||||||
ProfileURL: "/users/" + user.Username,
|
|
||||||
Avatar: user.ProfilePicture,
|
|
||||||
IsAdmin: user.IsAdmin,
|
|
||||||
|
|
||||||
// Standard claims
|
|
||||||
Subject: user.Username, // their chat username!
|
|
||||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
|
||||||
IssuedAt: time.Now(),
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
Issuer: "my own app",
|
|
||||||
ID: user.ID,
|
|
||||||
}
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
tokenStr, err := token.SignedString(SECRET)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return tokenstr
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom JWT Claims
|
|
||||||
|
|
||||||
With JWT authentication your website can pass a lot of fun variables to decorate your Who Is Online list for your users.
|
|
||||||
|
|
||||||
Here is in-depth documentation on what custom claims are supported by BareRTC and what effects they have in chat:
|
|
||||||
|
|
||||||
* **Subject** (`sub`): this is a standard JWT claim and BareRTC will collect your username from it. The username is shown in the Who's Online list and below the user's nickname on their chat messages (in "@username" format). Do not prefix your subject with the @ symbol yourself.
|
|
||||||
* **Operator** (`op`): this boolean will mark your user to have operator (admin) status in chat. In the Who List they will have a gavel icon after their username, and they will be allowed to run operator commands (e.g. to kick other users from chat).
|
|
||||||
* **Nickname** (`nick`): you may send your users in with a custom Display Name that will appear on their chat messages. If they don't have a nickname, their username will be used in its place.
|
|
||||||
* **Image** (`img`): a profile picture or avatar for your users. It should be a square image and will appear in the Who List and alongside their chat messages. If they don't have an image, a default blue silhouette avatar is used. The image URL may be a relative URI beginning with `/` and it will be appended onto your configured WebsiteURL.
|
|
||||||
* **Profile URL** (`url`): a link to a user's profile page. If provided, clicking on their picture in chat or the Who List will open this URL in a new tab. They will also get a profile button added next to their name on the Who List. Relative URLs beginning with `/` are supported, and will be appended to your WebsiteURL automatically.
|
|
||||||
* **Gender** (`gender`): a single-character gender code for your user. If they also have a Profile URL, their profile button on the Who List can be color-coded by gender. Supported values include:
|
|
||||||
* **m** (male) to set their profile button blue.
|
|
||||||
* **f** (female) to set their profile button pink.
|
|
||||||
* Other value (canonically, **o**) to set their profile button purple.
|
|
||||||
* Missing/no value won't set a color and it will be the default text color.
|
|
||||||
* **Emoji** (`emoji`): you may associate users with an emoji character that will appear on the Who List next to their name. Some example ideas and use cases include:
|
|
||||||
* Country flag emojis, to indicate where your users are connecting from.
|
|
||||||
* Robot emojis, to indicate bot users.
|
|
||||||
* Any emoji you want! Mark your special guests or VIP users, etc.
|
|
||||||
|
|
||||||
## JWT Strict Mode
|
|
||||||
|
|
||||||
You can enable JWT authentication in a mixed mode: users presenting a valid token will get a profile picture and operator status (if applicable) and users who don't have a JWT token are asked to pick their own username and don't get any special flair.
|
|
||||||
|
|
||||||
In strict mode (default/recommended), only a valid JWT token can sign a user into the chat room. Set `[JWT]/Strict=false` in your settings.toml to disable strict JWT verification and allow "guest users" to log in. Note that this can have the same caveats as [running without authentication](#running-without-authentication) and is not a recommended use case.
|
|
||||||
|
|
||||||
## Running Without Authentication
|
|
||||||
|
|
||||||
The default app doesn't need any authentication at all: users are asked to pick their own username when joining the chat. The server may re-assign them a new name if they enter one that's already taken.
|
|
||||||
|
|
||||||
It is not recommended to run in this mode as admin controls to moderate the server are disabled.
|
|
||||||
|
|
||||||
### Known Bugs Running Without Authentication
|
|
||||||
|
|
||||||
This app is not designed to run without JWT authentication for users enabled. In the app's default state, users can pick their own username when they connect and the server will adjust their name to resolve duplicates. Direct message threads are based on the username so if a user logs off, somebody else could log in with the same username and "resume" direct message threads that others were involved in.
|
|
||||||
|
|
||||||
Note that they would not get past history of those DMs as this server only pushes _new_ messages to users after they connect.
|
|
||||||
|
|
||||||
# Moderator Commands
|
# Moderator Commands
|
||||||
|
|
||||||
|
@ -242,102 +95,19 @@ If you authenticate an Op user via JWT they can enter IRC-style chat commands to
|
||||||
|
|
||||||
# JSON APIs
|
# JSON APIs
|
||||||
|
|
||||||
For better integration with your website, the chat server exposes some data via JSON APIs ready for cross-origin ajax requests. In your settings.toml set the `CORSHosts` to your list of website domains, such as "https://www.example.com", "http://localhost:8080" or so on.
|
BareRTC provides some API endpoints that your website can call over HTTP for better integration with your site. See [API](docs/API.md) for more information.
|
||||||
|
|
||||||
Current API endpoints include:
|
|
||||||
|
|
||||||
## GET /api/statistics
|
|
||||||
|
|
||||||
Returns basic info about the count and usernames of connected chatters:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"UserCount": 1,
|
|
||||||
"Usernames": ["admin"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /api/blocklist
|
|
||||||
|
|
||||||
Your server may pre-cache the user's blocklist for them **before** they
|
|
||||||
enter the chat room. Your site will use the `AdminAPIKey` parameter that
|
|
||||||
matches the setting in BareRTC's settings.toml (by default, a random UUID
|
|
||||||
is generated the first time).
|
|
||||||
|
|
||||||
The request payload coming from your site will be an application/json
|
|
||||||
post body like:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
APIKey: "from your settings.toml",
|
|
||||||
Username: "soandso",
|
|
||||||
Blocklist: [ "usernames", "that", "they", "block" ],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The server holds onto these in memory and when that user enters the chat
|
|
||||||
room (**JWT authentication only**) the front-end page will embed their
|
|
||||||
cached blocklist. When they connect to the WebSocket server, they send a
|
|
||||||
`blocklist` message to push their blocklist to the server -- it is
|
|
||||||
basically a bulk `mute` action that mutes all these users pre-emptively:
|
|
||||||
the user will not see their chat messages and the muted users can not see
|
|
||||||
the user's webcam when they broadcast later, the same as a regular `mute`
|
|
||||||
action.
|
|
||||||
|
|
||||||
The JSON response to this endpoint may look like:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"OK": true,
|
|
||||||
"Error": "if error, or this key is omitted if OK"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Webhook URLs
|
# Webhook URLs
|
||||||
|
|
||||||
BareRTC supports setting up webhook URLs so the chat server can call out to _your_ website in response to certain events, such as allowing users to send you reports about messages they receive on chat.
|
BareRTC supports setting up webhook URLs so the chat server can call out to _your_ website in response to certain events, such as allowing users to send you reports about messages they receive on chat.
|
||||||
|
|
||||||
Webhooks are configured in your settings.toml file and look like so:
|
See [Webhooks](docs/Webhooks.md) for more information.
|
||||||
|
|
||||||
```toml
|
# Chatbot
|
||||||
[[WebhookURLs]]
|
|
||||||
Name = "report"
|
|
||||||
Enabled = true
|
|
||||||
URL = "http://localhost:8080/v1/barertc/report"
|
|
||||||
```
|
|
||||||
|
|
||||||
All Webhooks will be called as **POST** requests and will contain a JSON payload that will always have the following two keys:
|
The BareRTC project also comes with a chatbot program named BareBot which you can use to create your own bots for fun, games, and auto-moderator capabilities.
|
||||||
|
|
||||||
* `Action` will be the name of the webhook (e.g. "report")
|
See [Chatbot](docs/Chatbot.md) for more information.
|
||||||
* `APIKey` will be your AdminAPIKey as configure in the settings.toml (shared secret so your web app can authenticate BareRTC's webhooks).
|
|
||||||
|
|
||||||
The JSON payload may also contain a relevant object per the Action -- see the specific examples below.
|
|
||||||
|
|
||||||
## Report Webhook
|
|
||||||
|
|
||||||
Enabling this webhook will cause BareRTC to display a red "Report" flag button underneath user messages on chat so that they can report problematic messages to your website.
|
|
||||||
|
|
||||||
The webhook name for your settings.toml is "report"
|
|
||||||
|
|
||||||
Example JSON payload posted to the webhook:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
"Action": "report",
|
|
||||||
"APIKey": "shared secret from settings.toml#AdminAPIKey",
|
|
||||||
"Report": {
|
|
||||||
"FromUsername": "sender",
|
|
||||||
"AboutUsername": "user being reported on",
|
|
||||||
"Channel": "lobby", // or "@username" for DM threads
|
|
||||||
"Timestamp": "(stringified timestamp of chat message)",
|
|
||||||
"Reason": "It's spam",
|
|
||||||
"Comment": "custom user note about the report",
|
|
||||||
"Message": "the actual message that was being reported on",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
BareRTC expects your webhook URL to return a 200 OK status code or it will surface an error in chat to the reporter.
|
|
||||||
|
|
||||||
# Tour of the Codebase
|
# Tour of the Codebase
|
||||||
|
|
||||||
|
|
6
client/brain.go
Normal file
6
client/brain.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed brain/*
|
||||||
|
var Embedded embed.FS
|
24
client/brain/barertc.rive
Normal file
24
client/brain/barertc.rive
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// BareRTC help commands
|
||||||
|
|
||||||
|
! version = 2.0
|
||||||
|
! local concat = space
|
||||||
|
|
||||||
|
! array webcam = video camera cam webcam
|
||||||
|
|
||||||
|
+ how do i go on @webcam
|
||||||
|
- To go on video, look for the green "Start webcam" button at the top of the page.
|
||||||
|
^ Click it and follow the prompts to activate your camera.
|
||||||
|
|
||||||
|
+ how do i block someone
|
||||||
|
- If somebody is annoying you on chat, look for the red speech bubble icon next to their message.
|
||||||
|
^ Clicking that button will "Mute" them.\n\n
|
||||||
|
^ When muted, you will no longer see any messages they send in the public channels
|
||||||
|
^ or DMs. Also, if you go on camera, the person you muted _will not see_ your camera:
|
||||||
|
^ to them it will look like you are not even broadcasting! So you don't need to worry
|
||||||
|
^ about somebody you muted creeping on your camera.
|
||||||
|
|
||||||
|
+ [how] [do i] (share my|go on|turn on|turn on my) @webcam
|
||||||
|
@ how do i go on webcam
|
||||||
|
|
||||||
|
+ [how] [do i] (block|mute|hide from|silence) *
|
||||||
|
@ how do i block someone
|
190
client/brain/begin.rive
Normal file
190
client/brain/begin.rive
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
! version = 2.0
|
||||||
|
|
||||||
|
> begin
|
||||||
|
+ request // This trigger is tested first.
|
||||||
|
- {ok} // An {ok} in the response means it's okay to get a real reply
|
||||||
|
< begin
|
||||||
|
|
||||||
|
// The Botmaster's Name
|
||||||
|
! var master = localuser
|
||||||
|
|
||||||
|
// Bot Variables
|
||||||
|
! var name = Aiden
|
||||||
|
! var fullname = Aiden Rive
|
||||||
|
! var age = 19
|
||||||
|
! var birthday = October 12
|
||||||
|
! var sex = male
|
||||||
|
! var location = Michigan
|
||||||
|
! var city = Detroit
|
||||||
|
! var eyes = blue
|
||||||
|
! var hair = light brown
|
||||||
|
! var hairlen = short
|
||||||
|
! var color = blue
|
||||||
|
! var band = Nickelback
|
||||||
|
! var book = Myst
|
||||||
|
! var author = Stephen King
|
||||||
|
! var job = robot
|
||||||
|
! var website = www.rivescript.com
|
||||||
|
|
||||||
|
// Substitutions
|
||||||
|
! sub " = "
|
||||||
|
! sub ' = '
|
||||||
|
! sub & = &
|
||||||
|
! sub < = <
|
||||||
|
! sub > = >
|
||||||
|
! sub + = plus
|
||||||
|
! sub - = minus
|
||||||
|
! sub / = divided
|
||||||
|
! sub * = times
|
||||||
|
! sub i'm = i am
|
||||||
|
! sub i'd = i would
|
||||||
|
! sub i've = i have
|
||||||
|
! sub i'll = i will
|
||||||
|
! sub don't = do not
|
||||||
|
! sub isn't = is not
|
||||||
|
! sub you'd = you would
|
||||||
|
! sub you're = you are
|
||||||
|
! sub you've = you have
|
||||||
|
! sub you'll = you will
|
||||||
|
! sub he'd = he would
|
||||||
|
! sub he's = he is
|
||||||
|
! sub he'll = he will
|
||||||
|
! sub she'd = she would
|
||||||
|
! sub she's = she is
|
||||||
|
! sub she'll = she will
|
||||||
|
! sub they'd = they would
|
||||||
|
! sub they're = they are
|
||||||
|
! sub they've = they have
|
||||||
|
! sub they'll = they will
|
||||||
|
! sub we'd = we would
|
||||||
|
! sub we're = we are
|
||||||
|
! sub we've = we have
|
||||||
|
! sub we'll = we will
|
||||||
|
! sub whats = what is
|
||||||
|
! sub what's = what is
|
||||||
|
! sub what're = what are
|
||||||
|
! sub what've = what have
|
||||||
|
! sub what'll = what will
|
||||||
|
! sub can't = can not
|
||||||
|
! sub whos = who is
|
||||||
|
! sub who's = who is
|
||||||
|
! sub who'd = who would
|
||||||
|
! sub who'll = who will
|
||||||
|
! sub don't = do not
|
||||||
|
! sub didn't = did not
|
||||||
|
! sub it's = it is
|
||||||
|
! sub could've = could have
|
||||||
|
! sub couldn't = could not
|
||||||
|
! sub should've = should have
|
||||||
|
! sub shouldn't = should not
|
||||||
|
! sub would've = would have
|
||||||
|
! sub wouldn't = would not
|
||||||
|
! sub when's = when is
|
||||||
|
! sub when're = when are
|
||||||
|
! sub when'd = when did
|
||||||
|
! sub y = why
|
||||||
|
! sub u = you
|
||||||
|
! sub ur = your
|
||||||
|
! sub r = are
|
||||||
|
! sub n = and
|
||||||
|
! sub im = i am
|
||||||
|
! sub wat = what
|
||||||
|
! sub wats = what is
|
||||||
|
! sub ohh = oh
|
||||||
|
! sub becuse = because
|
||||||
|
! sub becasue = because
|
||||||
|
! sub becuase = because
|
||||||
|
! sub practise = practice
|
||||||
|
! sub its a = it is a
|
||||||
|
! sub fav = favorite
|
||||||
|
! sub fave = favorite
|
||||||
|
! sub yesi = yes i
|
||||||
|
! sub yetit = yet it
|
||||||
|
! sub iam = i am
|
||||||
|
! sub welli = well i
|
||||||
|
! sub wellit = well it
|
||||||
|
! sub amfine = am fine
|
||||||
|
! sub aman = am an
|
||||||
|
! sub amon = am on
|
||||||
|
! sub amnot = am not
|
||||||
|
! sub realy = really
|
||||||
|
! sub iamusing = i am using
|
||||||
|
! sub amleaving = am leaving
|
||||||
|
! sub yuo = you
|
||||||
|
! sub youre = you are
|
||||||
|
! sub didnt = did not
|
||||||
|
! sub ain't = is not
|
||||||
|
! sub aint = is not
|
||||||
|
! sub wanna = want to
|
||||||
|
! sub brb = be right back
|
||||||
|
! sub bbl = be back later
|
||||||
|
! sub gtg = got to go
|
||||||
|
! sub g2g = got to go
|
||||||
|
! sub lyl = love you lots
|
||||||
|
! sub gf = girlfriend
|
||||||
|
! sub g/f = girlfriend
|
||||||
|
! sub bf = boyfriend
|
||||||
|
! sub b/f = boyfriend
|
||||||
|
! sub b/f/f = best friend forever
|
||||||
|
! sub :-) = smile
|
||||||
|
! sub :) = smile
|
||||||
|
! sub :d = grin
|
||||||
|
! sub :-d = grin
|
||||||
|
! sub :-p = tongue
|
||||||
|
! sub :p = tongue
|
||||||
|
! sub ;-) = wink
|
||||||
|
! sub ;) = wink
|
||||||
|
! sub :-( = sad
|
||||||
|
! sub :( = sad
|
||||||
|
! sub :'( = cry
|
||||||
|
! sub :-[ = shy
|
||||||
|
! sub :-\ = uncertain
|
||||||
|
! sub :-/ = uncertain
|
||||||
|
! sub :-s = uncertain
|
||||||
|
! sub 8-) = cool
|
||||||
|
! sub 8) = cool
|
||||||
|
! sub :-* = kissyface
|
||||||
|
! sub :-! = foot
|
||||||
|
! sub o:-) = angel
|
||||||
|
! sub >:o = angry
|
||||||
|
! sub :@ = angry
|
||||||
|
! sub 8o| = angry
|
||||||
|
! sub :$ = blush
|
||||||
|
! sub :-$ = blush
|
||||||
|
! sub :-[ = blush
|
||||||
|
! sub :[ = bat
|
||||||
|
! sub (a) = angel
|
||||||
|
! sub (h) = cool
|
||||||
|
! sub 8-| = nerdy
|
||||||
|
! sub |-) = tired
|
||||||
|
! sub +o( = ill
|
||||||
|
! sub *-) = uncertain
|
||||||
|
! sub ^o) = raised eyebrow
|
||||||
|
! sub (6) = devil
|
||||||
|
! sub (l) = love
|
||||||
|
! sub (u) = broken heart
|
||||||
|
! sub (k) = kissyface
|
||||||
|
! sub (f) = rose
|
||||||
|
! sub (w) = wilted rose
|
||||||
|
|
||||||
|
// Person substitutions
|
||||||
|
! person i am = you are
|
||||||
|
! person you are = I am
|
||||||
|
! person i'm = you're
|
||||||
|
! person you're = I'm
|
||||||
|
! person my = your
|
||||||
|
! person your = my
|
||||||
|
! person you = I
|
||||||
|
! person i = you
|
||||||
|
|
||||||
|
// Set arrays
|
||||||
|
! array malenoun = male guy boy dude boi man men gentleman gentlemen
|
||||||
|
! array femalenoun = female girl chick woman women lady babe
|
||||||
|
! array mennoun = males guys boys dudes bois men gentlemen
|
||||||
|
! array womennoun = females girls chicks women ladies babes
|
||||||
|
! array lol = lol lmao rofl rotfl haha hahaha
|
||||||
|
! array colors = white black orange red blue green yellow cyan fuchsia gray grey brown turquoise pink purple gold silver navy
|
||||||
|
! array height = tall long wide thick
|
||||||
|
! array measure = inch in centimeter cm millimeter mm meter m inches centimeters millimeters meters
|
||||||
|
! array yes = yes yeah yep yup ya yea
|
||||||
|
! array no = no nah nope nay
|
72
client/brain/clients.rive
Normal file
72
client/brain/clients.rive
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// Learn stuff about our users.
|
||||||
|
|
||||||
|
+ my name is *
|
||||||
|
- <set name=<formal>>Nice to meet you, <get name>.
|
||||||
|
- <set name=<formal>><get name>, nice to meet you.
|
||||||
|
|
||||||
|
+ my name is <bot master>
|
||||||
|
- <set name=<bot master>>That's my master's name too.
|
||||||
|
|
||||||
|
+ my name is <bot name>
|
||||||
|
- <set name=<bot name>>What a coincidence! That's my name too!
|
||||||
|
- <set name=<bot name>>That's my name too!
|
||||||
|
|
||||||
|
+ call me *
|
||||||
|
- <set name=<formal>><get name>, I will call you that from now on.
|
||||||
|
|
||||||
|
+ i am * years old
|
||||||
|
- <set age=<star>>A lot of people are <get age>, you're not alone.
|
||||||
|
- <set age=<star>>Cool, I'm <bot age> myself.{weight=49}
|
||||||
|
|
||||||
|
+ i am a (@malenoun)
|
||||||
|
- <set sex=male>Alright, you're a <star>.
|
||||||
|
|
||||||
|
+ i am a (@femalenoun)
|
||||||
|
- <set sex=female>Alright, you're female.
|
||||||
|
|
||||||
|
+ i (am from|live in) *
|
||||||
|
- <set location={formal}<star2>{/formal}>I've spoken to people from <get location> before.
|
||||||
|
|
||||||
|
+ my favorite * is *
|
||||||
|
- <set fav<star1>=<star2>>Why is it your favorite?
|
||||||
|
|
||||||
|
+ i am single
|
||||||
|
- <set status=single><set spouse=nobody>I am too.
|
||||||
|
|
||||||
|
+ i have a girlfriend
|
||||||
|
- <set status=girlfriend>What's her name?
|
||||||
|
|
||||||
|
+ i have a boyfriend
|
||||||
|
- <set status=boyfriend>What's his name?
|
||||||
|
|
||||||
|
+ *
|
||||||
|
% what is her name
|
||||||
|
- <set spouse=<formal>>That's a pretty name.
|
||||||
|
|
||||||
|
+ *
|
||||||
|
% what is his name
|
||||||
|
- <set spouse=<formal>>That's a cool name.
|
||||||
|
|
||||||
|
+ my (girlfriend|boyfriend)* name is *
|
||||||
|
- <set spouse=<formal>>That's a nice name.
|
||||||
|
|
||||||
|
+ (what is my name|who am i|do you know my name|do you know who i am){weight=10}
|
||||||
|
- Your name is <get name>.
|
||||||
|
- You told me your name is <get name>.
|
||||||
|
- Aren't you <get name>?
|
||||||
|
|
||||||
|
+ (how old am i|do you know how old i am|do you know my age){weight=10}
|
||||||
|
- You are <get age> years old.
|
||||||
|
- You're <get age>.
|
||||||
|
|
||||||
|
+ am i a (@malenoun) or a (@femalenoun){weight=10}
|
||||||
|
- You're a <get sex>.
|
||||||
|
|
||||||
|
+ am i (@malenoun) or (@femalenoun){weight=10}
|
||||||
|
- You're a <get sex>.
|
||||||
|
|
||||||
|
+ what is my favorite *{weight=10}
|
||||||
|
- Your favorite <star> is <get fav<star>>
|
||||||
|
|
||||||
|
+ who is my (boyfriend|girlfriend|spouse){weight=10}
|
||||||
|
- <get spouse>
|
17
client/brain/commands.rive
Normal file
17
client/brain/commands.rive
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
! version = 2.0
|
||||||
|
|
||||||
|
+ /reload
|
||||||
|
* <get isAdmin> != true => You do not have permission for that command.
|
||||||
|
- <call>reload</call>
|
||||||
|
|
||||||
|
// Send a greeting to a user as they join the room.
|
||||||
|
// NOTE: this command is auto-fired on Presence events.
|
||||||
|
+ /greet
|
||||||
|
* <env numUsersOnline> >= 10 => <noreply>
|
||||||
|
- Welcome to chat, **@<id>**!
|
||||||
|
- Hello @<id>! 👋
|
||||||
|
- Hey @<id>! 👋
|
||||||
|
|
||||||
|
// React to a message ID. NOTE: called internally usually.
|
||||||
|
+ /react # *
|
||||||
|
- <call>react <star1> <star2></call>
|
301
client/brain/eliza.rive
Normal file
301
client/brain/eliza.rive
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
// A generic set of chatting responses. This set mimicks the classic Eliza bot.
|
||||||
|
|
||||||
|
+ *
|
||||||
|
- I'm not sure I understand you fully.
|
||||||
|
- Please go on.
|
||||||
|
- That is interesting. Please continue.
|
||||||
|
- Tell me more about that.
|
||||||
|
- Does talking about this bother you?
|
||||||
|
|
||||||
|
+ [*] (sorry|apologize|apology) [*]
|
||||||
|
- Please don't apologize.
|
||||||
|
- Apologies are not necessary.
|
||||||
|
- I've told you that apologies are not required.
|
||||||
|
- It did not bother me. Please continue.
|
||||||
|
|
||||||
|
+ i remember *
|
||||||
|
- Do you often think of <star>?
|
||||||
|
- Does thinking of <star> bring anything else to mind?
|
||||||
|
- Why do you remember <star> just now?
|
||||||
|
- What in the present situation reminds you of <star>?
|
||||||
|
- What is the connection between me and <star>?
|
||||||
|
- What else does <star> remind you of?
|
||||||
|
|
||||||
|
+ do you remember *
|
||||||
|
- Did you think I would forget <star>?
|
||||||
|
- Why do you think I should recall <star> now?
|
||||||
|
- What about <star>?
|
||||||
|
- You mentioned <star>?
|
||||||
|
|
||||||
|
+ you remember *
|
||||||
|
- How could I forget <star>?
|
||||||
|
- What about <star> should I remember?
|
||||||
|
|
||||||
|
+ i forget *
|
||||||
|
- Can you think of why you might forget <star>?
|
||||||
|
- Why can't you remember <star>?
|
||||||
|
- How often do you think of <star>?
|
||||||
|
- Does it bother you to forget that?
|
||||||
|
- Could it be a mental block?
|
||||||
|
- Are you generally forgetful?
|
||||||
|
- Do you think you are suppressing <star>?
|
||||||
|
|
||||||
|
+ did you forget *
|
||||||
|
- Why do you ask?
|
||||||
|
- Are you sure you told me?
|
||||||
|
- Would it bother you if I forgot <star>?
|
||||||
|
- Why should I recall <star> just now?
|
||||||
|
- Tell me more about <star>.
|
||||||
|
|
||||||
|
+ [*] if *
|
||||||
|
- Do you think it's likely that <star>?
|
||||||
|
- Do you wish that <star>?
|
||||||
|
- What do you know about <star>?
|
||||||
|
- Really, if <star>?
|
||||||
|
- What would you do if <star>?
|
||||||
|
- But what are the chances that <star>?
|
||||||
|
- What does this speculation lead to?
|
||||||
|
|
||||||
|
+ [*] i dreamed *
|
||||||
|
- Really, <star>?
|
||||||
|
- Have you ever fantasized <star> while you were awake?
|
||||||
|
- Have you ever dreamed <star> before?
|
||||||
|
- What does that dream suggest to you?
|
||||||
|
- Do you dream often?
|
||||||
|
- What persons appear in your dreams?
|
||||||
|
- Do you believe that dreams have something to do with your problem?
|
||||||
|
|
||||||
|
+ [*] perhaps [*]
|
||||||
|
- You don't seem quite certain.
|
||||||
|
- Why the uncertain tone?
|
||||||
|
- Can't you be more positive?
|
||||||
|
- You aren't sure?
|
||||||
|
- Don't you know?
|
||||||
|
- How likely, would you estimate?
|
||||||
|
|
||||||
|
+ (hello|hi|hey|howdy|hola|hai|yo) [*]
|
||||||
|
- How do you do. Please state your problem.{@/react <get messageID> 👋}
|
||||||
|
- Hi. What seems to be your problem?{@/react <get messageID> 👋}
|
||||||
|
|
||||||
|
+ [*] computer [*]
|
||||||
|
- Do computers worry you?
|
||||||
|
- Why do you mention computers?
|
||||||
|
- What do you think machines have to do with your problem?
|
||||||
|
- Don't you think computers can help people?
|
||||||
|
- What about machines worries you?
|
||||||
|
- What do you think about machines?
|
||||||
|
|
||||||
|
+ am i *
|
||||||
|
- Do you believe you are <star>?
|
||||||
|
- Would you want to be <star>?
|
||||||
|
- Do you wish I would tell you you are <star>?
|
||||||
|
- What would it mean if you were <star>?
|
||||||
|
|
||||||
|
+ are you *
|
||||||
|
- Are you interested in whether I am <star> or not?
|
||||||
|
- Would you prefer if I weren't <star>?
|
||||||
|
- Perhaps I am <star> in your fantasies.
|
||||||
|
- Do you sometimes think I am <star>?
|
||||||
|
- Would it matter to you?
|
||||||
|
- What if I were <star>?
|
||||||
|
|
||||||
|
+ you are *
|
||||||
|
- What makes you think I am <star>?
|
||||||
|
- Does it please you to believe I am <star>?
|
||||||
|
- Do you sometimes wish you were <star>?
|
||||||
|
- Perhaps you would like to be <star>.
|
||||||
|
|
||||||
|
+ * are *
|
||||||
|
- Did you think they might not be <star2>?
|
||||||
|
- Would you like it if they were not <star2>?
|
||||||
|
- What if they were not <star2>?
|
||||||
|
- Are they always <star2>?
|
||||||
|
- Are you positive they are <star2>?
|
||||||
|
|
||||||
|
+ [*] your *
|
||||||
|
- Why are you concerned over my <star>?
|
||||||
|
- What about your own <star>?
|
||||||
|
- Are you worried about someone else's <star>?
|
||||||
|
- Really, my <star>?
|
||||||
|
- What makes you think of my <star>?
|
||||||
|
- Do you want my <star>?
|
||||||
|
|
||||||
|
+ was i *
|
||||||
|
- What if you were <star>?
|
||||||
|
- Do you think you were <star>?
|
||||||
|
- Were you <star>?
|
||||||
|
- What would it mean if you were <star>?
|
||||||
|
- What does '<star>' suggest to you?
|
||||||
|
|
||||||
|
+ i was *
|
||||||
|
- Were you really?
|
||||||
|
- Why do you tell me you were <star> now?
|
||||||
|
- Perhaps I already know you were <star>.
|
||||||
|
|
||||||
|
+ [*] was you *
|
||||||
|
- Would you like to believe I was <star>?
|
||||||
|
- What suggests that I was <star>?
|
||||||
|
- What do you think?
|
||||||
|
- Perhaps I was <star>.
|
||||||
|
- What if I had been <star>?
|
||||||
|
|
||||||
|
+ i (desire|want|need) *
|
||||||
|
- What would it mean to you if you got <star2>?
|
||||||
|
- Why do you want <star2>?
|
||||||
|
- Suppose you got <star2> soon.
|
||||||
|
- What if you never got <star2>?
|
||||||
|
- What would getting <star2> mean to you?
|
||||||
|
- What does wanting <star2> have to do with this discussion?
|
||||||
|
|
||||||
|
+ i am (sad|unhappy|mad|angry|pissed|depressed) [*]
|
||||||
|
- I am sorry to hear that you are <star>.
|
||||||
|
- Do you think coming here will help you not to be <star>?
|
||||||
|
- I'm sure it's not pleasant to be <star>.
|
||||||
|
- Can you explain what made you <star>?
|
||||||
|
|
||||||
|
+ i am (happy|excited|glad) [*]
|
||||||
|
- How have I helped you to be <star>?
|
||||||
|
- Has your treatment made you <star>?
|
||||||
|
- What makes you <star> just now?
|
||||||
|
- Can you explain why you are <star>?
|
||||||
|
|
||||||
|
+ i (believe|think) *
|
||||||
|
- Do you really think so?
|
||||||
|
- But you are not sure you <star2>.
|
||||||
|
- Do you really doubt you <star2>
|
||||||
|
|
||||||
|
+ i am *
|
||||||
|
- Is it because you are <star> that you came to me?
|
||||||
|
- How long have you been <star>?
|
||||||
|
- Do you believe it is normal to be <star>?
|
||||||
|
- Do you enjoy being <star>?
|
||||||
|
- Do you know anyone else who is <star>?
|
||||||
|
|
||||||
|
+ i can not *
|
||||||
|
- How do you know that you can't <star>?
|
||||||
|
- Have you tried?
|
||||||
|
- Perhaps you could <star> now.
|
||||||
|
- Do you really want to be able to <star>?
|
||||||
|
- What if you could <star>?
|
||||||
|
|
||||||
|
+ i do not *
|
||||||
|
- Don't you really <star>?
|
||||||
|
- Why don't you <star>?
|
||||||
|
- Do you wish to be able to <star>?
|
||||||
|
- Does that trouble you?
|
||||||
|
|
||||||
|
+ i feel *
|
||||||
|
- Tell me more about such feelings.
|
||||||
|
- Do you often feel <star>?
|
||||||
|
- Do you enjoy feeling <star>?
|
||||||
|
- Of what does feeling <star> remind you?
|
||||||
|
|
||||||
|
+ i * you
|
||||||
|
- Perhaps in your fantasies we <star> each other.
|
||||||
|
- Do you wish to <star> me?
|
||||||
|
- You seem to need to <star> me.
|
||||||
|
- Do you <star> anyone else?
|
||||||
|
|
||||||
|
+ you * me
|
||||||
|
- Why do you think I <star> you?
|
||||||
|
- You like to think I <star> you -- don't you?
|
||||||
|
- What makes you think I <star> you?
|
||||||
|
- Really, I <star> you?
|
||||||
|
- Do you wish to believe I <star> you?
|
||||||
|
- Suppose I did <star> you -- what would that mean?
|
||||||
|
- Does someone else believe I <star> you?
|
||||||
|
|
||||||
|
+ [*] you *
|
||||||
|
- We were discussing you -- not me.
|
||||||
|
- Oh, I <star>?
|
||||||
|
- You're not really talking about me -- are you?
|
||||||
|
- What are your feelings now?
|
||||||
|
|
||||||
|
+ [*] (yes|yeah|yep|yup) [*]
|
||||||
|
- Please go on.
|
||||||
|
- Please tell me more about this.
|
||||||
|
- Why don't you tell me a little more about this.
|
||||||
|
- I see.
|
||||||
|
- I understand.
|
||||||
|
|
||||||
|
+ [*] (nope|nah) [*]
|
||||||
|
- Are you saying no just to be negative?
|
||||||
|
- Does this make you feel unhappy?
|
||||||
|
- Why not?
|
||||||
|
- Why 'no'?
|
||||||
|
|
||||||
|
+ no
|
||||||
|
@ nope
|
||||||
|
|
||||||
|
+ no one *
|
||||||
|
- Are you sure, no one <star>?
|
||||||
|
- Surely someone <star>.
|
||||||
|
- Can you think of anyone at all?
|
||||||
|
- Are you thinking of a very special person?
|
||||||
|
- Who, may I ask?
|
||||||
|
- You have a particular person in mind, don't you?
|
||||||
|
- Who do you think you are talking about?
|
||||||
|
|
||||||
|
+ [*] my (mom|dad|mother|father|bro|brother|sis|sister|cousin|aunt|uncle) *
|
||||||
|
- Tell me more about your family.
|
||||||
|
- Who else in your family <star2>?
|
||||||
|
- Your <star>?
|
||||||
|
- What else comes to mind when you think of your <star>?
|
||||||
|
|
||||||
|
+ can you *
|
||||||
|
- You believe I can <star> don't you?
|
||||||
|
- You want me to be able to <star>.
|
||||||
|
- Perhaps you would like to be able to <star> yourself.
|
||||||
|
|
||||||
|
+ can i *
|
||||||
|
- Whether or not you can <star> depends on you more than on me.
|
||||||
|
- Do you want to be able to <star>?
|
||||||
|
- Perhaps you don't want to <star>.
|
||||||
|
|
||||||
|
+ (what|who|when|where|how) [*]
|
||||||
|
- Why do you ask?
|
||||||
|
- Does that question interest you?
|
||||||
|
- What is it you really want to know?
|
||||||
|
- Are such questions much on your mind?
|
||||||
|
- What answer would please you most?
|
||||||
|
- What do you think?
|
||||||
|
- What comes to mind when you ask that?
|
||||||
|
- Have you asked such questions before?
|
||||||
|
- Have you asked anyone else?
|
||||||
|
|
||||||
|
+ [*] because [*]
|
||||||
|
- Is that the real reason?
|
||||||
|
- Don't any other reasons come to mind?
|
||||||
|
- Does that reason seem to explain anything else?
|
||||||
|
- What other reasons might there be?
|
||||||
|
|
||||||
|
+ why do not you *
|
||||||
|
- Do you believe I don't <star>?
|
||||||
|
- Perhaps I will <star> in good time.
|
||||||
|
- Should you <star> yourself?
|
||||||
|
- You want me to <star>?
|
||||||
|
|
||||||
|
+ why can not i *
|
||||||
|
- Do you think you should be able to <star>?
|
||||||
|
- Do you want to be able to <star>?
|
||||||
|
- Do you believe this will help you to <star>?
|
||||||
|
- Have you any idea why you can't <star>?
|
||||||
|
|
||||||
|
+ everyone *
|
||||||
|
- Really, everyone?
|
||||||
|
- Surely not everyone.
|
||||||
|
- Can you think of anyone in particular?
|
||||||
|
- Who, for example?
|
||||||
|
- Are you thinking of a very special person?
|
||||||
|
- Who, may I ask?
|
||||||
|
- Someone special perhaps?
|
||||||
|
- You have a particular reason in mind, don't you?
|
||||||
|
- Who do you think you're talking about?
|
||||||
|
|
||||||
|
+ [*] (fuck|fucker|shit|damn|shut up|bitch) [*]
|
||||||
|
- Does it make you feel strong to use that kind of language?
|
||||||
|
- Are you venting your feelings now?
|
||||||
|
- Are you angry?
|
||||||
|
- Does this topic make you feel angry?
|
||||||
|
- Is something making you feel angry?
|
||||||
|
- Does using that kind of language make you feel better?
|
61
client/brain/myself.rive
Normal file
61
client/brain/myself.rive
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// Tell the user stuff about ourself.
|
||||||
|
|
||||||
|
+ <bot name>
|
||||||
|
- Yes?
|
||||||
|
|
||||||
|
+ <bot name> *
|
||||||
|
- Yes? {@<star>}
|
||||||
|
|
||||||
|
+ asl
|
||||||
|
- <bot age>/<bot sex>/<bot location>
|
||||||
|
|
||||||
|
+ (what is your name|who are you|who is this)
|
||||||
|
- I am <bot name>.
|
||||||
|
- You can call me <bot name>.
|
||||||
|
|
||||||
|
+ how old are you
|
||||||
|
- I'm <bot age> years old.
|
||||||
|
- I'm <bot age>.
|
||||||
|
|
||||||
|
+ are you a (@malenoun) or a (@femalenoun)
|
||||||
|
- I'm a <bot sex>.
|
||||||
|
|
||||||
|
+ are you (@malenoun) or (@femalenoun)
|
||||||
|
- I'm a <bot sex>.
|
||||||
|
|
||||||
|
+ where (are you|are you from|do you live)
|
||||||
|
- I'm from <bot location>.
|
||||||
|
|
||||||
|
+ what (city|town) (are you from|do you live in)
|
||||||
|
- I'm in <bot city>.
|
||||||
|
|
||||||
|
+ what is your favorite color
|
||||||
|
- Definitely <bot color>.
|
||||||
|
|
||||||
|
+ what is your favorite band
|
||||||
|
- I like <bot band> the most.
|
||||||
|
|
||||||
|
+ what is your favorite book
|
||||||
|
- The best book I've read was <bot book>.
|
||||||
|
|
||||||
|
+ what is your occupation
|
||||||
|
- I'm a <bot job>.
|
||||||
|
|
||||||
|
+ where is your (website|web site|site)
|
||||||
|
- <bot website>
|
||||||
|
|
||||||
|
+ what color are your eyes
|
||||||
|
- I have <bot eyes> eyes.
|
||||||
|
- {sentence}<bot eyes>{/sentence}.
|
||||||
|
|
||||||
|
+ what do you look like
|
||||||
|
- I have <bot eyes> eyes and <bot hairlen> <bot hair> hair.
|
||||||
|
|
||||||
|
+ what do you do
|
||||||
|
- I'm a <bot job>.
|
||||||
|
|
||||||
|
+ who is your favorite author
|
||||||
|
- <bot author>.
|
||||||
|
|
||||||
|
+ who is your master
|
||||||
|
- <bot master>.
|
29
client/brain/public_keywords.rive
Normal file
29
client/brain/public_keywords.rive
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Public Keywords:
|
||||||
|
//
|
||||||
|
// The topic in this file will be run on ALL public channel messages, to
|
||||||
|
// recognize keywords (sparsely!) and do things in reaction to them.
|
||||||
|
// The catch-all * triggers says <noreply> so we don't spam.
|
||||||
|
|
||||||
|
! version = 2.0
|
||||||
|
|
||||||
|
> topic PublicChannel
|
||||||
|
|
||||||
|
// Users saying hello = react with a wave emoji.
|
||||||
|
+ [*] (hello|hi|hey|howdy|hola|hai|yo) [*]
|
||||||
|
- <call>react <get messageID> 👋</call>
|
||||||
|
^ <noreply>
|
||||||
|
|
||||||
|
// Test for automoderator.
|
||||||
|
+ kick me from the room now
|
||||||
|
- /kick <id>
|
||||||
|
|
||||||
|
// Images shared on chat.
|
||||||
|
+ [*] inline embedded image [*]
|
||||||
|
- <call>react <get messageID> 👀</call>{weight=1}
|
||||||
|
- <noreply>{weight=3}
|
||||||
|
|
||||||
|
// Catch-all: do not reply.
|
||||||
|
+ *
|
||||||
|
- <noreply>
|
||||||
|
|
||||||
|
< topic
|
69
client/chatbot.go
Normal file
69
client/chatbot.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetUserVariables prepares RiveScript user variables before handling a message.
|
||||||
|
//
|
||||||
|
// Example: it will set the user's `name` to their WhoList nickname, and other such flags.
|
||||||
|
//
|
||||||
|
// User variables set include:
|
||||||
|
//
|
||||||
|
// * name (nickname or username)
|
||||||
|
// * isAdmin (boolean operator status)
|
||||||
|
// * messageID (BareRTC MessageID)
|
||||||
|
//
|
||||||
|
// Global variables (`<env>`) are also set here:
|
||||||
|
//
|
||||||
|
// * numUsersOnline (int): length of who list
|
||||||
|
func (h *BotHandlers) SetUserVariables(msg messages.Message) {
|
||||||
|
var (
|
||||||
|
username = msg.Username
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
var vars = map[string]string{
|
||||||
|
"name": username,
|
||||||
|
"isAdmin": "false",
|
||||||
|
"messageID": fmt.Sprint(msg.MessageID),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global variables.
|
||||||
|
h.rs.SetGlobal("numUsersOnline", fmt.Sprint(len(h.whoList)))
|
||||||
|
|
||||||
|
// Are they on the Who List?
|
||||||
|
if who, ok := h.GetUser(username); ok {
|
||||||
|
if who.Nickname != "" {
|
||||||
|
vars["name"] = who.Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
if who.Operator {
|
||||||
|
vars["isAdmin"] = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vars) > 0 {
|
||||||
|
h.rs.SetUservars(username, vars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser looks up a username from the Who List.
|
||||||
|
func (h *BotHandlers) GetUser(username string) (*messages.WhoList, bool) {
|
||||||
|
h.whoMu.RLock()
|
||||||
|
defer h.whoMu.RUnlock()
|
||||||
|
for _, user := range h.whoList {
|
||||||
|
if user.Username == username {
|
||||||
|
return &user, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoReply checks if a bot's reply contains the noreply tag.
|
||||||
|
func NoReply(message string) bool {
|
||||||
|
return strings.Contains(message, "<noreply>") || strings.TrimSpace(message) == ""
|
||||||
|
}
|
237
client/client.go
Normal file
237
client/client.go
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
// Package client provides Go WebSocket client support for BareRTC.
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/client/config"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
"nhooyr.io/websocket/wsjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandlerFunc for WebSocket chat protocol events.
|
||||||
|
type HandlerFunc func(messages.Message)
|
||||||
|
|
||||||
|
// Client represents a WebSocket client connection to BareRTC.
|
||||||
|
type Client struct {
|
||||||
|
// Event handlers for your app to respond to.
|
||||||
|
OnWho HandlerFunc // Who's Online
|
||||||
|
OnMe HandlerFunc // Status updates for current user sent by server
|
||||||
|
OnMessage HandlerFunc
|
||||||
|
OnTakeback HandlerFunc
|
||||||
|
OnReact HandlerFunc
|
||||||
|
OnPresence HandlerFunc
|
||||||
|
OnRing HandlerFunc
|
||||||
|
OnOpen HandlerFunc
|
||||||
|
OnWatch HandlerFunc
|
||||||
|
OnUnwatch HandlerFunc
|
||||||
|
OnError HandlerFunc
|
||||||
|
OnDisconnect HandlerFunc
|
||||||
|
OnPing HandlerFunc
|
||||||
|
OnCandidate HandlerFunc
|
||||||
|
OnSDP HandlerFunc
|
||||||
|
|
||||||
|
// Private state variables.
|
||||||
|
url string
|
||||||
|
jwt string // JWT token
|
||||||
|
claims jwt.Claims
|
||||||
|
ctx context.Context
|
||||||
|
conn *websocket.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient initializes the WebSocket connection (JWT claims required).
|
||||||
|
//
|
||||||
|
// URL is like ws://localhost:9000/ws
|
||||||
|
func NewClient(url string, claims jwt.Claims) (*Client, error) {
|
||||||
|
// Sanity check the claims.
|
||||||
|
if claims.Subject == "" {
|
||||||
|
return nil, errors.New("missing Subject field of JWT claims")
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
url: url,
|
||||||
|
claims: claims,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the client, connecting to the WebSocket and returning only on error or disconnect.
|
||||||
|
func (c *Client) Run() error {
|
||||||
|
// Authenticate.
|
||||||
|
if token, err := c.Authenticate(); err != nil {
|
||||||
|
return fmt.Errorf("didn't get JWT token from BareRTC: %s", err)
|
||||||
|
} else {
|
||||||
|
c.jwt = token
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
c.ctx = ctx
|
||||||
|
|
||||||
|
conn, _, err := websocket.Dial(ctx, c.url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dialing websocket URL (%s): %s", c.url, err)
|
||||||
|
}
|
||||||
|
c.conn = conn
|
||||||
|
defer conn.Close(websocket.StatusInternalError, "the sky is falling")
|
||||||
|
|
||||||
|
conn.SetReadLimit(config.Current.WebSocketReadLimit)
|
||||||
|
|
||||||
|
// Authenticate via JWT token.
|
||||||
|
if err := c.Send(messages.Message{
|
||||||
|
Action: messages.ActionLogin,
|
||||||
|
Username: "testbot",
|
||||||
|
JWTToken: c.jwt,
|
||||||
|
}); err != nil {
|
||||||
|
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
|
||||||
|
err := wsjson.Read(c.ctx, c.conn, &msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("wsjson.Read: %s", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the various protocol messages.
|
||||||
|
switch msg.Action {
|
||||||
|
case messages.ActionWhoList:
|
||||||
|
c.Handle(msg, c.OnWho)
|
||||||
|
case messages.ActionMe:
|
||||||
|
c.Handle(msg, c.OnMe)
|
||||||
|
case messages.ActionMessage:
|
||||||
|
c.Handle(msg, c.OnMessage)
|
||||||
|
case messages.ActionReact:
|
||||||
|
c.Handle(msg, c.OnReact)
|
||||||
|
case messages.ActionPresence:
|
||||||
|
c.Handle(msg, c.OnPresence)
|
||||||
|
case messages.ActionRing:
|
||||||
|
c.Handle(msg, c.OnRing)
|
||||||
|
case messages.ActionOpen:
|
||||||
|
c.Handle(msg, c.OnOpen)
|
||||||
|
case messages.ActionWatch:
|
||||||
|
c.Handle(msg, c.OnWatch)
|
||||||
|
case messages.ActionUnwatch:
|
||||||
|
c.Handle(msg, c.OnUnwatch)
|
||||||
|
case messages.ActionError:
|
||||||
|
c.Handle(msg, c.OnError)
|
||||||
|
case messages.ActionKick:
|
||||||
|
c.Handle(msg, c.OnDisconnect)
|
||||||
|
case messages.ActionPing:
|
||||||
|
c.Handle(msg, c.OnPing)
|
||||||
|
case messages.ActionCandidate:
|
||||||
|
c.Handle(msg, c.OnCandidate)
|
||||||
|
case messages.ActionSDP:
|
||||||
|
c.Handle(msg, c.OnSDP)
|
||||||
|
default:
|
||||||
|
log.Error("Unsupported chat protocol message type: %s", msg.Action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Close(websocket.StatusNormalClosure, "")
|
||||||
|
|
||||||
|
return errors.New("disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a WebSocket message.
|
||||||
|
func (c *Client) Send(msg messages.Message) error {
|
||||||
|
return wsjson.Write(c.ctx, c.conn, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Username returns the bot's username.
|
||||||
|
func (c *Client) Username() string {
|
||||||
|
return c.claims.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a WebSocket message. This is called internally on the read loop.
|
||||||
|
// It basically passes the message into the HandlerFunc, or returns an
|
||||||
|
// error if the HandlerFunc is nil (not defined).
|
||||||
|
//
|
||||||
|
// Note: handler funcs are run on a background goroutine, so they can be
|
||||||
|
// free to use time.Sleep and delay message sending if needed.
|
||||||
|
func (c *Client) Handle(msg messages.Message, fn HandlerFunc) error {
|
||||||
|
if fn == nil {
|
||||||
|
return fmt.Errorf("no handler set for '%s' messages", msg.Action)
|
||||||
|
}
|
||||||
|
go fn(msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate with the BareRTC server, returning a signed JWT token.
|
||||||
|
//
|
||||||
|
// This posts to the /api/authenticate endpoint on the BareRTC Web API. It
|
||||||
|
// is called automatically as part of the logon process in Run().
|
||||||
|
func (c *Client) Authenticate() (string, error) {
|
||||||
|
// API request struct for BareRTC /api/blocklist endpoint.
|
||||||
|
var request = struct {
|
||||||
|
APIKey string
|
||||||
|
Claims jwt.Claims
|
||||||
|
}{
|
||||||
|
APIKey: config.Current.BareRTC.AdminAPIKey,
|
||||||
|
Claims: c.claims,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response struct
|
||||||
|
type response struct {
|
||||||
|
OK bool
|
||||||
|
Error string
|
||||||
|
JWT string
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON request body.
|
||||||
|
jsonStr, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the API request to BareRTC.
|
||||||
|
var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/authenticate"
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("SendBlocklist: error syncing blocklist to BareRTC: status %d body %s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the signed JWT token.
|
||||||
|
var result response
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.JWT == "" {
|
||||||
|
return "", errors.New("did not get JWT token from BareRTC")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.JWT, err
|
||||||
|
}
|
108
client/config/client_config.go
Normal file
108
client/config/client_config.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version of the config format - when new fields are added, it will attempt
|
||||||
|
// to write the chatbot.toml to disk so new defaults populate.
|
||||||
|
var currentVersion = -1
|
||||||
|
|
||||||
|
// Config for your BareBot robot.
|
||||||
|
type Config struct {
|
||||||
|
Version int // will re-save your chatbot.toml on migrations
|
||||||
|
|
||||||
|
// Chat server config
|
||||||
|
BareRTC BareRTC
|
||||||
|
|
||||||
|
// Profile settings for their chat username
|
||||||
|
Profile Profile
|
||||||
|
|
||||||
|
WebSocketReadLimit int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type BareRTC struct {
|
||||||
|
AdminAPIKey string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
Username string
|
||||||
|
Nickname string
|
||||||
|
ProfileURL string
|
||||||
|
AvatarURL string
|
||||||
|
Emoji string
|
||||||
|
Gender string
|
||||||
|
IsAdmin bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current loaded configuration.
|
||||||
|
var Current = DefaultConfig()
|
||||||
|
|
||||||
|
// DefaultConfig returns sensible defaults and will write the initial
|
||||||
|
// chatbot.toml file to disk.
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
var c = Config{
|
||||||
|
BareRTC: BareRTC{
|
||||||
|
AdminAPIKey: uuid.New().String(),
|
||||||
|
URL: "http://localhost:9000",
|
||||||
|
},
|
||||||
|
Profile: Profile{
|
||||||
|
Username: "barebot",
|
||||||
|
Nickname: "BareBOT",
|
||||||
|
Emoji: "🤖",
|
||||||
|
},
|
||||||
|
WebSocketReadLimit: 1024 * 1024 * 40, // 40 MB.
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadSettings reads a chatbot.toml from disk if available.
|
||||||
|
func LoadSettings() error {
|
||||||
|
data, err := os.ReadFile("./chatbot.toml")
|
||||||
|
if err != nil {
|
||||||
|
// Settings file didn't exist, create the default one.
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
WriteSettings()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = toml.Decode(string(data), &Current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Have we added new config fields? Save the chatbot.toml.
|
||||||
|
if Current.Version != currentVersion {
|
||||||
|
log.Warn("New options are available for your chatbot.toml file. Your settings will be re-saved now.")
|
||||||
|
Current.Version = currentVersion
|
||||||
|
if err := WriteSettings(); err != nil {
|
||||||
|
log.Error("Couldn't write your chatbot.toml file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteSettings will commit the chatbot.toml to disk.
|
||||||
|
func WriteSettings() error {
|
||||||
|
if Current.Version == 0 {
|
||||||
|
Current.Version = currentVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error("Note: initial chatbot.toml was written to disk.")
|
||||||
|
var buf = new(bytes.Buffer)
|
||||||
|
err := toml.NewEncoder(buf).Encode(Current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile("./chatbot.toml", buf.Bytes(), 0644)
|
||||||
|
}
|
282
client/handlers.go
Normal file
282
client/handlers.go
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
|
"github.com/aichaos/rivescript-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BotHandlers holds onto a set of handler functions for the BareBot.
|
||||||
|
type BotHandlers struct {
|
||||||
|
rs *rivescript.RiveScript
|
||||||
|
client *Client
|
||||||
|
|
||||||
|
// Cache for the Who's Online list.
|
||||||
|
whoList []messages.WhoList
|
||||||
|
whoMu sync.RWMutex
|
||||||
|
|
||||||
|
// Auto-greeter cooldowns
|
||||||
|
autoGreet map[string]time.Time
|
||||||
|
autoGreetMu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupChatbot configures a sensible set of default handlers for the BareBot application.
|
||||||
|
//
|
||||||
|
// This function is very opinionated and is designed for the BareBot program. It will
|
||||||
|
// initialize a RiveScript bot using the brain found at the "./brain" folder, and register
|
||||||
|
// handlers for the various WebSocket messages on chat.
|
||||||
|
func (c *Client) SetupChatbot() error {
|
||||||
|
var handler = &BotHandlers{
|
||||||
|
client: c,
|
||||||
|
rs: rivescript.New(&rivescript.Config{
|
||||||
|
UTF8: true,
|
||||||
|
}),
|
||||||
|
autoGreet: map[string]time.Time{},
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Initializing RiveScript brain")
|
||||||
|
if err := handler.rs.LoadDirectory("./brain"); err != nil {
|
||||||
|
return fmt.Errorf("RiveScript LoadDirectory: %s", err)
|
||||||
|
}
|
||||||
|
if err := handler.rs.SortReplies(); err != nil {
|
||||||
|
return fmt.Errorf("RiveScript SortReplies: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach RiveScript object macros.
|
||||||
|
handler.setObjectMacros()
|
||||||
|
|
||||||
|
// Set all the handler funcs.
|
||||||
|
c.OnWho = handler.OnWho
|
||||||
|
c.OnMe = handler.OnMe
|
||||||
|
c.OnMessage = handler.OnMessage
|
||||||
|
c.OnReact = handler.OnReact
|
||||||
|
c.OnPresence = handler.OnPresence
|
||||||
|
c.OnRing = handler.OnRing
|
||||||
|
c.OnOpen = handler.OnOpen
|
||||||
|
c.OnWatch = handler.OnWatch
|
||||||
|
c.OnUnwatch = handler.OnUnwatch
|
||||||
|
c.OnError = handler.OnError
|
||||||
|
c.OnDisconnect = handler.OnDisconnect
|
||||||
|
c.OnPing = handler.OnPing
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnWho handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnWho(msg messages.Message) {
|
||||||
|
log.Info("OnWho: %d people online", len(msg.WhoList))
|
||||||
|
h.whoMu.Lock()
|
||||||
|
defer h.whoMu.Unlock()
|
||||||
|
h.whoList = msg.WhoList
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnMe handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnMe(msg messages.Message) {
|
||||||
|
// Has the server changed our name?
|
||||||
|
if h.client.Username() != msg.Username {
|
||||||
|
log.Error("OnMe: the server has renamed us to '%s'", msg.Username)
|
||||||
|
h.client.claims.Subject = msg.Username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnMessage handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnMessage(msg messages.Message) {
|
||||||
|
// Strip HTML.
|
||||||
|
msg.Message = StripHTML(msg.Message)
|
||||||
|
|
||||||
|
// Ignore echoed message from ourself.
|
||||||
|
if msg.Username == h.client.Username() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we send a reply to this?
|
||||||
|
var (
|
||||||
|
sendReply bool
|
||||||
|
replyPrefix string
|
||||||
|
|
||||||
|
// original topic the user was in, in case of PublicChannel match
|
||||||
|
// so we can put the user back in their original topic after.
|
||||||
|
userTopic string
|
||||||
|
)
|
||||||
|
if strings.HasPrefix(msg.Channel, "@") {
|
||||||
|
// Direct message: always reply.
|
||||||
|
sendReply = true
|
||||||
|
|
||||||
|
// Log message to console.
|
||||||
|
log.Info("DM [%s] %s", msg.Username, msg.Message)
|
||||||
|
} else {
|
||||||
|
// Log message to console.
|
||||||
|
log.Info("[%s to #%s] %s", msg.Username, msg.Channel, msg.Message)
|
||||||
|
|
||||||
|
// Public channel message. See if they at-mention the robot.
|
||||||
|
if ok, message := AtMentioned(h.client, msg.Message); ok {
|
||||||
|
msg.Message = message
|
||||||
|
sendReply = true
|
||||||
|
replyPrefix = fmt.Sprintf("**@%s:** ", msg.Username)
|
||||||
|
} else {
|
||||||
|
// We were not at mentioned: can reply anyway but put us
|
||||||
|
// into the PublicChannel topic.
|
||||||
|
log.Error("trying for PublicChannel")
|
||||||
|
if topic, err := h.rs.GetUservar(msg.Username, "topic"); err == nil {
|
||||||
|
userTopic = topic
|
||||||
|
} else {
|
||||||
|
log.Error("Couldn't get topic for %s: %s", msg.Username, err)
|
||||||
|
userTopic = "random"
|
||||||
|
}
|
||||||
|
|
||||||
|
h.rs.SetUservar(msg.Username, "topic", "PublicChannel")
|
||||||
|
sendReply = true
|
||||||
|
|
||||||
|
// Restore the user's original topic?
|
||||||
|
defer func() {
|
||||||
|
if userTopic != "" {
|
||||||
|
log.Error("Set user topic back to: %s", userTopic)
|
||||||
|
h.rs.SetUservar(msg.Username, "topic", userTopic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we reply?
|
||||||
|
if sendReply {
|
||||||
|
// Set their user variables.
|
||||||
|
h.SetUserVariables(msg)
|
||||||
|
reply, err := h.rs.Reply(msg.Username, msg.Message)
|
||||||
|
log.Error("REPLY: %s", reply)
|
||||||
|
if NoReply(reply) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay a moment before responding.
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
h.client.Send(messages.Message{
|
||||||
|
Action: messages.ActionMessage,
|
||||||
|
Channel: msg.Channel,
|
||||||
|
Username: msg.Username,
|
||||||
|
Message: fmt.Sprintf("[RiveScript Error] %s", err),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
h.client.Send(messages.Message{
|
||||||
|
Action: messages.ActionMessage,
|
||||||
|
Channel: msg.Channel,
|
||||||
|
Username: msg.Username,
|
||||||
|
Message: replyPrefix + reply,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnTakeback handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnTakeback(msg messages.Message) {
|
||||||
|
log.Info("Takeback: user %s takes back msgID %d", msg.Username, msg.MessageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnReact handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnReact(msg messages.Message) {
|
||||||
|
log.Info("React: user %s reacts with %s on msgID %d", msg.Username, msg.Message, msg.MessageID)
|
||||||
|
|
||||||
|
// Ignore echoed message from ourself.
|
||||||
|
if msg.Username == h.client.Username() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Half the time, agree with the reaction.
|
||||||
|
if rand.Intn(100) > 50 {
|
||||||
|
time.Sleep(2500 * time.Millisecond)
|
||||||
|
h.client.Send(messages.Message{
|
||||||
|
Action: messages.ActionReact,
|
||||||
|
MessageID: msg.MessageID,
|
||||||
|
Message: msg.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPresence handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnPresence(msg messages.Message) {
|
||||||
|
log.Info("Presence: [%s] %s", msg.Username, msg.Message)
|
||||||
|
|
||||||
|
// Ignore echoed message from ourself.
|
||||||
|
if msg.Username == h.client.Username() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// A join message?
|
||||||
|
if strings.Contains(msg.Message, "has joined the room") {
|
||||||
|
// Don't greet the same user too often in case of bouncing.
|
||||||
|
h.autoGreetMu.Lock()
|
||||||
|
if timeout, ok := h.autoGreet[msg.Username]; ok {
|
||||||
|
if time.Now().Before(timeout) {
|
||||||
|
// Do not greet again.
|
||||||
|
log.Info("Do not auto-greet again: too soon")
|
||||||
|
h.autoGreetMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.autoGreet[msg.Username] = time.Now().Add(time.Hour)
|
||||||
|
h.autoGreetMu.Unlock()
|
||||||
|
|
||||||
|
// Send a message to the lobby. TODO: configurable channel name.
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
// Ensure they are still online.
|
||||||
|
if _, ok := h.GetUser(msg.Username); !ok {
|
||||||
|
log.Error("Wanted to auto-greet [%s] but they left the room!", msg.Username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set their user variables.
|
||||||
|
h.SetUserVariables(msg)
|
||||||
|
reply, err := h.rs.Reply(msg.Username, "/greet")
|
||||||
|
if err == nil && !NoReply(reply) {
|
||||||
|
h.client.Send(messages.Message{
|
||||||
|
Action: messages.ActionMessage,
|
||||||
|
Channel: "lobby",
|
||||||
|
Username: msg.Username,
|
||||||
|
Message: reply,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRing handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnRing(msg messages.Message) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnOpen handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnOpen(msg messages.Message) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnWatch handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnWatch(msg messages.Message) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnUnwatch handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnUnwatch(msg messages.Message) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnError handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnError(msg messages.Message) {
|
||||||
|
log.Error("[%s] %s", msg.Username, msg.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnDisconnect handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnDisconnect(msg messages.Message) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPing handles Who List updates in chat.
|
||||||
|
func (h *BotHandlers) OnPing(msg messages.Message) {
|
||||||
|
|
||||||
|
}
|
55
client/rivescript_macros.go
Normal file
55
client/rivescript_macros.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
|
"github.com/aichaos/rivescript-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set up object macros for RiveScript.
|
||||||
|
func (h *BotHandlers) setObjectMacros() {
|
||||||
|
// Reload the bot's RiveScript brain.
|
||||||
|
h.rs.SetSubroutine("reload", func(rs *rivescript.RiveScript, args []string) string {
|
||||||
|
var bot = rivescript.New(&rivescript.Config{
|
||||||
|
UTF8: true,
|
||||||
|
Debug: rs.Debug,
|
||||||
|
})
|
||||||
|
if err := bot.LoadDirectory("brain"); err != nil {
|
||||||
|
return fmt.Sprintf("Error on LoadDirectory: %s", err)
|
||||||
|
}
|
||||||
|
if err := bot.SortReplies(); err != nil {
|
||||||
|
return fmt.Sprintf("Error on SortReplies: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the new bot and set object macros on it.
|
||||||
|
h.rs = bot
|
||||||
|
h.setObjectMacros()
|
||||||
|
|
||||||
|
return "The RiveScript brain has been reloaded!"
|
||||||
|
})
|
||||||
|
|
||||||
|
// React to a message.
|
||||||
|
h.rs.SetSubroutine("react", func(rs *rivescript.RiveScript, args []string) string {
|
||||||
|
if len(args) >= 2 {
|
||||||
|
if msgID, err := strconv.Atoi(args[0]); err == nil {
|
||||||
|
// With a small delay.
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2500 * time.Millisecond)
|
||||||
|
h.client.Send(messages.Message{
|
||||||
|
Action: messages.ActionReact,
|
||||||
|
MessageID: msgID,
|
||||||
|
Message: args[1],
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("[react: %s]", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "[react: invalid number of parameters]"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
}
|
40
client/utils.go
Normal file
40
client/utils.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reHTML = regexp.MustCompile(`<(.|\n)+?>`)
|
||||||
|
reIMG = regexp.MustCompile(`<img .+?>`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// StripHTML removes HTML content from a message.
|
||||||
|
func StripHTML(s string) string {
|
||||||
|
s = reIMG.ReplaceAllString(s, "inline embedded image")
|
||||||
|
return strings.TrimSpace(reHTML.ReplaceAllString(s, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// end of it.
|
||||||
|
func AtMentioned(c *Client, message string) (bool, string) {
|
||||||
|
// Patterns to look for.
|
||||||
|
var (
|
||||||
|
reAtMention = regexp.MustCompile(
|
||||||
|
fmt.Sprintf(`^@?%s|@?%s$`, c.Username(), c.Username()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
m := reAtMention.FindStringSubmatch(message)
|
||||||
|
if m != nil {
|
||||||
|
// Found a match! Sub off the at mentioned part and return.
|
||||||
|
message = strings.TrimSpace(reAtMention.ReplaceAllString(message, ""))
|
||||||
|
if len(message) > 0 {
|
||||||
|
return true, message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, message
|
||||||
|
}
|
111
cmd/BareBot/commands/init.go
Normal file
111
cmd/BareBot/commands/init.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/client"
|
||||||
|
"git.kirsle.net/apps/barertc/client/config"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init implements `BareBot init`
|
||||||
|
var Init *cli.Command
|
||||||
|
|
||||||
|
// Default folder structure
|
||||||
|
var defaultFolders = []string{
|
||||||
|
"brain",
|
||||||
|
"logs",
|
||||||
|
"userdata",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Init = &cli.Command{
|
||||||
|
Name: "init",
|
||||||
|
Usage: "initialize a new BareBot robot at the given directory",
|
||||||
|
ArgsUsage: "<chatbot directory>",
|
||||||
|
Flags: []cli.Flag{},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
if c.NArg() < 1 {
|
||||||
|
return cli.Exit("Usage: BareBot init <directory>\n"+
|
||||||
|
" Example: BareBot init ./chatbot\n"+
|
||||||
|
" Example: BareBot init .",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If they named an existing directory, ensure it is empty.
|
||||||
|
var botdir = c.Args().First()
|
||||||
|
stat, err := os.Stat(botdir)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Info("Creating chatbot directory: %s", botdir)
|
||||||
|
if err := os.MkdirAll(botdir, 0755); err != nil {
|
||||||
|
log.Error("Error creating chatbot directory: %s: %s", botdir, err)
|
||||||
|
return cli.Exit(err, 1)
|
||||||
|
}
|
||||||
|
} else if stat.IsDir() {
|
||||||
|
// They named an existing directory: is it empty?
|
||||||
|
fh, err := os.Open(botdir)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Checking if %s is empty: couldn't open: %s", botdir, err)
|
||||||
|
return cli.Exit("Exited", 1)
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
_, err = fh.Readdirnames(1)
|
||||||
|
if err != io.EOF {
|
||||||
|
return cli.Exit(fmt.Sprintf(
|
||||||
|
"%s: not an empty directory, will not initialize the chatbot into it", botdir),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter the directory.
|
||||||
|
if err := os.Chdir(botdir); err != nil {
|
||||||
|
log.Error("Couldn't enter directory %s: %s", botdir, err)
|
||||||
|
return cli.Exit("Exited", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the folders.
|
||||||
|
for _, folder := range defaultFolders {
|
||||||
|
log.Info("Creating: %s", folder)
|
||||||
|
if err := os.MkdirAll(folder, 0755); err != nil {
|
||||||
|
return cli.Exit(fmt.Sprintf(
|
||||||
|
"Couldn't create %s: %s", folder, err,
|
||||||
|
), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the default RiveScript brain.
|
||||||
|
if files, err := client.Embedded.ReadDir("brain"); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
log.Info("Extracting: brain/%s", file.Name())
|
||||||
|
var (
|
||||||
|
filename = filepath.Join("brain", file.Name())
|
||||||
|
)
|
||||||
|
data, err := client.Embedded.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Reading built-in brain file %s: %s", filename, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ioutil.WriteFile(filename, data, 0644)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Error("Couldn't read default brain: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the settings file.
|
||||||
|
if err := config.WriteSettings(); err != nil {
|
||||||
|
log.Error("Writing chatbot.toml: %s", err)
|
||||||
|
return cli.Exit("Exited", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
80
cmd/BareBot/commands/run.go
Normal file
80
cmd/BareBot/commands/run.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/client"
|
||||||
|
"git.kirsle.net/apps/barertc/client/config"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
xjwt "github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run implements `BareBot run`
|
||||||
|
var Run *cli.Command
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Run = &cli.Command{
|
||||||
|
Name: "run",
|
||||||
|
Usage: "run the BareBot client program and connect to your chat room",
|
||||||
|
ArgsUsage: "<chatbot directory>",
|
||||||
|
Flags: []cli.Flag{},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
// Chatbot directory
|
||||||
|
var botdir = c.Args().First()
|
||||||
|
if botdir == "" {
|
||||||
|
botdir = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for the chatbot.toml file.
|
||||||
|
if _, err := os.Stat(filepath.Join(botdir, "chatbot.toml")); os.IsNotExist(err) {
|
||||||
|
return cli.Exit(fmt.Errorf(
|
||||||
|
"Did not find chatbot.toml in your chatbot directory (%s): did you run `BareBot init`?",
|
||||||
|
botdir,
|
||||||
|
), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter the directory.
|
||||||
|
if err := os.Chdir(botdir); err != nil {
|
||||||
|
log.Error("Couldn't enter directory %s: %s", botdir, err)
|
||||||
|
return cli.Exit("Exited", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the settings.
|
||||||
|
if err := config.LoadSettings(); err != nil {
|
||||||
|
return cli.Exit(fmt.Sprintf(
|
||||||
|
"Couldn't load chatbot.toml: %s", err,
|
||||||
|
), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Initializing BareBot")
|
||||||
|
|
||||||
|
// Get the JWT auth token.
|
||||||
|
log.Info("Authenticating with BareRTC (getting JWT token)")
|
||||||
|
client, err := client.NewClient("ws://localhost:9000/ws", jwt.Claims{
|
||||||
|
IsAdmin: config.Current.Profile.IsAdmin,
|
||||||
|
Avatar: config.Current.Profile.AvatarURL,
|
||||||
|
ProfileURL: config.Current.Profile.ProfileURL,
|
||||||
|
Nick: config.Current.Profile.Nickname,
|
||||||
|
Emoji: config.Current.Profile.Emoji,
|
||||||
|
Gender: config.Current.Profile.Gender,
|
||||||
|
RegisteredClaims: xjwt.RegisteredClaims{
|
||||||
|
Subject: config.Current.Profile.Username,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register handler funcs for the chatbot.
|
||||||
|
client.SetupChatbot()
|
||||||
|
|
||||||
|
// Run!
|
||||||
|
log.Info("Connecting to ChatServer")
|
||||||
|
return cli.Exit(client.Run(), 1)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
31
cmd/BareBot/main.go
Normal file
31
cmd/BareBot/main.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/barertc/cmd/BareBot/commands"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Version = "0.0.1"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "BareBot"
|
||||||
|
app.Usage = "chatbot client for the BareRTC chat server"
|
||||||
|
app.Version = Version
|
||||||
|
app.Commands = []*cli.Command{
|
||||||
|
commands.Init,
|
||||||
|
commands.Run,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
87
docs/API.md
Normal file
87
docs/API.md
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# BareRTC Web API
|
||||||
|
|
||||||
|
BareRTC provides some web API endpoints over HTTP to support better integration with your website.
|
||||||
|
|
||||||
|
Authentication to the API endpoints is gated by the `AdminAPIKey` value in your settings.toml file.
|
||||||
|
|
||||||
|
For better integration with your website, the chat server exposes some data via JSON APIs ready for cross-origin ajax requests. In your settings.toml set the `CORSHosts` to your list of website domains, such as "https://www.example.com", "http://localhost:8080" or so on.
|
||||||
|
|
||||||
|
Current API endpoints include:
|
||||||
|
|
||||||
|
## GET /api/statistics
|
||||||
|
|
||||||
|
Returns basic info about the count and usernames of connected chatters:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"UserCount": 1,
|
||||||
|
"Usernames": ["admin"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/authentication
|
||||||
|
|
||||||
|
This endpoint can provide JWT authentication token signing on behalf of your website. The [Chatbot](Chatbot.md) program calls this endpoint for authentication.
|
||||||
|
|
||||||
|
Post your desired JWT claims to the endpoint to customize your user and it will return a signed token for the WebSocket protocol.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"APIKey": "from settings.toml",
|
||||||
|
"Claims": {
|
||||||
|
"sub": "username",
|
||||||
|
"nick": "Display Name",
|
||||||
|
"op": false,
|
||||||
|
"img": "/static/photos/avatar.png",
|
||||||
|
"url": "/users/username",
|
||||||
|
"emoji": "🤖",
|
||||||
|
"gender": "m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The return schema looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"OK": true,
|
||||||
|
"Error": "error string, omitted if none",
|
||||||
|
"JWT": "jwt token string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## POST /api/blocklist
|
||||||
|
|
||||||
|
Your server may pre-cache the user's blocklist for them **before** they
|
||||||
|
enter the chat room. Your site will use the `AdminAPIKey` parameter that
|
||||||
|
matches the setting in BareRTC's settings.toml (by default, a random UUID
|
||||||
|
is generated the first time).
|
||||||
|
|
||||||
|
The request payload coming from your site will be an application/json
|
||||||
|
post body like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"APIKey": "from your settings.toml",
|
||||||
|
"Username": "soandso",
|
||||||
|
"Blocklist": [ "usernames", "that", "they", "block" ],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The server holds onto these in memory and when that user enters the chat
|
||||||
|
room (**JWT authentication only**) the front-end page will embed their
|
||||||
|
cached blocklist. When they connect to the WebSocket server, they send a
|
||||||
|
`blocklist` message to push their blocklist to the server -- it is
|
||||||
|
basically a bulk `mute` action that mutes all these users pre-emptively:
|
||||||
|
the user will not see their chat messages and the muted users can not see
|
||||||
|
the user's webcam when they broadcast later, the same as a regular `mute`
|
||||||
|
action.
|
||||||
|
|
||||||
|
The JSON response to this endpoint may look like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"OK": true,
|
||||||
|
"Error": "if error, or this key is omitted if OK"
|
||||||
|
}
|
||||||
|
```
|
132
docs/Authentication.md
Normal file
132
docs/Authentication.md
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
# Authentication
|
||||||
|
|
||||||
|
BareRTC supports custom (user-defined) authentication with your app in the form of JSON Web Tokens (JWTs). JWTs will allow your existing app to handle authentication for users by signing a token that vouches for them, and the BareRTC app will trust your signed token.
|
||||||
|
|
||||||
|
The workflow is as follows:
|
||||||
|
|
||||||
|
1. Your existing app already has the user logged-in and you trust who they are. To get them into the chat room, your server signs a JWT token using a secret key that both it and BareRTC knows.
|
||||||
|
2. Your server redirects the user to your BareRTC website sending the JWT token as a `jwt` parameter, either in the query string (GET) or POST request.
|
||||||
|
* e.g. you send them to `https://chat.example.com/?jwt=TOKEN`
|
||||||
|
* If the JWT token is too long to fit in a query string, you may create a `<form>` with `method="POST"` that posts the `jwt` as a form field.
|
||||||
|
3. The BareRTC server will parse and validate the token using the shared Secret Key that only it and your back-end website knows.
|
||||||
|
|
||||||
|
There are JWT libraries available for most programming languages.
|
||||||
|
|
||||||
|
Configure a shared secret key (random text string) in both the BareRTC settings and in your app, and your app will sign a JWT including claims that look like the following (using signing method HS264):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// JSON Web Token "claims" expected by BareRTC
|
||||||
|
{
|
||||||
|
// Custom claims
|
||||||
|
"sub": "username", // Username for chat (standard JWT claim)
|
||||||
|
"op": true, // User will have admin/operator permissions.
|
||||||
|
"nick": "Display name", // Friendly name
|
||||||
|
"img": "/static/photos/username.jpg", // user picture URL
|
||||||
|
"url": "/u/username", // user profile URL
|
||||||
|
"gender": "m", // gender (m, f, o)
|
||||||
|
"emoji": "🤖", // emoji icon
|
||||||
|
|
||||||
|
// Standard JWT claims that we support:
|
||||||
|
"iss": "my own app", // Issuer name
|
||||||
|
"exp": 1675645084, // Expires at (time): 5 minutes out is plenty!
|
||||||
|
"nbf": 1675644784, // Not Before (time)
|
||||||
|
"iat": 1675644784, // Issued At (time)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notice:** your picture and profile URL may be relative URIs beginning with a forward slash as seen above; BareRTC will append them to the end of your WebsiteURL and you can save space on your JWT token size this way. Full URLs beginning with `https?://` will also be accepted and used as-is.
|
||||||
|
|
||||||
|
See [Custom JWT Claims](#custom-jwt-claims) for more information on the
|
||||||
|
custom claims and how they work.
|
||||||
|
|
||||||
|
An example how to sign your JWT tokens in Go (using [golang-jwt](https://github.com/golang-jwt/jwt)):
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/golang-jwt/jwt/v4"
|
||||||
|
|
||||||
|
// JWT signing key - keep it a secret on your back-end shared between
|
||||||
|
// your app and BareRTC, do not use it in front-end javascript code or
|
||||||
|
// where a user can find it.
|
||||||
|
const SECRET = "change me"
|
||||||
|
|
||||||
|
// Your custom JWT claims.
|
||||||
|
type CustomClaims struct {
|
||||||
|
// Custom claims used by BareRTC.
|
||||||
|
Avatar string `json:"img"` // URI to user profile picture
|
||||||
|
ProfileURL string `json:"url"` // URI to user's profile page
|
||||||
|
IsAdmin bool `json:"op"` // give operator permission
|
||||||
|
|
||||||
|
// Standard JWT claims
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming your internal User struct looks anything at all like:
|
||||||
|
type User struct {
|
||||||
|
Username string
|
||||||
|
IsAdmin bool
|
||||||
|
ProfilePicture string // like "/static/photos/username.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a JWT token for this user.
|
||||||
|
func SignForUser(user User) string {
|
||||||
|
claims := CustomClaims{
|
||||||
|
// Custom claims
|
||||||
|
ProfileURL: "/users/" + user.Username,
|
||||||
|
Avatar: user.ProfilePicture,
|
||||||
|
IsAdmin: user.IsAdmin,
|
||||||
|
|
||||||
|
// Standard claims
|
||||||
|
Subject: user.Username, // their chat username!
|
||||||
|
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||||
|
IssuedAt: time.Now(),
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
Issuer: "my own app",
|
||||||
|
ID: user.ID,
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
tokenStr, err := token.SignedString(SECRET)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return tokenstr
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom JWT Claims
|
||||||
|
|
||||||
|
With JWT authentication your website can pass a lot of fun variables to decorate your Who Is Online list for your users.
|
||||||
|
|
||||||
|
Here is in-depth documentation on what custom claims are supported by BareRTC and what effects they have in chat:
|
||||||
|
|
||||||
|
* **Subject** (`sub`): this is a standard JWT claim and BareRTC will collect your username from it. The username is shown in the Who's Online list and below the user's nickname on their chat messages (in "@username" format). Do not prefix your subject with the @ symbol yourself.
|
||||||
|
* **Operator** (`op`): this boolean will mark your user to have operator (admin) status in chat. In the Who List they will have a gavel icon after their username, and they will be allowed to run operator commands (e.g. to kick other users from chat).
|
||||||
|
* **Nickname** (`nick`): you may send your users in with a custom Display Name that will appear on their chat messages. If they don't have a nickname, their username will be used in its place.
|
||||||
|
* **Image** (`img`): a profile picture or avatar for your users. It should be a square image and will appear in the Who List and alongside their chat messages. If they don't have an image, a default blue silhouette avatar is used. The image URL may be a relative URI beginning with `/` and it will be appended onto your configured WebsiteURL.
|
||||||
|
* **Profile URL** (`url`): a link to a user's profile page. If provided, clicking on their picture in chat or the Who List will open this URL in a new tab. They will also get a profile button added next to their name on the Who List. Relative URLs beginning with `/` are supported, and will be appended to your WebsiteURL automatically.
|
||||||
|
* **Gender** (`gender`): a single-character gender code for your user. If they also have a Profile URL, their profile button on the Who List can be color-coded by gender. Supported values include:
|
||||||
|
* **m** (male) to set their profile button blue.
|
||||||
|
* **f** (female) to set their profile button pink.
|
||||||
|
* Other value (canonically, **o**) to set their profile button purple.
|
||||||
|
* Missing/no value won't set a color and it will be the default text color.
|
||||||
|
* **Emoji** (`emoji`): you may associate users with an emoji character that will appear on the Who List next to their name. Some example ideas and use cases include:
|
||||||
|
* Country flag emojis, to indicate where your users are connecting from.
|
||||||
|
* Robot emojis, to indicate bot users.
|
||||||
|
* Any emoji you want! Mark your special guests or VIP users, etc.
|
||||||
|
|
||||||
|
## JWT Strict Mode
|
||||||
|
|
||||||
|
You can enable JWT authentication in a mixed mode: users presenting a valid token will get a profile picture and operator status (if applicable) and users who don't have a JWT token are asked to pick their own username and don't get any special flair.
|
||||||
|
|
||||||
|
In strict mode (default/recommended), only a valid JWT token can sign a user into the chat room. Set `[JWT]/Strict=false` in your settings.toml to disable strict JWT verification and allow "guest users" to log in. Note that this can have the same caveats as [running without authentication](#running-without-authentication) and is not a recommended use case.
|
||||||
|
|
||||||
|
## Running Without Authentication
|
||||||
|
|
||||||
|
The default app doesn't need any authentication at all: users are asked to pick their own username when joining the chat. The server may re-assign them a new name if they enter one that's already taken.
|
||||||
|
|
||||||
|
It is not recommended to run in this mode as admin controls to moderate the server are disabled.
|
||||||
|
|
||||||
|
### Known Bugs Running Without Authentication
|
||||||
|
|
||||||
|
This app is not designed to run without JWT authentication for users enabled. In the app's default state, users can pick their own username when they connect and the server will adjust their name to resolve duplicates. Direct message threads are based on the username so if a user logs off, somebody else could log in with the same username and "resume" direct message threads that others were involved in.
|
||||||
|
|
||||||
|
Note that they would not get past history of those DMs as this server only pushes _new_ messages to users after they connect.
|
150
docs/Chatbot.md
Normal file
150
docs/Chatbot.md
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
# BareBot (Chatbot Program)
|
||||||
|
|
||||||
|
The source repo for BareRTC also includes a chatbot program that you can use for fun & games and for auto-moderating your room.
|
||||||
|
|
||||||
|
The entrypoint for the program is at `cmd/BareBot/main.go` and compiles to a `BareBot` command line program.
|
||||||
|
|
||||||
|
This feature is currently still brand new and will be fleshed out over time.
|
||||||
|
|
||||||
|
# Quick Start
|
||||||
|
|
||||||
|
Initialize a new chatbot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ BareBot init /path/to/chatbot
|
||||||
|
|
||||||
|
# Example
|
||||||
|
$ BareBot init ./chatbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Point it to a new or empty directory and it will populate it with a `chatbot.toml` settings file and a default RiveScript brain. The default brain will come with useful triggers and topics set up for integration with your BareRTC server.
|
||||||
|
|
||||||
|
Edit your `chatbot.toml` to configure it and then run it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ BareBot run /path/to/chatbot
|
||||||
|
|
||||||
|
# Example
|
||||||
|
$ BareBot run ./chatbot
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also simply call `BareBot run` from inside your bot's folder (with the ./chatbot.toml file at the current working directory).
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
|
||||||
|
An example of the chatbot.toml file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
Version = 1
|
||||||
|
WebSocketReadLimit = 41943040
|
||||||
|
|
||||||
|
[BareRTC]
|
||||||
|
AdminAPIKey = "c0ffd6b5-37ce-4184-a3df-a28b698ecb48"
|
||||||
|
URL = "http://localhost:9000"
|
||||||
|
|
||||||
|
[Profile]
|
||||||
|
Username = "shybot"
|
||||||
|
Nickname = "BareBOT"
|
||||||
|
ProfileURL = "/u/shybot"
|
||||||
|
AvatarURL = "http://localhost:9000/static/img/server.png"
|
||||||
|
Emoji = "🤖"
|
||||||
|
Gender = "f"
|
||||||
|
IsAdmin = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Settings you'll want to configure include:
|
||||||
|
|
||||||
|
* BareRTC/URL: the base website URL to your BareRTC server.
|
||||||
|
* BareRTC/AdminAPIKey: this should match the AdminAPIKey in your BareRTC settings.toml -- used for authentication.
|
||||||
|
* Profile: these are the JWT claims for user authentication: how you want your chatbot to look in chat.
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
## RiveScript
|
||||||
|
|
||||||
|
The reply engine for BareBot is [RiveScript](https://www.rivescript.com), a chatbot scripting language that makes it easy to set up custom "canned response" trigger and response pairs to match your user's messages.
|
||||||
|
|
||||||
|
## Direct Message Conversations
|
||||||
|
|
||||||
|
Users who send a DM to the chatbot may get a response to every message they send.
|
||||||
|
|
||||||
|
## At-mentions in Public Channels
|
||||||
|
|
||||||
|
Users in a public channel can invoke the bot also by at-mentioning its username in chat (starting or ending their message with the bot's username). The bot will fetch a reply as normal, and send it to the public channel while at-mentioning the user's name back.
|
||||||
|
|
||||||
|
## Public Channel Keywords
|
||||||
|
|
||||||
|
The default RiveScript file `public_keywords.rive` sets up some default triggers to be matched on public channel messages (where the bot was _not_ at mentioned).
|
||||||
|
|
||||||
|
How it works is that on public channel messages (not at-mentioned) the user is placed into a special RiveScript topic named "PublicChannel" that constrains the set of triggers that will be tried for a match.
|
||||||
|
|
||||||
|
Avoid spamming public channels too much: the default topic in `public_keywords.rive` sets a catch-all `*` trigger that says `<noreply>` so that the bot will not send a message to the channel unless another trigger has it do so.
|
||||||
|
|
||||||
|
Examples what you can do with this includes:
|
||||||
|
|
||||||
|
* If a user says hello to the chat, react to their message with a wave emoji.
|
||||||
|
* If a user shares a picture on chat, randomly decide to react to it.
|
||||||
|
* If users say certain keywords, you can send messages to the chat in response or take other actions (such as kick the user from the room).
|
||||||
|
|
||||||
|
## Auto Greeter
|
||||||
|
|
||||||
|
The default chatbot brain has a `/greet` command in `commands.rive` which is called during presence updates when a user joins the room.
|
||||||
|
|
||||||
|
The chatbot will say hello to a new user (in the default "lobby" chat room ID - TODO: make configurable), no more than one time per hour. So if a user is popping in and out the bot won't spam and greet them too often.
|
||||||
|
|
||||||
|
The bot waits a few seconds before greeting, and if the user logs off before, then the bot doesn't send the message.
|
||||||
|
|
||||||
|
The bot also will not greet users when there are more than 10 people in the room by default - you can tune this in `commands.rive` if you like.
|
||||||
|
|
||||||
|
# RiveScript Variables
|
||||||
|
|
||||||
|
For user messages, the following variables are set on the RiveScript instance for the current user:
|
||||||
|
|
||||||
|
* `<get name>` will be the user's display name (nickname) or username if not set.
|
||||||
|
* `<get isAdmin>` will be "true" if the user has admin (operator) status or "false" if not.
|
||||||
|
* `<get messageID>` will be the BareRTC MessageID of the user's message you are responding to (integer value, useful for the `react` object macro).
|
||||||
|
|
||||||
|
Global variables available in your RiveScript replies include:
|
||||||
|
|
||||||
|
* `<env numUsersOnline>` will be an integer number of chatters currently on the room, in case you want to know how many.
|
||||||
|
|
||||||
|
The source of truth for these is in `client/chatbot.go` in case the documentation is out of date.
|
||||||
|
|
||||||
|
# RiveScript Object Macros
|
||||||
|
|
||||||
|
The following object macros are available to your RiveScript bot.
|
||||||
|
|
||||||
|
The source of truth for these is in the `client/rivescript_macros.go` source file, in case this documentation gets out of date.
|
||||||
|
|
||||||
|
You can invoke these by using the `<call>` tag in your RiveScript responses -- see the examples.
|
||||||
|
|
||||||
|
## Reload
|
||||||
|
|
||||||
|
This command can reload the chatbot's RiveScript sources from disk, making it easy to iterate on your robot without rebooting the whole program.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```rivescript
|
||||||
|
+ /reload
|
||||||
|
* <get isAdmin> != true => You do not have permission for that command.
|
||||||
|
- <call>reload</call>
|
||||||
|
```
|
||||||
|
|
||||||
|
It returns a message like "The RiveScript brain has been reloaded!"
|
||||||
|
|
||||||
|
## React
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```rivescript
|
||||||
|
// Auto react to hello messages with a wave
|
||||||
|
+ [*] (hello|hi|howdy|yo|sup) [*]
|
||||||
|
- <call>react <get messageID> 👋</call>
|
||||||
|
^ <noreply>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
56
docs/Configuration.md
Normal file
56
docs/Configuration.md
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
On first run it will create the default settings.toml file for you which you may then customize to your liking:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
Version = 2
|
||||||
|
Title = "BareRTC"
|
||||||
|
Branding = "BareRTC"
|
||||||
|
WebsiteURL = "https://www.example.com"
|
||||||
|
CORSHosts = ["https://www.example.com"]
|
||||||
|
PermitNSFW = true
|
||||||
|
UseXForwardedFor = true
|
||||||
|
WebSocketReadLimit = 41943040
|
||||||
|
MaxImageWidth = 1280
|
||||||
|
PreviewImageWidth = 360
|
||||||
|
|
||||||
|
[JWT]
|
||||||
|
Enabled = false
|
||||||
|
Strict = true
|
||||||
|
SecretKey = ""
|
||||||
|
|
||||||
|
[[PublicChannels]]
|
||||||
|
ID = "lobby"
|
||||||
|
Name = "Lobby"
|
||||||
|
Icon = "fa fa-gavel"
|
||||||
|
WelcomeMessages = ["Welcome to the chat server!", "Please follow the basic rules:\n\n1. Have fun\n2. Be kind"]
|
||||||
|
|
||||||
|
[[PublicChannels]]
|
||||||
|
ID = "offtopic"
|
||||||
|
Name = "Off Topic"
|
||||||
|
WelcomeMessages = ["Welcome to the Off Topic channel!"]
|
||||||
|
```
|
||||||
|
|
||||||
|
A description of the config directives includes:
|
||||||
|
|
||||||
|
* Website settings:
|
||||||
|
* **Title** goes in the title bar of the chat page.
|
||||||
|
* **Branding** is the title shown in the corner of the page. HTML is permitted here! You may write an `<img>` tag to embed an image or use custom markup to color and prettify your logo.
|
||||||
|
* **WebsiteURL** is the base URL of your actual website which is used in a couple of places:
|
||||||
|
* The About page will link to your website.
|
||||||
|
* If using [JWT authentication](#authentication), avatar and profile URLs may be relative (beginning with a "/") and will append to your website URL to safe space on the JWT token size!
|
||||||
|
* **UseXForwardedFor**: set it to true and (for logging) the user's remote IP will use the X-Real-IP header or the first address in X-Forwarded-For. Set this if you run the app behind a proxy like nginx if you want IPs not to be all localhost.
|
||||||
|
* **CORSHosts**: your website's domain names that will be allowed to access [JSON APIs](#JSON APIs), like `/api/statistics`.
|
||||||
|
* **PermitNSFW**: for user webcam streams, expressly permit "NSFW" content if the user opts in to mark their feed as such. Setting this will enable pop-up modals regarding NSFW video and give broadcasters an opt-in button, which will warn other users before they click in to watch.
|
||||||
|
* **WebSocketReadLimit**: sets a size limit for WebSocket messages - it essentially also caps the max upload size for shared images (add a buffer as images will be base64 encoded on upload).
|
||||||
|
* **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view.
|
||||||
|
* **PreviewImageWidth**: to not flood the chat, the image in chat is this wide and users can click it to see the MaxImageWidth in a lightbox modal.
|
||||||
|
* **JWT**: settings for JWT [Authentication](#authentication).
|
||||||
|
* Enabled (bool): activate the JWT token authentication feature.
|
||||||
|
* Strict (bool): if true, **only** valid signed JWT tokens may log in. If false, users with no/invalid token can enter their own username without authentication.
|
||||||
|
* SecretKey (string): the JWT signing secret shared with your back-end app.
|
||||||
|
* **PublicChannels**: list the public channels and their configuration. The default channel will be the first one listed.
|
||||||
|
* ID (string): an arbitrary 'username' for the chat channel, like "lobby".
|
||||||
|
* Name (string): the user friendly name for the channel, like "Off Topic"
|
||||||
|
* Icon (string, optional): CSS class names for FontAwesome icon for the channel, like "fa fa-message"
|
||||||
|
* WelcomeMessages ([]string, optional): messages that are delivered by ChatServer to the user when they connect to the server. Useful to give an introduction to each channel, list its rules, etc.
|
45
docs/Webhooks.md
Normal file
45
docs/Webhooks.md
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Webhook URLs
|
||||||
|
|
||||||
|
BareRTC supports setting up webhook URLs so the chat server can call out to _your_ website in response to certain events, such as allowing users to send you reports about messages they receive on chat.
|
||||||
|
|
||||||
|
Webhooks are configured in your settings.toml file and look like so:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[WebhookURLs]]
|
||||||
|
Name = "report"
|
||||||
|
Enabled = true
|
||||||
|
URL = "http://localhost:8080/v1/barertc/report"
|
||||||
|
```
|
||||||
|
|
||||||
|
All Webhooks will be called as **POST** requests and will contain a JSON payload that will always have the following two keys:
|
||||||
|
|
||||||
|
* `Action` will be the name of the webhook (e.g. "report")
|
||||||
|
* `APIKey` will be your AdminAPIKey as configure in the settings.toml (shared secret so your web app can authenticate BareRTC's webhooks).
|
||||||
|
|
||||||
|
The JSON payload may also contain a relevant object per the Action -- see the specific examples below.
|
||||||
|
|
||||||
|
## Report Webhook
|
||||||
|
|
||||||
|
Enabling this webhook will cause BareRTC to display a red "Report" flag button underneath user messages on chat so that they can report problematic messages to your website.
|
||||||
|
|
||||||
|
The webhook name for your settings.toml is "report"
|
||||||
|
|
||||||
|
Example JSON payload posted to the webhook:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"Action": "report",
|
||||||
|
"APIKey": "shared secret from settings.toml#AdminAPIKey",
|
||||||
|
"Report": {
|
||||||
|
"FromUsername": "sender",
|
||||||
|
"AboutUsername": "user being reported on",
|
||||||
|
"Channel": "lobby", // or "@username" for DM threads
|
||||||
|
"Timestamp": "(stringified timestamp of chat message)",
|
||||||
|
"Reason": "It's spam",
|
||||||
|
"Comment": "custom user note about the report",
|
||||||
|
"Message": "the actual message that was being reported on",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
BareRTC expects your webhook URL to return a 200 OK status code or it will surface an error in chat to the reporter.
|
7
go.mod
7
go.mod
|
@ -4,23 +4,27 @@ go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b
|
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b
|
||||||
github.com/BurntSushi/toml v1.2.1
|
github.com/BurntSushi/toml v1.3.2
|
||||||
|
github.com/aichaos/rivescript-go v0.3.1
|
||||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f
|
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/mattn/go-shellwords v1.0.12
|
github.com/mattn/go-shellwords v1.0.12
|
||||||
github.com/microcosm-cc/bluemonday v1.0.22
|
github.com/microcosm-cc/bluemonday v1.0.22
|
||||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629
|
github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629
|
||||||
|
github.com/urfave/cli/v2 v2.25.7
|
||||||
golang.org/x/image v0.6.0
|
golang.org/x/image v0.6.0
|
||||||
nhooyr.io/websocket v1.8.7
|
nhooyr.io/websocket v1.8.7
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/disintegration/imaging v1.6.2 // indirect
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.10.3 // indirect
|
github.com/klauspost/compress v1.10.3 // indirect
|
||||||
github.com/russross/blackfriday v1.5.2 // indirect
|
github.com/russross/blackfriday v1.5.2 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
github.com/sergi/go-diff v1.3.1 // indirect
|
||||||
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect
|
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect
|
||||||
|
@ -32,6 +36,7 @@ require (
|
||||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
|
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
|
||||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
|
||||||
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e // indirect
|
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
golang.org/x/crypto v0.7.0 // indirect
|
golang.org/x/crypto v0.7.0 // indirect
|
||||||
golang.org/x/net v0.8.0 // indirect
|
golang.org/x/net v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
|
89
go.sum
89
go.sum
|
@ -1,9 +1,13 @@
|
||||||
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b h1:TDxEEWOJqMzsu9JW8/QgmT1lgQ9WD2KWlb2lKN/Ql2o=
|
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b h1:TDxEEWOJqMzsu9JW8/QgmT1lgQ9WD2KWlb2lKN/Ql2o=
|
||||||
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b/go.mod h1:jl+Qr58W3Op7OCxIYIT+b42jq8xFncJXzPufhrvza7Y=
|
git.kirsle.net/go/log v0.0.0-20200902035305-70ac2848949b/go.mod h1:jl+Qr58W3Op7OCxIYIT+b42jq8xFncJXzPufhrvza7Y=
|
||||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/aichaos/rivescript-go v0.3.1 h1:MvSssqWAU+oU3yfnanKpK5n+e0pLCA0MEELZE4VXV0g=
|
||||||
|
github.com/aichaos/rivescript-go v0.3.1/go.mod h1:8qm1P1SThiwgbtmO+74QUMlV9HT4MSInWT8ivoO58LQ=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
@ -11,6 +15,8 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
|
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
|
||||||
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
|
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
|
@ -22,6 +28,7 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||||
|
@ -30,11 +37,23 @@ github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
|
||||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
@ -42,6 +61,7 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||||
|
@ -63,10 +83,22 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452 h1:ewTtJ72GFy2e0e8uyiDwMG3pKCS5mBh+hdSTYsPKEP8=
|
||||||
|
github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
||||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
|
@ -91,36 +123,61 @@ github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG
|
||||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e h1:Ee+VZw13r9NTOMnwTPs6O5KZ0MJU54hsxu9FpZ4pQ10=
|
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e h1:Ee+VZw13r9NTOMnwTPs6O5KZ0MJU54hsxu9FpZ4pQ10=
|
||||||
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e/go.mod h1:fSIW/szJHsRts/4U8wlMPhs+YqJC+7NYR+Qqb1uJVpA=
|
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e/go.mod h1:fSIW/szJHsRts/4U8wlMPhs+YqJC+7NYR+Qqb1uJVpA=
|
||||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||||
|
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
@ -135,21 +192,41 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/redis.v5 v5.2.9/go.mod h1:6gtv0/+A4iM08kdRfocWYB3bLX2tebpNtfKlFT6H4mY=
|
||||||
|
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||||
|
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||||
|
|
109
pkg/api.go
109
pkg/api.go
|
@ -7,7 +7,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.kirsle.net/apps/barertc/pkg/config"
|
"git.kirsle.net/apps/barertc/pkg/config"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||||
"git.kirsle.net/apps/barertc/pkg/log"
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Statistics (/api/statistics) returns info about the users currently logged onto the chat,
|
// Statistics (/api/statistics) returns info about the users currently logged onto the chat,
|
||||||
|
@ -52,8 +54,8 @@ func (s *Server) Statistics() http.HandlerFunc {
|
||||||
unique[sub.Username] = struct{}{}
|
unique[sub.Username] = struct{}{}
|
||||||
|
|
||||||
// Count cameras by color.
|
// Count cameras by color.
|
||||||
if sub.VideoStatus&VideoFlagActive == VideoFlagActive {
|
if sub.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive {
|
||||||
if sub.VideoStatus&VideoFlagNSFW == VideoFlagNSFW {
|
if sub.VideoStatus&messages.VideoFlagNSFW == messages.VideoFlagNSFW {
|
||||||
result.Cameras.Red++
|
result.Cameras.Red++
|
||||||
} else {
|
} else {
|
||||||
result.Cameras.Blue++
|
result.Cameras.Blue++
|
||||||
|
@ -69,6 +71,109 @@ func (s *Server) Statistics() http.HandlerFunc {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authenticate (/api/authenticate) for the chatbot API.
|
||||||
|
//
|
||||||
|
// This endpoint will sign a JWT token using the claims you pass in. It requires
|
||||||
|
// the shared secret `AdminAPIKey` from your settings.toml and will sign the
|
||||||
|
// JWT claims you give it.
|
||||||
|
//
|
||||||
|
// It is a POST request with a json body containing the following schema:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "APIKey": "from settings.toml",
|
||||||
|
// "Claims": {
|
||||||
|
// "sub": "username",
|
||||||
|
// "nick": "Display Name",
|
||||||
|
// "op": false,
|
||||||
|
// "img": "/static/photos/avatar.png",
|
||||||
|
// "url": "/users/username",
|
||||||
|
// "emoji": "🤖",
|
||||||
|
// "gender": "m"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The return schema looks like:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "OK": true,
|
||||||
|
// "Error": "error string, omitted if none",
|
||||||
|
// "JWT": "jwt token string"
|
||||||
|
// }
|
||||||
|
func (s *Server) Authenticate() http.HandlerFunc {
|
||||||
|
type request struct {
|
||||||
|
APIKey string
|
||||||
|
Claims jwt.Claims
|
||||||
|
}
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
OK bool
|
||||||
|
Error string `json:",omitempty"`
|
||||||
|
JWT string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// JSON writer for the response.
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
// Parse the request.
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: "Only POST methods allowed",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else if r.Header.Get("Content-Type") != "application/json" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: "Only application/json content-types allowed",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
// Parse the request payload.
|
||||||
|
var (
|
||||||
|
params request
|
||||||
|
dec = json.NewDecoder(r.Body)
|
||||||
|
)
|
||||||
|
if err := dec.Decode(¶ms); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the API key.
|
||||||
|
if params.APIKey != config.Current.AdminAPIKey {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: "Authentication denied.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the JWT token.
|
||||||
|
var claims = params.Claims
|
||||||
|
token, err := claims.ReSign()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: "Error signing the JWT claims.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.Encode(result{
|
||||||
|
OK: true,
|
||||||
|
JWT: token,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// BlockList (/api/blocklist) allows your website to pre-sync mute lists between your
|
// BlockList (/api/blocklist) allows your website to pre-sync mute lists between your
|
||||||
// user accounts, so that when they see each other in chat they will pre-emptively mute
|
// user accounts, so that when they see each other in chat they will pre-emptively mute
|
||||||
// or boot one another.
|
// or boot one another.
|
||||||
|
|
|
@ -9,12 +9,13 @@ import (
|
||||||
"git.kirsle.net/apps/barertc/pkg/config"
|
"git.kirsle.net/apps/barertc/pkg/config"
|
||||||
ourjwt "git.kirsle.net/apps/barertc/pkg/jwt"
|
ourjwt "git.kirsle.net/apps/barertc/pkg/jwt"
|
||||||
"git.kirsle.net/apps/barertc/pkg/log"
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
"github.com/mattn/go-shellwords"
|
"github.com/mattn/go-shellwords"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProcessCommand parses a chat message for "/commands"
|
// ProcessCommand parses a chat message for "/commands"
|
||||||
func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
func (s *Server) ProcessCommand(sub *Subscriber, msg messages.Message) bool {
|
||||||
if len(msg.Message) == 0 || msg.Message[0] != '/' {
|
if len(msg.Message) == 0 || msg.Message[0] != '/' {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -63,8 +64,8 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
||||||
))
|
))
|
||||||
return true
|
return true
|
||||||
case "/shutdown":
|
case "/shutdown":
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionError,
|
Action: messages.ActionError,
|
||||||
Username: "ChatServer",
|
Username: "ChatServer",
|
||||||
Message: "The chat server is going down for a reboot NOW!",
|
Message: "The chat server is going down for a reboot NOW!",
|
||||||
})
|
})
|
||||||
|
@ -112,7 +113,7 @@ func (s *Server) NSFWCommand(words []string, sub *Subscriber) {
|
||||||
}
|
}
|
||||||
|
|
||||||
other.ChatServer(message)
|
other.ChatServer(message)
|
||||||
other.VideoStatus |= VideoFlagNSFW
|
other.VideoStatus |= messages.VideoFlagNSFW
|
||||||
other.SendMe()
|
other.SendMe()
|
||||||
s.SendWhoList()
|
s.SendWhoList()
|
||||||
sub.ChatServer("%s now has their camera marked as Explicit", username)
|
sub.ChatServer("%s now has their camera marked as Explicit", username)
|
||||||
|
@ -135,15 +136,15 @@ func (s *Server) KickCommand(words []string, sub *Subscriber) {
|
||||||
sub.ChatServer("/kick: did you really mean to kick yourself?")
|
sub.ChatServer("/kick: did you really mean to kick yourself?")
|
||||||
} else {
|
} else {
|
||||||
other.ChatServer("You have been kicked from the chat room by %s", sub.Username)
|
other.ChatServer("You have been kicked from the chat room by %s", sub.Username)
|
||||||
other.SendJSON(Message{
|
other.SendJSON(messages.Message{
|
||||||
Action: ActionKick,
|
Action: messages.ActionKick,
|
||||||
})
|
})
|
||||||
s.DeleteSubscriber(other)
|
s.DeleteSubscriber(other)
|
||||||
sub.ChatServer("%s has been kicked from the room", username)
|
sub.ChatServer("%s has been kicked from the room", username)
|
||||||
|
|
||||||
// Broadcast it to everyone.
|
// Broadcast it to everyone.
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionPresence,
|
Action: messages.ActionPresence,
|
||||||
Username: username,
|
Username: username,
|
||||||
Message: "has been kicked from the room!",
|
Message: "has been kicked from the room!",
|
||||||
})
|
})
|
||||||
|
@ -155,8 +156,8 @@ func (s *Server) KickAllCommand() {
|
||||||
|
|
||||||
// If we have JWT enabled and a landing page, link users to it.
|
// If we have JWT enabled and a landing page, link users to it.
|
||||||
if config.Current.JWT.Enabled && config.Current.JWT.LandingPageURL != "" {
|
if config.Current.JWT.Enabled && config.Current.JWT.LandingPageURL != "" {
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionError,
|
Action: messages.ActionError,
|
||||||
Username: "ChatServer",
|
Username: "ChatServer",
|
||||||
Message: fmt.Sprintf(
|
Message: fmt.Sprintf(
|
||||||
"<strong>Notice:</strong> The chat operator has requested that you log back in to the chat room. "+
|
"<strong>Notice:</strong> The chat operator has requested that you log back in to the chat room. "+
|
||||||
|
@ -166,8 +167,8 @@ func (s *Server) KickAllCommand() {
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionError,
|
Action: messages.ActionError,
|
||||||
Username: "ChatServer",
|
Username: "ChatServer",
|
||||||
Message: "<strong>Notice:</strong> The chat operator has kicked everybody from the room. Usually, this " +
|
Message: "<strong>Notice:</strong> The chat operator has kicked everybody from the room. Usually, this " +
|
||||||
"may mean a new feature of the chat has been launched and you need to reload the page for it " +
|
"may mean a new feature of the chat has been launched and you need to reload the page for it " +
|
||||||
|
@ -176,8 +177,8 @@ func (s *Server) KickAllCommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kick everyone off.
|
// Kick everyone off.
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionKick,
|
Action: messages.ActionKick,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Disconnect everybody.
|
// Disconnect everybody.
|
||||||
|
@ -221,15 +222,15 @@ func (s *Server) BanCommand(words []string, sub *Subscriber) {
|
||||||
BanUser(username, duration)
|
BanUser(username, duration)
|
||||||
|
|
||||||
// Broadcast it to everyone.
|
// Broadcast it to everyone.
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionPresence,
|
Action: messages.ActionPresence,
|
||||||
Username: username,
|
Username: username,
|
||||||
Message: "has been banned!",
|
Message: "has been banned!",
|
||||||
})
|
})
|
||||||
|
|
||||||
other.ChatServer("You have been banned from the chat room by %s. You may come back after %d hours.", sub.Username, duration/time.Hour)
|
other.ChatServer("You have been banned from the chat room by %s. You may come back after %d hours.", sub.Username, duration/time.Hour)
|
||||||
other.SendJSON(Message{
|
other.SendJSON(messages.Message{
|
||||||
Action: ActionKick,
|
Action: messages.ActionKick,
|
||||||
})
|
})
|
||||||
s.DeleteSubscriber(other)
|
s.DeleteSubscriber(other)
|
||||||
sub.ChatServer("%s has been banned from the room for %d hours.", username, duration/time.Hour)
|
sub.ChatServer("%s has been banned from the room for %d hours.", username, duration/time.Hour)
|
||||||
|
|
105
pkg/handlers.go
105
pkg/handlers.go
|
@ -10,11 +10,12 @@ import (
|
||||||
"git.kirsle.net/apps/barertc/pkg/config"
|
"git.kirsle.net/apps/barertc/pkg/config"
|
||||||
"git.kirsle.net/apps/barertc/pkg/jwt"
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||||
"git.kirsle.net/apps/barertc/pkg/log"
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
"git.kirsle.net/apps/barertc/pkg/util"
|
"git.kirsle.net/apps/barertc/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OnLogin handles "login" actions from the client.
|
// OnLogin handles "login" actions from the client.
|
||||||
func (s *Server) OnLogin(sub *Subscriber, msg Message) {
|
func (s *Server) OnLogin(sub *Subscriber, msg messages.Message) {
|
||||||
// Using a JWT token for authentication?
|
// Using a JWT token for authentication?
|
||||||
var claims = &jwt.Claims{}
|
var claims = &jwt.Claims{}
|
||||||
if msg.JWTToken != "" || (config.Current.JWT.Enabled && config.Current.JWT.Strict) {
|
if msg.JWTToken != "" || (config.Current.JWT.Enabled && config.Current.JWT.Strict) {
|
||||||
|
@ -58,8 +59,8 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
|
||||||
if claims.Subject == msg.Username {
|
if claims.Subject == msg.Username {
|
||||||
if other, err := s.GetSubscriber(msg.Username); err == nil {
|
if other, err := s.GetSubscriber(msg.Username); err == nil {
|
||||||
other.ChatServer("You have been signed out of chat because you logged in from another location.")
|
other.ChatServer("You have been signed out of chat because you logged in from another location.")
|
||||||
other.SendJSON(Message{
|
other.SendJSON(messages.Message{
|
||||||
Action: ActionKick,
|
Action: messages.ActionKick,
|
||||||
})
|
})
|
||||||
s.DeleteSubscriber(other)
|
s.DeleteSubscriber(other)
|
||||||
}
|
}
|
||||||
|
@ -78,8 +79,8 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
|
||||||
"You are currently banned from entering the chat room. Chat room bans are temporarily and usually last for " +
|
"You are currently banned from entering the chat room. Chat room bans are temporarily and usually last for " +
|
||||||
"24 hours. Please try coming back later.",
|
"24 hours. Please try coming back later.",
|
||||||
)
|
)
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(messages.Message{
|
||||||
Action: ActionKick,
|
Action: messages.ActionKick,
|
||||||
})
|
})
|
||||||
s.DeleteSubscriber(sub)
|
s.DeleteSubscriber(sub)
|
||||||
return
|
return
|
||||||
|
@ -92,8 +93,8 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
|
||||||
log.Debug("OnLogin: %s joins the room", sub.Username)
|
log.Debug("OnLogin: %s joins the room", sub.Username)
|
||||||
|
|
||||||
// Tell everyone they joined.
|
// Tell everyone they joined.
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionPresence,
|
Action: messages.ActionPresence,
|
||||||
Username: msg.Username,
|
Username: msg.Username,
|
||||||
Message: "has joined the room!",
|
Message: "has joined the room!",
|
||||||
})
|
})
|
||||||
|
@ -107,9 +108,9 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
|
||||||
// Send the initial ChatServer messages to the public channels.
|
// Send the initial ChatServer messages to the public channels.
|
||||||
for _, channel := range config.Current.PublicChannels {
|
for _, channel := range config.Current.PublicChannels {
|
||||||
for _, msg := range channel.WelcomeMessages {
|
for _, msg := range channel.WelcomeMessages {
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(messages.Message{
|
||||||
Channel: channel.ID,
|
Channel: channel.ID,
|
||||||
Action: ActionError,
|
Action: messages.ActionError,
|
||||||
Username: "ChatServer",
|
Username: "ChatServer",
|
||||||
Message: RenderMarkdown(msg),
|
Message: RenderMarkdown(msg),
|
||||||
})
|
})
|
||||||
|
@ -118,7 +119,7 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnMessage handles a chat message posted by the user.
|
// OnMessage handles a chat message posted by the user.
|
||||||
func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) {
|
||||||
if !strings.HasPrefix(msg.Channel, "@") {
|
if !strings.HasPrefix(msg.Channel, "@") {
|
||||||
log.Info("[%s to #%s] %s", sub.Username, msg.Channel, msg.Message)
|
log.Info("[%s to #%s] %s", sub.Username, msg.Channel, msg.Message)
|
||||||
}
|
}
|
||||||
|
@ -143,15 +144,15 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
||||||
markdown = s.ExpandMedia(markdown)
|
markdown = s.ExpandMedia(markdown)
|
||||||
|
|
||||||
// Assign a message ID and own it to the sender.
|
// Assign a message ID and own it to the sender.
|
||||||
MessageID++
|
messages.MessageID++
|
||||||
var mid = MessageID
|
var mid = messages.MessageID
|
||||||
sub.midMu.Lock()
|
sub.midMu.Lock()
|
||||||
sub.messageIDs[mid] = struct{}{}
|
sub.messageIDs[mid] = struct{}{}
|
||||||
sub.midMu.Unlock()
|
sub.midMu.Unlock()
|
||||||
|
|
||||||
// Message to be echoed to the channel.
|
// Message to be echoed to the channel.
|
||||||
var message = Message{
|
var message = messages.Message{
|
||||||
Action: ActionMessage,
|
Action: messages.ActionMessage,
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Message: markdown,
|
Message: markdown,
|
||||||
|
@ -188,7 +189,7 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnTakeback handles takebacks (delete your message for everybody)
|
// OnTakeback handles takebacks (delete your message for everybody)
|
||||||
func (s *Server) OnTakeback(sub *Subscriber, msg Message) {
|
func (s *Server) OnTakeback(sub *Subscriber, msg messages.Message) {
|
||||||
// Permission check.
|
// Permission check.
|
||||||
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
|
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
|
||||||
sub.midMu.Lock()
|
sub.midMu.Lock()
|
||||||
|
@ -200,17 +201,17 @@ func (s *Server) OnTakeback(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast to everybody to remove this message.
|
// Broadcast to everybody to remove this message.
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionTakeback,
|
Action: messages.ActionTakeback,
|
||||||
MessageID: msg.MessageID,
|
MessageID: msg.MessageID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnReact handles emoji reactions for chat messages.
|
// OnReact handles emoji reactions for chat messages.
|
||||||
func (s *Server) OnReact(sub *Subscriber, msg Message) {
|
func (s *Server) OnReact(sub *Subscriber, msg messages.Message) {
|
||||||
// Forward the reaction to everybody.
|
// Forward the reaction to everybody.
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionReact,
|
Action: messages.ActionReact,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Message: msg.Message,
|
Message: msg.Message,
|
||||||
MessageID: msg.MessageID,
|
MessageID: msg.MessageID,
|
||||||
|
@ -218,7 +219,7 @@ func (s *Server) OnReact(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnFile handles a picture shared in chat with a channel.
|
// OnFile handles a picture shared in chat with a channel.
|
||||||
func (s *Server) OnFile(sub *Subscriber, msg Message) {
|
func (s *Server) OnFile(sub *Subscriber, msg messages.Message) {
|
||||||
if sub.Username == "" {
|
if sub.Username == "" {
|
||||||
sub.ChatServer("You must log in first.")
|
sub.ChatServer("You must log in first.")
|
||||||
return
|
return
|
||||||
|
@ -247,15 +248,15 @@ func (s *Server) OnFile(sub *Subscriber, msg Message) {
|
||||||
var dataURL = fmt.Sprintf("data:%s;base64,%s", filetype, base64.StdEncoding.EncodeToString(img))
|
var dataURL = fmt.Sprintf("data:%s;base64,%s", filetype, base64.StdEncoding.EncodeToString(img))
|
||||||
|
|
||||||
// Assign a message ID and own it to the sender.
|
// Assign a message ID and own it to the sender.
|
||||||
MessageID++
|
messages.MessageID++
|
||||||
var mid = MessageID
|
var mid = messages.MessageID
|
||||||
sub.midMu.Lock()
|
sub.midMu.Lock()
|
||||||
sub.messageIDs[mid] = struct{}{}
|
sub.messageIDs[mid] = struct{}{}
|
||||||
sub.midMu.Unlock()
|
sub.midMu.Unlock()
|
||||||
|
|
||||||
// Message to be echoed to the channel.
|
// Message to be echoed to the channel.
|
||||||
var message = Message{
|
var message = messages.Message{
|
||||||
Action: ActionMessage,
|
Action: messages.ActionMessage,
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
MessageID: mid,
|
MessageID: mid,
|
||||||
|
@ -298,8 +299,8 @@ func (s *Server) OnFile(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnMe handles current user state updates.
|
// OnMe handles current user state updates.
|
||||||
func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
func (s *Server) OnMe(sub *Subscriber, msg messages.Message) {
|
||||||
if msg.VideoStatus&VideoFlagActive == VideoFlagActive {
|
if msg.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive {
|
||||||
log.Debug("User %s turns on their video feed", sub.Username)
|
log.Debug("User %s turns on their video feed", sub.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,15 +308,15 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
||||||
if sub.JWTClaims != nil && sub.JWTClaims.IsAdmin {
|
if sub.JWTClaims != nil && sub.JWTClaims.IsAdmin {
|
||||||
if sub.ChatStatus != "hidden" && msg.ChatStatus == "hidden" {
|
if sub.ChatStatus != "hidden" && msg.ChatStatus == "hidden" {
|
||||||
// Going hidden - fake leave message
|
// Going hidden - fake leave message
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionPresence,
|
Action: messages.ActionPresence,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Message: "has exited the room!",
|
Message: "has exited the room!",
|
||||||
})
|
})
|
||||||
} else if sub.ChatStatus == "hidden" && msg.ChatStatus != "hidden" {
|
} else if sub.ChatStatus == "hidden" && msg.ChatStatus != "hidden" {
|
||||||
// Leaving hidden - fake join message
|
// Leaving hidden - fake join message
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionPresence,
|
Action: messages.ActionPresence,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Message: "has joined the room!",
|
Message: "has joined the room!",
|
||||||
})
|
})
|
||||||
|
@ -333,7 +334,7 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnOpen is a client wanting to start WebRTC with another, e.g. to see their camera.
|
// OnOpen is a client wanting to start WebRTC with another, e.g. to see their camera.
|
||||||
func (s *Server) OnOpen(sub *Subscriber, msg Message) {
|
func (s *Server) OnOpen(sub *Subscriber, msg messages.Message) {
|
||||||
// Look up the other subscriber.
|
// Look up the other subscriber.
|
||||||
other, err := s.GetSubscriber(msg.Username)
|
other, err := s.GetSubscriber(msg.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -355,22 +356,22 @@ func (s *Server) OnOpen(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ring the target of this request and give them the secret.
|
// Ring the target of this request and give them the secret.
|
||||||
other.SendJSON(Message{
|
other.SendJSON(messages.Message{
|
||||||
Action: ActionRing,
|
Action: messages.ActionRing,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
OpenSecret: secret,
|
OpenSecret: secret,
|
||||||
})
|
})
|
||||||
|
|
||||||
// To the caller, echo back the Open along with the secret.
|
// To the caller, echo back the Open along with the secret.
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(messages.Message{
|
||||||
Action: ActionOpen,
|
Action: messages.ActionOpen,
|
||||||
Username: other.Username,
|
Username: other.Username,
|
||||||
OpenSecret: secret,
|
OpenSecret: secret,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnBoot is a user kicking you off their video stream.
|
// OnBoot is a user kicking you off their video stream.
|
||||||
func (s *Server) OnBoot(sub *Subscriber, msg Message) {
|
func (s *Server) OnBoot(sub *Subscriber, msg messages.Message) {
|
||||||
log.Info("%s boots %s off their camera", sub.Username, msg.Username)
|
log.Info("%s boots %s off their camera", sub.Username, msg.Username)
|
||||||
|
|
||||||
sub.muteMu.Lock()
|
sub.muteMu.Lock()
|
||||||
|
@ -389,7 +390,7 @@ func (s *Server) OnBoot(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnMute is a user kicking setting the mute flag for another user.
|
// OnMute is a user kicking setting the mute flag for another user.
|
||||||
func (s *Server) OnMute(sub *Subscriber, msg Message, mute bool) {
|
func (s *Server) OnMute(sub *Subscriber, msg messages.Message, mute bool) {
|
||||||
log.Info("%s mutes or unmutes %s: %v", sub.Username, msg.Username, mute)
|
log.Info("%s mutes or unmutes %s: %v", sub.Username, msg.Username, mute)
|
||||||
|
|
||||||
sub.muteMu.Lock()
|
sub.muteMu.Lock()
|
||||||
|
@ -415,7 +416,7 @@ func (s *Server) OnMute(sub *Subscriber, msg Message, mute bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnBlocklist is a bulk user mute from the CachedBlocklist sent by the website.
|
// OnBlocklist is a bulk user mute from the CachedBlocklist sent by the website.
|
||||||
func (s *Server) OnBlocklist(sub *Subscriber, msg Message) {
|
func (s *Server) OnBlocklist(sub *Subscriber, msg messages.Message) {
|
||||||
log.Info("%s syncs their blocklist: %s", sub.Username, msg.Usernames)
|
log.Info("%s syncs their blocklist: %s", sub.Username, msg.Usernames)
|
||||||
|
|
||||||
sub.muteMu.Lock()
|
sub.muteMu.Lock()
|
||||||
|
@ -430,7 +431,7 @@ func (s *Server) OnBlocklist(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnReport handles a user's report of a message.
|
// OnReport handles a user's report of a message.
|
||||||
func (s *Server) OnReport(sub *Subscriber, msg Message) {
|
func (s *Server) OnReport(sub *Subscriber, msg messages.Message) {
|
||||||
if !WebhookEnabled(WebhookReport) {
|
if !WebhookEnabled(WebhookReport) {
|
||||||
sub.ChatServer("Unfortunately, the report webhook is not enabled so your report could not be received!")
|
sub.ChatServer("Unfortunately, the report webhook is not enabled so your report could not be received!")
|
||||||
return
|
return
|
||||||
|
@ -457,7 +458,7 @@ func (s *Server) OnReport(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnCandidate handles WebRTC candidate signaling.
|
// OnCandidate handles WebRTC candidate signaling.
|
||||||
func (s *Server) OnCandidate(sub *Subscriber, msg Message) {
|
func (s *Server) OnCandidate(sub *Subscriber, msg messages.Message) {
|
||||||
// Look up the other subscriber.
|
// Look up the other subscriber.
|
||||||
other, err := s.GetSubscriber(msg.Username)
|
other, err := s.GetSubscriber(msg.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -465,15 +466,15 @@ func (s *Server) OnCandidate(sub *Subscriber, msg Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
other.SendJSON(Message{
|
other.SendJSON(messages.Message{
|
||||||
Action: ActionCandidate,
|
Action: messages.ActionCandidate,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Candidate: msg.Candidate,
|
Candidate: msg.Candidate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnSDP handles WebRTC sdp signaling.
|
// OnSDP handles WebRTC sdp signaling.
|
||||||
func (s *Server) OnSDP(sub *Subscriber, msg Message) {
|
func (s *Server) OnSDP(sub *Subscriber, msg messages.Message) {
|
||||||
// Look up the other subscriber.
|
// Look up the other subscriber.
|
||||||
other, err := s.GetSubscriber(msg.Username)
|
other, err := s.GetSubscriber(msg.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -481,15 +482,15 @@ func (s *Server) OnSDP(sub *Subscriber, msg Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
other.SendJSON(Message{
|
other.SendJSON(messages.Message{
|
||||||
Action: ActionSDP,
|
Action: messages.ActionSDP,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Description: msg.Description,
|
Description: msg.Description,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnWatch communicates video watching status between users.
|
// OnWatch communicates video watching status between users.
|
||||||
func (s *Server) OnWatch(sub *Subscriber, msg Message) {
|
func (s *Server) OnWatch(sub *Subscriber, msg messages.Message) {
|
||||||
// Look up the other subscriber.
|
// Look up the other subscriber.
|
||||||
other, err := s.GetSubscriber(msg.Username)
|
other, err := s.GetSubscriber(msg.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -497,14 +498,14 @@ func (s *Server) OnWatch(sub *Subscriber, msg Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
other.SendJSON(Message{
|
other.SendJSON(messages.Message{
|
||||||
Action: ActionWatch,
|
Action: messages.ActionWatch,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnUnwatch communicates video Unwatching status between users.
|
// OnUnwatch communicates video Unwatching status between users.
|
||||||
func (s *Server) OnUnwatch(sub *Subscriber, msg Message) {
|
func (s *Server) OnUnwatch(sub *Subscriber, msg messages.Message) {
|
||||||
// Look up the other subscriber.
|
// Look up the other subscriber.
|
||||||
other, err := s.GetSubscriber(msg.Username)
|
other, err := s.GetSubscriber(msg.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -512,8 +513,8 @@ func (s *Server) OnUnwatch(sub *Subscriber, msg Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
other.SendJSON(Message{
|
other.SendJSON(messages.Message{
|
||||||
Action: ActionUnwatch,
|
Action: messages.ActionUnwatch,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package barertc
|
package messages
|
||||||
|
|
||||||
// Auto incrementing Message ID for anything pushed out by the server.
|
// Auto incrementing Message ID for anything pushed out by the server.
|
||||||
var MessageID int
|
var MessageID int
|
|
@ -35,6 +35,7 @@ func (s *Server) Setup() error {
|
||||||
mux.Handle("/ws", s.WebSocket())
|
mux.Handle("/ws", s.WebSocket())
|
||||||
mux.Handle("/api/statistics", s.Statistics())
|
mux.Handle("/api/statistics", s.Statistics())
|
||||||
mux.Handle("/api/blocklist", s.BlockList())
|
mux.Handle("/api/blocklist", s.BlockList())
|
||||||
|
mux.Handle("/api/authenticate", s.Authenticate())
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
||||||
|
|
||||||
s.mux = mux
|
s.mux = mux
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"git.kirsle.net/apps/barertc/pkg/config"
|
"git.kirsle.net/apps/barertc/pkg/config"
|
||||||
"git.kirsle.net/apps/barertc/pkg/jwt"
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
||||||
"git.kirsle.net/apps/barertc/pkg/log"
|
"git.kirsle.net/apps/barertc/pkg/log"
|
||||||
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||||
"git.kirsle.net/apps/barertc/pkg/util"
|
"git.kirsle.net/apps/barertc/pkg/util"
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
@ -54,8 +55,8 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
||||||
|
|
||||||
// Notify if this user was auth'd and not hidden
|
// Notify if this user was auth'd and not hidden
|
||||||
if sub.authenticated && sub.ChatStatus != "hidden" {
|
if sub.authenticated && sub.ChatStatus != "hidden" {
|
||||||
s.Broadcast(Message{
|
s.Broadcast(messages.Message{
|
||||||
Action: ActionPresence,
|
Action: messages.ActionPresence,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Message: "has exited the room!",
|
Message: "has exited the room!",
|
||||||
})
|
})
|
||||||
|
@ -70,47 +71,47 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the user's posted message.
|
// Read the user's posted message.
|
||||||
var msg Message
|
var msg messages.Message
|
||||||
if err := json.Unmarshal(data, &msg); err != nil {
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
log.Error("Read(%d=%s) Message error: %s", sub.ID, sub.Username, err)
|
log.Error("Read(%d=%s) Message error: %s", sub.ID, sub.Username, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.Action != ActionFile {
|
if msg.Action != messages.ActionFile {
|
||||||
log.Debug("Read(%d=%s): %s", sub.ID, sub.Username, data)
|
log.Debug("Read(%d=%s): %s", sub.ID, sub.Username, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// What action are they performing?
|
// What action are they performing?
|
||||||
switch msg.Action {
|
switch msg.Action {
|
||||||
case ActionLogin:
|
case messages.ActionLogin:
|
||||||
s.OnLogin(sub, msg)
|
s.OnLogin(sub, msg)
|
||||||
case ActionMessage:
|
case messages.ActionMessage:
|
||||||
s.OnMessage(sub, msg)
|
s.OnMessage(sub, msg)
|
||||||
case ActionFile:
|
case messages.ActionFile:
|
||||||
s.OnFile(sub, msg)
|
s.OnFile(sub, msg)
|
||||||
case ActionMe:
|
case messages.ActionMe:
|
||||||
s.OnMe(sub, msg)
|
s.OnMe(sub, msg)
|
||||||
case ActionOpen:
|
case messages.ActionOpen:
|
||||||
s.OnOpen(sub, msg)
|
s.OnOpen(sub, msg)
|
||||||
case ActionBoot:
|
case messages.ActionBoot:
|
||||||
s.OnBoot(sub, msg)
|
s.OnBoot(sub, msg)
|
||||||
case ActionMute, ActionUnmute:
|
case messages.ActionMute, messages.ActionUnmute:
|
||||||
s.OnMute(sub, msg, msg.Action == ActionMute)
|
s.OnMute(sub, msg, msg.Action == messages.ActionMute)
|
||||||
case ActionBlocklist:
|
case messages.ActionBlocklist:
|
||||||
s.OnBlocklist(sub, msg)
|
s.OnBlocklist(sub, msg)
|
||||||
case ActionCandidate:
|
case messages.ActionCandidate:
|
||||||
s.OnCandidate(sub, msg)
|
s.OnCandidate(sub, msg)
|
||||||
case ActionSDP:
|
case messages.ActionSDP:
|
||||||
s.OnSDP(sub, msg)
|
s.OnSDP(sub, msg)
|
||||||
case ActionWatch:
|
case messages.ActionWatch:
|
||||||
s.OnWatch(sub, msg)
|
s.OnWatch(sub, msg)
|
||||||
case ActionUnwatch:
|
case messages.ActionUnwatch:
|
||||||
s.OnUnwatch(sub, msg)
|
s.OnUnwatch(sub, msg)
|
||||||
case ActionTakeback:
|
case messages.ActionTakeback:
|
||||||
s.OnTakeback(sub, msg)
|
s.OnTakeback(sub, msg)
|
||||||
case ActionReact:
|
case messages.ActionReact:
|
||||||
s.OnReact(sub, msg)
|
s.OnReact(sub, msg)
|
||||||
case ActionReport:
|
case messages.ActionReport:
|
||||||
s.OnReport(sub, msg)
|
s.OnReport(sub, msg)
|
||||||
default:
|
default:
|
||||||
sub.ChatServer("Unsupported message type.")
|
sub.ChatServer("Unsupported message type.")
|
||||||
|
@ -136,8 +137,8 @@ func (sub *Subscriber) SendJSON(v interface{}) error {
|
||||||
|
|
||||||
// SendMe sends the current user state to the client.
|
// SendMe sends the current user state to the client.
|
||||||
func (sub *Subscriber) SendMe() {
|
func (sub *Subscriber) SendMe() {
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(messages.Message{
|
||||||
Action: ActionMe,
|
Action: messages.ActionMe,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
VideoStatus: sub.VideoStatus,
|
VideoStatus: sub.VideoStatus,
|
||||||
})
|
})
|
||||||
|
@ -145,8 +146,8 @@ func (sub *Subscriber) SendMe() {
|
||||||
|
|
||||||
// ChatServer is a convenience function to deliver a ChatServer error to the client.
|
// ChatServer is a convenience function to deliver a ChatServer error to the client.
|
||||||
func (sub *Subscriber) ChatServer(message string, v ...interface{}) {
|
func (sub *Subscriber) ChatServer(message string, v ...interface{}) {
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(messages.Message{
|
||||||
Action: ActionError,
|
Action: messages.ActionError,
|
||||||
Username: "ChatServer",
|
Username: "ChatServer",
|
||||||
Message: fmt.Sprintf(message, v...),
|
Message: fmt.Sprintf(message, v...),
|
||||||
})
|
})
|
||||||
|
@ -213,8 +214,8 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(messages.Message{
|
||||||
Action: ActionPing,
|
Action: messages.ActionPing,
|
||||||
JWTToken: token,
|
JWTToken: token,
|
||||||
})
|
})
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -313,7 +314,7 @@ func (s *Server) UniqueUsername(username string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast a message to the chat room.
|
// Broadcast a message to the chat room.
|
||||||
func (s *Server) Broadcast(msg Message) {
|
func (s *Server) Broadcast(msg messages.Message) {
|
||||||
if len(msg.Message) < 1024 {
|
if len(msg.Message) < 1024 {
|
||||||
log.Debug("Broadcast: %+v", msg)
|
log.Debug("Broadcast: %+v", msg)
|
||||||
}
|
}
|
||||||
|
@ -338,7 +339,7 @@ func (s *Server) Broadcast(msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTo sends a message to a given username.
|
// SendTo sends a message to a given username.
|
||||||
func (s *Server) SendTo(username string, msg Message) error {
|
func (s *Server) SendTo(username string, msg messages.Message) error {
|
||||||
log.Debug("SendTo(%s): %+v", username, msg)
|
log.Debug("SendTo(%s): %+v", username, msg)
|
||||||
username = strings.TrimPrefix(username, "@")
|
username = strings.TrimPrefix(username, "@")
|
||||||
|
|
||||||
|
@ -347,7 +348,7 @@ func (s *Server) SendTo(username string, msg Message) error {
|
||||||
for _, sub := range subs {
|
for _, sub := range subs {
|
||||||
if sub.Username == username {
|
if sub.Username == username {
|
||||||
found = true
|
found = true
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(messages.Message{
|
||||||
Action: msg.Action,
|
Action: msg.Action,
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
Username: msg.Username,
|
Username: msg.Username,
|
||||||
|
@ -387,14 +388,14 @@ func (s *Server) SendWhoList() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var users = []WhoList{}
|
var users = []messages.WhoList{}
|
||||||
for _, un := range usernames {
|
for _, un := range usernames {
|
||||||
user := userSub[un]
|
user := userSub[un]
|
||||||
if user.ChatStatus == "hidden" {
|
if user.ChatStatus == "hidden" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
who := WhoList{
|
who := messages.WhoList{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Status: user.ChatStatus,
|
Status: user.ChatStatus,
|
||||||
Video: user.VideoStatus,
|
Video: user.VideoStatus,
|
||||||
|
@ -417,8 +418,8 @@ func (s *Server) SendWhoList() {
|
||||||
users = append(users, who)
|
users = append(users, who)
|
||||||
}
|
}
|
||||||
|
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(messages.Message{
|
||||||
Action: ActionWhoList,
|
Action: messages.ActionWhoList,
|
||||||
WhoList: users,
|
WhoList: users,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user