BareRTC/pkg/handlers.go

553 lines
15 KiB
Go
Raw Normal View History

package barertc
import (
2023-03-22 04:29:24 +00:00
"encoding/base64"
"fmt"
2023-03-22 04:29:24 +00:00
"path/filepath"
"strings"
2023-08-07 04:06:27 +00:00
"time"
"git.kirsle.net/apps/barertc/pkg/config"
"git.kirsle.net/apps/barertc/pkg/jwt"
"git.kirsle.net/apps/barertc/pkg/log"
2023-08-14 02:21:27 +00:00
"git.kirsle.net/apps/barertc/pkg/messages"
"git.kirsle.net/apps/barertc/pkg/util"
)
// OnLogin handles "login" actions from the client.
2023-08-14 02:21:27 +00:00
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) {
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
}
if claims.Subject != "" {
log.Debug("JWT claims: %+v", claims)
}
// Somehow no username?
if msg.Username == "" {
msg.Username = "anonymous"
}
// Ensure the username is unique, or rename it.
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,
})
s.DeleteSubscriber(other)
}
// 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-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
})
s.DeleteSubscriber(sub)
return
}
// Use their username.
sub.Username = msg.Username
sub.authenticated = true
sub.DND = msg.DND
2023-08-07 04:06:27 +00:00
sub.loginAt = time.Now()
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,
Username: msg.Username,
Message: "has joined the room!",
})
// Send the user back their settings.
sub.SendMe()
// Send the WhoList to everybody.
s.SendWhoList()
// 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{
Channel: channel.ID,
2023-08-14 02:21:27 +00:00
Action: messages.ActionError,
Username: "ChatServer",
Message: RenderMarkdown(msg),
})
}
}
}
// 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)
}
if sub.Username == "" {
sub.ChatServer("You must log in first.")
return
}
2023-02-11 06:46:39 +00:00
// Process commands.
if handled := s.ProcessCommand(sub, msg); handled {
return
}
// 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()
var mid = messages.NextMessageID()
2023-06-24 20:08:15 +00:00
sub.messageIDs[mid] = struct{}{}
sub.midMu.Unlock()
// 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,
}
// 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-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.
rcpt, err := s.GetSubscriber(strings.TrimPrefix(msg.Channel, "@"))
2023-08-14 05:59:35 +00:00
if err == nil && 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
}
// If the sender already mutes the recipient, reply back with the error.
2023-03-29 01:09:13 +00:00
if err == nil && 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-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)
}
return
}
// Broadcast a chat message to the room.
s.Broadcast(message)
}
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) {
2023-06-24 20:08:15 +00:00
// Permission check.
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
sub.midMu.Lock()
_, ok := sub.messageIDs[msg.MessageID]
sub.midMu.Unlock()
if !ok {
2023-06-24 20:08:15 +00:00
sub.ChatServer("That is not your message to take back.")
return
}
}
// 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
}
// 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()
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
// 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)
}
// OnMe handles current user state updates.
2023-08-14 02:21:27 +00:00
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)
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,
Message: "has exited the room!",
})
} 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,
Message: "has joined the room!",
})
}
} else if msg.ChatStatus == "hidden" {
// normal users can not set this status
msg.ChatStatus = "away"
}
sub.VideoStatus = msg.VideoStatus
2023-03-28 04:13:04 +00:00
sub.ChatStatus = msg.ChatStatus
sub.DND = msg.DND
// Sync the WhoList to everybody.
s.SendWhoList()
}
// 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) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
2023-02-06 06:16:36 +00:00
log.Error(err.Error())
return
}
// Make up a WebRTC shared secret and send it to both of them.
secret := util.RandomString(16)
log.Info("WebRTC: %s opens %s with secret %s", sub.Username, other.Username, secret)
// 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)
}
}
// 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,
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,
Username: other.Username,
OpenSecret: secret,
})
}
// OnBoot is a user kicking you off their video stream.
2023-08-14 02:21:27 +00:00
func (s *Server) OnBoot(sub *Subscriber, msg messages.Message) {
log.Info("%s boots %s off their camera", sub.Username, msg.Username)
sub.muteMu.Lock()
sub.booted[msg.Username] = struct{}{}
sub.muteMu.Unlock()
// 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,
)
}
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) {
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()
// 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,
)
}
// 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) {
log.Info("%s blocks %s: %v", sub.Username, msg.Username)
// 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()
}
// 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) {
log.Info("%s syncs their blocklist: %s", sub.Username, msg.Usernames)
sub.muteMu.Lock()
for _, username := range msg.Usernames {
2023-09-04 20:36:12 +00:00
sub.blocked[username] = struct{}{}
}
sub.muteMu.Unlock()
// Send the Who List in case our cam will show as disabled to the muted party.
s.SendWhoList()
}
// 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) {
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.
2023-08-14 02:21:27 +00:00
func (s *Server) OnCandidate(sub *Subscriber, msg messages.Message) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
2023-02-06 06:16:36 +00:00
log.Error(err.Error())
return
}
2023-08-14 02:21:27 +00:00
other.SendJSON(messages.Message{
Action: messages.ActionCandidate,
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) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
2023-02-06 06:16:36 +00:00
log.Error(err.Error())
return
}
2023-08-14 02:21:27 +00:00
other.SendJSON(messages.Message{
Action: messages.ActionSDP,
Username: sub.Username,
Description: msg.Description,
})
}
// OnWatch communicates video watching status between users.
2023-08-14 02:21:27 +00:00
func (s *Server) OnWatch(sub *Subscriber, msg messages.Message) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
2023-02-06 06:16:36 +00:00
log.Error(err.Error())
return
}
2023-08-14 02:21:27 +00:00
other.SendJSON(messages.Message{
Action: messages.ActionWatch,
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) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
2023-02-06 06:16:36 +00:00
log.Error(err.Error())
return
}
2023-08-14 02:21:27 +00:00
other.SendJSON(messages.Message{
Action: messages.ActionUnwatch,
Username: sub.Username,
})
}