github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/routes/convos.go (about) 1 package routes 2 3 import ( 4 "database/sql" 5 "errors" 6 "html" 7 "math/rand" 8 "net/http" 9 "strconv" 10 "strings" 11 12 //"log" 13 14 c "github.com/Azareal/Gosora/common" 15 p "github.com/Azareal/Gosora/common/phrases" 16 ) 17 18 func convoNotice(h *c.Header) { 19 //h.AddNotice("convo_dev") 20 c := rand.Intn(3) 21 h.AddNotice("convo_rand_" + strconv.Itoa(c)) 22 } 23 24 func Convos(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header) c.RouteError { 25 accountEditHead("convos", w, r, user, h) 26 h.AddScript("convo.js") 27 h.AddSheet(h.Theme.Name + "/convo.css") 28 convoNotice(h) 29 ccount := c.Convos.GetUserCount(user.ID) 30 page, _ := strconv.Atoi(r.FormValue("page")) 31 offset, page, lastPage := c.PageOffset(ccount, page, c.Config.ItemsPerPage) 32 pageList := c.Paginate(page, lastPage, 5) 33 34 convos, err := c.Convos.GetUserExtra(user.ID, offset) 35 //log.Printf("convos: %+v\n", convos) 36 if err != sql.ErrNoRows && err != nil { 37 return c.InternalError(err, w, r) 38 } 39 40 var cRows []c.ConvoListRow 41 for _, convo := range convos { 42 var parti []*c.User 43 notMe := false 44 for _, u := range convo.Users { 45 if u.ID == user.ID { 46 continue 47 } 48 parti = append(parti, u) 49 notMe = true 50 } 51 if !notMe { 52 parti = convo.Users 53 } 54 cRows = append(cRows, c.ConvoListRow{convo, parti, len(parti) == 1}) 55 } 56 57 pi := c.Account{h, "dashboard", "convos", c.ConvoListPage{h, cRows, c.Paginator{pageList, page, lastPage}}} 58 return renderTemplate("account", w, r, h, pi) 59 } 60 61 func Convo(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header, scid string) c.RouteError { 62 accountEditHead("convo", w, r, user, h) 63 h.AddSheet(h.Theme.Name + "/convo.css") 64 convoNotice(h) 65 cid, err := strconv.Atoi(scid) 66 if err != nil { 67 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user) 68 } 69 convo, err := c.Convos.Get(cid) 70 if err == sql.ErrNoRows { 71 return c.NotFound(w, r, h) 72 } else if err != nil { 73 return c.InternalError(err, w, r) 74 } 75 pcount := convo.PostsCount() 76 if pcount == 0 { 77 return c.NotFound(w, r, h) 78 } 79 80 page, _ := strconv.Atoi(r.FormValue("page")) 81 offset, page, lastPage := c.PageOffset(pcount, page, c.Config.ItemsPerPage) 82 pageList := c.Paginate(page, lastPage, 5) 83 84 posts, err := convo.Posts(offset, c.Config.ItemsPerPage) 85 // TODO: Report a better error for no posts 86 if err == sql.ErrNoRows { 87 return c.NotFound(w, r, h) 88 } else if err != nil { 89 return c.InternalError(err, w, r) 90 } 91 92 uids, err := convo.Uids() 93 if err == sql.ErrNoRows { 94 return c.NotFound(w, r, h) 95 } else if err != nil { 96 return c.InternalError(err, w, r) 97 } 98 umap, err := c.Users.BulkGetMap(uids) 99 if err == sql.ErrNoRows { 100 return c.NotFound(w, r, h) 101 } else if err != nil { 102 return c.InternalError(err, w, r) 103 } 104 users := make([]*c.User, len(umap)) 105 i := 0 106 for _, user := range umap { 107 users[i] = user 108 i++ 109 } 110 111 pitems := make([]c.ConvoViewRow, len(posts)) 112 for i, post := range posts { 113 uuser, ok := umap[post.CreatedBy] 114 if !ok { 115 return c.InternalError(errors.New("convo post creator not in umap"), w, r) 116 } 117 canModify := user.ID == post.CreatedBy || user.IsSuperMod 118 pitems[i] = c.ConvoViewRow{post, uuser, "", 4, canModify} 119 } 120 121 canReply := user.Perms.UseConvos || user.Perms.UseConvosOnlyWithMod 122 if !user.Perms.UseConvos && user.Perms.UseConvosOnlyWithMod { 123 u, err := c.Users.Get(convo.CreatedBy) 124 if err != nil { 125 return c.InternalError(err, w, r) 126 } 127 if !u.IsSuperMod { 128 canReply = false 129 } 130 } 131 132 pi := c.Account{h, "dashboard", "convo", c.ConvoViewPage{h, convo, pitems, users, canReply, c.Paginator{pageList, page, lastPage}}} 133 return renderTemplate("account", w, r, h, pi) 134 } 135 136 func ConvosCreate(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header) c.RouteError { 137 accountEditHead("create_convo", w, r, user, h) 138 if !user.Perms.UseConvos && !user.Perms.UseConvosOnlyWithMod { 139 return c.NoPermissions(w, r, user) 140 } 141 convoNotice(h) 142 uid, err := strconv.Atoi(r.FormValue("with")) 143 if err != nil { 144 return c.LocalError("invalid integer in parameter with", w, r, user) 145 } 146 recp, err := c.Users.Get(uid) 147 if err != nil { 148 return c.LocalError("Unable to fetch user", w, r, user) 149 } 150 // TODO: Avoid potential double escape? 151 pi := c.Account{h, "dashboard", "create_convo", c.ConvoCreatePage{h, html.EscapeString(recp.Name)}} 152 return renderTemplate("account", w, r, h, pi) 153 } 154 155 func ConvosCreateSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 156 _, ferr := c.SimpleUserCheck(w, r, user) 157 if ferr != nil { 158 return ferr 159 } 160 if !user.Perms.UseConvos && !user.Perms.UseConvosOnlyWithMod { 161 return c.NoPermissions(w, r, user) 162 } 163 164 sRecps := c.SanitiseSingleLine(r.PostFormValue("recp")) 165 body := c.PreparseMessage(r.PostFormValue("body")) 166 rlist := []int{} 167 168 // De-dupe recipients 169 var recps []string 170 unames := make(map[string]struct{}) 171 for _, recp := range strings.Split(sRecps, ",") { 172 recp = strings.TrimSpace(recp) 173 _, exists := unames[recp] 174 if !exists { 175 recps = append(recps, recp) 176 unames[recp] = struct{}{} 177 } 178 } 179 180 max := 10 // max number of recipients that can be added at once 181 for i, recp := range recps { 182 if i >= max { 183 break 184 } 185 186 u, err := c.Users.GetByName(recp) 187 if err == sql.ErrNoRows { 188 return c.LocalError("One of the recipients doesn't exist", w, r, user) 189 } else if err != nil { 190 return c.InternalError(err, w, r) 191 } 192 // 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. 193 if !u.Perms.UseConvos && !u.Perms.UseConvosOnlyWithMod { 194 return c.LocalError("One of the recipients doesn't have permission to use the conversations system", w, r, user) 195 } 196 if !user.Perms.UseConvos && !u.IsSuperMod && user.Perms.UseConvosOnlyWithMod { 197 return c.LocalError("You are only allowed to message global moderators.", w, r, user) 198 } 199 if !user.IsSuperMod && !u.Perms.UseConvos && u.Perms.UseConvosOnlyWithMod { 200 return c.LocalError("One of the recipients doesn't have permission to engage with conversation with you.", w, r, user) 201 } 202 blocked, err := c.UserBlocks.IsBlockedBy(u.ID, user.ID) 203 if err != nil { 204 return c.InternalError(err, w, r) 205 } 206 // Supermods can bypass blocks so they can tell people off when they do something stupid or have to convey important information 207 if blocked && !user.IsSuperMod { 208 return c.LocalError("You don't have permission to send messages to one of these users.", w, r, user) 209 } 210 211 rlist = append(rlist, u.ID) 212 } 213 214 cid, err := c.Convos.Create(body, user.ID, rlist) 215 if err != nil { 216 return c.InternalError(err, w, r) 217 } 218 219 // TODO: Don't bother making the subscription if the convo creator is the only recipient? 220 for _, uid := range rlist { 221 if uid == user.ID { 222 continue 223 } 224 err := c.Subscriptions.Add(uid, cid, "convo") 225 if err != nil { 226 return c.InternalError(err, w, r) 227 } 228 } 229 err = c.Subscriptions.Add(user.ID, cid, "convo") 230 if err != nil { 231 return c.InternalError(err, w, r) 232 } 233 234 err = c.AddActivityAndNotifyAll(c.Alert{ActorID: user.ID, Event: "create", ElementType: "convo", ElementID: cid, Actor: user}) 235 if err != nil { 236 return c.InternalError(err, w, r) 237 } 238 239 http.Redirect(w, r, "/user/convo/"+strconv.Itoa(cid), http.StatusSeeOther) 240 return nil 241 } 242 243 /*func ConvosDeleteSubmit(w http.ResponseWriter, r *http.Request, user *c.User, scid string) c.RouteError { 244 _, ferr := c.SimpleUserCheck(w, r, user) 245 if ferr != nil { 246 return ferr 247 } 248 cid, err := strconv.Atoi(scid) 249 if err != nil { 250 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user) 251 } 252 if err := c.Convos.Delete(cid); err != nil { 253 return c.InternalError(err, w, r) 254 } 255 http.Redirect(w, r, "/user/convos/", http.StatusSeeOther) 256 return nil 257 }*/ 258 259 func ConvosCreateReplySubmit(w http.ResponseWriter, r *http.Request, user *c.User, scid string) c.RouteError { 260 _, ferr := c.SimpleUserCheck(w, r, user) 261 if ferr != nil { 262 return ferr 263 } 264 if !user.Perms.UseConvos && !user.Perms.UseConvosOnlyWithMod { 265 return c.NoPermissions(w, r, user) 266 } 267 cid, err := strconv.Atoi(scid) 268 if err != nil { 269 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user) 270 } 271 272 convo, err := c.Convos.Get(cid) 273 if err == sql.ErrNoRows { 274 return c.NotFound(w, r, nil) 275 } else if err != nil { 276 return c.InternalError(err, w, r) 277 } 278 pcount := convo.PostsCount() 279 if pcount == 0 { 280 return c.NotFound(w, r, nil) 281 } 282 if !convo.Has(user.ID) { 283 return c.LocalError("You are not in this conversation.", w, r, user) 284 } 285 // TODO: Let the user reply if they're the convo creator in a convo with a mod 286 if !user.Perms.UseConvos && user.Perms.UseConvosOnlyWithMod { 287 u, err := c.Users.Get(convo.CreatedBy) 288 if err != nil { 289 return c.InternalError(err, w, r) 290 } 291 if !u.IsSuperMod { 292 return c.LocalError("You're only allowed to talk to global moderators.", w, r, user) 293 } 294 } 295 296 body := c.PreparseMessage(r.PostFormValue("content")) 297 post := &c.ConversationPost{CID: cid, Body: body, CreatedBy: user.ID} 298 pid, err := post.Create() 299 if err != nil { 300 return c.InternalError(err, w, r) 301 } 302 err = c.AddActivityAndNotifyAll(c.Alert{ActorID: user.ID, Event: "reply", ElementType: "convo", ElementID: cid, Actor: user, Extra: strconv.Itoa(pid)}) 303 if err != nil { 304 return c.InternalError(err, w, r) 305 } 306 307 http.Redirect(w, r, "/user/convo/"+strconv.Itoa(convo.ID), http.StatusSeeOther) 308 return nil 309 } 310 311 func ConvosDeleteReplySubmit(w http.ResponseWriter, r *http.Request, u *c.User, scpid string) c.RouteError { 312 _, ferr := c.SimpleUserCheck(w, r, u) 313 if ferr != nil { 314 return ferr 315 } 316 cpid, err := strconv.Atoi(scpid) 317 if err != nil { 318 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u) 319 } 320 321 post := &c.ConversationPost{ID: cpid} 322 err = post.Fetch() 323 if err == sql.ErrNoRows { 324 return c.NotFound(w, r, nil) 325 } else if err != nil { 326 return c.InternalError(err, w, r) 327 } 328 329 convo, err := c.Convos.Get(post.CID) 330 if err == sql.ErrNoRows { 331 return c.NotFound(w, r, nil) 332 } else if err != nil { 333 return c.InternalError(err, w, r) 334 } 335 pcount := convo.PostsCount() 336 if pcount == 0 { 337 return c.NotFound(w, r, nil) 338 } 339 if u.ID != post.CreatedBy && !u.IsSuperMod { 340 return c.NoPermissions(w, r, u) 341 } 342 343 posts, err := convo.Posts(0, c.Config.ItemsPerPage) 344 // TODO: Report a better error for no posts 345 if err == sql.ErrNoRows { 346 return c.NotFound(w, r, nil) 347 } else if err != nil { 348 return c.InternalError(err, w, r) 349 } 350 351 if post.ID == posts[0].ID { 352 err = c.Convos.Delete(convo.ID) 353 } else { 354 err = post.Delete() 355 } 356 if err != nil { 357 return c.InternalError(err, w, r) 358 } 359 360 http.Redirect(w, r, "/user/convo/"+strconv.Itoa(post.CID), http.StatusSeeOther) 361 return nil 362 } 363 364 func ConvosEditReplySubmit(w http.ResponseWriter, r *http.Request, user *c.User, scpid string) c.RouteError { 365 _, ferr := c.SimpleUserCheck(w, r, user) 366 if ferr != nil { 367 return ferr 368 } 369 cpid, err := strconv.Atoi(scpid) 370 if err != nil { 371 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, user) 372 } 373 if !user.Perms.UseConvos { 374 return c.NoPermissions(w, r, user) 375 } 376 js := r.PostFormValue("js") == "1" 377 378 post := &c.ConversationPost{ID: cpid} 379 err = post.Fetch() 380 if err == sql.ErrNoRows { 381 return c.NotFound(w, r, nil) 382 } else if err != nil { 383 return c.InternalError(err, w, r) 384 } 385 386 convo, err := c.Convos.Get(post.CID) 387 if err == sql.ErrNoRows { 388 return c.NotFound(w, r, nil) 389 } else if err != nil { 390 return c.InternalError(err, w, r) 391 } 392 pcount := convo.PostsCount() 393 if pcount == 0 { 394 return c.NotFound(w, r, nil) 395 } 396 if user.ID != post.CreatedBy && !user.IsSuperMod { 397 return c.NoPermissions(w, r, user) 398 } 399 if !convo.Has(user.ID) { 400 return c.LocalError("You are not in this conversation.", w, r, user) 401 } 402 403 post.Body = c.PreparseMessage(r.PostFormValue("edit_item")) 404 err = post.Update() 405 if err != nil { 406 return c.InternalError(err, w, r) 407 } 408 return actionSuccess(w, r, "/user/convo/"+strconv.Itoa(post.CID), js) 409 } 410 411 func RelationsBlockCreate(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header, spid string) c.RouteError { 412 h.Title = p.GetTitlePhrase("create_block") 413 pid, err := strconv.Atoi(spid) 414 if err != nil { 415 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u) 416 } 417 puser, err := c.Users.Get(pid) 418 if err == sql.ErrNoRows { 419 return c.LocalError("The user you're trying to block doesn't exist.", w, r, u) 420 } else if err != nil { 421 return c.InternalError(err, w, r) 422 } 423 424 pi := c.Page{h, nil, c.AreYouSure{"/user/block/create/submit/" + strconv.Itoa(puser.ID), p.GetTmplPhrase("create_block_msg")}} 425 return renderTemplate("are_you_sure", w, r, h, pi) 426 } 427 428 func RelationsBlockCreateSubmit(w http.ResponseWriter, r *http.Request, u *c.User, spid string) c.RouteError { 429 pid, err := strconv.Atoi(spid) 430 if err != nil { 431 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u) 432 } 433 puser, err := c.Users.Get(pid) 434 if err == sql.ErrNoRows { 435 return c.LocalError("The user you're trying to block doesn't exist.", w, r, u) 436 } else if err != nil { 437 return c.InternalError(err, w, r) 438 } 439 if u.ID == puser.ID { 440 return c.LocalError("You can't block yourself.", w, r, u) 441 } 442 443 err = c.UserBlocks.Add(u.ID, puser.ID) 444 if err != nil { 445 return c.InternalError(err, w, r) 446 } 447 448 http.Redirect(w, r, "/user/"+strconv.Itoa(puser.ID), http.StatusSeeOther) 449 return nil 450 } 451 452 func RelationsBlockRemove(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header, spid string) c.RouteError { 453 h.Title = p.GetTitlePhrase("remove_block") 454 pid, err := strconv.Atoi(spid) 455 if err != nil { 456 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u) 457 } 458 puser, err := c.Users.Get(pid) 459 if err == sql.ErrNoRows { 460 return c.LocalError("The user you're trying to block doesn't exist.", w, r, u) 461 } else if err != nil { 462 return c.InternalError(err, w, r) 463 } 464 465 pi := c.Page{h, nil, c.AreYouSure{"/user/block/remove/submit/" + strconv.Itoa(puser.ID), p.GetTmplPhrase("remove_block_msg")}} 466 return renderTemplate("are_you_sure", w, r, h, pi) 467 } 468 469 func RelationsBlockRemoveSubmit(w http.ResponseWriter, r *http.Request, u *c.User, spid string) c.RouteError { 470 pid, err := strconv.Atoi(spid) 471 if err != nil { 472 return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u) 473 } 474 puser, err := c.Users.Get(pid) 475 if err == sql.ErrNoRows { 476 return c.LocalError("The user you're trying to unblock doesn't exist.", w, r, u) 477 } else if err != nil { 478 return c.InternalError(err, w, r) 479 } 480 481 err = c.UserBlocks.Remove(u.ID, puser.ID) 482 if err != nil { 483 return c.InternalError(err, w, r) 484 } 485 486 http.Redirect(w, r, "/user/"+strconv.Itoa(puser.ID), http.StatusSeeOther) 487 return nil 488 }