gosora/routes/convos.go
Azareal b1af963916 Add per-user profile comment visibility settings.
Honor user blocks in ProfileReplyEditSubmit.
Reduce boilerplate.

Update account_privacy_profile_comments phrase.
Add account_privacy_profile_comments_public phrase.
Add account_privacy_profile_comments_registered phrase.
Add account_privacy_profile_comments_self phrase.
Add account_privacy_enable_embeds phrase.

Add profile_comments column to users table.
Add who_can_convo column to users table.

You will need to run the updater / patcher for this commit.
2020-07-15 07:50:29 +10:00

483 lines
14 KiB
Go

package routes
import (
"database/sql"
"errors"
"html"
"net/http"
"strconv"
"strings"
//"log"
c "github.com/Azareal/Gosora/common"
p "github.com/Azareal/Gosora/common/phrases"
)
func Convos(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header) c.RouteError {
accountEditHead("convos", w, r, user, h)
h.AddScript("convo.js")
h.AddSheet(h.Theme.Name + "/convo.css")
h.AddNotice("convo_dev")
ccount := c.Convos.GetUserCount(user.ID)
page, _ := strconv.Atoi(r.FormValue("page"))
offset, page, lastPage := c.PageOffset(ccount, page, c.Config.ItemsPerPage)
pageList := c.Paginate(page, lastPage, 5)
convos, err := c.Convos.GetUserExtra(user.ID, offset)
//log.Printf("convos: %+v\n", convos)
if err != sql.ErrNoRows && err != nil {
return c.InternalError(err, w, r)
}
var cRows []c.ConvoListRow
for _, convo := range convos {
var parti []*c.User
notMe := false
for _, u := range convo.Users {
if u.ID == user.ID {
continue
}
parti = append(parti, u)
notMe = true
}
if !notMe {
parti = convo.Users
}
cRows = append(cRows, c.ConvoListRow{convo, parti, len(parti) == 1})
}
pi := c.Account{h, "dashboard", "convos", c.ConvoListPage{h, cRows, c.Paginator{pageList, page, lastPage}}}
return renderTemplate("account", w, r, h, pi)
}
func Convo(w http.ResponseWriter, r *http.Request, user *c.User, header *c.Header, scid string) c.RouteError {
accountEditHead("convo", w, r, user, header)
header.AddSheet(header.Theme.Name + "/convo.css")
header.AddNotice("convo_dev")
cid, err := strconv.Atoi(scid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user)
}
convo, err := c.Convos.Get(cid)
if err == sql.ErrNoRows {
return c.NotFound(w, r, header)
} else if err != nil {
return c.InternalError(err, w, r)
}
pcount := convo.PostsCount()
if pcount == 0 {
return c.NotFound(w, r, header)
}
page, _ := strconv.Atoi(r.FormValue("page"))
offset, page, lastPage := c.PageOffset(pcount, page, c.Config.ItemsPerPage)
pageList := c.Paginate(page, lastPage, 5)
posts, err := convo.Posts(offset, c.Config.ItemsPerPage)
// TODO: Report a better error for no posts
if err == sql.ErrNoRows {
return c.NotFound(w, r, header)
} else if err != nil {
return c.InternalError(err, w, r)
}
uids, err := convo.Uids()
if err == sql.ErrNoRows {
return c.NotFound(w, r, header)
} else if err != nil {
return c.InternalError(err, w, r)
}
umap, err := c.Users.BulkGetMap(uids)
if err == sql.ErrNoRows {
return c.NotFound(w, r, header)
} else if err != nil {
return c.InternalError(err, w, r)
}
users := make([]*c.User, len(umap))
i := 0
for _, user := range umap {
users[i] = user
i++
}
pitems := make([]c.ConvoViewRow, len(posts))
for i, post := range posts {
uuser, ok := umap[post.CreatedBy]
if !ok {
return c.InternalError(errors.New("convo post creator not in umap"), w, r)
}
canModify := user.ID == post.CreatedBy || user.IsSuperMod
pitems[i] = c.ConvoViewRow{post, uuser, "", 4, canModify}
}
canReply := user.Perms.UseConvos || user.Perms.UseConvosOnlyWithMod
if !user.Perms.UseConvos && user.Perms.UseConvosOnlyWithMod {
u, err := c.Users.Get(convo.CreatedBy)
if err != nil {
return c.InternalError(err, w, r)
}
if !u.IsSuperMod {
canReply = false
}
}
pi := c.Account{header, "dashboard", "convo", c.ConvoViewPage{header, convo, pitems, users, canReply, c.Paginator{pageList, page, lastPage}}}
return renderTemplate("account", w, r, header, pi)
}
func ConvosCreate(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header) c.RouteError {
accountEditHead("create_convo", w, r, user, h)
if !user.Perms.UseConvos && !user.Perms.UseConvosOnlyWithMod {
return c.NoPermissions(w, r, user)
}
h.AddNotice("convo_dev")
uid, err := strconv.Atoi(r.FormValue("with"))
if err != nil {
return c.LocalError("invalid integer in parameter with", w, r, user)
}
u, err := c.Users.Get(uid)
if err != nil {
return c.LocalError("Unable to fetch user", w, r, user)
}
// TODO: Avoid potential double escape?
pi := c.Account{h, "dashboard", "create_convo", c.ConvoCreatePage{h, html.EscapeString(u.Name)}}
return renderTemplate("account", w, r, h, pi)
}
func ConvosCreateSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError {
_, ferr := c.SimpleUserCheck(w, r, user)
if ferr != nil {
return ferr
}
if !user.Perms.UseConvos && !user.Perms.UseConvosOnlyWithMod {
return c.NoPermissions(w, r, user)
}
sRecps := c.SanitiseSingleLine(r.PostFormValue("recp"))
body := c.PreparseMessage(r.PostFormValue("body"))
rlist := []int{}
// De-dupe recipients
var recps []string
unames := make(map[string]struct{})
for _, recp := range strings.Split(sRecps, ",") {
recp = strings.TrimSpace(recp)
_, exists := unames[recp]
if !exists {
recps = append(recps, recp)
unames[recp] = struct{}{}
}
}
max := 10 // max number of recipients that can be added at once
for i, recp := range recps {
if i >= max {
break
}
u, err := c.Users.GetByName(recp)
if err == sql.ErrNoRows {
return c.LocalError("One of the recipients doesn't exist", w, r, user)
} else if err != nil {
return c.InternalError(err, w, r)
}
// TODO: Should we kick them out of existing conversations if they're moved into a group without permission or the permission is revoked from their group? We might want to give them a chance to delete their messages though to avoid privacy headaches here and it may only be temporarily to tackle a specific incident.
if !u.Perms.UseConvos && !u.Perms.UseConvosOnlyWithMod {
return c.LocalError("One of the recipients doesn't have permission to use the conversations system", w, r, user)
}
if !user.Perms.UseConvos && !u.IsSuperMod && user.Perms.UseConvosOnlyWithMod {
return c.LocalError("You are only allowed to message global moderators.", w, r, user)
}
if !user.IsSuperMod && !u.Perms.UseConvos && u.Perms.UseConvosOnlyWithMod {
return c.LocalError("One of the recipients doesn't have permission to engage with conversation with you.", w, r, user)
}
blocked, err := c.UserBlocks.IsBlockedBy(u.ID, user.ID)
if err != nil {
return c.InternalError(err, w, r)
}
// Supermods can bypass blocks so they can tell people off when they do something stupid or have to convey important information
if blocked && !user.IsSuperMod {
return c.LocalError("You don't have permission to send messages to one of these users.", w, r, user)
}
rlist = append(rlist, u.ID)
}
cid, err := c.Convos.Create(body, user.ID, rlist)
if err != nil {
return c.InternalError(err, w, r)
}
// TODO: Don't bother making the subscription if the convo creator is the only recipient?
for _, uid := range rlist {
if uid == user.ID {
continue
}
err := c.Subscriptions.Add(uid, cid, "convo")
if err != nil {
return c.InternalError(err, w, r)
}
}
err = c.Subscriptions.Add(user.ID, cid, "convo")
if err != nil {
return c.InternalError(err, w, r)
}
err = c.AddActivityAndNotifyAll(c.Alert{ActorID: user.ID, Event: "create", ElementType: "convo", ElementID: cid, Actor: user})
if err != nil {
return c.InternalError(err, w, r)
}
http.Redirect(w, r, "/user/convo/"+strconv.Itoa(cid), http.StatusSeeOther)
return nil
}
/*func ConvosDeleteSubmit(w http.ResponseWriter, r *http.Request, user *c.User, scid string) c.RouteError {
_, ferr := c.SimpleUserCheck(w, r, user)
if ferr != nil {
return ferr
}
cid, err := strconv.Atoi(scid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user)
}
if err := c.Convos.Delete(cid); err != nil {
return c.InternalError(err, w, r)
}
http.Redirect(w, r, "/user/convos/", http.StatusSeeOther)
return nil
}*/
func ConvosCreateReplySubmit(w http.ResponseWriter, r *http.Request, user *c.User, scid string) c.RouteError {
_, ferr := c.SimpleUserCheck(w, r, user)
if ferr != nil {
return ferr
}
if !user.Perms.UseConvos && !user.Perms.UseConvosOnlyWithMod {
return c.NoPermissions(w, r, user)
}
cid, err := strconv.Atoi(scid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user)
}
convo, err := c.Convos.Get(cid)
if err == sql.ErrNoRows {
return c.NotFound(w, r, nil)
} else if err != nil {
return c.InternalError(err, w, r)
}
pcount := convo.PostsCount()
if pcount == 0 {
return c.NotFound(w, r, nil)
}
if !convo.Has(user.ID) {
return c.LocalError("You are not in this conversation.", w, r, user)
}
// TODO: Let the user reply if they're the convo creator in a convo with a mod
if !user.Perms.UseConvos && user.Perms.UseConvosOnlyWithMod {
u, err := c.Users.Get(convo.CreatedBy)
if err != nil {
return c.InternalError(err, w, r)
}
if !u.IsSuperMod {
return c.LocalError("You're only allowed to talk to global moderators.", w, r, user)
}
}
body := c.PreparseMessage(r.PostFormValue("content"))
post := &c.ConversationPost{CID: cid, Body: body, CreatedBy: user.ID}
pid, err := post.Create()
if err != nil {
return c.InternalError(err, w, r)
}
err = c.AddActivityAndNotifyAll(c.Alert{ActorID: user.ID, Event: "reply", ElementType: "convo", ElementID: cid, Actor: user, Extra: strconv.Itoa(pid)})
if err != nil {
return c.InternalError(err, w, r)
}
http.Redirect(w, r, "/user/convo/"+strconv.Itoa(convo.ID), http.StatusSeeOther)
return nil
}
func ConvosDeleteReplySubmit(w http.ResponseWriter, r *http.Request, user *c.User, scpid string) c.RouteError {
_, ferr := c.SimpleUserCheck(w, r, user)
if ferr != nil {
return ferr
}
cpid, err := strconv.Atoi(scpid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user)
}
post := &c.ConversationPost{ID: cpid}
err = post.Fetch()
if err == sql.ErrNoRows {
return c.NotFound(w, r, nil)
} else if err != nil {
return c.InternalError(err, w, r)
}
convo, err := c.Convos.Get(post.CID)
if err == sql.ErrNoRows {
return c.NotFound(w, r, nil)
} else if err != nil {
return c.InternalError(err, w, r)
}
pcount := convo.PostsCount()
if pcount == 0 {
return c.NotFound(w, r, nil)
}
if user.ID != post.CreatedBy && !user.IsSuperMod {
return c.NoPermissions(w, r, user)
}
posts, err := convo.Posts(0, c.Config.ItemsPerPage)
// TODO: Report a better error for no posts
if err == sql.ErrNoRows {
return c.NotFound(w, r, nil)
} else if err != nil {
return c.InternalError(err, w, r)
}
if post.ID == posts[0].ID {
err = c.Convos.Delete(convo.ID)
} else {
err = post.Delete()
}
if err != nil {
return c.InternalError(err, w, r)
}
http.Redirect(w, r, "/user/convo/"+strconv.Itoa(post.CID), http.StatusSeeOther)
return nil
}
func ConvosEditReplySubmit(w http.ResponseWriter, r *http.Request, user *c.User, scpid string) c.RouteError {
_, ferr := c.SimpleUserCheck(w, r, user)
if ferr != nil {
return ferr
}
cpid, err := strconv.Atoi(scpid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user)
}
if !user.Perms.UseConvos {
return c.NoPermissions(w, r, user)
}
js := r.PostFormValue("js") == "1"
post := &c.ConversationPost{ID: cpid}
err = post.Fetch()
if err == sql.ErrNoRows {
return c.NotFound(w, r, nil)
} else if err != nil {
return c.InternalError(err, w, r)
}
convo, err := c.Convos.Get(post.CID)
if err == sql.ErrNoRows {
return c.NotFound(w, r, nil)
} else if err != nil {
return c.InternalError(err, w, r)
}
pcount := convo.PostsCount()
if pcount == 0 {
return c.NotFound(w, r, nil)
}
if user.ID != post.CreatedBy && !user.IsSuperMod {
return c.NoPermissions(w, r, user)
}
if !convo.Has(user.ID) {
return c.LocalError("You are not in this conversation.", w, r, user)
}
post.Body = c.PreparseMessage(r.PostFormValue("edit_item"))
err = post.Update()
if err != nil {
return c.InternalError(err, w, r)
}
return actionSuccess(w, r, "/user/convo/"+strconv.Itoa(post.CID), js)
}
func RelationsBlockCreate(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header, spid string) c.RouteError {
h.Title = p.GetTitlePhrase("create_block")
pid, err := strconv.Atoi(spid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u)
}
puser, err := c.Users.Get(pid)
if err == sql.ErrNoRows {
return c.LocalError("The user you're trying to block doesn't exist.", w, r, u)
} else if err != nil {
return c.InternalError(err, w, r)
}
pi := c.Page{h, nil, c.AreYouSure{"/user/block/create/submit/" + strconv.Itoa(puser.ID), p.GetTmplPhrase("create_block_msg")}}
return renderTemplate("are_you_sure", w, r, h, pi)
}
func RelationsBlockCreateSubmit(w http.ResponseWriter, r *http.Request, u *c.User, spid string) c.RouteError {
pid, err := strconv.Atoi(spid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u)
}
puser, err := c.Users.Get(pid)
if err == sql.ErrNoRows {
return c.LocalError("The user you're trying to block doesn't exist.", w, r, u)
} else if err != nil {
return c.InternalError(err, w, r)
}
if u.ID == puser.ID {
return c.LocalError("You can't block yourself.", w, r, u)
}
err = c.UserBlocks.Add(u.ID, puser.ID)
if err != nil {
return c.InternalError(err, w, r)
}
http.Redirect(w, r, "/user/"+strconv.Itoa(puser.ID), http.StatusSeeOther)
return nil
}
func RelationsBlockRemove(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header, spid string) c.RouteError {
h.Title = p.GetTitlePhrase("remove_block")
pid, err := strconv.Atoi(spid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u)
}
puser, err := c.Users.Get(pid)
if err == sql.ErrNoRows {
return c.LocalError("The user you're trying to block doesn't exist.", w, r, u)
} else if err != nil {
return c.InternalError(err, w, r)
}
pi := c.Page{h, nil, c.AreYouSure{"/user/block/remove/submit/" + strconv.Itoa(puser.ID), p.GetTmplPhrase("remove_block_msg")}}
return renderTemplate("are_you_sure", w, r, h, pi)
}
func RelationsBlockRemoveSubmit(w http.ResponseWriter, r *http.Request, u *c.User, spid string) c.RouteError {
pid, err := strconv.Atoi(spid)
if err != nil {
return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u)
}
puser, err := c.Users.Get(pid)
if err == sql.ErrNoRows {
return c.LocalError("The user you're trying to unblock doesn't exist.", w, r, u)
} else if err != nil {
return c.InternalError(err, w, r)
}
err = c.UserBlocks.Remove(u.ID, puser.ID)
if err != nil {
return c.InternalError(err, w, r)
}
http.Redirect(w, r, "/user/"+strconv.Itoa(puser.ID), http.StatusSeeOther)
return nil
}