diff --git a/pkg/chat/chat_api.go b/pkg/chat/chat_api.go new file mode 100644 index 0000000..017f138 --- /dev/null +++ b/pkg/chat/chat_api.go @@ -0,0 +1,163 @@ +package chat + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "code.nonshy.com/nonshy/website/pkg/config" + "code.nonshy.com/nonshy/website/pkg/log" + "code.nonshy.com/nonshy/website/pkg/models" +) + +// MaybeDisconnectUser may send a DisconnectUserNow to BareRTC if the user should not be allowed in the chat room. +// +// For example, they have set their profile to private and become a shy account, or they deactivated or got banned. +// +// If the user is presently in the chat room, they will be removed and given an appropriate ChatServer message. +// +// Returns a boolean OK (they were online in chat, and were removed) with the error only returning in case of a +// communication or JSON encode error with BareRTC. If they were online and removed, an admin feedback notice is +// also generated for visibility and confirmation of success. +func MaybeDisconnectUser(user *models.User) (bool, error) { + // What reason to remove them? If a message is provided, the DisconnectUserNow API will be called. + var because = "You have been signed out of chat because " + var reasons = []struct { + If bool + Message string + }{ + { + If: !user.Certified, + Message: because + "your nonshy account is not Certified, or its Certified status has been revoked.", + }, + { + If: user.IsShy(), + Message: because + "you had updated your nonshy profile to become too private. " + + "'Shy Accounts' are not permitted to remain in the chat room.", + }, + { + If: user.Status == models.UserStatusDisabled, + Message: because + "you have deactivated your nonshy account.", + }, + { + If: user.Status == models.UserStatusBanned, + Message: because + "your nonshy account has been banned.", + }, + { + // Catch-all for any non-active user status. + If: user.Status != models.UserStatusActive, + Message: because + "your nonshy account is no longer eligible to remain in the chat room.", + }, + } + + for _, reason := range reasons { + if reason.If { + i, err := DisconnectUserNow(user, reason.Message) + if err != nil { + return false, err + } + + // Were they online and were removed? Notify the admin for visibility. + if i > 0 { + fb := &models.Feedback{ + Intent: "report", + Subject: "Auto-Disconnect from Chat", + UserID: user.ID, + TableName: "users", + TableID: user.ID, + Message: fmt.Sprintf( + "A user was automatically disconnected from the chat room!\n\n"+ + "* Username: %s\n"+ + "* Number of users removed: %d\n"+ + "* Message sent to them: %s\n\n"+ + "Note: this is an informative message only. Users are expected to be removed from "+ + "chat when they do things such as deactivate their account, or private their profile "+ + "or pictures, and thus become ineligible to remain in the chat room.", + user.Username, + i, + reason.Message, + ), + } + + // Save the feedback. + if err := models.CreateFeedback(fb); err != nil { + log.Error("Couldn't save feedback from user updating their DOB: %s", err) + } + } + + // First removal reason wins. + break + } + } + + return false, nil +} + +// DisconnectUserNow tells the chat room to remove the user now if they are presently online. +func DisconnectUserNow(user *models.User, message string) (int, error) { + // API request struct for BareRTC /api/block/now endpoint. + var request = struct { + APIKey string + Usernames []string + Message string + Kick bool + }{ + APIKey: config.Current.CronAPIKey, + Usernames: []string{ + user.Username, + }, + Message: message, + Kick: false, + } + + type response struct { + OK bool + Removed int + Error string `json:",omitempty"` + } + + // JSON request body. + jsonStr, err := json.Marshal(request) + if err != nil { + return 0, err + } + + // Make the API request to BareRTC. + var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/disconnect/now" + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr)) + if err != nil { + return 0, err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{ + Timeout: 10 * time.Second, + } + resp, err := client.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + // Ingest the JSON response to see the count and error. + var ( + result response + body, _ = io.ReadAll(resp.Body) + ) + err = json.Unmarshal(body, &result) + if err != nil { + return 0, err + } + + if resp.StatusCode != http.StatusOK || !result.OK { + log.Error("DisconnectUserNow: error from BareRTC: status %d body %s", resp.StatusCode, body) + return result.Removed, errors.New(result.Error) + } + + return result.Removed, nil +} diff --git a/pkg/controller/account/deactivate.go b/pkg/controller/account/deactivate.go index eaccbc1..c7052a7 100644 --- a/pkg/controller/account/deactivate.go +++ b/pkg/controller/account/deactivate.go @@ -4,6 +4,8 @@ import ( "net/http" "strings" + "code.nonshy.com/nonshy/website/pkg/chat" + "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/session" "code.nonshy.com/nonshy/website/pkg/templates" @@ -42,6 +44,11 @@ func Deactivate() http.HandlerFunc { session.Flash(w, r, "Your account has been deactivated and you are now logged out. If you wish to re-activate your account, sign in again with your username and password.") templates.Redirect(w, "/") + // Maybe kick them from chat if this deletion makes them into a Shy Account. + if _, err := chat.MaybeDisconnectUser(currentUser); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err) + } + // Log the change. models.LogEvent(currentUser, nil, models.ChangeLogLifecycle, "users", currentUser.ID, "Deactivated their account.") return diff --git a/pkg/controller/account/delete.go b/pkg/controller/account/delete.go index f65ddc4..d0c46e6 100644 --- a/pkg/controller/account/delete.go +++ b/pkg/controller/account/delete.go @@ -5,6 +5,8 @@ import ( "net/http" "strings" + "code.nonshy.com/nonshy/website/pkg/chat" + "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/models/deletion" "code.nonshy.com/nonshy/website/pkg/session" @@ -43,6 +45,11 @@ func Delete() http.HandlerFunc { session.Flash(w, r, "Your account has been deleted.") templates.Redirect(w, "/") + // Kick them from the chat room if they are online. + if _, err := chat.DisconnectUserNow(currentUser, "You have been signed out of chat because you had deleted your account."); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err) + } + // Log the change. models.LogDeleted(nil, nil, "users", currentUser.ID, fmt.Sprintf("Username %s has deleted their account.", currentUser.Username), nil) return diff --git a/pkg/controller/account/settings.go b/pkg/controller/account/settings.go index b454f49..da882a3 100644 --- a/pkg/controller/account/settings.go +++ b/pkg/controller/account/settings.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "code.nonshy.com/nonshy/website/pkg/chat" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/geoip" "code.nonshy.com/nonshy/website/pkg/log" @@ -426,6 +427,11 @@ func Settings() http.HandlerFunc { session.FlashError(w, r, "Unknown POST intent value. Please try again.") } + // Maybe kick them from the chat room if they had become a Shy Account. + if _, err := chat.MaybeDisconnectUser(user); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", user.Username, user.ID, err) + } + templates.Redirect(w, r.URL.Path+hashtag+".") return } diff --git a/pkg/controller/account/signup.go b/pkg/controller/account/signup.go index 3d0df19..4cb509f 100644 --- a/pkg/controller/account/signup.go +++ b/pkg/controller/account/signup.go @@ -102,14 +102,6 @@ func Signup() http.HandlerFunc { vars["Email"] = email vars["Username"] = username - // Is the app not configured to send email? - if !config.Current.Mail.Enabled && !config.SkipEmailVerification { - session.FlashError(w, r, "This app is not configured to send email so you can not sign up at this time. "+ - "Please contact the website administrator about this issue!") - templates.Redirect(w, r.URL.Path) - return - } - // Validate the email. if _, err := nm.ParseAddress(email); err != nil { session.FlashError(w, r, "The email address you entered is not valid: %s", err) @@ -157,6 +149,16 @@ func Signup() http.HandlerFunc { session.FlashError(w, r, "Error creating a link to send you: %s", err) } + // Is the app not configured to send email? + if !config.Current.Mail.Enabled && !config.SkipEmailVerification { + // Log the signup token for local dev. + log.Error("Signup: the app is not configured to send email. To continue, visit the URL: /signup?token=%s", token.Token) + session.FlashError(w, r, "This app is not configured to send email so you can not sign up at this time. "+ + "Please contact the website administrator about this issue!") + templates.Redirect(w, r.URL.Path) + return + } + err := mail.Send(mail.Message{ To: email, Subject: "Verify your e-mail address", diff --git a/pkg/controller/admin/user_actions.go b/pkg/controller/admin/user_actions.go index 5198a42..3e397c1 100644 --- a/pkg/controller/admin/user_actions.go +++ b/pkg/controller/admin/user_actions.go @@ -6,7 +6,9 @@ import ( "strconv" "strings" + "code.nonshy.com/nonshy/website/pkg/chat" "code.nonshy.com/nonshy/website/pkg/config" + "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/models/deletion" "code.nonshy.com/nonshy/website/pkg/session" @@ -157,6 +159,11 @@ func UserActions() http.HandlerFunc { session.Flash(w, r, "User ban status updated!") templates.Redirect(w, "/u/"+user.Username) + // Maybe kick them from chat room now. + if _, err := chat.MaybeDisconnectUser(user); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", user.Username, user.ID, err) + } + // Log the change. models.LogEvent(user, currentUser, models.ChangeLogBanned, "users", currentUser.ID, fmt.Sprintf("User ban status updated to: %s", status)) return @@ -196,6 +203,11 @@ func UserActions() http.HandlerFunc { } templates.Redirect(w, "/admin") + // Kick them from the chat room if they are online. + if _, err := chat.DisconnectUserNow(user, "You have been signed out of chat because your account has been deleted."); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", user.Username, user.ID, err) + } + // Log the change. models.LogDeleted(nil, currentUser, "users", user.ID, fmt.Sprintf("Username %s has been deleted by an admin.", user.Username), nil) return diff --git a/pkg/controller/photo/certification.go b/pkg/controller/photo/certification.go index a28362d..f4c2ea2 100644 --- a/pkg/controller/photo/certification.go +++ b/pkg/controller/photo/certification.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strconv" + "code.nonshy.com/nonshy/website/pkg/chat" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/geoip" "code.nonshy.com/nonshy/website/pkg/log" @@ -90,6 +91,11 @@ func Certification() http.HandlerFunc { // Log the change. models.LogDeleted(currentUser, nil, "certification_photos", currentUser.ID, "Removed their certification photo.", cert) + // Kick them from the chat room if they are online. + if _, err := chat.MaybeDisconnectUser(currentUser); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err) + } + session.Flash(w, r, "Your certification photo has been deleted.") templates.Redirect(w, r.URL.Path) return @@ -141,6 +147,11 @@ func Certification() http.HandlerFunc { session.FlashError(w, r, "Error saving your User data: %s", err) } + // Kick them from the chat room if they are online. + if _, err := chat.MaybeDisconnectUser(currentUser); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err) + } + // Notify the admin email to check out this photo. if err := mail.Send(mail.Message{ To: config.Current.AdminEmail, @@ -306,6 +317,11 @@ func AdminCertification() http.HandlerFunc { // Log the change. models.LogEvent(user, currentUser, models.ChangeLogRejected, "certification_photos", user.ID, "Rejected the certification photo with comment: "+comment) + // Kick them from the chat room if they are online. + if _, err := chat.MaybeDisconnectUser(user); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", user.Username, user.ID, err) + } + // Did we silently ignore it? if comment == "(ignore)" { session.FlashError(w, r, "The certification photo was ignored with no comment, and will not notify the sender.") diff --git a/pkg/controller/photo/edit_delete.go b/pkg/controller/photo/edit_delete.go index fcaed2d..de925c0 100644 --- a/pkg/controller/photo/edit_delete.go +++ b/pkg/controller/photo/edit_delete.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strconv" + "code.nonshy.com/nonshy/website/pkg/chat" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/models" @@ -128,8 +129,6 @@ func Edit() http.HandlerFunc { setProfilePic = false } - log.Error("SAVING PHOTO: %+v", photo) - if err := photo.Save(); err != nil { session.FlashError(w, r, "Couldn't save photo: %s", err) } @@ -149,6 +148,11 @@ func Edit() http.HandlerFunc { // Log the change. models.LogUpdated(currentUser, requestUser, "photos", photo.ID, "Updated the photo's settings.", diffs) + // Maybe kick them from the chat if this photo save makes them a Shy Account. + if _, err := chat.MaybeDisconnectUser(currentUser); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err) + } + // If this picture has moved to Private, revoke any notification we gave about it before. if goingPrivate || goingCircle { log.Info("The picture is GOING PRIVATE (to %s), revoke any notifications about it", photo.Visibility) @@ -272,6 +276,11 @@ func Delete() http.HandlerFunc { session.Flash(w, r, "Photo deleted!") + // Maybe kick them from chat if this deletion makes them into a Shy Account. + if _, err := chat.MaybeDisconnectUser(currentUser); err != nil { + log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err) + } + // Return the user to their gallery. templates.Redirect(w, "/u/"+currentUser.Username+"/photos") return diff --git a/web/templates/account/age_gate.html b/web/templates/account/age_gate.html index 8af2eb3..bdcc205 100644 --- a/web/templates/account/age_gate.html +++ b/web/templates/account/age_gate.html @@ -94,6 +94,7 @@ window.alert(`NOTE: Your input was interpreted to be in MM/DD/YYYY order and has been read as: ${answer}`); } else if (!answer.match(/^\d{4}-\d{2}-\d{2}/)) { window.alert(`Please enter the date in YYYY-MM-DD format.`); + return; } $dob.value = answer; diff --git a/web/templates/account/signup.html b/web/templates/account/signup.html index 88f7666..f853444 100644 --- a/web/templates/account/signup.html +++ b/web/templates/account/signup.html @@ -154,6 +154,9 @@

Your birthdate won't be shown to other members and is used to show your current age on your profile. Please enter your correct birthdate. +
+ On mobile and scrolling for your year is tedious? + Click to type your birthdate instead.

{{end}} @@ -225,8 +228,32 @@ window.addEventListener("DOMContentLoaded", (event) => { }) }; - $username.addEventListener("change", onChange); - $username.addEventListener("blur", onChange); + if ($username != undefined) { + $username.addEventListener("change", onChange); + $username.addEventListener("blur", onChange); + } + + // DOB manual entry script, on signup completion page. + let $manualEntry = document.querySelector("#manualEntry"), + $dob = document.querySelector("#dob"); + + if ($manualEntry != undefined) { + $manualEntry.addEventListener("click", function(e) { + e.preventDefault(); + + let answer = window.prompt("Enter your birthdate in 'YYYY-MM-DD' format").trim().replace(/\//g, '-'); + if (answer.match(/^(\d{2})-(\d{2})-(\d{4})/)) { + let group = answer.match(/^(\d{2})-(\d{2})-(\d{4})/); + answer = `${group[3]}-${group[1]}-${group[2]}`; + window.alert(`NOTE: Your input was interpreted to be in MM/DD/YYYY order and has been read as: ${answer}`); + } else if (!answer.match(/^\d{4}-\d{2}-\d{2}/)) { + window.alert(`Please enter the date in YYYY-MM-DD format.`); + return; + } + + $dob.value = answer; + }); + } }); {{end}}