BareBot Chatbot Client

This commit is contained in:
Noah 2023-08-13 19:21:27 -07:00
parent 2cfabaf251
commit 9c05af2c2e
31 changed files with 2496 additions and 352 deletions

248
README.md
View File

@ -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] /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
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!"]
```
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.
See [Configuration](docs/Configuration.md) for in-depth explanations on the available config settings and what they do.
# 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.
See [Authentication](docs/Authentication.md) for more information.
# Moderator Commands
@ -242,102 +95,19 @@ If you authenticate an Op user via JWT they can enter IRC-style chat commands to
# 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.
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"
}
```
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.
# 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:
See [Webhooks](docs/Webhooks.md) for more information.
```toml
[[WebhookURLs]]
Name = "report"
Enabled = true
URL = "http://localhost:8080/v1/barertc/report"
```
# Chatbot
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")
* `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.
See [Chatbot](docs/Chatbot.md) for more information.
# Tour of the Codebase

6
client/brain.go Normal file
View File

@ -0,0 +1,6 @@
package client
import "embed"
//go:embed brain/*
var Embedded embed.FS

24
client/brain/barertc.rive Normal file
View 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
View 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 &quot; = "
! sub &apos; = '
! sub &amp; = &
! sub &lt; = <
! sub &gt; = >
! 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
View 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>

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

View 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
View 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
View 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
}

View 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
View 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) {
}

View 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
View 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
}

View 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
},
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -4,23 +4,27 @@ go 1.19
require (
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/golang-jwt/jwt/v4 v4.4.3
github.com/google/uuid v1.3.0
github.com/mattn/go-shellwords v1.0.12
github.com/microcosm-cc/bluemonday v1.0.22
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
nhooyr.io/websocket v1.8.7
)
require (
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/gorilla/css v1.0.0 // indirect
github.com/klauspost/compress v1.10.3 // 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/sergi/go-diff v1.3.1 // 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/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // 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/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect

89
go.sum
View File

@ -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/go.mod h1:jl+Qr58W3Op7OCxIYIT+b42jq8xFncJXzPufhrvza7Y=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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/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/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
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/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
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/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
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/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/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.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
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.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/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
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/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
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/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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/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/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/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/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/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
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/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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
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/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/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/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=
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.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
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.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
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.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-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-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.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/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-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.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-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-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-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-20220520151302-bc2c85ada10a/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.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.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.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/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-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.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-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-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 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.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.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/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=

View File

@ -7,7 +7,9 @@ import (
"sync"
"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/messages"
)
// 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{}{}
// Count cameras by color.
if sub.VideoStatus&VideoFlagActive == VideoFlagActive {
if sub.VideoStatus&VideoFlagNSFW == VideoFlagNSFW {
if sub.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive {
if sub.VideoStatus&messages.VideoFlagNSFW == messages.VideoFlagNSFW {
result.Cameras.Red++
} else {
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(&params); 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
// user accounts, so that when they see each other in chat they will pre-emptively mute
// or boot one another.

View File

@ -9,12 +9,13 @@ import (
"git.kirsle.net/apps/barertc/pkg/config"
ourjwt "git.kirsle.net/apps/barertc/pkg/jwt"
"git.kirsle.net/apps/barertc/pkg/log"
"git.kirsle.net/apps/barertc/pkg/messages"
"github.com/golang-jwt/jwt/v4"
"github.com/mattn/go-shellwords"
)
// 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] != '/' {
return false
}
@ -63,8 +64,8 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
))
return true
case "/shutdown":
s.Broadcast(Message{
Action: ActionError,
s.Broadcast(messages.Message{
Action: messages.ActionError,
Username: "ChatServer",
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.VideoStatus |= VideoFlagNSFW
other.VideoStatus |= messages.VideoFlagNSFW
other.SendMe()
s.SendWhoList()
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?")
} else {
other.ChatServer("You have been kicked from the chat room by %s", sub.Username)
other.SendJSON(Message{
Action: ActionKick,
other.SendJSON(messages.Message{
Action: messages.ActionKick,
})
s.DeleteSubscriber(other)
sub.ChatServer("%s has been kicked from the room", username)
// Broadcast it to everyone.
s.Broadcast(Message{
Action: ActionPresence,
s.Broadcast(messages.Message{
Action: messages.ActionPresence,
Username: username,
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 config.Current.JWT.Enabled && config.Current.JWT.LandingPageURL != "" {
s.Broadcast(Message{
Action: ActionError,
s.Broadcast(messages.Message{
Action: messages.ActionError,
Username: "ChatServer",
Message: fmt.Sprintf(
"<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 {
s.Broadcast(Message{
Action: ActionError,
s.Broadcast(messages.Message{
Action: messages.ActionError,
Username: "ChatServer",
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 " +
@ -176,8 +177,8 @@ func (s *Server) KickAllCommand() {
}
// Kick everyone off.
s.Broadcast(Message{
Action: ActionKick,
s.Broadcast(messages.Message{
Action: messages.ActionKick,
})
// Disconnect everybody.
@ -221,15 +222,15 @@ func (s *Server) BanCommand(words []string, sub *Subscriber) {
BanUser(username, duration)
// Broadcast it to everyone.
s.Broadcast(Message{
Action: ActionPresence,
s.Broadcast(messages.Message{
Action: messages.ActionPresence,
Username: username,
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.SendJSON(Message{
Action: ActionKick,
other.SendJSON(messages.Message{
Action: messages.ActionKick,
})
s.DeleteSubscriber(other)
sub.ChatServer("%s has been banned from the room for %d hours.", username, duration/time.Hour)

View File

@ -10,11 +10,12 @@ import (
"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/messages"
"git.kirsle.net/apps/barertc/pkg/util"
)
// 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?
var claims = &jwt.Claims{}
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 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.SendJSON(Message{
Action: ActionKick,
other.SendJSON(messages.Message{
Action: messages.ActionKick,
})
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 " +
"24 hours. Please try coming back later.",
)
sub.SendJSON(Message{
Action: ActionKick,
sub.SendJSON(messages.Message{
Action: messages.ActionKick,
})
s.DeleteSubscriber(sub)
return
@ -92,8 +93,8 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
log.Debug("OnLogin: %s joins the room", sub.Username)
// Tell everyone they joined.
s.Broadcast(Message{
Action: ActionPresence,
s.Broadcast(messages.Message{
Action: messages.ActionPresence,
Username: msg.Username,
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.
for _, channel := range config.Current.PublicChannels {
for _, msg := range channel.WelcomeMessages {
sub.SendJSON(Message{
sub.SendJSON(messages.Message{
Channel: channel.ID,
Action: ActionError,
Action: messages.ActionError,
Username: "ChatServer",
Message: RenderMarkdown(msg),
})
@ -118,7 +119,7 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
}
// 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, "@") {
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)
// Assign a message ID and own it to the sender.
MessageID++
var mid = MessageID
messages.MessageID++
var mid = messages.MessageID
sub.midMu.Lock()
sub.messageIDs[mid] = struct{}{}
sub.midMu.Unlock()
// Message to be echoed to the channel.
var message = Message{
Action: ActionMessage,
var message = messages.Message{
Action: messages.ActionMessage,
Channel: msg.Channel,
Username: sub.Username,
Message: markdown,
@ -188,7 +189,7 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
}
// 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.
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
sub.midMu.Lock()
@ -200,17 +201,17 @@ func (s *Server) OnTakeback(sub *Subscriber, msg Message) {
}
// Broadcast to everybody to remove this message.
s.Broadcast(Message{
Action: ActionTakeback,
s.Broadcast(messages.Message{
Action: messages.ActionTakeback,
MessageID: msg.MessageID,
})
}
// 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.
s.Broadcast(Message{
Action: ActionReact,
s.Broadcast(messages.Message{
Action: messages.ActionReact,
Username: sub.Username,
Message: msg.Message,
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.
func (s *Server) OnFile(sub *Subscriber, msg Message) {
func (s *Server) OnFile(sub *Subscriber, msg messages.Message) {
if sub.Username == "" {
sub.ChatServer("You must log in first.")
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))
// Assign a message ID and own it to the sender.
MessageID++
var mid = MessageID
messages.MessageID++
var mid = messages.MessageID
sub.midMu.Lock()
sub.messageIDs[mid] = struct{}{}
sub.midMu.Unlock()
// Message to be echoed to the channel.
var message = Message{
Action: ActionMessage,
var message = messages.Message{
Action: messages.ActionMessage,
Channel: msg.Channel,
Username: sub.Username,
MessageID: mid,
@ -298,8 +299,8 @@ func (s *Server) OnFile(sub *Subscriber, msg Message) {
}
// OnMe handles current user state updates.
func (s *Server) OnMe(sub *Subscriber, msg Message) {
if msg.VideoStatus&VideoFlagActive == VideoFlagActive {
func (s *Server) OnMe(sub *Subscriber, msg messages.Message) {
if msg.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive {
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.ChatStatus != "hidden" && msg.ChatStatus == "hidden" {
// Going hidden - fake leave message
s.Broadcast(Message{
Action: ActionPresence,
s.Broadcast(messages.Message{
Action: messages.ActionPresence,
Username: sub.Username,
Message: "has exited the room!",
})
} else if sub.ChatStatus == "hidden" && msg.ChatStatus != "hidden" {
// Leaving hidden - fake join message
s.Broadcast(Message{
Action: ActionPresence,
s.Broadcast(messages.Message{
Action: messages.ActionPresence,
Username: sub.Username,
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.
func (s *Server) OnOpen(sub *Subscriber, msg Message) {
func (s *Server) OnOpen(sub *Subscriber, msg messages.Message) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
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.
other.SendJSON(Message{
Action: ActionRing,
other.SendJSON(messages.Message{
Action: messages.ActionRing,
Username: sub.Username,
OpenSecret: secret,
})
// To the caller, echo back the Open along with the secret.
sub.SendJSON(Message{
Action: ActionOpen,
sub.SendJSON(messages.Message{
Action: messages.ActionOpen,
Username: other.Username,
OpenSecret: secret,
})
}
// 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)
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.
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)
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.
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)
sub.muteMu.Lock()
@ -430,7 +431,7 @@ func (s *Server) OnBlocklist(sub *Subscriber, msg 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) {
sub.ChatServer("Unfortunately, the report webhook is not enabled so your report could not be received!")
return
@ -457,7 +458,7 @@ func (s *Server) OnReport(sub *Subscriber, msg Message) {
}
// 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.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
@ -465,15 +466,15 @@ func (s *Server) OnCandidate(sub *Subscriber, msg Message) {
return
}
other.SendJSON(Message{
Action: ActionCandidate,
other.SendJSON(messages.Message{
Action: messages.ActionCandidate,
Username: sub.Username,
Candidate: msg.Candidate,
})
}
// 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.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
@ -481,15 +482,15 @@ func (s *Server) OnSDP(sub *Subscriber, msg Message) {
return
}
other.SendJSON(Message{
Action: ActionSDP,
other.SendJSON(messages.Message{
Action: messages.ActionSDP,
Username: sub.Username,
Description: msg.Description,
})
}
// 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.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
@ -497,14 +498,14 @@ func (s *Server) OnWatch(sub *Subscriber, msg Message) {
return
}
other.SendJSON(Message{
Action: ActionWatch,
other.SendJSON(messages.Message{
Action: messages.ActionWatch,
Username: sub.Username,
})
}
// 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.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
@ -512,8 +513,8 @@ func (s *Server) OnUnwatch(sub *Subscriber, msg Message) {
return
}
other.SendJSON(Message{
Action: ActionUnwatch,
other.SendJSON(messages.Message{
Action: messages.ActionUnwatch,
Username: sub.Username,
})
}

View File

@ -1,4 +1,4 @@
package barertc
package messages
// Auto incrementing Message ID for anything pushed out by the server.
var MessageID int

View File

@ -35,6 +35,7 @@ func (s *Server) Setup() error {
mux.Handle("/ws", s.WebSocket())
mux.Handle("/api/statistics", s.Statistics())
mux.Handle("/api/blocklist", s.BlockList())
mux.Handle("/api/authenticate", s.Authenticate())
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
s.mux = mux

View File

@ -14,6 +14,7 @@ import (
"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/messages"
"git.kirsle.net/apps/barertc/pkg/util"
"nhooyr.io/websocket"
)
@ -54,8 +55,8 @@ func (sub *Subscriber) ReadLoop(s *Server) {
// Notify if this user was auth'd and not hidden
if sub.authenticated && sub.ChatStatus != "hidden" {
s.Broadcast(Message{
Action: ActionPresence,
s.Broadcast(messages.Message{
Action: messages.ActionPresence,
Username: sub.Username,
Message: "has exited the room!",
})
@ -70,47 +71,47 @@ func (sub *Subscriber) ReadLoop(s *Server) {
}
// Read the user's posted message.
var msg Message
var msg messages.Message
if err := json.Unmarshal(data, &msg); err != nil {
log.Error("Read(%d=%s) Message error: %s", sub.ID, sub.Username, err)
continue
}
if msg.Action != ActionFile {
if msg.Action != messages.ActionFile {
log.Debug("Read(%d=%s): %s", sub.ID, sub.Username, data)
}
// What action are they performing?
switch msg.Action {
case ActionLogin:
case messages.ActionLogin:
s.OnLogin(sub, msg)
case ActionMessage:
case messages.ActionMessage:
s.OnMessage(sub, msg)
case ActionFile:
case messages.ActionFile:
s.OnFile(sub, msg)
case ActionMe:
case messages.ActionMe:
s.OnMe(sub, msg)
case ActionOpen:
case messages.ActionOpen:
s.OnOpen(sub, msg)
case ActionBoot:
case messages.ActionBoot:
s.OnBoot(sub, msg)
case ActionMute, ActionUnmute:
s.OnMute(sub, msg, msg.Action == ActionMute)
case ActionBlocklist:
case messages.ActionMute, messages.ActionUnmute:
s.OnMute(sub, msg, msg.Action == messages.ActionMute)
case messages.ActionBlocklist:
s.OnBlocklist(sub, msg)
case ActionCandidate:
case messages.ActionCandidate:
s.OnCandidate(sub, msg)
case ActionSDP:
case messages.ActionSDP:
s.OnSDP(sub, msg)
case ActionWatch:
case messages.ActionWatch:
s.OnWatch(sub, msg)
case ActionUnwatch:
case messages.ActionUnwatch:
s.OnUnwatch(sub, msg)
case ActionTakeback:
case messages.ActionTakeback:
s.OnTakeback(sub, msg)
case ActionReact:
case messages.ActionReact:
s.OnReact(sub, msg)
case ActionReport:
case messages.ActionReport:
s.OnReport(sub, msg)
default:
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.
func (sub *Subscriber) SendMe() {
sub.SendJSON(Message{
Action: ActionMe,
sub.SendJSON(messages.Message{
Action: messages.ActionMe,
Username: sub.Username,
VideoStatus: sub.VideoStatus,
})
@ -145,8 +146,8 @@ func (sub *Subscriber) SendMe() {
// ChatServer is a convenience function to deliver a ChatServer error to the client.
func (sub *Subscriber) ChatServer(message string, v ...interface{}) {
sub.SendJSON(Message{
Action: ActionError,
sub.SendJSON(messages.Message{
Action: messages.ActionError,
Username: "ChatServer",
Message: fmt.Sprintf(message, v...),
})
@ -213,8 +214,8 @@ func (s *Server) WebSocket() http.HandlerFunc {
}
}
sub.SendJSON(Message{
Action: ActionPing,
sub.SendJSON(messages.Message{
Action: messages.ActionPing,
JWTToken: token,
})
case <-ctx.Done():
@ -313,7 +314,7 @@ func (s *Server) UniqueUsername(username string) (string, error) {
}
// 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 {
log.Debug("Broadcast: %+v", msg)
}
@ -338,7 +339,7 @@ func (s *Server) Broadcast(msg Message) {
}
// 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)
username = strings.TrimPrefix(username, "@")
@ -347,7 +348,7 @@ func (s *Server) SendTo(username string, msg Message) error {
for _, sub := range subs {
if sub.Username == username {
found = true
sub.SendJSON(Message{
sub.SendJSON(messages.Message{
Action: msg.Action,
Channel: msg.Channel,
Username: msg.Username,
@ -387,14 +388,14 @@ func (s *Server) SendWhoList() {
continue
}
var users = []WhoList{}
var users = []messages.WhoList{}
for _, un := range usernames {
user := userSub[un]
if user.ChatStatus == "hidden" {
continue
}
who := WhoList{
who := messages.WhoList{
Username: user.Username,
Status: user.ChatStatus,
Video: user.VideoStatus,
@ -417,8 +418,8 @@ func (s *Server) SendWhoList() {
users = append(users, who)
}
sub.SendJSON(Message{
Action: ActionWhoList,
sub.SendJSON(messages.Message{
Action: messages.ActionWhoList,
WhoList: users,
})
}