diff --git a/cmd/BareRTC/main.go b/cmd/BareRTC/main.go
index 4813b62..1e8119e 100644
--- a/cmd/BareRTC/main.go
+++ b/cmd/BareRTC/main.go
@@ -2,6 +2,7 @@ package main
import (
"flag"
+ "fmt"
"math/rand"
"time"
@@ -29,7 +30,9 @@ func main() {
}
// Load configuration.
- config.LoadSettings()
+ if err := config.LoadSettings(); err != nil {
+ panic(fmt.Sprintf("Error loading settings.toml: %s", err))
+ }
app := barertc.NewServer()
app.Setup()
diff --git a/pkg/commands.go b/pkg/commands.go
index 41301b9..7468016 100644
--- a/pkg/commands.go
+++ b/pkg/commands.go
@@ -72,6 +72,9 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
case "/kickall":
s.KickAllCommand()
return true
+ case "/reconfigure":
+ s.ReconfigureCommand(sub)
+ return true
case "/op":
s.OpCommand(words, sub)
return true
@@ -259,6 +262,17 @@ func (s *Server) BansCommand(words []string, sub *Subscriber) {
)
}
+// ReconfigureCommand handles the `/reconfigure` operator command.
+func (s *Server) ReconfigureCommand(sub *Subscriber) {
+ // Reload the settings.
+ if err := config.LoadSettings(); err != nil {
+ sub.ChatServer("Error reloading the server config: %s", err)
+ return
+ }
+
+ sub.ChatServer("The server config file has been reloaded successfully!")
+}
+
// OpCommand handles the `/op` operator command.
func (s *Server) OpCommand(words []string, sub *Subscriber) {
if len(words) == 1 {
diff --git a/pkg/config/config.go b/pkg/config/config.go
index cbe08f5..67b62f4 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -13,7 +13,7 @@ import (
// Version of the config format - when new fields are added, it will attempt
// to write the settings.toml to disk so new defaults populate.
-var currentVersion = 5
+var currentVersion = 6
// Config for your BareRTC app.
type Config struct {
@@ -43,6 +43,8 @@ type Config struct {
TURN TurnConfig
PublicChannels []Channel
+
+ WebhookURLs []WebhookURL
}
type TurnConfig struct {
@@ -67,6 +69,13 @@ type Channel struct {
WelcomeMessages []string
}
+// WebhookURL allows tighter integration with your website.
+type WebhookURL struct {
+ Name string
+ Enabled bool
+ URL string
+}
+
// Current loaded configuration.
var Current = DefaultConfig()
@@ -107,6 +116,12 @@ func DefaultConfig() Config {
"stun:stun.l.google.com:19302",
},
},
+ WebhookURLs: []WebhookURL{
+ {
+ Name: "report",
+ URL: "https://example.com/barertc/report",
+ },
+ },
}
c.JWT.Strict = true
return c
@@ -126,6 +141,9 @@ func LoadSettings() error {
}
_, err = toml.Decode(string(data), &Current)
+ if err != nil {
+ return err
+ }
// Have we added new config fields? Save the settings.toml.
if Current.Version != currentVersion {
diff --git a/pkg/handlers.go b/pkg/handlers.go
index ecec39f..979bd74 100644
--- a/pkg/handlers.go
+++ b/pkg/handlers.go
@@ -429,6 +429,33 @@ func (s *Server) OnBlocklist(sub *Subscriber, msg Message) {
s.SendWhoList()
}
+// OnReport handles a user's report of a message.
+func (s *Server) OnReport(sub *Subscriber, msg Message) {
+ if !WebhookEnabled(WebhookReport) {
+ sub.ChatServer("Unfortunately, the report webhook is not enabled so your report could not be received!")
+ return
+ }
+
+ // Post to the report webhook.
+ if err := PostWebhook(WebhookReport, WebhookRequest{
+ Action: WebhookReport,
+ APIKey: config.Current.AdminAPIKey,
+ Report: WebhookRequestReport{
+ FromUsername: sub.Username,
+ AboutUsername: msg.Username,
+ Channel: msg.Channel,
+ Timestamp: msg.Timestamp,
+ Reason: msg.Reason,
+ Message: msg.Message,
+ Comment: msg.Comment,
+ },
+ }); err != nil {
+ sub.ChatServer("Error sending the report to the website: %s", err)
+ } else {
+ sub.ChatServer("Your report has been delivered successfully.")
+ }
+}
+
// OnCandidate handles WebRTC candidate signaling.
func (s *Server) OnCandidate(sub *Subscriber, msg Message) {
// Look up the other subscriber.
diff --git a/pkg/messages.go b/pkg/messages.go
index bdfdfe5..316afc0 100644
--- a/pkg/messages.go
+++ b/pkg/messages.go
@@ -38,6 +38,11 @@ type Message struct {
// Send on `blocklist` actions, for doing a `mute` on a list of users
Usernames []string `json:"usernames,omitempty"`
+ // Sent on `report` actions.
+ Timestamp string `json:"timestamp,omitempty"`
+ Reason string `json:"reason,omitempty"`
+ Comment string `json:"comment,omitempty"`
+
// WebRTC negotiation messages: proxy their signaling messages
// between the two users to negotiate peer connection.
Candidate string `json:"candidate,omitempty"` // candidate
@@ -51,6 +56,7 @@ const (
ActionMute = "mute" // mute a user's chat messages
ActionUnmute = "unmute"
ActionBlocklist = "blocklist" // mute in bulk for usernames
+ ActionReport = "report" // user reports a message
// Actions sent by server or client
ActionMessage = "message" // post a message to the room
diff --git a/pkg/webhook_messages.go b/pkg/webhook_messages.go
new file mode 100644
index 0000000..561c2fc
--- /dev/null
+++ b/pkg/webhook_messages.go
@@ -0,0 +1,21 @@
+package barertc
+
+// WebhookRequest is a JSON request wrapper around all webhook messages.
+type WebhookRequest struct {
+ Action string
+ APIKey string
+
+ // Relevant body per request.
+ Report WebhookRequestReport `json:",omitempty"`
+}
+
+// WebhookRequestReport is the body for 'report' webhook messages.
+type WebhookRequestReport struct {
+ FromUsername string
+ AboutUsername string
+ Channel string
+ Timestamp string
+ Reason string
+ Message string
+ Comment string
+}
diff --git a/pkg/webhooks.go b/pkg/webhooks.go
new file mode 100644
index 0000000..f2f3115
--- /dev/null
+++ b/pkg/webhooks.go
@@ -0,0 +1,81 @@
+package barertc
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "time"
+
+ "git.kirsle.net/apps/barertc/pkg/config"
+ "git.kirsle.net/apps/barertc/pkg/log"
+)
+
+// The available and supported webhook event names.
+const (
+ WebhookReport = "report"
+)
+
+// WebhookEnabled checks if the named webhook is enabled.
+func WebhookEnabled(name string) bool {
+ for _, webhook := range config.Current.WebhookURLs {
+ if webhook.Name == name && webhook.Enabled {
+ return true
+ }
+ }
+ return false
+}
+
+// GetWebhook gets a configured webhook.
+func GetWebhook(name string) (config.WebhookURL, bool) {
+ for _, webhook := range config.Current.WebhookURLs {
+ if webhook.Name == name {
+ return webhook, true
+ }
+ }
+
+ return config.WebhookURL{}, false
+}
+
+// PostWebhook submits a JSON body to one of the app's configured webhooks.
+func PostWebhook(name string, payload any) error {
+ webhook, ok := GetWebhook(name)
+ if !ok {
+ return errors.New("PostWebhook(%s): webhook name %s is not configured")
+ } else if !webhook.Enabled {
+ return errors.New("PostWebhook(%s): webhook is not enabled")
+ }
+
+ // JSON request body.
+ jsonStr, err := json.Marshal(payload)
+ if err != nil {
+ return err
+ }
+
+ // Make the API request to BareRTC.
+ var url = webhook.URL
+ log.Debug("PostWebhook(%s): to %s we send: %s", name, url, jsonStr)
+ 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()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ log.Error("PostWebhook(%s): unexpected response from webhook URL %s (code %d): %s", name, url, resp.StatusCode, body)
+ return errors.New("unexpected error from webhook URL")
+ }
+
+ return nil
+}
diff --git a/pkg/websocket.go b/pkg/websocket.go
index 0ec34ca..be461de 100644
--- a/pkg/websocket.go
+++ b/pkg/websocket.go
@@ -110,6 +110,8 @@ func (sub *Subscriber) ReadLoop(s *Server) {
s.OnTakeback(sub, msg)
case ActionReact:
s.OnReact(sub, msg)
+ case ActionReport:
+ s.OnReport(sub, msg)
default:
sub.ChatServer("Unsupported message type.")
}
diff --git a/web/static/css/chat.css b/web/static/css/chat.css
index e296459..bdc7fc9 100644
--- a/web/static/css/chat.css
+++ b/web/static/css/chat.css
@@ -36,6 +36,12 @@ body {
color: #ff9999 !important;
}
+/* Max height for message in report modal */
+.report-modal-message {
+ max-height: 150px;
+ overflow: auto;
+}
+
/************************
* Main CSS Grid Layout *
************************/
diff --git a/web/static/js/BareRTC.js b/web/static/js/BareRTC.js
index e2bd878..b619267 100644
--- a/web/static/js/BareRTC.js
+++ b/web/static/js/BareRTC.js
@@ -49,6 +49,7 @@ const app = Vue.createApp({
channels: PublicChannels,
website: WebsiteURL,
permitNSFW: PermitNSFW,
+ webhookURLs: WebhookURLs,
fontSizeClasses: [
[ "x-2", "Very small chat room text" ],
[ "x-1", "50% smaller chat room text" ],
@@ -58,6 +59,14 @@ const app = Vue.createApp({
[ "x3", "3x larger chat room text" ],
[ "x4", "4x larger chat room text" ],
],
+ reportClassifications: [
+ "It's spam",
+ "It's abusive (racist, homophobic, etc.)",
+ "It's malicious (e.g. link to a malware website)",
+ "It's illegal (e.g. controlled substances)",
+ "It's child porn (CP, CSAM, pedophilia, etc.)",
+ "Other (please describe)",
+ ],
sounds: {
available: SoundEffects,
settings: DefaultSounds,
@@ -230,6 +239,15 @@ const app = Vue.createApp({
dontShowAgain: false,
user: null, // staged User we wanted to open
},
+
+ reportModal: {
+ visible: false,
+ busy: false,
+ message: {},
+ origMessage: {}, // pointer, so we can set the "reported" flag
+ classification: "It's spam",
+ comment: "",
+ },
}
},
mounted() {
@@ -1884,6 +1902,7 @@ const app = Vue.createApp({
this.channels[channel].updated = new Date().getTime();
this.channels[channel].history.push({
action: action,
+ channel: channel,
username: username,
message: message,
msgID: messageID,
@@ -1984,6 +2003,7 @@ const app = Vue.createApp({
// Format a datetime nicely for chat timestamp.
prettyDate(date) {
+ if (date == undefined) return '';
let hours = date.getHours(),
minutes = String(date.getMinutes()).padStart(2, '0'),
ampm = hours >= 11 ? "pm" : "am";
@@ -2157,7 +2177,61 @@ const app = Vue.createApp({
if (this.status === "online") {
this.status = "idle";
}
- }
+ },
+
+ /*
+ * Webhook methods
+ */
+ isWebhookEnabled(name) {
+ for (let webhook of this.config.webhookURLs) {
+ if (webhook.Name === name && webhook.Enabled) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ reportMessage(message) {
+ // User is reporting a message on chat.
+ if (message.reported) {
+ if (!window.confirm("You have already reported this message. Do you want to report it again?")) return;
+ }
+
+ // Clone the message.
+ let clone = Object.assign({}, message);
+
+ // Sub out attached images.
+ clone.message = clone.message.replace(//g, "[inline image]");
+
+ this.reportModal.message = clone;
+ this.reportModal.origMessage = message;
+ this.reportModal.classification = this.config.reportClassifications[0];
+ this.reportModal.comment = "";
+ this.reportModal.visible = true;
+ },
+ doReport() {
+ // Submit the queued up report.
+ if (this.reportModal.busy) return;
+ this.reportModal.busy = true;
+
+ let msg = this.reportModal.message;
+
+ this.ws.conn.send(JSON.stringify({
+ action: "report",
+ channel: msg.channel,
+ username: msg.username,
+ timestamp: ""+msg.at,
+ reason: this.reportModal.classification,
+ message: msg.message,
+ comment: this.reportModal.comment,
+ }));
+
+ this.reportModal.busy = false;
+ this.reportModal.visible = false;
+
+ // Set the "reported" flag.
+ this.reportModal.origMessage.reported = true;
+ },
}
});
diff --git a/web/templates/chat.html b/web/templates/chat.html
index 96d4748..e6ffd92 100644
--- a/web/templates/chat.html
+++ b/web/templates/chat.html
@@ -458,6 +458,87 @@
+
+