8fca36836c
* On a user gallery page: if the current user can not see their default profile pic (friends-only or private), include a notice and link to the FAQ about this. * Add a new placeholder avatar for profile pics that are set to "Inner circle only" when viewed by members outside the circle.
305 lines
8.2 KiB
Go
305 lines
8.2 KiB
Go
package chat
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
|
"code.nonshy.com/nonshy/website/pkg/geoip"
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
|
"code.nonshy.com/nonshy/website/pkg/middleware"
|
|
"code.nonshy.com/nonshy/website/pkg/models"
|
|
"code.nonshy.com/nonshy/website/pkg/photo"
|
|
"code.nonshy.com/nonshy/website/pkg/session"
|
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
|
"code.nonshy.com/nonshy/website/pkg/worker"
|
|
"github.com/golang-jwt/jwt/v4"
|
|
)
|
|
|
|
// JWT claims.
|
|
type Claims struct {
|
|
// Custom claims.
|
|
IsAdmin bool `json:"op,omitempty"`
|
|
VIP bool `json:"vip,omitempty"`
|
|
Avatar string `json:"img,omitempty"`
|
|
ProfileURL string `json:"url,omitempty"`
|
|
Nickname string `json:"nick,omitempty"`
|
|
Emoji string `json:"emoji,omitempty"`
|
|
Gender string `json:"gender,omitempty"`
|
|
|
|
// Standard claims. Notes:
|
|
// subject = username
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// Gender returns the BareRTC gender string for the user's gender selection.
|
|
func Gender(u *models.User) string {
|
|
switch u.GetProfileField("gender") {
|
|
case "Man", "Trans (FTM)":
|
|
return "m"
|
|
case "Woman", "Trans (MTF)":
|
|
return "f"
|
|
case "Non-binary", "Trans", "Other":
|
|
return "o"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Landing page for chat rooms.
|
|
func Landing() http.HandlerFunc {
|
|
tmpl := templates.Must("chat.html")
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Get the current user.
|
|
currentUser, err := session.CurrentUser(r)
|
|
if err != nil {
|
|
session.FlashError(w, r, "Couldn't get current user: %s", err)
|
|
templates.Redirect(w, "/")
|
|
return
|
|
}
|
|
|
|
// Are they logging into the chat room?
|
|
var (
|
|
intent = r.FormValue("intent")
|
|
isShy = currentUser.IsShy()
|
|
birthday = r.FormValue("birthday") == "true" && currentUser.IsBirthday()
|
|
)
|
|
if intent == "join" {
|
|
// Maintenance mode?
|
|
if middleware.ChatMaintenance(currentUser, w, r) {
|
|
return
|
|
}
|
|
|
|
// If we are shy, block chat for now.
|
|
if isShy {
|
|
session.FlashError(w, r,
|
|
"You have a Shy Account and are not allowed in the chat room at this time where our non-shy members may "+
|
|
"be on camera.",
|
|
)
|
|
templates.Redirect(w, "/chat")
|
|
return
|
|
}
|
|
|
|
// Get our Chat JWT secret.
|
|
var (
|
|
secret = []byte(config.Current.BareRTC.JWTSecret)
|
|
chatURL = config.Current.BareRTC.URL
|
|
)
|
|
if len(secret) == 0 || chatURL == "" {
|
|
session.FlashError(w, r, "Couldn't sign you into the chat: JWT secret key or chat URL not configured!")
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Avatar URL - masked if non-public.
|
|
avatar := photo.URLPath(currentUser.ProfilePhoto.CroppedFilename)
|
|
switch currentUser.ProfilePhoto.Visibility {
|
|
case models.PhotoPrivate:
|
|
avatar = "/static/img/shy-private.png"
|
|
case models.PhotoFriends:
|
|
avatar = "/static/img/shy-friends.png"
|
|
case models.PhotoInnerCircle:
|
|
avatar = "/static/img/shy-secret.png"
|
|
}
|
|
|
|
// Country flag emoji.
|
|
emoji, err := geoip.GetChatFlagEmoji(r)
|
|
if err != nil {
|
|
emoji, err = geoip.CountryFlagEmojiWithCode("US")
|
|
if err != nil {
|
|
emoji = "🏴☠️"
|
|
}
|
|
}
|
|
|
|
// Birthday cake emoji?
|
|
if birthday {
|
|
emoji = "🍰 It's my birthday!"
|
|
}
|
|
|
|
// Create the JWT claims.
|
|
claims := Claims{
|
|
IsAdmin: currentUser.HasAdminScope(config.ScopeChatModerator),
|
|
VIP: currentUser.IsInnerCircle(),
|
|
Avatar: avatar,
|
|
ProfileURL: "/u/" + currentUser.Username,
|
|
Nickname: currentUser.NameOrUsername(),
|
|
Emoji: emoji,
|
|
Gender: Gender(currentUser),
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
Issuer: config.Title,
|
|
Subject: currentUser.Username,
|
|
ID: fmt.Sprintf("%d", currentUser.ID),
|
|
},
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
ss, err := token.SignedString(secret)
|
|
if err != nil {
|
|
session.FlashError(w, r, "Couldn't sign you into the chat: %s", err)
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Send over their blocklist to the chat server.
|
|
if err := SendBlocklist(currentUser); err != nil {
|
|
log.Error("SendBlocklist: %s", err)
|
|
}
|
|
|
|
// Redirect them to the chat room.
|
|
templates.Redirect(w, strings.TrimSuffix(chatURL, "/")+"/?jwt="+ss)
|
|
return
|
|
}
|
|
|
|
// Get the ChatStatistics and select our friend names from it.
|
|
var (
|
|
stats = FilteredChatStatistics(currentUser)
|
|
friendsOnline = models.FilterFriendUsernames(currentUser, stats.Usernames)
|
|
)
|
|
|
|
sort.Strings(friendsOnline)
|
|
|
|
var vars = map[string]interface{}{
|
|
"ChatAPI": strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/statistics",
|
|
"IsShyUser": isShy,
|
|
|
|
// Pre-populate the "who's online" widget from backend cache data
|
|
"ChatStatistics": stats,
|
|
"FriendsOnline": friendsOnline,
|
|
}
|
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
|
|
// FilteredChatStatistics will return a copy of the cached ChatStatistics but where the Usernames list is
|
|
// filtered down (and the online user counts, accordingly) by blocklists.
|
|
func FilteredChatStatistics(currentUser *models.User) worker.ChatStatistics {
|
|
var stats = worker.GetChatStatistics()
|
|
var result = worker.ChatStatistics{
|
|
UserCount: stats.UserCount,
|
|
Usernames: []string{},
|
|
Cameras: stats.Cameras,
|
|
}
|
|
|
|
// Who are we blocking?
|
|
var blockedUsernames = map[string]interface{}{}
|
|
for _, username := range models.BlockedUsernames(currentUser) {
|
|
blockedUsernames[username] = nil
|
|
}
|
|
|
|
// Filter the online users listing.
|
|
for _, username := range stats.Usernames {
|
|
if _, ok := blockedUsernames[username]; !ok {
|
|
result.Usernames = append(result.Usernames, username)
|
|
}
|
|
}
|
|
|
|
// Sort the names.
|
|
sort.Strings(result.Usernames)
|
|
|
|
return result
|
|
}
|
|
|
|
// SendBlocklist syncs the user blocklist to the chat server prior to sending them over.
|
|
func SendBlocklist(user *models.User) error {
|
|
// Get the user's blocklist.
|
|
blockedUsernames := models.BlockedUsernames(user)
|
|
log.Info("SendBlocklist(%s) to BareRTC: %d blocked usernames", user.Username, len(blockedUsernames))
|
|
|
|
// API request struct for BareRTC /api/blocklist endpoint.
|
|
var request = struct {
|
|
APIKey string
|
|
Username string
|
|
Blocklist []string
|
|
}{
|
|
config.Current.CronAPIKey,
|
|
user.Username,
|
|
blockedUsernames,
|
|
}
|
|
|
|
// JSON request body.
|
|
jsonStr, err := json.Marshal(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make the API request to BareRTC.
|
|
var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/blocklist"
|
|
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("SendBlocklist: error syncing blocklist to BareRTC: status %d body %s", resp.StatusCode, body)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// BlockUserNow syncs the new block action to the chat server now, in case the user is already online.
|
|
func BlockUserNow(currentUser, user *models.User) error {
|
|
// API request struct for BareRTC /api/block/now endpoint.
|
|
var request = struct {
|
|
APIKey string
|
|
Usernames []string
|
|
}{
|
|
config.Current.CronAPIKey,
|
|
[]string{
|
|
currentUser.Username,
|
|
user.Username,
|
|
},
|
|
}
|
|
|
|
// JSON request body.
|
|
jsonStr, err := json.Marshal(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make the API request to BareRTC.
|
|
var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/block/now"
|
|
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("BlockUserNow: error syncing block to BareRTC: status %d body %s", resp.StatusCode, body)
|
|
}
|
|
|
|
return nil
|
|
}
|