2023-01-27 04:34:58 +00:00
|
|
|
package barertc
|
|
|
|
|
|
|
|
import (
|
2023-03-22 04:29:24 +00:00
|
|
|
"encoding/base64"
|
2023-01-27 04:34:58 +00:00
|
|
|
"fmt"
|
2023-03-22 04:29:24 +00:00
|
|
|
"path/filepath"
|
2023-02-05 08:53:50 +00:00
|
|
|
"strings"
|
2023-08-07 04:06:27 +00:00
|
|
|
"time"
|
2023-01-27 04:34:58 +00:00
|
|
|
|
2023-02-06 01:42:09 +00:00
|
|
|
"git.kirsle.net/apps/barertc/pkg/config"
|
|
|
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
2023-01-27 04:34:58 +00:00
|
|
|
"git.kirsle.net/apps/barertc/pkg/log"
|
2023-08-14 02:21:27 +00:00
|
|
|
"git.kirsle.net/apps/barertc/pkg/messages"
|
2024-03-29 06:20:09 +00:00
|
|
|
"git.kirsle.net/apps/barertc/pkg/models"
|
2023-02-05 08:53:50 +00:00
|
|
|
"git.kirsle.net/apps/barertc/pkg/util"
|
2023-01-27 04:34:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// OnLogin handles "login" actions from the client.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnLogin(sub *Subscriber, msg messages.Message) {
|
2023-02-06 01:42:09 +00:00
|
|
|
// Using a JWT token for authentication?
|
|
|
|
var claims = &jwt.Claims{}
|
|
|
|
if msg.JWTToken != "" || (config.Current.JWT.Enabled && config.Current.JWT.Strict) {
|
|
|
|
parsed, ok, err := jwt.ParseAndValidate(msg.JWTToken)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Error parsing JWT token in WebSocket login: %s", err)
|
|
|
|
sub.ChatServer("Your authentication has expired. Please go back and launch the chat room again.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sanity check the username.
|
|
|
|
if msg.Username != parsed.Subject {
|
|
|
|
log.Error("JWT login had a different username: %s vs %s", parsed.Subject, msg.Username)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Strict enforcement?
|
|
|
|
if config.Current.JWT.Strict && !ok {
|
|
|
|
log.Error("JWT enforcement is strict and user did not pass JWT checks")
|
|
|
|
sub.ChatServer("Server side authentication is required. Please go back and launch the chat room from your logged-in account.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
claims = parsed
|
|
|
|
msg.Username = claims.Subject
|
|
|
|
sub.JWTClaims = claims
|
|
|
|
}
|
|
|
|
|
2023-02-09 04:01:06 +00:00
|
|
|
if claims.Subject != "" {
|
|
|
|
log.Debug("JWT claims: %+v", claims)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Somehow no username?
|
|
|
|
if msg.Username == "" {
|
|
|
|
msg.Username = "anonymous"
|
|
|
|
}
|
2023-02-06 01:42:09 +00:00
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// Ensure the username is unique, or rename it.
|
2023-07-18 03:38:07 +00:00
|
|
|
username, err := s.UniqueUsername(msg.Username)
|
|
|
|
if err != nil {
|
|
|
|
// If JWT authentication was used: disconnect the original (conflicting) username.
|
|
|
|
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.")
|
2023-08-14 02:21:27 +00:00
|
|
|
other.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionKick,
|
2023-07-18 03:38:07 +00:00
|
|
|
})
|
2023-10-24 02:05:02 +00:00
|
|
|
other.authenticated = false
|
|
|
|
other.Username = ""
|
2023-07-18 03:38:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// They will take over their original username.
|
|
|
|
username = msg.Username
|
|
|
|
}
|
|
|
|
|
|
|
|
// If JWT auth was not used: UniqueUsername already gave them a uniquely spelled name.
|
|
|
|
}
|
|
|
|
msg.Username = username
|
2023-01-27 04:34:58 +00:00
|
|
|
|
2023-08-05 03:31:21 +00:00
|
|
|
// Is the username currently banned?
|
|
|
|
if IsBanned(msg.Username) {
|
|
|
|
sub.ChatServer(
|
|
|
|
"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.",
|
|
|
|
)
|
2023-08-14 02:21:27 +00:00
|
|
|
sub.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionKick,
|
2023-08-05 03:31:21 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// Use their username.
|
|
|
|
sub.Username = msg.Username
|
2023-02-06 01:42:09 +00:00
|
|
|
sub.authenticated = true
|
2023-08-29 00:49:50 +00:00
|
|
|
sub.DND = msg.DND
|
2023-08-07 04:06:27 +00:00
|
|
|
sub.loginAt = time.Now()
|
2023-01-27 04:34:58 +00:00
|
|
|
log.Debug("OnLogin: %s joins the room", sub.Username)
|
|
|
|
|
|
|
|
// Tell everyone they joined.
|
2023-08-14 02:21:27 +00:00
|
|
|
s.Broadcast(messages.Message{
|
|
|
|
Action: messages.ActionPresence,
|
2023-01-27 04:34:58 +00:00
|
|
|
Username: msg.Username,
|
2024-03-15 06:04:24 +00:00
|
|
|
Message: messages.PresenceJoined,
|
2023-01-27 04:34:58 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// Send the user back their settings.
|
|
|
|
sub.SendMe()
|
|
|
|
|
|
|
|
// Send the WhoList to everybody.
|
|
|
|
s.SendWhoList()
|
2023-02-06 01:42:09 +00:00
|
|
|
|
|
|
|
// Send the initial ChatServer messages to the public channels.
|
|
|
|
for _, channel := range config.Current.PublicChannels {
|
|
|
|
for _, msg := range channel.WelcomeMessages {
|
2023-08-14 02:21:27 +00:00
|
|
|
sub.SendJSON(messages.Message{
|
2023-02-06 01:42:09 +00:00
|
|
|
Channel: channel.ID,
|
2023-08-14 02:21:27 +00:00
|
|
|
Action: messages.ActionError,
|
2023-02-06 01:42:09 +00:00
|
|
|
Username: "ChatServer",
|
|
|
|
Message: RenderMarkdown(msg),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2023-01-27 04:34:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// OnMessage handles a chat message posted by the user.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) {
|
2023-07-28 05:29:56 +00:00
|
|
|
if !strings.HasPrefix(msg.Channel, "@") {
|
|
|
|
log.Info("[%s to #%s] %s", sub.Username, msg.Channel, msg.Message)
|
|
|
|
}
|
|
|
|
|
2024-09-10 04:08:31 +00:00
|
|
|
if sub.Username == "" || !sub.authenticated {
|
2023-01-27 06:54:02 +00:00
|
|
|
sub.ChatServer("You must log in first.")
|
2023-01-27 04:34:58 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-11 06:46:39 +00:00
|
|
|
// Process commands.
|
|
|
|
if handled := s.ProcessCommand(sub, msg); handled {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-06 01:42:09 +00:00
|
|
|
// Translate their message as Markdown syntax.
|
|
|
|
markdown := RenderMarkdown(msg.Message)
|
|
|
|
if markdown == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-25 05:47:58 +00:00
|
|
|
// Detect and expand media such as YouTube videos.
|
|
|
|
markdown = s.ExpandMedia(markdown)
|
|
|
|
|
2023-06-24 20:08:15 +00:00
|
|
|
// Assign a message ID and own it to the sender.
|
|
|
|
sub.midMu.Lock()
|
2023-08-29 00:49:50 +00:00
|
|
|
var mid = messages.NextMessageID()
|
2023-06-24 20:08:15 +00:00
|
|
|
sub.messageIDs[mid] = struct{}{}
|
|
|
|
sub.midMu.Unlock()
|
|
|
|
|
2023-02-05 08:53:50 +00:00
|
|
|
// Message to be echoed to the channel.
|
2023-08-14 02:21:27 +00:00
|
|
|
var message = messages.Message{
|
|
|
|
Action: messages.ActionMessage,
|
2023-06-24 20:08:15 +00:00
|
|
|
Channel: msg.Channel,
|
|
|
|
Username: sub.Username,
|
|
|
|
Message: markdown,
|
|
|
|
MessageID: mid,
|
2023-02-05 08:53:50 +00:00
|
|
|
}
|
|
|
|
|
2023-09-30 02:10:34 +00:00
|
|
|
// Run message filters.
|
|
|
|
if filter, ok := s.filterMessage(sub, msg, &message); ok {
|
|
|
|
// What do we do with the matched filter?
|
|
|
|
|
|
|
|
// If we will not send this message out, do echo it back to
|
|
|
|
// the sender (possibly with censors applied).
|
|
|
|
if !filter.ForwardMessage {
|
|
|
|
s.SendTo(sub.Username, message)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Is ChatServer to say something?
|
|
|
|
if filter.ChatServerResponse != "" {
|
|
|
|
sub.ChatServer(filter.ChatServerResponse)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Are we to report the message to the site admin?
|
|
|
|
if filter.ReportMessage {
|
|
|
|
// If the user is OP, just tell them we would.
|
|
|
|
if sub.IsAdmin() {
|
|
|
|
sub.ChatServer("Your recent chat context would have been reported to your main website.")
|
2024-09-10 04:08:31 +00:00
|
|
|
} else if err := s.reportFilteredMessage(sub, msg); err != nil {
|
|
|
|
// Send the report to the main website.
|
2023-09-30 02:10:34 +00:00
|
|
|
log.Error("Reporting filtered message: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are not forwarding this message, stop here.
|
|
|
|
if !filter.ForwardMessage {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-05 08:53:50 +00:00
|
|
|
// Is this a DM?
|
|
|
|
if strings.HasPrefix(msg.Channel, "@") {
|
|
|
|
// Echo the message only to both parties.
|
|
|
|
s.SendTo(sub.Username, message)
|
|
|
|
message.Channel = "@" + sub.Username
|
2023-03-23 03:21:04 +00:00
|
|
|
|
2023-08-14 05:59:35 +00:00
|
|
|
// Don't deliver it if the receiver has muted us. Note: admin users, even if muted,
|
|
|
|
// can still deliver a DM to the one who muted them.
|
2023-03-23 03:21:04 +00:00
|
|
|
rcpt, err := s.GetSubscriber(strings.TrimPrefix(msg.Channel, "@"))
|
2024-09-10 04:08:31 +00:00
|
|
|
if err != nil {
|
2023-12-04 05:46:14 +00:00
|
|
|
// Recipient was no longer online: the message won't be sent.
|
|
|
|
sub.ChatServer("Could not deliver your message: %s appears not to be online.", msg.Channel)
|
|
|
|
return
|
2024-09-10 04:08:31 +00:00
|
|
|
} else if rcpt.Mutes(sub.Username) && !sub.IsAdmin() {
|
|
|
|
log.Debug("Do not send message to %s: they have muted or booted %s", rcpt.Username, sub.Username)
|
|
|
|
return
|
2023-03-23 03:21:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the sender already mutes the recipient, reply back with the error.
|
2024-09-10 04:08:31 +00:00
|
|
|
if sub.Mutes(rcpt.Username) && !sub.IsAdmin() {
|
2023-03-23 03:21:04 +00:00
|
|
|
sub.ChatServer("You have muted %s and so your message has not been sent.", rcpt.Username)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-04 20:36:12 +00:00
|
|
|
// If there is blocking happening, do not send.
|
|
|
|
if sub.Blocks(rcpt) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-11-11 22:59:49 +00:00
|
|
|
// Log this conversation?
|
2023-11-26 02:36:38 +00:00
|
|
|
if IsLoggingUsername(sub) && IsLoggingUsername(rcpt) {
|
|
|
|
// Both sides are logged, copy it to both logs.
|
|
|
|
LogMessage(sub, rcpt.Username, sub.Username, msg)
|
|
|
|
LogMessage(rcpt, sub.Username, sub.Username, msg)
|
|
|
|
} else if IsLoggingUsername(sub) {
|
2023-11-11 22:59:49 +00:00
|
|
|
// The sender of this message is being logged.
|
|
|
|
LogMessage(sub, rcpt.Username, sub.Username, msg)
|
|
|
|
} else if IsLoggingUsername(rcpt) {
|
|
|
|
// The recipient of this message is being logged.
|
|
|
|
LogMessage(rcpt, sub.Username, sub.Username, msg)
|
|
|
|
}
|
|
|
|
|
2024-03-29 06:20:09 +00:00
|
|
|
// Add it to the DM history SQLite database.
|
|
|
|
if err := (models.DirectMessage{}).LogMessage(sub.Username, rcpt.Username, message); err != nil && err != models.ErrNotInitialized {
|
|
|
|
log.Error("Logging DM history to SQLite: %s", err)
|
|
|
|
}
|
|
|
|
|
2023-02-06 21:27:29 +00:00
|
|
|
if err := s.SendTo(msg.Channel, message); err != nil {
|
|
|
|
sub.ChatServer("Your message could not be delivered: %s", err)
|
|
|
|
}
|
2023-02-05 08:53:50 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-11-11 22:59:49 +00:00
|
|
|
// Are we logging this public channel?
|
|
|
|
if IsLoggingChannel(msg.Channel) {
|
|
|
|
LogChannel(s, msg.Channel, sub.Username, msg)
|
|
|
|
}
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// Broadcast a chat message to the room.
|
2023-02-06 01:42:09 +00:00
|
|
|
s.Broadcast(message)
|
2023-01-27 04:34:58 +00:00
|
|
|
}
|
|
|
|
|
2023-06-24 20:08:15 +00:00
|
|
|
// OnTakeback handles takebacks (delete your message for everybody)
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnTakeback(sub *Subscriber, msg messages.Message) {
|
2024-03-29 06:20:09 +00:00
|
|
|
// In case we're in a DM thread, remove this message ID from the history table
|
|
|
|
// if the username matches.
|
|
|
|
wasRemovedFromHistory, err := (models.DirectMessage{}).TakebackMessage(sub.Username, msg.MessageID, sub.IsAdmin())
|
|
|
|
if err != nil && err != models.ErrNotInitialized {
|
|
|
|
log.Error("Error taking back DM history message (%s, %d): %s", sub.Username, msg.MessageID, err)
|
|
|
|
}
|
|
|
|
|
2023-06-24 20:08:15 +00:00
|
|
|
// Permission check.
|
|
|
|
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
|
|
|
|
sub.midMu.Lock()
|
2023-08-29 00:49:50 +00:00
|
|
|
_, ok := sub.messageIDs[msg.MessageID]
|
|
|
|
sub.midMu.Unlock()
|
2024-03-29 06:20:09 +00:00
|
|
|
|
2023-08-29 00:49:50 +00:00
|
|
|
if !ok {
|
2024-03-29 06:20:09 +00:00
|
|
|
// The messageID is not found in the current chat session, but did we remove
|
|
|
|
// it from past DM history for the correct current user?
|
|
|
|
if !wasRemovedFromHistory {
|
|
|
|
sub.ChatServer("That is not your message to take back.")
|
|
|
|
return
|
|
|
|
}
|
2023-06-24 20:08:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Broadcast to everybody to remove this message.
|
2023-08-14 02:21:27 +00:00
|
|
|
s.Broadcast(messages.Message{
|
|
|
|
Action: messages.ActionTakeback,
|
2023-06-24 20:08:15 +00:00
|
|
|
MessageID: msg.MessageID,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-01 03:00:21 +00:00
|
|
|
// OnReact handles emoji reactions for chat messages.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnReact(sub *Subscriber, msg messages.Message) {
|
2023-07-01 03:00:21 +00:00
|
|
|
// Forward the reaction to everybody.
|
2023-08-14 02:21:27 +00:00
|
|
|
s.Broadcast(messages.Message{
|
|
|
|
Action: messages.ActionReact,
|
2023-07-01 03:00:21 +00:00
|
|
|
Username: sub.Username,
|
|
|
|
Message: msg.Message,
|
|
|
|
MessageID: msg.MessageID,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-03-22 04:29:24 +00:00
|
|
|
// OnFile handles a picture shared in chat with a channel.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnFile(sub *Subscriber, msg messages.Message) {
|
2023-03-22 04:29:24 +00:00
|
|
|
if sub.Username == "" {
|
|
|
|
sub.ChatServer("You must log in first.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-09-19 05:16:33 +00:00
|
|
|
// Moderation rules?
|
|
|
|
if rule := sub.GetModerationRule(); rule != nil {
|
|
|
|
|
|
|
|
// Are they barred from watching cameras on chat?
|
|
|
|
if rule.NoImage {
|
2024-09-21 03:33:42 +00:00
|
|
|
sub.ChatServer(config.Current.Strings.ModRuleErrorNoImage)
|
2024-09-19 05:16:33 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-03-22 04:29:24 +00:00
|
|
|
// Detect image type and convert it into an <img src="data:"> tag.
|
|
|
|
var (
|
|
|
|
filename = msg.Message
|
|
|
|
ext = filepath.Ext(filename)
|
|
|
|
filetype string
|
|
|
|
)
|
|
|
|
switch strings.ToLower(ext) {
|
|
|
|
case ".jpg", ".jpeg":
|
|
|
|
filetype = "image/jpeg"
|
|
|
|
case ".gif":
|
|
|
|
filetype = "image/gif"
|
|
|
|
case ".png":
|
|
|
|
filetype = "image/png"
|
|
|
|
default:
|
|
|
|
sub.ChatServer("Unsupported image type, should be a jpeg, GIF or png.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Process the image: scale it down, strip metadata, etc.
|
|
|
|
img, pvWidth, pvHeight := ProcessImage(filetype, msg.Bytes)
|
|
|
|
var dataURL = fmt.Sprintf("data:%s;base64,%s", filetype, base64.StdEncoding.EncodeToString(img))
|
|
|
|
|
2023-06-24 20:08:15 +00:00
|
|
|
// Assign a message ID and own it to the sender.
|
|
|
|
sub.midMu.Lock()
|
2023-08-29 00:49:50 +00:00
|
|
|
var mid = messages.NextMessageID()
|
2023-06-24 20:08:15 +00:00
|
|
|
sub.messageIDs[mid] = struct{}{}
|
|
|
|
sub.midMu.Unlock()
|
|
|
|
|
2023-03-22 04:29:24 +00:00
|
|
|
// Message to be echoed to the channel.
|
2023-08-14 02:21:27 +00:00
|
|
|
var message = messages.Message{
|
|
|
|
Action: messages.ActionMessage,
|
2023-06-24 20:08:15 +00:00
|
|
|
Channel: msg.Channel,
|
|
|
|
Username: sub.Username,
|
|
|
|
MessageID: mid,
|
2023-03-22 04:29:24 +00:00
|
|
|
|
|
|
|
// Their image embedded via a data: URI - no server storage needed!
|
|
|
|
Message: fmt.Sprintf(
|
|
|
|
`<img src="%s" width="%d" height="%d" onclick="setModalImage(this.src)" style="cursor: pointer">`,
|
|
|
|
dataURL,
|
|
|
|
pvWidth, pvHeight,
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Is this a DM?
|
|
|
|
if strings.HasPrefix(msg.Channel, "@") {
|
|
|
|
// Echo the message only to both parties.
|
|
|
|
s.SendTo(sub.Username, message)
|
|
|
|
message.Channel = "@" + sub.Username
|
2023-03-23 03:21:04 +00:00
|
|
|
|
|
|
|
// Don't deliver it if the receiver has muted us.
|
|
|
|
rcpt, err := s.GetSubscriber(strings.TrimPrefix(msg.Channel, "@"))
|
|
|
|
if err == nil && rcpt.Mutes(sub.Username) {
|
|
|
|
log.Debug("Do not send message to %s: they have muted or booted %s", rcpt.Username, sub.Username)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the sender already mutes the recipient, reply back with the error.
|
|
|
|
if sub.Mutes(rcpt.Username) {
|
|
|
|
sub.ChatServer("You have muted %s and so your message has not been sent.", rcpt.Username)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-04 20:36:12 +00:00
|
|
|
// If there is blocking happening, do not send.
|
|
|
|
if sub.Blocks(rcpt) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-22 04:29:24 +00:00
|
|
|
if err := s.SendTo(msg.Channel, message); err != nil {
|
|
|
|
sub.ChatServer("Your message could not be delivered: %s", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Broadcast a chat message to the room.
|
|
|
|
s.Broadcast(message)
|
|
|
|
}
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// OnMe handles current user state updates.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnMe(sub *Subscriber, msg messages.Message) {
|
2024-05-17 06:33:19 +00:00
|
|
|
// Reflect a 'me' message back at them? (e.g. if server forces their camera NSFW)
|
|
|
|
var reflect bool
|
|
|
|
|
2023-08-14 02:21:27 +00:00
|
|
|
if msg.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive {
|
2023-01-27 04:34:58 +00:00
|
|
|
log.Debug("User %s turns on their video feed", sub.Username)
|
2024-05-17 06:33:19 +00:00
|
|
|
|
|
|
|
// Moderation rules?
|
2024-09-19 05:16:33 +00:00
|
|
|
if rule := sub.GetModerationRule(); rule != nil {
|
2024-05-17 06:33:19 +00:00
|
|
|
|
|
|
|
// Are they barred from sharing their camera on chat?
|
2024-09-19 05:16:33 +00:00
|
|
|
if rule.NoBroadcast || rule.NoVideo {
|
2024-05-17 06:33:19 +00:00
|
|
|
sub.SendCut()
|
2024-09-21 03:33:42 +00:00
|
|
|
sub.ChatServer(config.Current.Strings.ModRuleErrorNoBroadcast)
|
2024-05-17 06:33:19 +00:00
|
|
|
msg.VideoStatus = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// Is their camera forced to always be explicit?
|
|
|
|
if rule.CameraAlwaysNSFW && !(msg.VideoStatus&messages.VideoFlagNSFW == messages.VideoFlagNSFW) {
|
|
|
|
msg.VideoStatus |= messages.VideoFlagNSFW
|
|
|
|
reflect = true // send them a 'me' echo afterward to inform the front-end page properly of this
|
2024-09-21 03:33:42 +00:00
|
|
|
sub.ChatServer(config.Current.Strings.ModRuleErrorCameraAlwaysNSFW)
|
2024-05-17 06:33:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2023-03-29 01:34:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Hidden status: for operators only, + fake a join/exit chat message.
|
|
|
|
if sub.JWTClaims != nil && sub.JWTClaims.IsAdmin {
|
|
|
|
if sub.ChatStatus != "hidden" && msg.ChatStatus == "hidden" {
|
|
|
|
// Going hidden - fake leave message
|
2023-08-14 02:21:27 +00:00
|
|
|
s.Broadcast(messages.Message{
|
|
|
|
Action: messages.ActionPresence,
|
2023-03-29 01:34:50 +00:00
|
|
|
Username: sub.Username,
|
2024-03-15 06:04:24 +00:00
|
|
|
Message: messages.PresenceExited,
|
2023-03-29 01:34:50 +00:00
|
|
|
})
|
|
|
|
} else if sub.ChatStatus == "hidden" && msg.ChatStatus != "hidden" {
|
|
|
|
// Leaving hidden - fake join message
|
2023-08-14 02:21:27 +00:00
|
|
|
s.Broadcast(messages.Message{
|
|
|
|
Action: messages.ActionPresence,
|
2023-03-29 01:34:50 +00:00
|
|
|
Username: sub.Username,
|
2024-03-15 06:04:24 +00:00
|
|
|
Message: messages.PresenceJoined,
|
2023-03-29 01:34:50 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
} else if msg.ChatStatus == "hidden" {
|
|
|
|
// normal users can not set this status
|
|
|
|
msg.ChatStatus = "away"
|
2023-01-27 04:34:58 +00:00
|
|
|
}
|
|
|
|
|
2023-07-01 01:41:06 +00:00
|
|
|
sub.VideoStatus = msg.VideoStatus
|
2023-03-28 04:13:04 +00:00
|
|
|
sub.ChatStatus = msg.ChatStatus
|
2023-08-29 00:49:50 +00:00
|
|
|
sub.DND = msg.DND
|
2023-01-27 04:34:58 +00:00
|
|
|
|
|
|
|
// Sync the WhoList to everybody.
|
|
|
|
s.SendWhoList()
|
2024-05-17 06:33:19 +00:00
|
|
|
|
|
|
|
// Reflect a 'me' message back?
|
|
|
|
if reflect {
|
|
|
|
sub.SendMe()
|
|
|
|
}
|
2023-01-27 04:34:58 +00:00
|
|
|
}
|
2023-01-27 06:54:02 +00:00
|
|
|
|
|
|
|
// OnOpen is a client wanting to start WebRTC with another, e.g. to see their camera.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnOpen(sub *Subscriber, msg messages.Message) {
|
2024-09-19 05:16:33 +00:00
|
|
|
// Moderation rules?
|
|
|
|
if rule := sub.GetModerationRule(); rule != nil {
|
|
|
|
|
|
|
|
// Are they barred from watching cameras on chat?
|
|
|
|
if rule.NoVideo {
|
2024-09-21 03:33:42 +00:00
|
|
|
sub.ChatServer(config.Current.Strings.ModRuleErrorNoVideo)
|
2024-09-19 05:16:33 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
// Look up the other subscriber.
|
|
|
|
other, err := s.GetSubscriber(msg.Username)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-12-10 23:30:25 +00:00
|
|
|
// Enforce whether the viewer has permission to see this camera.
|
|
|
|
if ok, reason := s.IsVideoNotAllowed(sub, other); !ok {
|
|
|
|
sub.ChatServer(
|
|
|
|
"Could not open that video: %s", reason,
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
// Make up a WebRTC shared secret and send it to both of them.
|
2023-02-05 08:53:50 +00:00
|
|
|
secret := util.RandomString(16)
|
2023-01-27 06:54:02 +00:00
|
|
|
log.Info("WebRTC: %s opens %s with secret %s", sub.Username, other.Username, secret)
|
|
|
|
|
2023-08-05 02:24:42 +00:00
|
|
|
// If the current user is an admin and was booted or muted, inform them.
|
|
|
|
if sub.IsAdmin() {
|
|
|
|
if other.Boots(sub.Username) {
|
|
|
|
sub.ChatServer("Note: %s had booted you off their camera before, and won't be notified of your watch.", other.Username)
|
|
|
|
} else if other.Mutes(sub.Username) {
|
|
|
|
sub.ChatServer("Note: %s had muted you before, and won't be notified of your watch.", other.Username)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
// Ring the target of this request and give them the secret.
|
2023-08-14 02:21:27 +00:00
|
|
|
other.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionRing,
|
2023-01-27 06:54:02 +00:00
|
|
|
Username: sub.Username,
|
|
|
|
OpenSecret: secret,
|
|
|
|
})
|
|
|
|
|
|
|
|
// To the caller, echo back the Open along with the secret.
|
2023-08-14 02:21:27 +00:00
|
|
|
sub.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionOpen,
|
2023-01-27 06:54:02 +00:00
|
|
|
Username: other.Username,
|
|
|
|
OpenSecret: secret,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-12-10 23:30:25 +00:00
|
|
|
// IsVideoNotAllowed verifies whether a viewer can open a broadcaster's camera.
|
|
|
|
//
|
|
|
|
// Returns a boolean and an error message to return if false.
|
|
|
|
func (s *Server) IsVideoNotAllowed(sub *Subscriber, other *Subscriber) (bool, string) {
|
|
|
|
var (
|
|
|
|
ourVideoActive = (sub.VideoStatus & messages.VideoFlagActive) == messages.VideoFlagActive
|
|
|
|
theirVideoActive = (other.VideoStatus & messages.VideoFlagActive) == messages.VideoFlagActive
|
|
|
|
theirMutualRequired = (other.VideoStatus & messages.VideoFlagMutualRequired) == messages.VideoFlagMutualRequired
|
|
|
|
theirVIPRequired = (other.VideoStatus & messages.VideoFlagOnlyVIP) == messages.VideoFlagOnlyVIP
|
|
|
|
)
|
|
|
|
|
|
|
|
// Conditions in which we can not watch their video.
|
|
|
|
var conditions = []struct {
|
|
|
|
If bool
|
|
|
|
Error string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
If: !theirVideoActive,
|
|
|
|
Error: "Their video is not currently enabled.",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
If: theirMutualRequired && !ourVideoActive,
|
|
|
|
Error: fmt.Sprintf("%s has requested that you should share your own camera too before opening theirs.", other.Username),
|
|
|
|
},
|
|
|
|
{
|
2024-05-17 06:33:19 +00:00
|
|
|
If: theirVIPRequired && !sub.IsVIP() && !sub.IsAdmin(),
|
2023-12-10 23:30:25 +00:00
|
|
|
Error: "You do not have permission to view that camera.",
|
|
|
|
},
|
|
|
|
{
|
2024-05-17 06:33:19 +00:00
|
|
|
If: (other.Mutes(sub.Username) || other.Blocks(sub)) && !sub.IsAdmin(),
|
2023-12-10 23:30:25 +00:00
|
|
|
Error: "You do not have permission to view that camera.",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, c := range conditions {
|
|
|
|
if c.If {
|
|
|
|
return false, c.Error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, ""
|
|
|
|
}
|
|
|
|
|
2023-03-23 03:21:04 +00:00
|
|
|
// OnBoot is a user kicking you off their video stream.
|
2023-10-07 20:22:41 +00:00
|
|
|
func (s *Server) OnBoot(sub *Subscriber, msg messages.Message, boot bool) {
|
2023-03-23 03:21:04 +00:00
|
|
|
sub.muteMu.Lock()
|
|
|
|
|
2023-10-07 20:22:41 +00:00
|
|
|
if boot {
|
|
|
|
log.Info("%s boots %s off their camera", sub.Username, msg.Username)
|
|
|
|
sub.booted[msg.Username] = struct{}{}
|
|
|
|
|
|
|
|
// If the subject of the boot is an admin, inform them they have been booted.
|
|
|
|
if other, err := s.GetSubscriber(msg.Username); err == nil && other.IsAdmin() {
|
|
|
|
other.ChatServer(
|
|
|
|
"%s has booted you off of their camera!",
|
|
|
|
sub.Username,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.Info("%s unboots %s from their camera", sub.Username, msg.Username)
|
|
|
|
delete(sub.booted, msg.Username)
|
2023-08-05 02:24:42 +00:00
|
|
|
}
|
|
|
|
|
2023-10-07 20:22:41 +00:00
|
|
|
sub.muteMu.Unlock()
|
|
|
|
|
2023-03-23 03:21:04 +00:00
|
|
|
s.SendWhoList()
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnMute is a user kicking setting the mute flag for another user.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnMute(sub *Subscriber, msg messages.Message, mute bool) {
|
2023-03-23 03:21:04 +00:00
|
|
|
log.Info("%s mutes or unmutes %s: %v", sub.Username, msg.Username, mute)
|
|
|
|
|
|
|
|
sub.muteMu.Lock()
|
|
|
|
|
|
|
|
if mute {
|
|
|
|
sub.muted[msg.Username] = struct{}{}
|
|
|
|
} else {
|
|
|
|
delete(sub.muted, msg.Username)
|
|
|
|
}
|
|
|
|
|
|
|
|
sub.muteMu.Unlock()
|
|
|
|
|
2023-08-05 02:24:42 +00:00
|
|
|
// If the subject of the mute is an admin, inform them they have been booted.
|
|
|
|
if other, err := s.GetSubscriber(msg.Username); err == nil && other.IsAdmin() {
|
|
|
|
other.ChatServer(
|
|
|
|
"%s has muted you! Your new mute status is: %v",
|
|
|
|
sub.Username, mute,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-03-23 03:21:04 +00:00
|
|
|
// Send the Who List in case our cam will show as disabled to the muted party.
|
|
|
|
s.SendWhoList()
|
|
|
|
}
|
|
|
|
|
2023-09-04 20:36:12 +00:00
|
|
|
// OnBlock is a user placing a hard block (hide from) another user.
|
|
|
|
func (s *Server) OnBlock(sub *Subscriber, msg messages.Message) {
|
2024-06-13 05:49:30 +00:00
|
|
|
log.Info("%s blocks %s", sub.Username, msg.Username)
|
2023-09-04 20:36:12 +00:00
|
|
|
|
|
|
|
// If the subject of the block is an admin, return an error.
|
|
|
|
if other, err := s.GetSubscriber(msg.Username); err == nil && other.IsAdmin() {
|
|
|
|
sub.ChatServer(
|
|
|
|
"You are not allowed to block a chat operator.",
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
sub.muteMu.Lock()
|
|
|
|
sub.blocked[msg.Username] = struct{}{}
|
|
|
|
sub.muteMu.Unlock()
|
|
|
|
|
|
|
|
// Send the Who List so the blocker/blockee can disappear from each other's list.
|
|
|
|
s.SendWhoList()
|
|
|
|
}
|
|
|
|
|
2023-07-30 17:32:08 +00:00
|
|
|
// OnBlocklist is a bulk user mute from the CachedBlocklist sent by the website.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnBlocklist(sub *Subscriber, msg messages.Message) {
|
2023-09-30 19:32:09 +00:00
|
|
|
log.Info("[%s] syncs their blocklist: %s", sub.Username, msg.Usernames)
|
2023-07-30 17:32:08 +00:00
|
|
|
|
|
|
|
sub.muteMu.Lock()
|
|
|
|
for _, username := range msg.Usernames {
|
2023-09-08 02:43:03 +00:00
|
|
|
sub.muted[username] = struct{}{}
|
2023-09-04 20:36:12 +00:00
|
|
|
sub.blocked[username] = struct{}{}
|
2023-07-30 17:32:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sub.muteMu.Unlock()
|
|
|
|
|
|
|
|
// Send the Who List in case our cam will show as disabled to the muted party.
|
|
|
|
s.SendWhoList()
|
|
|
|
}
|
|
|
|
|
2023-08-13 04:35:41 +00:00
|
|
|
// OnReport handles a user's report of a message.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnReport(sub *Subscriber, msg messages.Message) {
|
2023-08-13 04:35:41 +00:00
|
|
|
if !WebhookEnabled(WebhookReport) {
|
|
|
|
sub.ChatServer("Unfortunately, the report webhook is not enabled so your report could not be received!")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-20 23:17:02 +00:00
|
|
|
// Attach recent message context to DMs.
|
|
|
|
if strings.HasPrefix(msg.Channel, "@") {
|
|
|
|
context := getDirectMessageContext(sub.Username, msg.Username)
|
|
|
|
msg.Message += "\n\nRecent message context:\n\n" + context
|
|
|
|
}
|
|
|
|
|
2023-08-13 04:35:41 +00:00
|
|
|
// Post to the report webhook.
|
2023-10-07 20:22:41 +00:00
|
|
|
if _, err := PostWebhook(WebhookReport, WebhookRequest{
|
2023-08-13 04:35:41 +00:00
|
|
|
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.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
// OnCandidate handles WebRTC candidate signaling.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnCandidate(sub *Subscriber, msg messages.Message) {
|
2023-01-27 06:54:02 +00:00
|
|
|
// Look up the other subscriber.
|
|
|
|
other, err := s.GetSubscriber(msg.Username)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-14 02:21:27 +00:00
|
|
|
other.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionCandidate,
|
2023-01-27 06:54:02 +00:00
|
|
|
Username: sub.Username,
|
|
|
|
Candidate: msg.Candidate,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnSDP handles WebRTC sdp signaling.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnSDP(sub *Subscriber, msg messages.Message) {
|
2023-01-27 06:54:02 +00:00
|
|
|
// Look up the other subscriber.
|
|
|
|
other, err := s.GetSubscriber(msg.Username)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-14 02:21:27 +00:00
|
|
|
other.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionSDP,
|
2023-01-27 06:54:02 +00:00
|
|
|
Username: sub.Username,
|
|
|
|
Description: msg.Description,
|
|
|
|
})
|
|
|
|
}
|
2023-02-06 04:26:00 +00:00
|
|
|
|
|
|
|
// OnWatch communicates video watching status between users.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnWatch(sub *Subscriber, msg messages.Message) {
|
2023-02-06 04:26:00 +00:00
|
|
|
// Look up the other subscriber.
|
|
|
|
other, err := s.GetSubscriber(msg.Username)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-14 02:21:27 +00:00
|
|
|
other.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionWatch,
|
2023-02-06 04:26:00 +00:00
|
|
|
Username: sub.Username,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnUnwatch communicates video Unwatching status between users.
|
2023-08-14 02:21:27 +00:00
|
|
|
func (s *Server) OnUnwatch(sub *Subscriber, msg messages.Message) {
|
2023-02-06 04:26:00 +00:00
|
|
|
// Look up the other subscriber.
|
|
|
|
other, err := s.GetSubscriber(msg.Username)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-14 02:21:27 +00:00
|
|
|
other.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionUnwatch,
|
2023-02-06 04:26:00 +00:00
|
|
|
Username: sub.Username,
|
|
|
|
})
|
|
|
|
}
|