2023-02-10 07:03:06 +00:00
|
|
|
package barertc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2023-10-07 20:22:41 +00:00
|
|
|
"fmt"
|
2023-02-10 07:03:06 +00:00
|
|
|
"net/http"
|
2023-08-29 00:49:50 +00:00
|
|
|
"os"
|
2023-07-30 17:32:08 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
2023-08-29 00:49:50 +00:00
|
|
|
"time"
|
2023-02-10 07:03:06 +00:00
|
|
|
|
|
|
|
"git.kirsle.net/apps/barertc/pkg/config"
|
2023-08-14 02:21:27 +00:00
|
|
|
"git.kirsle.net/apps/barertc/pkg/jwt"
|
2023-07-30 17:32:08 +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-10 07:03:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Statistics (/api/statistics) returns info about the users currently logged onto the chat,
|
|
|
|
// for your website to call via CORS. The URL to your site needs to be in the CORSHosts array
|
|
|
|
// of your settings.toml.
|
|
|
|
func (s *Server) Statistics() http.HandlerFunc {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Handle the CORS header from your trusted domains.
|
|
|
|
if origin := r.Header.Get("Origin"); origin != "" {
|
|
|
|
var found bool
|
|
|
|
for _, allowed := range config.Current.CORSHosts {
|
|
|
|
if allowed == origin {
|
|
|
|
found = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if found {
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var result = struct {
|
|
|
|
UserCount int
|
|
|
|
Usernames []string
|
2023-06-10 19:54:13 +00:00
|
|
|
Cameras struct {
|
|
|
|
Blue int
|
|
|
|
Red int
|
|
|
|
}
|
2023-02-10 07:03:06 +00:00
|
|
|
}{
|
|
|
|
Usernames: []string{},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Count all users + collect unique usernames.
|
|
|
|
var unique = map[string]struct{}{}
|
|
|
|
for _, sub := range s.IterSubscribers() {
|
2023-03-29 01:09:13 +00:00
|
|
|
if sub.authenticated && sub.ChatStatus != "hidden" {
|
2023-02-10 07:03:06 +00:00
|
|
|
result.UserCount++
|
|
|
|
if _, ok := unique[sub.Username]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
result.Usernames = append(result.Usernames, sub.Username)
|
|
|
|
unique[sub.Username] = struct{}{}
|
2023-06-10 19:54:13 +00:00
|
|
|
|
|
|
|
// Count cameras by color.
|
2023-08-14 02:21:27 +00:00
|
|
|
if sub.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive {
|
|
|
|
if sub.VideoStatus&messages.VideoFlagNSFW == messages.VideoFlagNSFW {
|
2023-06-10 19:54:13 +00:00
|
|
|
result.Cameras.Red++
|
|
|
|
} else {
|
|
|
|
result.Cameras.Blue++
|
|
|
|
}
|
|
|
|
}
|
2023-02-10 07:03:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
enc := json.NewEncoder(w)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
enc.Encode(result)
|
|
|
|
})
|
|
|
|
}
|
2023-07-30 17:32:08 +00:00
|
|
|
|
2023-08-14 02:21:27 +00:00
|
|
|
// 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(¶ms); 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,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-08-29 00:49:50 +00:00
|
|
|
// Shutdown (/api/shutdown) the chat server, hopefully to reboot it.
|
|
|
|
//
|
|
|
|
// This endpoint is equivalent to the operator '/shutdown' command but may be
|
|
|
|
// invoked by your website, or your chatbot. It requires the AdminAPIKey.
|
|
|
|
//
|
|
|
|
// It is a POST request with a json body containing the following schema:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "APIKey": "from settings.toml",
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The return schema looks like:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "OK": true,
|
|
|
|
// "Error": "error string, omitted if none",
|
|
|
|
// }
|
|
|
|
func (s *Server) ShutdownAPI() http.HandlerFunc {
|
|
|
|
type request struct {
|
|
|
|
APIKey string
|
|
|
|
Claims jwt.Claims
|
|
|
|
}
|
|
|
|
|
|
|
|
type result struct {
|
|
|
|
OK bool
|
|
|
|
Error 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(¶ms); 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send the response.
|
|
|
|
enc.Encode(result{
|
|
|
|
OK: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Defer a shutdown a moment later.
|
|
|
|
go func() {
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
os.Exit(1)
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Attempt to broadcast, but if deadlocked this might not go out.
|
|
|
|
go func() {
|
|
|
|
s.Broadcast(messages.Message{
|
|
|
|
Action: messages.ActionError,
|
|
|
|
Username: "ChatServer",
|
|
|
|
Message: "The chat server is going down for a reboot NOW!",
|
|
|
|
})
|
|
|
|
}()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-30 17:32:08 +00:00
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
// It is a POST request with a json body containing the following schema:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "APIKey": "from settings.toml",
|
|
|
|
// "Username": "soandso",
|
|
|
|
// "Blocklist": [ "list", "of", "other", "usernames" ],
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The chat server will remember these mappings (until rebooted). How they are
|
|
|
|
// used is that the blocklist is embedded in the front-end page when the username
|
|
|
|
// signs in later. As part of the On Connect handler, the front-end will send the
|
|
|
|
// list of usernames in a bulk `mute` command to the server. This way even if the
|
|
|
|
// chat server reboots while the user is connected, when it comes back up and the user
|
|
|
|
// reconnects they will retransmit their block list.
|
|
|
|
func (s *Server) BlockList() http.HandlerFunc {
|
|
|
|
type request struct {
|
|
|
|
APIKey string
|
|
|
|
Username string
|
|
|
|
Blocklist []string
|
|
|
|
}
|
|
|
|
|
|
|
|
type result struct {
|
|
|
|
OK bool
|
|
|
|
Error 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(¶ms); 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store the cached blocklist.
|
|
|
|
SetCachedBlocklist(params.Username, params.Blocklist)
|
|
|
|
enc.Encode(result{
|
|
|
|
OK: true,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-30 19:32:09 +00:00
|
|
|
// BlockNow (/api/block/now) allows your website to add to a current online chatter's
|
|
|
|
// blocked list immediately.
|
|
|
|
//
|
|
|
|
// For example: the BlockList endpoint does a bulk sync of the blocklist at the time
|
|
|
|
// a user joins the chat room, but if users are already on chat when the blocking begins,
|
|
|
|
// it doesn't take effect until one or the other re-joins the room. This API endpoint
|
|
|
|
// can apply the blocking immediately to the currently online users.
|
|
|
|
//
|
|
|
|
// It is a POST request with a json body containing the following schema:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "APIKey": "from settings.toml",
|
|
|
|
// "Usernames": [ "source", "target" ]
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The pair of usernames will be the two users who block one another (in any order).
|
|
|
|
// If any of the users are currently connected to the chat, they will all mutually
|
|
|
|
// block one another immediately.
|
|
|
|
func (s *Server) BlockNow() http.HandlerFunc {
|
|
|
|
type request struct {
|
|
|
|
APIKey string
|
|
|
|
Usernames []string
|
|
|
|
}
|
|
|
|
|
|
|
|
type result struct {
|
|
|
|
OK bool
|
|
|
|
Error 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(¶ms); 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if any of these users are online, and update their blocklist accordingly.
|
|
|
|
var changed bool
|
|
|
|
for _, username := range params.Usernames {
|
|
|
|
if sub, err := s.GetSubscriber(username); err == nil {
|
|
|
|
for _, otherName := range params.Usernames {
|
|
|
|
if username == otherName {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
log.Info("BlockNow API: %s is currently on chat, add block for %+v", username, otherName)
|
|
|
|
|
|
|
|
sub.muteMu.Lock()
|
|
|
|
sub.muted[otherName] = struct{}{}
|
|
|
|
sub.blocked[otherName] = struct{}{}
|
|
|
|
sub.muteMu.Unlock()
|
|
|
|
|
|
|
|
// Changes have been made to online users.
|
|
|
|
changed = true
|
|
|
|
|
|
|
|
// Send a server-side "block" command to the subscriber, so their front-end page might
|
|
|
|
// update the cachedBlocklist so there's no leakage in case of chat server rebooting.
|
|
|
|
sub.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionBlock,
|
|
|
|
Username: otherName,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If any changes to blocklists were made: send the Who List.
|
|
|
|
if changed {
|
|
|
|
s.SendWhoList()
|
|
|
|
}
|
|
|
|
|
|
|
|
enc.Encode(result{
|
|
|
|
OK: true,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-03-15 06:04:24 +00:00
|
|
|
// DisconnectNow (/api/disconnect/now) allows your website to remove a user from
|
|
|
|
// the chat room if they are currently online.
|
|
|
|
//
|
|
|
|
// For example: a user on your website has deactivated their account, and so
|
|
|
|
// should not be allowed to remain in the chat room.
|
|
|
|
//
|
|
|
|
// It is a POST request with a json body containing the following schema:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "APIKey": "from settings.toml",
|
|
|
|
// "Usernames": [ "alice", "bob" ],
|
|
|
|
// "Message": "An optional ChatServer message to send them first.",
|
|
|
|
// "Kick": false,
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The `Message` parameter, if provided, will be sent to that user as a
|
|
|
|
// ChatServer error before they are removed from the room. You can use this
|
|
|
|
// to provide them context as to why they are being kicked. For example:
|
|
|
|
// "You have been logged out of chat because you deactivated your profile on
|
|
|
|
// the main website."
|
|
|
|
//
|
|
|
|
// The `Kick` boolean is whether the removal should manifest to other users
|
|
|
|
// in chat as a "kick" (sending a presence message of "has been kicked from
|
|
|
|
// the room!"). By default (false), BareRTC will tell the user to disconnect
|
|
|
|
// and it will manifest as a regular "has left the room" event to other online
|
|
|
|
// chatters.
|
|
|
|
func (s *Server) DisconnectNow() http.HandlerFunc {
|
|
|
|
type request struct {
|
|
|
|
APIKey string
|
|
|
|
Usernames []string
|
|
|
|
Message string
|
|
|
|
Kick bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type result struct {
|
|
|
|
OK bool
|
|
|
|
Removed int
|
|
|
|
Error 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(¶ms); 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if any of these users are online, and disconnect them from the chat.
|
|
|
|
var removed int
|
|
|
|
for _, username := range params.Usernames {
|
|
|
|
if sub, err := s.GetSubscriber(username); err == nil {
|
|
|
|
// Broadcast to everybody that the user left the chat.
|
|
|
|
message := messages.PresenceExited
|
|
|
|
if params.Kick {
|
|
|
|
message = messages.PresenceKicked
|
|
|
|
}
|
|
|
|
s.Broadcast(messages.Message{
|
|
|
|
Action: messages.ActionPresence,
|
|
|
|
Username: username,
|
|
|
|
Message: message,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Custom message to send to them?
|
|
|
|
if params.Message != "" {
|
|
|
|
sub.ChatServer(params.Message)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Disconnect them.
|
|
|
|
sub.SendJSON(messages.Message{
|
|
|
|
Action: messages.ActionKick,
|
|
|
|
})
|
|
|
|
sub.authenticated = false
|
|
|
|
sub.Username = ""
|
|
|
|
|
|
|
|
removed++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If any changes to blocklists were made: send the Who List.
|
|
|
|
if removed > 0 {
|
|
|
|
s.SendWhoList()
|
|
|
|
}
|
|
|
|
|
|
|
|
enc.Encode(result{
|
|
|
|
OK: true,
|
|
|
|
Removed: removed,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-10-07 20:22:41 +00:00
|
|
|
// UserProfile (/api/profile) fetches profile information about a user.
|
|
|
|
//
|
|
|
|
// This endpoint will proxy to your WebhookURL for the "profile" endpoint.
|
|
|
|
// If your webhook is not configured or not reachable, this endpoint returns
|
|
|
|
// an error to the caller.
|
|
|
|
//
|
|
|
|
// Authentication: the caller must send their current chat JWT token when
|
|
|
|
// hitting this endpoint.
|
|
|
|
//
|
|
|
|
// It is a POST request with a json body containing the following schema:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "JWTToken": "the caller's jwt token",
|
|
|
|
// "Username": [ "soandso" ]
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The response JSON will look like the following (this also mirrors the
|
|
|
|
// response json as sent by your site's webhook URL):
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "OK": true,
|
|
|
|
// "Error": "only on errors",
|
|
|
|
// "ProfileFields": [
|
|
|
|
// {
|
|
|
|
// "Name": "Age",
|
|
|
|
// "Value": "30yo",
|
|
|
|
// },
|
|
|
|
// {
|
|
|
|
// "Name": "Gender",
|
|
|
|
// "Value": "Man",
|
|
|
|
// },
|
|
|
|
// ...
|
|
|
|
// ]
|
|
|
|
// }
|
|
|
|
func (s *Server) UserProfile() http.HandlerFunc {
|
|
|
|
type request struct {
|
|
|
|
JWTToken string
|
|
|
|
Username string
|
|
|
|
}
|
|
|
|
|
|
|
|
type profileField struct {
|
|
|
|
Name string
|
|
|
|
Value string
|
|
|
|
}
|
|
|
|
type result struct {
|
|
|
|
OK bool
|
|
|
|
Error string `json:",omitempty"`
|
|
|
|
ProfileFields []profileField `json:",omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type webhookRequest struct {
|
|
|
|
Action string
|
|
|
|
APIKey string
|
|
|
|
Username string
|
|
|
|
}
|
|
|
|
|
|
|
|
type webhookResponse struct {
|
|
|
|
StatusCode int
|
|
|
|
Data result
|
|
|
|
}
|
|
|
|
|
|
|
|
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(¶ms); err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Are JWT tokens enabled on the server?
|
|
|
|
if !config.Current.JWT.Enabled || params.JWTToken == "" {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: "JWT authentication is not available.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the user's JWT token.
|
|
|
|
_, _, err := jwt.ParseAndValidate(params.JWTToken)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch the profile data from your website.
|
|
|
|
data, err := PostWebhook("profile", webhookRequest{
|
|
|
|
Action: "profile",
|
|
|
|
APIKey: config.Current.AdminAPIKey,
|
|
|
|
Username: params.Username,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Couldn't get profile information: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Success? Try and parse the response into our expected format.
|
|
|
|
var resp webhookResponse
|
|
|
|
if err := json.Unmarshal(data, &resp); err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
|
|
|
// A nice error message?
|
|
|
|
if resp.Data.Error != "" {
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: resp.Data.Error,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: fmt.Sprintf("Didn't get expected response for profile data: %s", err),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point the expected resp mirrors our own, so return it.
|
|
|
|
if resp.StatusCode != http.StatusOK || resp.Data.Error != "" {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
}
|
|
|
|
enc.Encode(resp.Data)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-03-29 06:20:09 +00:00
|
|
|
// MessageHistory (/api/message/history) fetches past direct messages for a user.
|
|
|
|
//
|
|
|
|
// This endpoint looks up earlier chat messages between the current user and a target.
|
|
|
|
// It will only run with a valid JWT auth token, to protect users' privacy.
|
|
|
|
//
|
|
|
|
// It is a POST request with a json body containing the following schema:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "JWTToken": "the caller's jwt token",
|
|
|
|
// "Username": "other party",
|
|
|
|
// "BeforeID": 1234,
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The "BeforeID" parameter is for pagination and is optional: by default the most
|
|
|
|
// recent page of messages are returned. To retrieve an older page, the BeforeID will
|
|
|
|
// contain the MessageID of the oldest message you received so far, so that the message
|
|
|
|
// before that will be the first returned on the next page.
|
|
|
|
//
|
|
|
|
// The response JSON will look like the following:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "OK": true,
|
|
|
|
// "Error": "only on error responses",
|
|
|
|
// "Messages": [
|
|
|
|
// {
|
|
|
|
// // Standard BareRTC Message objects...
|
|
|
|
// "MessageID": 1234,
|
|
|
|
// "Username": "other party",
|
|
|
|
// "Message": "hello!",
|
|
|
|
// }
|
|
|
|
// ],
|
|
|
|
// "Remaining": 42,
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The Remaining value is how many older messages still exist to be loaded.
|
|
|
|
func (s *Server) MessageHistory() http.HandlerFunc {
|
|
|
|
type request struct {
|
|
|
|
JWTToken string
|
|
|
|
Username string
|
|
|
|
BeforeID int64
|
|
|
|
}
|
|
|
|
|
|
|
|
type result struct {
|
|
|
|
OK bool
|
|
|
|
Error string `json:",omitempty"`
|
|
|
|
Messages []messages.Message
|
|
|
|
Remaining int
|
|
|
|
}
|
|
|
|
|
|
|
|
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(¶ms); err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Are JWT tokens enabled on the server?
|
|
|
|
if !config.Current.JWT.Enabled || params.JWTToken == "" {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: "JWT authentication is not available.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the user's JWT token.
|
|
|
|
claims, _, err := jwt.ParseAndValidate(params.JWTToken)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the user from the chat roster.
|
|
|
|
sub, err := s.GetSubscriber(claims.Subject)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: "You are not logged into the chat room.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch a page of message history.
|
|
|
|
messages, remaining, err := models.PaginateDirectMessages(sub.Username, params.Username, params.BeforeID)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
enc.Encode(result{
|
|
|
|
OK: true,
|
|
|
|
Messages: messages,
|
|
|
|
Remaining: remaining,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-04-12 06:28:35 +00:00
|
|
|
// ClearMessages (/api/message/clear) deletes all the stored direct messages for a user.
|
|
|
|
//
|
|
|
|
// It can be called by the authenticated user themself (with JWTToken), or from your website
|
|
|
|
// (with APIKey) in which case you can remotely clear history for a user.
|
|
|
|
//
|
|
|
|
// It is a POST request with a json body containing the following schema:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "JWTToken": "the caller's jwt token",
|
|
|
|
// "APIKey": "your website's admin API key"
|
|
|
|
// "Username": "if using your APIKey to specify a user to delete",
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The response JSON will look like the following:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "OK": true,
|
|
|
|
// "Error": "only on error responses",
|
|
|
|
// "MessagesErased": 123,
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// The Remaining value is how many older messages still exist to be loaded.
|
|
|
|
func (s *Server) ClearMessages() http.HandlerFunc {
|
|
|
|
type request struct {
|
|
|
|
JWTToken string
|
|
|
|
APIKey string
|
|
|
|
Username string
|
|
|
|
}
|
|
|
|
|
|
|
|
type result struct {
|
|
|
|
OK bool
|
|
|
|
Error string `json:",omitempty"`
|
|
|
|
MessagesErased int `json:""`
|
|
|
|
}
|
|
|
|
|
|
|
|
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(¶ms); err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Authenticate this request.
|
|
|
|
if params.APIKey != "" {
|
|
|
|
// By admin API key.
|
|
|
|
if params.APIKey != config.Current.AdminAPIKey {
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: "Authentication denied.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Are JWT tokens enabled on the server?
|
|
|
|
if !config.Current.JWT.Enabled || params.JWTToken == "" {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: "JWT authentication is not available.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the user's JWT token.
|
|
|
|
claims, _, err := jwt.ParseAndValidate(params.JWTToken)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the username to clear.
|
|
|
|
params.Username = claims.Subject
|
|
|
|
}
|
|
|
|
|
|
|
|
// Erase their message history.
|
|
|
|
count, err := (models.DirectMessage{}).ClearMessages(params.Username)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
enc.Encode(result{
|
|
|
|
Error: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
enc.Encode(result{
|
|
|
|
OK: true,
|
|
|
|
MessagesErased: count,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-30 17:32:08 +00:00
|
|
|
// Blocklist cache sent over from your website.
|
|
|
|
var (
|
|
|
|
// Map of username to the list of usernames they block.
|
|
|
|
cachedBlocklist map[string][]string
|
|
|
|
cachedBlocklistMu sync.RWMutex
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
cachedBlocklist = map[string][]string{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCachedBlocklist returns the blocklist for a username.
|
|
|
|
func GetCachedBlocklist(username string) []string {
|
|
|
|
cachedBlocklistMu.RLock()
|
|
|
|
defer cachedBlocklistMu.RUnlock()
|
|
|
|
if list, ok := cachedBlocklist[username]; ok {
|
|
|
|
log.Debug("GetCachedBlocklist(%s) blocks %s", username, list)
|
|
|
|
return list
|
|
|
|
}
|
|
|
|
log.Debug("GetCachedBlocklist(%s): no blocklist stored", username)
|
|
|
|
return []string{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetCachedBlocklist sets the blocklist cache for a user.
|
|
|
|
func SetCachedBlocklist(username string, blocklist []string) {
|
|
|
|
log.Info("SetCachedBlocklist: %s mutes users %s", username, strings.Join(blocklist, ", "))
|
|
|
|
cachedBlocklistMu.Lock()
|
|
|
|
defer cachedBlocklistMu.Unlock()
|
|
|
|
cachedBlocklist[username] = blocklist
|
|
|
|
}
|