Webhooks and Report Users
* Added support for Webhooks and you can configure a Report Message hook to let users report messages on chat. * Add /reconfigure command to dynamically reload the server settings.toml * TODO: documentation for the webhooks.
This commit is contained in:
parent
e7e1fc3d5b
commit
05eb852bb9
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -29,7 +30,9 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration.
|
// Load configuration.
|
||||||
config.LoadSettings()
|
if err := config.LoadSettings(); err != nil {
|
||||||
|
panic(fmt.Sprintf("Error loading settings.toml: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
app := barertc.NewServer()
|
app := barertc.NewServer()
|
||||||
app.Setup()
|
app.Setup()
|
||||||
|
|
|
@ -72,6 +72,9 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
||||||
case "/kickall":
|
case "/kickall":
|
||||||
s.KickAllCommand()
|
s.KickAllCommand()
|
||||||
return true
|
return true
|
||||||
|
case "/reconfigure":
|
||||||
|
s.ReconfigureCommand(sub)
|
||||||
|
return true
|
||||||
case "/op":
|
case "/op":
|
||||||
s.OpCommand(words, sub)
|
s.OpCommand(words, sub)
|
||||||
return true
|
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.
|
// OpCommand handles the `/op` operator command.
|
||||||
func (s *Server) OpCommand(words []string, sub *Subscriber) {
|
func (s *Server) OpCommand(words []string, sub *Subscriber) {
|
||||||
if len(words) == 1 {
|
if len(words) == 1 {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
// Version of the config format - when new fields are added, it will attempt
|
// Version of the config format - when new fields are added, it will attempt
|
||||||
// to write the settings.toml to disk so new defaults populate.
|
// to write the settings.toml to disk so new defaults populate.
|
||||||
var currentVersion = 5
|
var currentVersion = 6
|
||||||
|
|
||||||
// Config for your BareRTC app.
|
// Config for your BareRTC app.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -43,6 +43,8 @@ type Config struct {
|
||||||
TURN TurnConfig
|
TURN TurnConfig
|
||||||
|
|
||||||
PublicChannels []Channel
|
PublicChannels []Channel
|
||||||
|
|
||||||
|
WebhookURLs []WebhookURL
|
||||||
}
|
}
|
||||||
|
|
||||||
type TurnConfig struct {
|
type TurnConfig struct {
|
||||||
|
@ -67,6 +69,13 @@ type Channel struct {
|
||||||
WelcomeMessages []string
|
WelcomeMessages []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebhookURL allows tighter integration with your website.
|
||||||
|
type WebhookURL struct {
|
||||||
|
Name string
|
||||||
|
Enabled bool
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
// Current loaded configuration.
|
// Current loaded configuration.
|
||||||
var Current = DefaultConfig()
|
var Current = DefaultConfig()
|
||||||
|
|
||||||
|
@ -107,6 +116,12 @@ func DefaultConfig() Config {
|
||||||
"stun:stun.l.google.com:19302",
|
"stun:stun.l.google.com:19302",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
WebhookURLs: []WebhookURL{
|
||||||
|
{
|
||||||
|
Name: "report",
|
||||||
|
URL: "https://example.com/barertc/report",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
c.JWT.Strict = true
|
c.JWT.Strict = true
|
||||||
return c
|
return c
|
||||||
|
@ -126,6 +141,9 @@ func LoadSettings() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = toml.Decode(string(data), &Current)
|
_, err = toml.Decode(string(data), &Current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Have we added new config fields? Save the settings.toml.
|
// Have we added new config fields? Save the settings.toml.
|
||||||
if Current.Version != currentVersion {
|
if Current.Version != currentVersion {
|
||||||
|
|
|
@ -429,6 +429,33 @@ func (s *Server) OnBlocklist(sub *Subscriber, msg Message) {
|
||||||
s.SendWhoList()
|
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.
|
// OnCandidate handles WebRTC candidate signaling.
|
||||||
func (s *Server) OnCandidate(sub *Subscriber, msg Message) {
|
func (s *Server) OnCandidate(sub *Subscriber, msg Message) {
|
||||||
// Look up the other subscriber.
|
// Look up the other subscriber.
|
||||||
|
|
|
@ -38,6 +38,11 @@ type Message struct {
|
||||||
// Send on `blocklist` actions, for doing a `mute` on a list of users
|
// Send on `blocklist` actions, for doing a `mute` on a list of users
|
||||||
Usernames []string `json:"usernames,omitempty"`
|
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
|
// WebRTC negotiation messages: proxy their signaling messages
|
||||||
// between the two users to negotiate peer connection.
|
// between the two users to negotiate peer connection.
|
||||||
Candidate string `json:"candidate,omitempty"` // candidate
|
Candidate string `json:"candidate,omitempty"` // candidate
|
||||||
|
@ -51,6 +56,7 @@ const (
|
||||||
ActionMute = "mute" // mute a user's chat messages
|
ActionMute = "mute" // mute a user's chat messages
|
||||||
ActionUnmute = "unmute"
|
ActionUnmute = "unmute"
|
||||||
ActionBlocklist = "blocklist" // mute in bulk for usernames
|
ActionBlocklist = "blocklist" // mute in bulk for usernames
|
||||||
|
ActionReport = "report" // user reports a message
|
||||||
|
|
||||||
// Actions sent by server or client
|
// Actions sent by server or client
|
||||||
ActionMessage = "message" // post a message to the room
|
ActionMessage = "message" // post a message to the room
|
||||||
|
|
21
pkg/webhook_messages.go
Normal file
21
pkg/webhook_messages.go
Normal file
|
@ -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
|
||||||
|
}
|
81
pkg/webhooks.go
Normal file
81
pkg/webhooks.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -110,6 +110,8 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
||||||
s.OnTakeback(sub, msg)
|
s.OnTakeback(sub, msg)
|
||||||
case ActionReact:
|
case ActionReact:
|
||||||
s.OnReact(sub, msg)
|
s.OnReact(sub, msg)
|
||||||
|
case ActionReport:
|
||||||
|
s.OnReport(sub, msg)
|
||||||
default:
|
default:
|
||||||
sub.ChatServer("Unsupported message type.")
|
sub.ChatServer("Unsupported message type.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,12 @@ body {
|
||||||
color: #ff9999 !important;
|
color: #ff9999 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Max height for message in report modal */
|
||||||
|
.report-modal-message {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/************************
|
/************************
|
||||||
* Main CSS Grid Layout *
|
* Main CSS Grid Layout *
|
||||||
************************/
|
************************/
|
||||||
|
|
|
@ -49,6 +49,7 @@ const app = Vue.createApp({
|
||||||
channels: PublicChannels,
|
channels: PublicChannels,
|
||||||
website: WebsiteURL,
|
website: WebsiteURL,
|
||||||
permitNSFW: PermitNSFW,
|
permitNSFW: PermitNSFW,
|
||||||
|
webhookURLs: WebhookURLs,
|
||||||
fontSizeClasses: [
|
fontSizeClasses: [
|
||||||
[ "x-2", "Very small chat room text" ],
|
[ "x-2", "Very small chat room text" ],
|
||||||
[ "x-1", "50% smaller chat room text" ],
|
[ "x-1", "50% smaller chat room text" ],
|
||||||
|
@ -58,6 +59,14 @@ const app = Vue.createApp({
|
||||||
[ "x3", "3x larger chat room text" ],
|
[ "x3", "3x larger chat room text" ],
|
||||||
[ "x4", "4x 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: {
|
sounds: {
|
||||||
available: SoundEffects,
|
available: SoundEffects,
|
||||||
settings: DefaultSounds,
|
settings: DefaultSounds,
|
||||||
|
@ -230,6 +239,15 @@ const app = Vue.createApp({
|
||||||
dontShowAgain: false,
|
dontShowAgain: false,
|
||||||
user: null, // staged User we wanted to open
|
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() {
|
mounted() {
|
||||||
|
@ -1884,6 +1902,7 @@ const app = Vue.createApp({
|
||||||
this.channels[channel].updated = new Date().getTime();
|
this.channels[channel].updated = new Date().getTime();
|
||||||
this.channels[channel].history.push({
|
this.channels[channel].history.push({
|
||||||
action: action,
|
action: action,
|
||||||
|
channel: channel,
|
||||||
username: username,
|
username: username,
|
||||||
message: message,
|
message: message,
|
||||||
msgID: messageID,
|
msgID: messageID,
|
||||||
|
@ -1984,6 +2003,7 @@ const app = Vue.createApp({
|
||||||
|
|
||||||
// Format a datetime nicely for chat timestamp.
|
// Format a datetime nicely for chat timestamp.
|
||||||
prettyDate(date) {
|
prettyDate(date) {
|
||||||
|
if (date == undefined) return '';
|
||||||
let hours = date.getHours(),
|
let hours = date.getHours(),
|
||||||
minutes = String(date.getMinutes()).padStart(2, '0'),
|
minutes = String(date.getMinutes()).padStart(2, '0'),
|
||||||
ampm = hours >= 11 ? "pm" : "am";
|
ampm = hours >= 11 ? "pm" : "am";
|
||||||
|
@ -2157,8 +2177,62 @@ const app = Vue.createApp({
|
||||||
if (this.status === "online") {
|
if (this.status === "online") {
|
||||||
this.status = "idle";
|
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(/<img .+?>/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;
|
||||||
|
},
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.mount("#BareRTC-App");
|
app.mount("#BareRTC-App");
|
||||||
|
|
|
@ -458,6 +458,87 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Report Modal -->
|
||||||
|
<div class="modal" :class="{'is-active': reportModal.visible}">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header has-background-warning">
|
||||||
|
<p class="card-header-title has-text-dark">Report a message</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<!-- Message preview we are reporting on
|
||||||
|
TODO: make it DRY: style copied/referenced from chat history cards -->
|
||||||
|
<div class="box mb-2 px-4 pt-3 pb-1 position-relative">
|
||||||
|
<div class="media mb-0">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img v-if="avatarForUsername(reportModal.message.username)"
|
||||||
|
:src="avatarForUsername(reportModal.message.username)">
|
||||||
|
<img v-else src="/static/img/shy.png">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
<!-- User nickname/display name -->
|
||||||
|
[[nicknameForUsername(reportModal.message.username)]]
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User @username below it which may link to a profile URL if JWT -->
|
||||||
|
<div>
|
||||||
|
<small class="has-text-grey">
|
||||||
|
@[[reportModal.message.username]]
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message copy -->
|
||||||
|
<div class="content pl-5 py-3 mb-5 report-modal-message"
|
||||||
|
v-html="reportModal.message.message">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field mb-1">
|
||||||
|
<label class="label" for="classification">Report classification:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="classification"
|
||||||
|
v-model="reportModal.classification"
|
||||||
|
:disabled="busy">
|
||||||
|
<option v-for="i in config.reportClassifications"
|
||||||
|
:value="i">[[i]]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="reportComment">Comment:</label>
|
||||||
|
<textarea class="textarea"
|
||||||
|
v-model="reportModal.comment"
|
||||||
|
:disabled="busy"
|
||||||
|
cols="80" rows="2"
|
||||||
|
placeholder="Optional: describe the issue"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="control has-text-centered">
|
||||||
|
<button type="button"
|
||||||
|
class="button is-link mr-4"
|
||||||
|
:disabled="busy"
|
||||||
|
@click="doReport()">Submit report</button>
|
||||||
|
<button type="button"
|
||||||
|
class="button"
|
||||||
|
@click="reportModal.visible=false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Photo Detail Modal -->
|
<!-- Photo Detail Modal -->
|
||||||
<div class="modal" id="photo-modal">
|
<div class="modal" id="photo-modal">
|
||||||
<div class="modal-background" onclick="document.querySelector('#photo-modal').classList.remove('is-active')"></div>
|
<div class="modal-background" onclick="document.querySelector('#photo-modal').classList.remove('is-active')"></div>
|
||||||
|
@ -947,8 +1028,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Report & Emoji buttons -->
|
||||||
|
<div v-if="msg.msgID" class="emoji-button columns is-mobile is-gapless mb-0">
|
||||||
|
<!-- Report message button -->
|
||||||
|
<div class="column" v-if="isWebhookEnabled('report')">
|
||||||
|
<button class="button is-small is-outlined mr-1"
|
||||||
|
:class="{'is-danger': !msg.reported,
|
||||||
|
'has-text-grey': msg.reported}"
|
||||||
|
title="Report this message"
|
||||||
|
@click="reportMessage(msg)">
|
||||||
|
<i class="fa fa-flag"></i>
|
||||||
|
<i class="fa fa-check ml-1" v-if="msg.reported"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Emoji reactions menu -->
|
<!-- Emoji reactions menu -->
|
||||||
<div v-if="msg.msgID" class="dropdown is-right emoji-button"
|
<div class="column dropdown"
|
||||||
:class="{'is-up': i >= 2}"
|
:class="{'is-up': i >= 2}"
|
||||||
onclick="this.classList.toggle('is-active')">
|
onclick="this.classList.toggle('is-active')">
|
||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
|
@ -978,9 +1073,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Message box -->
|
<!-- Message box -->
|
||||||
<div class="content pl-5 py-3 mb-0">
|
<div class="content pl-5 py-3 mb-5">
|
||||||
<em v-if="msg.action === 'presence'">[[msg.message]]</em>
|
<em v-if="msg.action === 'presence'">[[msg.message]]</em>
|
||||||
<div v-else v-html="msg.message"></div>
|
<div v-else v-html="msg.message"></div>
|
||||||
|
|
||||||
|
@ -1252,6 +1348,7 @@ const PublicChannels = {{.Config.GetChannels}};
|
||||||
const WebsiteURL = "{{.Config.WebsiteURL}}";
|
const WebsiteURL = "{{.Config.WebsiteURL}}";
|
||||||
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
||||||
const TURN = {{.Config.TURN}};
|
const TURN = {{.Config.TURN}};
|
||||||
|
const WebhookURLs = {{.Config.WebhookURLs}};
|
||||||
const UserJWTToken = {{.JWTTokenString}};
|
const UserJWTToken = {{.JWTTokenString}};
|
||||||
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
|
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
|
||||||
const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user