Add option to delete all of a user's posts.

Add limited dynanicism to langf.
Eliminate a few queries when updating user stats.
Minor clean-up.

Namespace the profile language strings.
This commit is contained in:
Azareal 2020-01-14 15:07:00 +10:00
parent a569772e9c
commit 69a2430e5a
26 changed files with 535 additions and 298 deletions

View File

@ -37,6 +37,7 @@ type ForumStore interface {
Delete(id int) error Delete(id int) error
AddTopic(tid int, uid int, fid int) error AddTopic(tid int, uid int, fid int) error
RemoveTopic(fid int) error RemoveTopic(fid int) error
RemoveTopics(fid, count int) error
UpdateLastTopic(tid int, uid int, fid int) error UpdateLastTopic(tid int, uid int, fid int) error
Exists(id int) bool Exists(id int) bool
GetAll() ([]*Forum, error) GetAll() ([]*Forum, error)
@ -45,7 +46,7 @@ type ForumStore interface {
GetAllVisibleIDs() ([]int, error) GetAllVisibleIDs() ([]int, error)
//GetChildren(parentID int, parentType string) ([]*Forum,error) //GetChildren(parentID int, parentType string) ([]*Forum,error)
//GetFirstChild(parentID int, parentType string) (*Forum,error) //GetFirstChild(parentID int, parentType string) (*Forum,error)
Create(forumName string, forumDesc string, active bool, preset string) (int, error) Create(name string, desc string, active bool, preset string) (int, error)
UpdateOrder(updateMap map[int]int) error UpdateOrder(updateMap map[int]int) error
Count() int Count() int
@ -53,7 +54,7 @@ type ForumStore interface {
type ForumCache interface { type ForumCache interface {
CacheGet(id int) (*Forum, error) CacheGet(id int) (*Forum, error)
CacheSet(forum *Forum) error CacheSet(f *Forum) error
CacheDelete(id int) CacheDelete(id int)
Length() int Length() int
} }
@ -311,21 +312,18 @@ func (s *MemoryForumStore) AddTopic(tid int, uid int, fid int) error {
return s.Reload(fid) return s.Reload(fid)
} }
// TODO: Make this update more atomic func (s *MemoryForumStore) RefreshTopic(fid int) (err error) {
func (s *MemoryForumStore) RemoveTopic(fid int) error {
_, err := s.removeTopics.Exec(1, fid)
if err != nil {
return err
}
var tid int var tid int
err = s.lastTopic.QueryRow(fid).Scan(&tid) err = s.lastTopic.QueryRow(fid).Scan(&tid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
f, err := s.CacheGet(fid)
if err != nil || f.LastTopicID != 0 {
_, err = s.updateCache.Exec(0, 0, fid) _, err = s.updateCache.Exec(0, 0, fid)
if err != nil { if err != nil {
return err return err
} }
s.Reload(fid) s.Reload(fid)
}
return nil return nil
} }
if err != nil { if err != nil {
@ -345,6 +343,22 @@ func (s *MemoryForumStore) RemoveTopic(fid int) error {
return nil return nil
} }
// TODO: Make this update more atomic
func (s *MemoryForumStore) RemoveTopic(fid int) error {
_, err := s.removeTopics.Exec(1, fid)
if err != nil {
return err
}
return s.RefreshTopic(fid)
}
func (s *MemoryForumStore) RemoveTopics(fid int, count int) error {
_, err := s.removeTopics.Exec(count, fid)
if err != nil {
return err
}
return s.RefreshTopic(fid)
}
// DEPRECATED. forum.Update() will be the way to do this in the future, once it's completed // DEPRECATED. forum.Update() will be the way to do this in the future, once it's completed
// TODO: Have a pointer to the last topic rather than storing it on the forum itself // TODO: Have a pointer to the last topic rather than storing it on the forum itself
func (s *MemoryForumStore) UpdateLastTopic(tid int, uid int, fid int) error { func (s *MemoryForumStore) UpdateLastTopic(tid int, uid int, fid int) error {

View File

@ -141,7 +141,7 @@ func (s *DefaultPageStore) GetByName(name string) (*CustomPage, error) {
return p, s.parseAllowedGroups(rawAllowedGroups, p) return p, s.parseAllowedGroups(rawAllowedGroups, p)
} }
func (s *DefaultPageStore) GetOffset(offset int, perPage int) (pages []*CustomPage, err error) { func (s *DefaultPageStore) GetOffset(offset, perPage int) (pages []*CustomPage, err error) {
rows, err := s.getOffset.Query(offset, perPage) rows, err := s.getOffset.Query(offset, perPage)
if err != nil { if err != nil {
return pages, err return pages, err

View File

@ -403,7 +403,7 @@ func AddHashLinkType(prefix string, handler func(*strings.Builder, string, *int)
hashLinkTypes[prefix[0]] = prefix hashLinkTypes[prefix[0]] = prefix
} }
func WriteURL(sb *strings.Builder, url string, label string) { func WriteURL(sb *strings.Builder, url, label string) {
sb.Write(URLOpen) sb.Write(URLOpen)
sb.WriteString(url) sb.WriteString(url)
sb.Write(URLOpen2) sb.Write(URLOpen2)

View File

@ -126,7 +126,7 @@ func InitPhrases(lang string) error {
langPack.TmplIndicesToPhrases = make([][][]byte, len(langTmplIndicesToNames)) langPack.TmplIndicesToPhrases = make([][][]byte, len(langTmplIndicesToNames))
for tmplID, phraseNames := range langTmplIndicesToNames { for tmplID, phraseNames := range langTmplIndicesToNames {
var phraseSet = make([][]byte, len(phraseNames)) phraseSet := make([][]byte, len(phraseNames))
for index, phraseName := range phraseNames { for index, phraseName := range phraseNames {
phrase, ok := langPack.TmplPhrases[phraseName] phrase, ok := langPack.TmplPhrases[phraseName]
if !ok { if !ok {

View File

@ -32,8 +32,8 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
ur := "users_replies" ur := "users_replies"
profileReplyStmts = ProfileReplyStmts{ profileReplyStmts = ProfileReplyStmts{
edit: acc.Update(ur).Set("content = ?, parsed_content = ?").Where("rid = ?").Prepare(), edit: acc.Update(ur).Set("content=?,parsed_content=?").Where("rid=?").Prepare(),
delete: acc.Delete(ur).Where("rid = ?").Prepare(), delete: acc.Delete(ur).Where("rid=?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -45,6 +45,7 @@ func BlankProfileReply(id int) *ProfileReply {
} }
// TODO: Write tests for this // TODO: Write tests for this
// TODO: Remove alerts.
func (r *ProfileReply) Delete() error { func (r *ProfileReply) Delete() error {
_, err := profileReplyStmts.delete.Exec(r.ID) _, err := profileReplyStmts.delete.Exec(r.ID)
return err return err

View File

@ -67,13 +67,14 @@ type ReplyStmts struct {
func init() { func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
re := "replies"
replyStmts = ReplyStmts{ replyStmts = ReplyStmts{
isLiked: acc.Select("likes").Columns("targetItem").Where("sentBy = ? and targetItem = ? and targetType='replies'").Prepare(), isLiked: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='replies'").Prepare(),
createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy").Fields("?,?,?,?").Prepare(), createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy").Fields("?,?,?,?").Prepare(),
edit: acc.Update("replies").Set("content = ?, parsed_content = ?").Where("rid = ? AND poll = 0").Prepare(), edit: acc.Update(re).Set("content = ?, parsed_content = ?").Where("rid = ? AND poll = 0").Prepare(),
setPoll: acc.Update("replies").Set("poll = ?").Where("rid = ? AND poll = 0").Prepare(), setPoll: acc.Update(re).Set("poll = ?").Where("rid = ? AND poll = 0").Prepare(),
delete: acc.Delete("replies").Where("rid = ?").Prepare(), delete: acc.Delete(re).Where("rid = ?").Prepare(),
addLikesToReply: acc.Update("replies").Set("likeCount = likeCount + ?").Where("rid = ?").Prepare(), addLikesToReply: acc.Update(re).Set("likeCount = likeCount + ?").Where("rid = ?").Prepare(),
removeRepliesFromTopic: acc.Update("topics").Set("postCount = postCount - ?").Where("tid = ?").Prepare(), removeRepliesFromTopic: acc.Update("topics").Set("postCount = postCount - ?").Where("tid = ?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
@ -105,6 +106,8 @@ func (r *Reply) Like(uid int) (err error) {
return err return err
} }
// TODO: Refresh topic list?
// TODO: Remove alerts.
func (r *Reply) Delete() error { func (r *Reply) Delete() error {
_, err := replyStmts.delete.Exec(r.ID) _, err := replyStmts.delete.Exec(r.ID)
if err != nil { if err != nil {
@ -112,9 +115,9 @@ func (r *Reply) Delete() error {
} }
// TODO: Move this bit to *Topic // TODO: Move this bit to *Topic
_, err = replyStmts.removeRepliesFromTopic.Exec(1, r.ParentID) _, err = replyStmts.removeRepliesFromTopic.Exec(1, r.ParentID)
tcache := Topics.GetCache() tc := Topics.GetCache()
if tcache != nil { if tc != nil {
tcache.Remove(r.ParentID) tc.Remove(r.ParentID)
} }
_ = Rstore.GetCache().Remove(r.ID) _ = Rstore.GetCache().Remove(r.ID)
return err return err

View File

@ -1,7 +1,7 @@
package common package common
import ( import (
"log" //"log"
"sync" "sync"
"sync/atomic" "sync/atomic"
) )
@ -90,7 +90,7 @@ func (s *MemoryReplyCache) Set(item *Reply) error {
// Add adds a reply to the cache, similar to Set, but it's only intended for new items. This method might be deprecated in the near future, use Set. May return a capacity overflow error. // Add adds a reply to the cache, similar to Set, but it's only intended for new items. This method might be deprecated in the near future, use Set. May return a capacity overflow error.
// ? Is this redundant if we have Set? Are the efficiency wins worth this? Is this even used? // ? Is this redundant if we have Set? Are the efficiency wins worth this? Is this even used?
func (s *MemoryReplyCache) Add(item *Reply) error { func (s *MemoryReplyCache) Add(item *Reply) error {
log.Print("MemoryReplyCache.Add") //log.Print("MemoryReplyCache.Add")
s.Lock() s.Lock()
if int(s.length) >= s.capacity { if int(s.length) >= s.capacity {
s.Unlock() s.Unlock()

View File

@ -1172,10 +1172,10 @@ ArgLoop:
for i := pos + 2; i < len(node.Args); i++ { for i := pos + 2; i < len(node.Args); i++ {
op := node.Args[i].String() op := node.Args[i].String()
if op != "" { if op != "" {
if op[0] == '.' || op[0] == '$' { if /*op[0] == '.' || */op[0] == '$' {
panic("langf args cannot be dynamic") panic("langf args cannot be dynamic")
} }
if op[0] != '"' && !unicode.IsDigit(rune(op[0])) { if op[0] != '.' && op[0] != '"' && !unicode.IsDigit(rune(op[0])) {
break break
} }
olist = append(olist, op) olist = append(olist, op)
@ -1187,6 +1187,14 @@ ArgLoop:
ob := "," ob := ","
for _, op := range olist { for _, op := range olist {
if op[0] == '.' {
param, val3 := c.compileIfVarSub(con, op)
if !val3.IsValid() {
panic("val3 is invalid")
}
ob += param + ","
continue
}
allNum := true allNum := true
for _, o := range op { for _, o := range op {
if !unicode.IsDigit(o) { if !unicode.IsDigit(o) {

View File

@ -43,10 +43,10 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
t := "themes" t := "themes"
themeStmts = ThemeStmts{ themeStmts = ThemeStmts{
getAll: acc.Select(t).Columns("uname, default").Prepare(), getAll: acc.Select(t).Columns("uname,default").Prepare(),
isDefault: acc.Select(t).Columns("default").Where("uname = ?").Prepare(), isDefault: acc.Select(t).Columns("default").Where("uname = ?").Prepare(),
update: acc.Update(t).Set("default = ?").Where("uname = ?").Prepare(), update: acc.Update(t).Set("default = ?").Where("uname = ?").Prepare(),
add: acc.Insert(t).Columns("uname, default").Fields("?,?").Prepare(), add: acc.Insert(t).Columns("uname,default").Fields("?,?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -275,7 +275,7 @@ func ResetTemplateOverrides() {
} }
// CreateThemeTemplate creates a theme template on the current default theme // CreateThemeTemplate creates a theme template on the current default theme
func CreateThemeTemplate(theme string, name string) { func CreateThemeTemplate(theme, name string) {
Themes[theme].TmplPtr[name] = func(pi Page, w http.ResponseWriter) error { Themes[theme].TmplPtr[name] = func(pi Page, w http.ResponseWriter) error {
mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[name] mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[name]
if !ok { if !ok {

View File

@ -339,6 +339,7 @@ func (t *Topic) Delete() error {
return err return err
} }
// TODO: Clear reply cache too
_, err = topicStmts.delete.Exec(t.ID) _, err = topicStmts.delete.Exec(t.ID)
t.cacheRemove() t.cacheRemove()
if err != nil { if err != nil {

View File

@ -123,8 +123,6 @@ type UserStmts struct {
delete *sql.Stmt delete *sql.Stmt
setAvatar *sql.Stmt setAvatar *sql.Stmt
setName *sql.Stmt setName *sql.Stmt
incTopics *sql.Stmt
updateLevel *sql.Stmt
update *sql.Stmt update *sql.Stmt
// TODO: Split these into a sub-struct // TODO: Split these into a sub-struct
@ -132,7 +130,13 @@ type UserStmts struct {
incPosts *sql.Stmt incPosts *sql.Stmt
incBigposts *sql.Stmt incBigposts *sql.Stmt
incMegaposts *sql.Stmt incMegaposts *sql.Stmt
incPostStats *sql.Stmt
incBigpostStats *sql.Stmt
incMegapostStats *sql.Stmt
incLiked *sql.Stmt incLiked *sql.Stmt
incTopics *sql.Stmt
updateLevel *sql.Stmt
resetStats *sql.Stmt
decLiked *sql.Stmt decLiked *sql.Stmt
updateLastIP *sql.Stmt updateLastIP *sql.Stmt
@ -141,6 +145,10 @@ type UserStmts struct {
setPassword *sql.Stmt setPassword *sql.Stmt
scheduleAvatarResize *sql.Stmt scheduleAvatarResize *sql.Stmt
deletePosts *sql.Stmt
deleteProfilePosts *sql.Stmt
deleteReplyPosts *sql.Stmt
} }
var userStmts UserStmts var userStmts UserStmts
@ -155,14 +163,19 @@ func init() {
delete: acc.Delete(u).Where(w).Prepare(), delete: acc.Delete(u).Where(w).Prepare(),
setAvatar: acc.Update(u).Set("avatar=?").Where(w).Prepare(), setAvatar: acc.Update(u).Set("avatar=?").Where(w).Prepare(),
setName: acc.Update(u).Set("name=?").Where(w).Prepare(), setName: acc.Update(u).Set("name=?").Where(w).Prepare(),
incTopics: acc.SimpleUpdate(u, "topics=topics+?", w),
updateLevel: acc.SimpleUpdate(u, "level=?", w),
update: acc.Update(u).Set("name=?,email=?,group=?").Where(w).Prepare(), // TODO: Implement user_count for users_groups on things which use this update: acc.Update(u).Set("name=?,email=?,group=?").Where(w).Prepare(), // TODO: Implement user_count for users_groups on things which use this
incScore: acc.Update(u).Set("score=score+?").Where(w).Prepare(), incScore: acc.Update(u).Set("score=score+?").Where(w).Prepare(),
incPosts: acc.Update(u).Set("posts=posts+?").Where(w).Prepare(), incPosts: acc.Update(u).Set("posts=posts+?").Where(w).Prepare(),
incBigposts: acc.Update(u).Set("posts=posts+?,bigposts=bigposts+?").Where(w).Prepare(), incBigposts: acc.Update(u).Set("posts=posts+?,bigposts=bigposts+?").Where(w).Prepare(),
incMegaposts: acc.Update(u).Set("posts=posts+?,bigposts=bigposts+?,megaposts=megaposts+?").Where(w).Prepare(), incMegaposts: acc.Update(u).Set("posts=posts+?,bigposts=bigposts+?,megaposts=megaposts+?").Where(w).Prepare(),
incPostStats: acc.Update(u).Set("posts=posts+?,score=score+?,level=?").Where(w).Prepare(),
incBigpostStats: acc.Update(u).Set("posts=posts+?,bigposts=bigposts+?,score=score+?,level=?").Where(w).Prepare(),
incMegapostStats: acc.Update(u).Set("posts=posts+?,bigposts=bigposts+?,megaposts=megaposts+?,score=score+?,level=?").Where(w).Prepare(),
incTopics: acc.SimpleUpdate(u, "topics=topics+?", w),
updateLevel: acc.SimpleUpdate(u, "level=?", w),
resetStats: acc.Update(u).Set("score=0,posts=0,bigposts=0,megaposts=0,topics=0,level=0").Where(w).Prepare(),
incLiked: acc.Update(u).Set("liked=liked+?,lastLiked=UTC_TIMESTAMP()").Where(w).Prepare(), incLiked: acc.Update(u).Set("liked=liked+?,lastLiked=UTC_TIMESTAMP()").Where(w).Prepare(),
decLiked: acc.Update(u).Set("liked=liked-?").Where(w).Prepare(), decLiked: acc.Update(u).Set("liked=liked-?").Where(w).Prepare(),
//recalcLastLiked: acc... //recalcLastLiked: acc...
@ -172,6 +185,10 @@ func init() {
setPassword: acc.Update(u).Set("password=?,salt=?").Where(w).Prepare(), setPassword: acc.Update(u).Set("password=?,salt=?").Where(w).Prepare(),
scheduleAvatarResize: acc.Insert("users_avatar_queue").Columns("uid").Fields("?").Prepare(), scheduleAvatarResize: acc.Insert("users_avatar_queue").Columns("uid").Fields("?").Prepare(),
deletePosts: acc.Select("topics").Columns("tid,parentID").Where("createdBy=?").Prepare(),
deleteProfilePosts: acc.Select("users_replies").Columns("rid").Where("createdBy=?").Prepare(),
deleteReplyPosts: acc.Select("replies").Columns("rid,tid").Where("createdBy=?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -302,6 +319,107 @@ func (u *User) Delete() error {
return nil return nil
} }
func (u *User) DeletePosts() error {
rows, err := userStmts.deletePosts.Query(u.ID)
if err != nil {
return err
}
defer rows.Close()
defer TopicListThaw.Thaw()
defer u.CacheRemove()
updatedForums := make(map[int]int) // forum[count]
tc := Topics.GetCache()
for rows.Next() {
var tid, parentID int
err := rows.Scan(&tid, &parentID)
if err != nil {
return err
}
// TODO: Clear reply cache too
_, err = topicStmts.delete.Exec(tid)
if tc != nil {
tc.Remove(tid)
}
if err != nil {
return err
}
updatedForums[parentID] = updatedForums[parentID] + 1
_, err = topicStmts.deleteActivitySubs.Exec(tid)
if err != nil {
return err
}
_, err = topicStmts.deleteActivity.Exec(tid)
if err != nil {
return err
}
}
if err = rows.Err(); err != nil {
return err
}
err = u.ResetPostStats()
if err != nil {
return err
}
for fid, count := range updatedForums {
err := Forums.RemoveTopics(fid, count)
if err != nil && err != ErrNoRows {
return err
}
}
rows, err = userStmts.deleteProfilePosts.Query(u.ID)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var rid int
err := rows.Scan(&rid)
if err != nil {
return err
}
_, err = profileReplyStmts.delete.Exec(rid)
if err != nil {
return err
}
// TODO: Remove alerts.
}
err = rows.Err()
if err != nil {
return err
}
rows, err = userStmts.deleteReplyPosts.Query(u.ID)
if err != nil {
return err
}
defer rows.Close()
rc := Rstore.GetCache()
for rows.Next() {
var rid, tid int
err := rows.Scan(&rid,&tid)
if err != nil {
return err
}
_, err = replyStmts.delete.Exec(rid)
if err != nil {
return err
}
// TODO: Move this bit to *Topic
_, err = replyStmts.removeRepliesFromTopic.Exec(1, tid)
if tc != nil {
tc.Remove(tid)
}
_ = rc.Remove(rid)
// TODO: Remove alerts.
}
return rows.Err()
}
func (u *User) bindStmt(stmt *sql.Stmt, params ...interface{}) (err error) { func (u *User) bindStmt(stmt *sql.Stmt, params ...interface{}) (err error) {
params = append(params, u.ID) params = append(params, u.ID)
_, err = stmt.Exec(params...) _, err = stmt.Exec(params...)
@ -339,7 +457,7 @@ func (u *User) ChangeGroup(group int) (err error) {
} }
func (u *User) GetIP() string { func (u *User) GetIP() string {
spl := strings.Split(u.LastIP,"-") spl := strings.Split(u.LastIP, "-")
return spl[len(spl)-1] return spl[len(spl)-1]
} }
@ -365,7 +483,6 @@ func (u *User) Update(name, email string, group int) (err error) {
} }
func (u *User) IncreasePostStats(wcount int, topic bool) (err error) { func (u *User) IncreasePostStats(wcount int, topic bool) (err error) {
var mod int
baseScore := 1 baseScore := 1
if topic { if topic {
_, err = userStmts.incTopics.Exec(1, u.ID) _, err = userStmts.incTopics.Exec(1, u.ID)
@ -376,38 +493,28 @@ func (u *User) IncreasePostStats(wcount int, topic bool) (err error) {
} }
settings := SettingBox.Load().(SettingMap) settings := SettingBox.Load().(SettingMap)
var mod, level int
if wcount >= settings["megapost_min_words"].(int) { if wcount >= settings["megapost_min_words"].(int) {
_, err = userStmts.incMegaposts.Exec(1, 1, 1, u.ID)
mod = 4 mod = 4
level = GetLevel(u.Score + baseScore + mod)
_, err = userStmts.incMegapostStats.Exec(1, 1, 1, baseScore+mod, level, u.ID)
} else if wcount >= settings["bigpost_min_words"].(int) { } else if wcount >= settings["bigpost_min_words"].(int) {
_, err = userStmts.incBigposts.Exec(1, 1, u.ID)
mod = 1 mod = 1
level = GetLevel(u.Score + baseScore + mod)
_, err = userStmts.incBigpostStats.Exec(1, 1, baseScore+mod, level, u.ID)
} else { } else {
_, err = userStmts.incPosts.Exec(1, u.ID) level = GetLevel(u.Score + baseScore + mod)
_, err = userStmts.incPostStats.Exec(1, baseScore+mod, level, u.ID)
} }
if err != nil { if err != nil {
return err return err
} }
_, err = userStmts.incScore.Exec(baseScore+mod, u.ID)
if err != nil {
return err
}
//log.Print(u.Score + baseScore + mod)
// TODO: Use a transaction to prevent level desyncs?
level := GetLevel(u.Score + baseScore + mod)
//log.Print(level)
_, err = userStmts.updateLevel.Exec(level, u.ID)
if err != nil {
return err
}
err = GroupPromotions.PromoteIfEligible(u, level, u.Posts+1) err = GroupPromotions.PromoteIfEligible(u, level, u.Posts+1)
u.CacheRemove() u.CacheRemove()
return err return err
} }
func (u *User) DecreasePostStats(wcount int, topic bool) (err error) { func (u *User) DecreasePostStats(wcount int, topic bool) (err error) {
var mod int
baseScore := -1 baseScore := -1
if topic { if topic {
_, err = userStmts.incTopics.Exec(-1, u.ID) _, err = userStmts.incTopics.Exec(-1, u.ID)
@ -417,26 +524,24 @@ func (u *User) DecreasePostStats(wcount int, topic bool) (err error) {
baseScore = -2 baseScore = -2
} }
// TODO: Use a transaction to prevent level desyncs?
var mod int
settings := SettingBox.Load().(SettingMap) settings := SettingBox.Load().(SettingMap)
if wcount >= settings["megapost_min_words"].(int) { if wcount >= settings["megapost_min_words"].(int) {
_, err = userStmts.incMegaposts.Exec(-1, -1, -1, u.ID)
mod = 4 mod = 4
_, err = userStmts.incMegapostStats.Exec(-1, -1, -1, baseScore-mod, GetLevel(u.Score-baseScore-mod), u.ID)
} else if wcount >= settings["bigpost_min_words"].(int) { } else if wcount >= settings["bigpost_min_words"].(int) {
_, err = userStmts.incBigposts.Exec(-1, -1, u.ID)
mod = 1 mod = 1
_, err = userStmts.incBigpostStats.Exec(-1, -1, baseScore-mod, GetLevel(u.Score-baseScore-mod), u.ID)
} else { } else {
_, err = userStmts.incPosts.Exec(-1, u.ID) _, err = userStmts.incPostStats.Exec(-1, baseScore-mod, GetLevel(u.Score-baseScore-mod), u.ID)
} }
if err != nil { u.CacheRemove()
return err return err
} }
_, err = userStmts.incScore.Exec(baseScore-mod, u.ID) func (u *User) ResetPostStats() (err error) {
if err != nil { _, err = userStmts.resetStats.Exec(u.ID)
return err
}
// TODO: Use a transaction to prevent level desyncs?
_, err = userStmts.updateLevel.Exec(GetLevel(u.Score-baseScore-mod), u.ID)
u.CacheRemove() u.CacheRemove()
return err return err
} }

View File

@ -119,7 +119,7 @@ func (s *DefaultUserStore) GetByName(name string) (*User, error) {
// TODO: Optimise this, so we don't wind up hitting the database every-time for small gaps // TODO: Optimise this, so we don't wind up hitting the database every-time for small gaps
// TODO: Make this a little more consistent with DefaultGroupStore's GetRange method // TODO: Make this a little more consistent with DefaultGroupStore's GetRange method
func (s *DefaultUserStore) GetOffset(offset int, perPage int) (users []*User, err error) { func (s *DefaultUserStore) GetOffset(offset, perPage int) (users []*User, err error) {
rows, err := s.getOffset.Query(offset, perPage) rows, err := s.getOffset.Query(offset, perPage)
if err != nil { if err != nil {
return users, err return users, err

View File

@ -276,10 +276,10 @@ func InitWidgets() (fi error) {
return nil return nil
} }
func releaseWidgets(widgets []*Widget) { func releaseWidgets(ws []*Widget) {
for _, widget := range widgets { for _, w := range ws {
if widget.ShutdownFunc != nil { if w.ShutdownFunc != nil {
widget.ShutdownFunc(widget) w.ShutdownFunc(w)
} }
} }
} }

View File

@ -148,6 +148,7 @@ var RouteMap = map[string]interface{}{
"routes.UnbanUser": routes.UnbanUser, "routes.UnbanUser": routes.UnbanUser,
"routes.ActivateUser": routes.ActivateUser, "routes.ActivateUser": routes.ActivateUser,
"routes.IPSearch": routes.IPSearch, "routes.IPSearch": routes.IPSearch,
"routes.DeletePostsSubmit": routes.DeletePostsSubmit,
"routes.CreateTopicSubmit": routes.CreateTopicSubmit, "routes.CreateTopicSubmit": routes.CreateTopicSubmit,
"routes.EditTopicSubmit": routes.EditTopicSubmit, "routes.EditTopicSubmit": routes.EditTopicSubmit,
"routes.DeleteTopicSubmit": routes.DeleteTopicSubmit, "routes.DeleteTopicSubmit": routes.DeleteTopicSubmit,
@ -319,48 +320,49 @@ var routeMapEnum = map[string]int{
"routes.UnbanUser": 122, "routes.UnbanUser": 122,
"routes.ActivateUser": 123, "routes.ActivateUser": 123,
"routes.IPSearch": 124, "routes.IPSearch": 124,
"routes.CreateTopicSubmit": 125, "routes.DeletePostsSubmit": 125,
"routes.EditTopicSubmit": 126, "routes.CreateTopicSubmit": 126,
"routes.DeleteTopicSubmit": 127, "routes.EditTopicSubmit": 127,
"routes.StickTopicSubmit": 128, "routes.DeleteTopicSubmit": 128,
"routes.UnstickTopicSubmit": 129, "routes.StickTopicSubmit": 129,
"routes.LockTopicSubmit": 130, "routes.UnstickTopicSubmit": 130,
"routes.UnlockTopicSubmit": 131, "routes.LockTopicSubmit": 131,
"routes.MoveTopicSubmit": 132, "routes.UnlockTopicSubmit": 132,
"routes.LikeTopicSubmit": 133, "routes.MoveTopicSubmit": 133,
"routes.AddAttachToTopicSubmit": 134, "routes.LikeTopicSubmit": 134,
"routes.RemoveAttachFromTopicSubmit": 135, "routes.AddAttachToTopicSubmit": 135,
"routes.ViewTopic": 136, "routes.RemoveAttachFromTopicSubmit": 136,
"routes.CreateReplySubmit": 137, "routes.ViewTopic": 137,
"routes.ReplyEditSubmit": 138, "routes.CreateReplySubmit": 138,
"routes.ReplyDeleteSubmit": 139, "routes.ReplyEditSubmit": 139,
"routes.ReplyLikeSubmit": 140, "routes.ReplyDeleteSubmit": 140,
"routes.AddAttachToReplySubmit": 141, "routes.ReplyLikeSubmit": 141,
"routes.RemoveAttachFromReplySubmit": 142, "routes.AddAttachToReplySubmit": 142,
"routes.ProfileReplyCreateSubmit": 143, "routes.RemoveAttachFromReplySubmit": 143,
"routes.ProfileReplyEditSubmit": 144, "routes.ProfileReplyCreateSubmit": 144,
"routes.ProfileReplyDeleteSubmit": 145, "routes.ProfileReplyEditSubmit": 145,
"routes.PollVote": 146, "routes.ProfileReplyDeleteSubmit": 146,
"routes.PollResults": 147, "routes.PollVote": 147,
"routes.AccountLogin": 148, "routes.PollResults": 148,
"routes.AccountRegister": 149, "routes.AccountLogin": 149,
"routes.AccountLogout": 150, "routes.AccountRegister": 150,
"routes.AccountLoginSubmit": 151, "routes.AccountLogout": 151,
"routes.AccountLoginMFAVerify": 152, "routes.AccountLoginSubmit": 152,
"routes.AccountLoginMFAVerifySubmit": 153, "routes.AccountLoginMFAVerify": 153,
"routes.AccountRegisterSubmit": 154, "routes.AccountLoginMFAVerifySubmit": 154,
"routes.AccountPasswordReset": 155, "routes.AccountRegisterSubmit": 155,
"routes.AccountPasswordResetSubmit": 156, "routes.AccountPasswordReset": 156,
"routes.AccountPasswordResetToken": 157, "routes.AccountPasswordResetSubmit": 157,
"routes.AccountPasswordResetTokenSubmit": 158, "routes.AccountPasswordResetToken": 158,
"routes.DynamicRoute": 159, "routes.AccountPasswordResetTokenSubmit": 159,
"routes.UploadedFile": 160, "routes.DynamicRoute": 160,
"routes.StaticFile": 161, "routes.UploadedFile": 161,
"routes.RobotsTxt": 162, "routes.StaticFile": 162,
"routes.SitemapXml": 163, "routes.RobotsTxt": 163,
"routes.OpenSearchXml": 164, "routes.SitemapXml": 164,
"routes.BadRoute": 165, "routes.OpenSearchXml": 165,
"routes.HTTPSRedirect": 166, "routes.BadRoute": 166,
"routes.HTTPSRedirect": 167,
} }
var reverseRouteMapEnum = map[int]string{ var reverseRouteMapEnum = map[int]string{
0: "routes.Overview", 0: "routes.Overview",
@ -488,48 +490,49 @@ var reverseRouteMapEnum = map[int]string{
122: "routes.UnbanUser", 122: "routes.UnbanUser",
123: "routes.ActivateUser", 123: "routes.ActivateUser",
124: "routes.IPSearch", 124: "routes.IPSearch",
125: "routes.CreateTopicSubmit", 125: "routes.DeletePostsSubmit",
126: "routes.EditTopicSubmit", 126: "routes.CreateTopicSubmit",
127: "routes.DeleteTopicSubmit", 127: "routes.EditTopicSubmit",
128: "routes.StickTopicSubmit", 128: "routes.DeleteTopicSubmit",
129: "routes.UnstickTopicSubmit", 129: "routes.StickTopicSubmit",
130: "routes.LockTopicSubmit", 130: "routes.UnstickTopicSubmit",
131: "routes.UnlockTopicSubmit", 131: "routes.LockTopicSubmit",
132: "routes.MoveTopicSubmit", 132: "routes.UnlockTopicSubmit",
133: "routes.LikeTopicSubmit", 133: "routes.MoveTopicSubmit",
134: "routes.AddAttachToTopicSubmit", 134: "routes.LikeTopicSubmit",
135: "routes.RemoveAttachFromTopicSubmit", 135: "routes.AddAttachToTopicSubmit",
136: "routes.ViewTopic", 136: "routes.RemoveAttachFromTopicSubmit",
137: "routes.CreateReplySubmit", 137: "routes.ViewTopic",
138: "routes.ReplyEditSubmit", 138: "routes.CreateReplySubmit",
139: "routes.ReplyDeleteSubmit", 139: "routes.ReplyEditSubmit",
140: "routes.ReplyLikeSubmit", 140: "routes.ReplyDeleteSubmit",
141: "routes.AddAttachToReplySubmit", 141: "routes.ReplyLikeSubmit",
142: "routes.RemoveAttachFromReplySubmit", 142: "routes.AddAttachToReplySubmit",
143: "routes.ProfileReplyCreateSubmit", 143: "routes.RemoveAttachFromReplySubmit",
144: "routes.ProfileReplyEditSubmit", 144: "routes.ProfileReplyCreateSubmit",
145: "routes.ProfileReplyDeleteSubmit", 145: "routes.ProfileReplyEditSubmit",
146: "routes.PollVote", 146: "routes.ProfileReplyDeleteSubmit",
147: "routes.PollResults", 147: "routes.PollVote",
148: "routes.AccountLogin", 148: "routes.PollResults",
149: "routes.AccountRegister", 149: "routes.AccountLogin",
150: "routes.AccountLogout", 150: "routes.AccountRegister",
151: "routes.AccountLoginSubmit", 151: "routes.AccountLogout",
152: "routes.AccountLoginMFAVerify", 152: "routes.AccountLoginSubmit",
153: "routes.AccountLoginMFAVerifySubmit", 153: "routes.AccountLoginMFAVerify",
154: "routes.AccountRegisterSubmit", 154: "routes.AccountLoginMFAVerifySubmit",
155: "routes.AccountPasswordReset", 155: "routes.AccountRegisterSubmit",
156: "routes.AccountPasswordResetSubmit", 156: "routes.AccountPasswordReset",
157: "routes.AccountPasswordResetToken", 157: "routes.AccountPasswordResetSubmit",
158: "routes.AccountPasswordResetTokenSubmit", 158: "routes.AccountPasswordResetToken",
159: "routes.DynamicRoute", 159: "routes.AccountPasswordResetTokenSubmit",
160: "routes.UploadedFile", 160: "routes.DynamicRoute",
161: "routes.StaticFile", 161: "routes.UploadedFile",
162: "routes.RobotsTxt", 162: "routes.StaticFile",
163: "routes.SitemapXml", 163: "routes.RobotsTxt",
164: "routes.OpenSearchXml", 164: "routes.SitemapXml",
165: "routes.BadRoute", 165: "routes.OpenSearchXml",
166: "routes.HTTPSRedirect", 166: "routes.BadRoute",
167: "routes.HTTPSRedirect",
} }
var osMapEnum = map[string]int{ var osMapEnum = map[string]int{
"unknown": 0, "unknown": 0,
@ -687,7 +690,7 @@ type HTTPSRedirect struct {}
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
co.RouteViewCounter.Bump(166) co.RouteViewCounter.Bump(167)
dest := "https://" + req.Host + req.URL.String() dest := "https://" + req.Host + req.URL.String()
http.Redirect(w, req, dest, http.StatusTemporaryRedirect) http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
} }
@ -895,7 +898,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
co.GlobalViewCounter.Bump() co.GlobalViewCounter.Bump()
if prefix == "/s" { //old prefix: /static if prefix == "/s" { //old prefix: /static
co.RouteViewCounter.Bump(161) co.RouteViewCounter.Bump(162)
req.URL.Path += extraData req.URL.Path += extraData
routes.StaticFile(w, req) routes.StaticFile(w, req)
return return
@ -2132,6 +2135,19 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
err = routes.IPSearch(w,req,user,head) err = routes.IPSearch(w,req,user,head)
case "/users/delete-posts/submit/":
err = c.NoSessionMismatch(w,req,user)
if err != nil {
return err
}
err = c.MemberOnly(w,req,user)
if err != nil {
return err
}
co.RouteViewCounter.Bump(125)
err = routes.DeletePostsSubmit(w,req,user,extraData)
} }
case "/topic": case "/topic":
switch(req.URL.Path) { switch(req.URL.Path) {
@ -2150,7 +2166,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(125) co.RouteViewCounter.Bump(126)
err = routes.CreateTopicSubmit(w,req,user) err = routes.CreateTopicSubmit(w,req,user)
case "/topic/edit/submit/": case "/topic/edit/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2163,7 +2179,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(126) co.RouteViewCounter.Bump(127)
err = routes.EditTopicSubmit(w,req,user,extraData) err = routes.EditTopicSubmit(w,req,user,extraData)
case "/topic/delete/submit/": case "/topic/delete/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2177,7 +2193,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
} }
req.URL.Path += extraData req.URL.Path += extraData
co.RouteViewCounter.Bump(127) co.RouteViewCounter.Bump(128)
err = routes.DeleteTopicSubmit(w,req,user) err = routes.DeleteTopicSubmit(w,req,user)
case "/topic/stick/submit/": case "/topic/stick/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2190,7 +2206,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(128) co.RouteViewCounter.Bump(129)
err = routes.StickTopicSubmit(w,req,user,extraData) err = routes.StickTopicSubmit(w,req,user,extraData)
case "/topic/unstick/submit/": case "/topic/unstick/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2203,7 +2219,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(129) co.RouteViewCounter.Bump(130)
err = routes.UnstickTopicSubmit(w,req,user,extraData) err = routes.UnstickTopicSubmit(w,req,user,extraData)
case "/topic/lock/submit/": case "/topic/lock/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2217,7 +2233,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
} }
req.URL.Path += extraData req.URL.Path += extraData
co.RouteViewCounter.Bump(130) co.RouteViewCounter.Bump(131)
err = routes.LockTopicSubmit(w,req,user) err = routes.LockTopicSubmit(w,req,user)
case "/topic/unlock/submit/": case "/topic/unlock/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2230,7 +2246,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(131) co.RouteViewCounter.Bump(132)
err = routes.UnlockTopicSubmit(w,req,user,extraData) err = routes.UnlockTopicSubmit(w,req,user,extraData)
case "/topic/move/submit/": case "/topic/move/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2243,7 +2259,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(132) co.RouteViewCounter.Bump(133)
err = routes.MoveTopicSubmit(w,req,user,extraData) err = routes.MoveTopicSubmit(w,req,user,extraData)
case "/topic/like/submit/": case "/topic/like/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2256,7 +2272,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(133) co.RouteViewCounter.Bump(134)
err = routes.LikeTopicSubmit(w,req,user,extraData) err = routes.LikeTopicSubmit(w,req,user,extraData)
case "/topic/attach/add/submit/": case "/topic/attach/add/submit/":
err = c.MemberOnly(w,req,user) err = c.MemberOnly(w,req,user)
@ -2273,7 +2289,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(134) co.RouteViewCounter.Bump(135)
err = routes.AddAttachToTopicSubmit(w,req,user,extraData) err = routes.AddAttachToTopicSubmit(w,req,user,extraData)
case "/topic/attach/remove/submit/": case "/topic/attach/remove/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2286,10 +2302,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(135) co.RouteViewCounter.Bump(136)
err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData) err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData)
default: default:
co.RouteViewCounter.Bump(136) co.RouteViewCounter.Bump(137)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2313,7 +2329,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(137) co.RouteViewCounter.Bump(138)
err = routes.CreateReplySubmit(w,req,user) err = routes.CreateReplySubmit(w,req,user)
case "/reply/edit/submit/": case "/reply/edit/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2326,7 +2342,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(138) co.RouteViewCounter.Bump(139)
err = routes.ReplyEditSubmit(w,req,user,extraData) err = routes.ReplyEditSubmit(w,req,user,extraData)
case "/reply/delete/submit/": case "/reply/delete/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2339,7 +2355,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(139) co.RouteViewCounter.Bump(140)
err = routes.ReplyDeleteSubmit(w,req,user,extraData) err = routes.ReplyDeleteSubmit(w,req,user,extraData)
case "/reply/like/submit/": case "/reply/like/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2352,7 +2368,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(140) co.RouteViewCounter.Bump(141)
err = routes.ReplyLikeSubmit(w,req,user,extraData) err = routes.ReplyLikeSubmit(w,req,user,extraData)
case "/reply/attach/add/submit/": case "/reply/attach/add/submit/":
err = c.MemberOnly(w,req,user) err = c.MemberOnly(w,req,user)
@ -2369,7 +2385,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(141) co.RouteViewCounter.Bump(142)
err = routes.AddAttachToReplySubmit(w,req,user,extraData) err = routes.AddAttachToReplySubmit(w,req,user,extraData)
case "/reply/attach/remove/submit/": case "/reply/attach/remove/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2382,7 +2398,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(142) co.RouteViewCounter.Bump(143)
err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData) err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData)
} }
case "/profile": case "/profile":
@ -2398,7 +2414,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(143) co.RouteViewCounter.Bump(144)
err = routes.ProfileReplyCreateSubmit(w,req,user) err = routes.ProfileReplyCreateSubmit(w,req,user)
case "/profile/reply/edit/submit/": case "/profile/reply/edit/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2411,7 +2427,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(144) co.RouteViewCounter.Bump(145)
err = routes.ProfileReplyEditSubmit(w,req,user,extraData) err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
case "/profile/reply/delete/submit/": case "/profile/reply/delete/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2424,7 +2440,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(145) co.RouteViewCounter.Bump(146)
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
} }
case "/poll": case "/poll":
@ -2440,23 +2456,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(146) co.RouteViewCounter.Bump(147)
err = routes.PollVote(w,req,user,extraData) err = routes.PollVote(w,req,user,extraData)
case "/poll/results/": case "/poll/results/":
co.RouteViewCounter.Bump(147) co.RouteViewCounter.Bump(148)
err = routes.PollResults(w,req,user,extraData) err = routes.PollResults(w,req,user,extraData)
} }
case "/accounts": case "/accounts":
switch(req.URL.Path) { switch(req.URL.Path) {
case "/accounts/login/": case "/accounts/login/":
co.RouteViewCounter.Bump(148) co.RouteViewCounter.Bump(149)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
} }
err = routes.AccountLogin(w,req,user,head) err = routes.AccountLogin(w,req,user,head)
case "/accounts/create/": case "/accounts/create/":
co.RouteViewCounter.Bump(149) co.RouteViewCounter.Bump(150)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2473,7 +2489,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(150) co.RouteViewCounter.Bump(151)
err = routes.AccountLogout(w,req,user) err = routes.AccountLogout(w,req,user)
case "/accounts/login/submit/": case "/accounts/login/submit/":
err = c.ParseForm(w,req,user) err = c.ParseForm(w,req,user)
@ -2481,10 +2497,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(151) co.RouteViewCounter.Bump(152)
err = routes.AccountLoginSubmit(w,req,user) err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/mfa_verify/": case "/accounts/mfa_verify/":
co.RouteViewCounter.Bump(152) co.RouteViewCounter.Bump(153)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2496,7 +2512,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(153) co.RouteViewCounter.Bump(154)
err = routes.AccountLoginMFAVerifySubmit(w,req,user) err = routes.AccountLoginMFAVerifySubmit(w,req,user)
case "/accounts/create/submit/": case "/accounts/create/submit/":
err = c.ParseForm(w,req,user) err = c.ParseForm(w,req,user)
@ -2504,10 +2520,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(154) co.RouteViewCounter.Bump(155)
err = routes.AccountRegisterSubmit(w,req,user) err = routes.AccountRegisterSubmit(w,req,user)
case "/accounts/password-reset/": case "/accounts/password-reset/":
co.RouteViewCounter.Bump(155) co.RouteViewCounter.Bump(156)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2519,10 +2535,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(156) co.RouteViewCounter.Bump(157)
err = routes.AccountPasswordResetSubmit(w,req,user) err = routes.AccountPasswordResetSubmit(w,req,user)
case "/accounts/password-reset/token/": case "/accounts/password-reset/token/":
co.RouteViewCounter.Bump(157) co.RouteViewCounter.Bump(158)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2534,7 +2550,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(158) co.RouteViewCounter.Bump(159)
err = routes.AccountPasswordResetTokenSubmit(w,req,user) err = routes.AccountPasswordResetTokenSubmit(w,req,user)
} }
/*case "/sitemaps": // TODO: Count these views /*case "/sitemaps": // TODO: Count these views
@ -2551,7 +2567,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
h.Del("Content-Type") h.Del("Content-Type")
h.Del("Content-Encoding") h.Del("Content-Encoding")
} }
co.RouteViewCounter.Bump(160) co.RouteViewCounter.Bump(161)
req.URL.Path += extraData req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this? // TODO: Find a way to propagate errors up from this?
r.UploadHandler(w,req) // TODO: Count these views r.UploadHandler(w,req) // TODO: Count these views
@ -2561,7 +2577,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
// TODO: Add support for favicons and robots.txt files // TODO: Add support for favicons and robots.txt files
switch(extraData) { switch(extraData) {
case "robots.txt": case "robots.txt":
co.RouteViewCounter.Bump(162) co.RouteViewCounter.Bump(163)
return routes.RobotsTxt(w,req) return routes.RobotsTxt(w,req)
case "favicon.ico": case "favicon.ico":
gzw, ok := w.(c.GzipResponseWriter) gzw, ok := w.(c.GzipResponseWriter)
@ -2575,10 +2591,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
routes.StaticFile(w,req) routes.StaticFile(w,req)
return nil return nil
case "opensearch.xml": case "opensearch.xml":
co.RouteViewCounter.Bump(164) co.RouteViewCounter.Bump(165)
return routes.OpenSearchXml(w,req) return routes.OpenSearchXml(w,req)
/*case "sitemap.xml": /*case "sitemap.xml":
co.RouteViewCounter.Bump(163) co.RouteViewCounter.Bump(164)
return routes.SitemapXml(w,req)*/ return routes.SitemapXml(w,req)*/
} }
return c.NotFound(w,req,nil) return c.NotFound(w,req,nil)
@ -2589,7 +2605,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
r.RUnlock() r.RUnlock()
if ok { if ok {
co.RouteViewCounter.Bump(159) // TODO: Be more specific about *which* dynamic route it is co.RouteViewCounter.Bump(160) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData req.URL.Path += extraData
return handle(w,req,user) return handle(w,req,user)
} }
@ -2600,7 +2616,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
} else { } else {
r.DumpRequest(req,"Bad Route") r.DumpRequest(req,"Bad Route")
} }
co.RouteViewCounter.Bump(165) co.RouteViewCounter.Bump(166)
return c.NotFound(w,req,nil) return c.NotFound(w,req,nil)
} }
return err return err

View File

@ -720,33 +720,38 @@
"paginator.next_page":"", "paginator.next_page":"",
"paginator.next_page_aria":"Go to the next page", "paginator.next_page_aria":"Go to the next page",
"profile_login_for_options":"Login for options", "profile.login_for_options":"Login for options",
"profile_send_message":"Send Message", "profile.send_message":"Send Message",
"profile_add_friend":"Add Friend", "profile.add_friend":"Add Friend",
"profile_unban":"Unban", "profile.unban":"Unban",
"profile_ban":"Ban", "profile.ban":"Ban",
"profile_block":"Block", "profile.delete_posts":"Delete Posts",
"profile_unblock":"Unblock", "profile.block":"Block",
"profile_report_user_tooltip":"Report User", "profile.unblock":"Unblock",
"profile_report_user_aria":"Report User", "profile.report_user_tooltip":"Report User",
"profile_ban_user_head":"Ban User", "profile.report_user_aria":"Report User",
"profile_ban_user_notice":"If all the fields are left blank, the ban will be permanent.", "profile.ban_user_head":"Ban User",
"profile_ban_user_days":"Days", "profile.ban_user_notice":"If all the fields are left blank, the ban will be permanent.",
"profile_ban_user_weeks":"Weeks", "profile.ban_user_days":"Days",
"profile_ban_user_months":"Months", "profile.ban_user_weeks":"Weeks",
"profile_ban_user_reason":"Reason", "profile.ban_user_months":"Months",
"profile_ban_user_button":"Ban User", "profile.ban_user_reason":"Reason",
"profile_comments_head":"Comments", "profile.ban_user_button":"Ban User",
"profile_comments_edit_tooltip":"Edit Item", "profile.ban_delete_posts":"Delete Posts",
"profile_comments_edit_aria":"Edit Item", "profile.delete_posts_head":"Delete Posts",
"profile_comments_delete_tooltip":"Delete Item", "profile.delete_posts_notice":"Would you like to delete %d posts?",
"profile_comments_delete_aria":"Delete Item", "profile.delete_posts_button":"Delete Posts",
"profile_comments_report_tooltip":"Report Item", "profile.comments_head":"Comments",
"profile_comments_report_aria":"Report Item", "profile.comments_edit_tooltip":"Edit Item",
"profile_comments_form_content":"Insert comment here", "profile.comments_edit_aria":"Edit Item",
"profile_comments_form_button":"Create Reply", "profile.comments_delete_tooltip":"Delete Item",
"profile_comments_form_guest":"You need to login to comment on this profile.", "profile.comments_delete_aria":"Delete Item",
"profile_owner_tag":"Profile Owner", "profile.comments_report_tooltip":"Report Item",
"profile.comments_report_aria":"Report Item",
"profile.comments_form_content":"Insert comment here",
"profile.comments_form_button":"Create Reply",
"profile.comments_form_guest":"You need to login to comment on this profile.",
"profile.owner_tag":"Profile Owner",
"ip_search_head":"IP Search", "ip_search_head":"IP Search",
"ip_search_search_button":"Search", "ip_search_search_button":"Search",

View File

@ -4,6 +4,9 @@ function handle_profile_hashbit() {
case "ban_user": case "ban_user":
hash_class = "ban_user_hash"; hash_class = "ban_user_hash";
break; break;
case "delete_posts":
hash_class = "delete_posts_hash";
break;
default: default:
console.log("Unknown hashbit"); console.log("Unknown hashbit");
return; return;

View File

@ -505,7 +505,7 @@ func skipFunctionCall(data string, index int) int {
return index return index
} }
func writeFile(name string, content string) (err error) { func writeFile(name, content string) (err error) {
f, err := os.Create(name) f, err := os.Create(name)
if err != nil { if err != nil {
return err return err

View File

@ -111,6 +111,7 @@ func usersRoutes() *RouteGroup {
Action("routes.UnbanUser", "/users/unban/", "extraData"), Action("routes.UnbanUser", "/users/unban/", "extraData"),
Action("routes.ActivateUser", "/users/activate/", "extraData"), Action("routes.ActivateUser", "/users/activate/", "extraData"),
MView("routes.IPSearch", "/users/ips/"), // TODO: .Perms("ViewIPs")? MView("routes.IPSearch", "/users/ips/"), // TODO: .Perms("ViewIPs")?
Action("routes.DeletePostsSubmit", "/users/delete-posts/submit/", "extraData"),
) )
} }

View File

@ -88,7 +88,7 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user c.User, header *c.
if group.Tag != "" { if group.Tag != "" {
ru.Tag = group.Tag ru.Tag = group.Tag
} else if puser.ID == ru.CreatedBy { } else if puser.ID == ru.CreatedBy {
ru.Tag = phrases.GetTmplPhrase("profile_owner_tag") ru.Tag = phrases.GetTmplPhrase("profile.owner_tag")
} }
// TODO: Add a hook here // TODO: Add a hook here

View File

@ -13,7 +13,6 @@ func BanUserSubmit(w http.ResponseWriter, r *http.Request, user c.User, suid str
if !user.Perms.BanUsers { if !user.Perms.BanUsers {
return c.NoPermissions(w, r, user) return c.NoPermissions(w, r, user)
} }
uid, err := strconv.Atoi(suid) uid, err := strconv.Atoi(suid)
if err != nil { if err != nil {
return c.LocalError("The provided UserID is not a valid number.", w, r, user) return c.LocalError("The provided UserID is not a valid number.", w, r, user)
@ -28,7 +27,6 @@ func BanUserSubmit(w http.ResponseWriter, r *http.Request, user c.User, suid str
} else if err != nil { } else if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
// TODO: Is there a difference between IsMod and IsSuperMod? Should we delete the redundant one? // TODO: Is there a difference between IsMod and IsSuperMod? Should we delete the redundant one?
if targetUser.IsMod { if targetUser.IsMod {
return c.LocalError("You may not ban another staff member.", w, r, user) return c.LocalError("You may not ban another staff member.", w, r, user)
@ -40,44 +38,59 @@ func BanUserSubmit(w http.ResponseWriter, r *http.Request, user c.User, suid str
return c.LocalError("The user you're trying to unban is already banned.", w, r, user) return c.LocalError("The user you're trying to unban is already banned.", w, r, user)
} }
durationDays, err := strconv.Atoi(r.FormValue("ban-duration-days")) durDays, err := strconv.Atoi(r.FormValue("dur-days"))
if err != nil { if err != nil {
return c.LocalError("You can only use whole numbers for the number of days", w, r, user) return c.LocalError("You can only use whole numbers for the number of days", w, r, user)
} }
durWeeks, err := strconv.Atoi(r.FormValue("dur-weeks"))
durationWeeks, err := strconv.Atoi(r.FormValue("ban-duration-weeks"))
if err != nil { if err != nil {
return c.LocalError("You can only use whole numbers for the number of weeks", w, r, user) return c.LocalError("You can only use whole numbers for the number of weeks", w, r, user)
} }
durMonths, err := strconv.Atoi(r.FormValue("dur-months"))
durationMonths, err := strconv.Atoi(r.FormValue("ban-duration-months"))
if err != nil { if err != nil {
return c.LocalError("You can only use whole numbers for the number of months", w, r, user) return c.LocalError("You can only use whole numbers for the number of months", w, r, user)
} }
deletePosts := false
var duration time.Duration switch r.FormValue("delete-posts") {
if durationDays > 1 && durationWeeks > 1 && durationMonths > 1 { case "1":
duration, _ = time.ParseDuration("0") deletePosts = true
} else {
var seconds int
seconds += durationDays * int(c.Day)
seconds += durationWeeks * int(c.Week)
seconds += durationMonths * int(c.Month)
duration, _ = time.ParseDuration(strconv.Itoa(seconds) + "s")
} }
err = targetUser.Ban(duration, user.ID) var dur time.Duration
if durDays > 1 && durWeeks > 1 && durMonths > 1 {
dur, _ = time.ParseDuration("0")
} else {
var secs int
secs += durDays * int(c.Day)
secs += durWeeks * int(c.Week)
secs += durMonths * int(c.Month)
dur, _ = time.ParseDuration(strconv.Itoa(secs) + "s")
}
err = targetUser.Ban(dur, user.ID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return c.LocalError("The user you're trying to ban no longer exists.", w, r, user) return c.LocalError("The user you're trying to ban no longer exists.", w, r, user)
} else if err != nil { } else if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
err = c.ModLogs.Create("ban", uid, "user", user.GetIP(), user.ID) err = c.ModLogs.Create("ban", uid, "user", user.GetIP(), user.ID)
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
if deletePosts {
err = targetUser.DeletePosts()
if err == sql.ErrNoRows {
return c.LocalError("The user you're trying to ban no longer exists.", w, r, user)
} else if err != nil {
return c.InternalError(err, w, r)
}
err = c.ModLogs.Create("delete-posts", uid, "user", user.GetIP(), user.ID)
if err != nil {
return c.InternalError(err, w, r)
}
}
// TODO: Trickle the hookTable down from the router // TODO: Trickle the hookTable down from the router
hTbl := c.GetHookTable() hTbl := c.GetHookTable()
skip, rerr := hTbl.VhookSkippable("action_end_ban_user", targetUser.ID, &user) skip, rerr := hTbl.VhookSkippable("action_end_ban_user", targetUser.ID, &user)
@ -93,7 +106,6 @@ func UnbanUser(w http.ResponseWriter, r *http.Request, user c.User, suid string)
if !user.Perms.BanUsers { if !user.Perms.BanUsers {
return c.NoPermissions(w, r, user) return c.NoPermissions(w, r, user)
} }
uid, err := strconv.Atoi(suid) uid, err := strconv.Atoi(suid)
if err != nil { if err != nil {
return c.LocalError("The provided UserID is not a valid number.", w, r, user) return c.LocalError("The provided UserID is not a valid number.", w, r, user)
@ -139,7 +151,6 @@ func ActivateUser(w http.ResponseWriter, r *http.Request, user c.User, suid stri
if !user.Perms.ActivateUsers { if !user.Perms.ActivateUsers {
return c.NoPermissions(w, r, user) return c.NoPermissions(w, r, user)
} }
uid, err := strconv.Atoi(suid) uid, err := strconv.Atoi(suid)
if err != nil { if err != nil {
return c.LocalError("The provided UserID is not a valid number.", w, r, user) return c.LocalError("The provided UserID is not a valid number.", w, r, user)
@ -175,3 +186,45 @@ func ActivateUser(w http.ResponseWriter, r *http.Request, user c.User, suid stri
http.Redirect(w, r, "/user/"+strconv.Itoa(targetUser.ID), http.StatusSeeOther) http.Redirect(w, r, "/user/"+strconv.Itoa(targetUser.ID), http.StatusSeeOther)
return nil return nil
} }
func DeletePostsSubmit(w http.ResponseWriter, r *http.Request, user c.User, suid string) c.RouteError {
if !user.Perms.BanUsers {
return c.NoPermissions(w, r, user)
}
uid, err := strconv.Atoi(suid)
if err != nil {
return c.LocalError("The provided UserID is not a valid number.", w, r, user)
}
targetUser, err := c.Users.Get(uid)
if err == sql.ErrNoRows {
return c.LocalError("The user you're trying to purge posts of no longer exists.", w, r, user)
} else if err != nil {
return c.InternalError(err, w, r)
}
// TODO: Is there a difference between IsMod and IsSuperMod? Should we delete the redundant one?
if targetUser.IsMod {
return c.LocalError("You may not purge the posts of another staff member.", w, r, user)
}
err = targetUser.DeletePosts()
if err == sql.ErrNoRows {
return c.LocalError("The user you're trying to purge posts of no longer exists.", w, r, user)
} else if err != nil {
return c.InternalError(err, w, r)
}
err = c.ModLogs.Create("delete-posts", uid, "user", user.GetIP(), user.ID)
if err != nil {
return c.InternalError(err, w, r)
}
// TODO: Trickle the hookTable down from the router
hTbl := c.GetHookTable()
skip, rerr := hTbl.VhookSkippable("action_end_delete_posts", targetUser.ID, &user)
if skip || rerr != nil {
return rerr
}
http.Redirect(w, r, "/user/"+strconv.Itoa(uid), http.StatusSeeOther)
return nil
}

View File

@ -14,10 +14,10 @@
<form action="/user/convo/create/submit/{{.Convo.ID}}?s={{.CurrentUser.Session}}" method="post"> <form action="/user/convo/create/submit/{{.Convo.ID}}?s={{.CurrentUser.Session}}" method="post">
<div class="colstack_item topic_reply_form" style="border-top:none;"> <div class="colstack_item topic_reply_form" style="border-top:none;">
<div class="formrow"> <div class="formrow">
<div class="formitem"><textarea class="input_content" name="content" placeholder="{{lang "profile_comments_form_content"}}"></textarea></div> <div class="formitem"><textarea class="input_content" name="content" placeholder="{{lang "profile.comments_form_content"}}"></textarea></div>
</div> </div>
<div class="formrow quick_button_row"> <div class="formrow quick_button_row">
<div class="formitem"><button name="reply-button" class="formbutton">{{lang "profile_comments_form_button"}}</button></div> <div class="formitem"><button name="reply-button" class="formbutton">{{lang "profile.comments_form_button"}}</button></div>
</div> </div>
</div> </div>
</form> </form>

View File

@ -6,11 +6,11 @@
<span class="controls"> <span class="controls">
<a href="{{.User.Link}}" class="real_username username">{{.User.Name}}</a>&nbsp;&nbsp; <a href="{{.User.Link}}" class="real_username username">{{.User.Name}}</a>&nbsp;&nbsp;
{{if .CanModify}}<a href="/user/convo/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_edit_tooltip"}}" aria-label="{{lang "profile_comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a> {{if .CanModify}}<a href="/user/convo/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile.comments_edit_tooltip"}}" aria-label="{{lang "profile.comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a>
<a href="/user/convo/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_delete_tooltip"}}" aria-label="{{lang "profile_comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a>{{end}} <a href="/user/convo/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile.comments_delete_tooltip"}}" aria-label="{{lang "profile.comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a>{{end}}
<a class="mod_button" href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&type=convo-reply"><button class="username report_item flag_label" title="{{lang "profile_comments_report_tooltip"}}" aria-label="{{lang "profile_comments_report_aria"}}"></button></a> <a class="mod_button" href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&type=convo-reply"><button class="username report_item flag_label" title="{{lang "profile.comments_report_tooltip"}}" aria-label="{{lang "profile.comments_report_aria"}}"></button></a>
{{if .User.Tag}}<a class="username hide_on_mobile user_tag" style="float:right;">{{.User.Tag}}</a>{{end}} {{if .User.Tag}}<a class="username hide_on_mobile user_tag" style="float:right;">{{.User.Tag}}</a>{{end}}
</span> </span>

View File

@ -10,12 +10,12 @@
</div> </div>
<span class="controls"> <span class="controls">
{{if .CanModify}} {{if .CanModify}}
<a href="/user/convo/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_edit_tooltip"}}" aria-label="{{lang "profile_comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a> <a href="/user/convo/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile.comments_edit_tooltip"}}" aria-label="{{lang "profile.comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a>
<a href="/user/convo/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_delete_tooltip"}}" aria-label="{{lang "profile_comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a> <a href="/user/convo/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile.comments_delete_tooltip"}}" aria-label="{{lang "profile.comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a>
{{end}} {{end}}
<a class="mod_button" href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&type=convo-reply"><button class="username report_item flag_label" title="{{lang "profile_comments_report_tooltip"}}" aria-label="{{lang "profile_comments_report_aria"}}"></button></a> <a class="mod_button" href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&type=convo-reply"><button class="username report_item flag_label" title="{{lang "profile.comments_report_tooltip"}}" aria-label="{{lang "profile.comments_report_aria"}}"></button></a>
</span> </span>
</div> </div>
<div class="content_column"> <div class="content_column">

View File

@ -5,7 +5,7 @@
<div id="profile_left_pane" class="rowmenu"> <div id="profile_left_pane" class="rowmenu">
<div class="topBlock"> <div class="topBlock">
<div class="rowitem avatarRow"> <div class="rowitem avatarRow">
<a href="{{.ProfileOwner.Avatar}}"><img src="{{.ProfileOwner.Avatar}}" class="avatar" alt="Avatar" title="{{.ProfileOwner.Name}}'s Avatar" aria-hidden="true" /></a> <a href="{{.ProfileOwner.Avatar}}"><img src="{{.ProfileOwner.Avatar}}" class="avatar" alt="Avatar" title="{{.ProfileOwner.Name}}'s Avatar" aria-hidden="true"/></a>
</div> </div>
<div class="rowitem nameRow"> <div class="rowitem nameRow">
<span class="profileName" title="{{.ProfileOwner.Name}}">{{.ProfileOwner.Name}}</span>{{if .ProfileOwner.Tag}}<span class="username" title="{{.ProfileOwner.Tag}}">{{.ProfileOwner.Tag}}</span>{{end}} <span class="profileName" title="{{.ProfileOwner.Name}}">{{.ProfileOwner.Name}}</span>{{if .ProfileOwner.Tag}}<span class="username" title="{{.ProfileOwner.Tag}}">{{.ProfileOwner.Tag}}</span>{{end}}
@ -25,23 +25,29 @@
</div> </div>
<div class="passiveBlock"> <div class="passiveBlock">
{{if not .CurrentUser.Loggedin}}<div class="rowitem passive"> {{if not .CurrentUser.Loggedin}}<div class="rowitem passive">
<a class="profile_menu_item">{{lang "profile_login_for_options"}}</a> <a class="profile_menu_item">{{lang "profile.login_for_options"}}</a>
</div>{{else}} </div>{{else}}
{{if .CanMessage}}<div class="rowitem passive"> {{if .CanMessage}}<div class="rowitem passive">
<a href="/user/convos/create/" class="profile_menu_item">{{lang "profile_send_message"}}</a> <a href="/user/convos/create/" class="profile_menu_item">{{lang "profile.send_message"}}</a>
</div>{{end}} </div>{{end}}
<!--<div class="rowitem passive"> <!--<div class="rowitem passive">
<a class="profile_menu_item">{{lang "profile_add_friend"}}</a> <a class="profile_menu_item">{{lang "profile.add_friend"}}</a>
</div>--> </div>-->
{{if (.CurrentUser.IsSuperMod) and not (.ProfileOwner.IsSuperMod)}}<div class="rowitem passive"> {{if (.CurrentUser.IsSuperMod) and not (.ProfileOwner.IsSuperMod)}}<div class="rowitem passive">
{{if .ProfileOwner.IsBanned}}<a href="/users/unban/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}" class="profile_menu_item">{{lang "profile_unban"}}</a> {{if .ProfileOwner.IsBanned}}<a href="/users/unban/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}" class="profile_menu_item">{{lang "profile.unban"}}</a>
{{else}}<a href="#ban_user" class="profile_menu_item">{{lang "profile_ban"}}</a>{{end}} {{else}}<a href="#ban_user" class="profile_menu_item">{{lang "profile.ban"}}</a>{{end}}
</div>{{end}}
<div class="rowitem passive">
{{if .Blocked}}<a href="/user/block/remove/{{.ProfileOwner.ID}}" class="profile_menu_item">{{lang "profile_unblock"}}</a>{{else}}<a href="/user/block/create/{{.ProfileOwner.ID}}" class="profile_menu_item">{{lang "profile_block"}}</a>{{end}}
</div> </div>
<div class="rowitem passive"> <div class="rowitem passive">
<a href="/report/submit/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}&type=user" class="profile_menu_item report_item" aria-label="{{lang "profile_report_user_aria"}}" title="{{lang "profile_report_user_tooltip"}}"></a> <a href="#delete_posts" class="profile_menu_item">{{lang "profile.delete_posts"}}</a>
</div>
{{end}}
<div class="rowitem passive">
{{if .Blocked}}<a href="/user/block/remove/{{.ProfileOwner.ID}}" class="profile_menu_item">{{lang "profile.unblock"}}</a>{{else}}<a href="/user/block/create/{{.ProfileOwner.ID}}" class="profile_menu_item">{{lang "profile.block"}}</a>{{end}}
</div>
<div class="rowitem passive">
<a href="/report/submit/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}&type=user" class="profile_menu_item report_item" aria-label="{{lang "profile.report_user_aria"}}" title="{{lang "profile.report_user_tooltip"}}"></a>
</div> </div>
{{end}} {{end}}
</div> </div>
@ -53,37 +59,58 @@
{{if .CurrentUser.Perms.BanUsers}} {{if .CurrentUser.Perms.BanUsers}}
<!-- TODO: Inline the display:none; CSS --> <!-- TODO: Inline the display:none; CSS -->
<div id="ban_user_head" class="colstack_item colstack_head hash_hide ban_user_hash" style="display:none;"> <div id="ban_user_head" class="colstack_item colstack_head hash_hide ban_user_hash" style="display:none;">
<div class="rowitem"><h1><a>{{lang "profile_ban_user_head"}}</a></h1></div> <div class="rowitem"><h1><a>{{lang "profile.ban_user_head"}}</a></h1></div>
</div> </div>
<form id="ban_user_form" class="hash_hide ban_user_hash" action="/users/ban/submit/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}" method="post" style="display:none;"> <form id="ban_user_form" class="hash_hide ban_user_hash" action="/users/ban/submit/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}" method="post" style="display:none;">
<div class="the_form"> <div class="the_form">
{{/** TODO: Put a JS duration calculator here instead of this text? **/}} {{/** TODO: Put a JS duration calculator here instead of this text? **/}}
<div class="colline">{{lang "profile_ban_user_notice"}}</div> <div class="colline">{{lang "profile.ban_user_notice"}}</div>
<div class="colstack_item"> <div class="colstack_item">
<div class="formrow real_first_child"> <div class="formrow real_first_child">
<div class="formitem formlabel"><a>{{lang "profile_ban_user_days"}}</a></div> <div class="formitem formlabel"><a>{{lang "profile.ban_user_days"}}</a></div>
<div class="formitem"> <div class="formitem">
<input name="ban-duration-days" type="number" value=0 min=0 /> <input name="dur-days" type="number" value=0 min=0/>
</div> </div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "profile_ban_user_weeks"}}</a></div> <div class="formitem formlabel"><a>{{lang "profile.ban_user_weeks"}}</a></div>
<div class="formitem"> <div class="formitem">
<input name="ban-duration-weeks" type="number" value=0 min=0 /> <input name="dur-weeks" type="number" value=0 min=0/>
</div> </div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "profile_ban_user_months"}}</a></div> <div class="formitem formlabel"><a>{{lang "profile.ban_user_months"}}</a></div>
<div class="formitem"> <div class="formitem">
<input name="ban-duration-months" type="number" value=0 min=0 /> <input name="dur-months" type="number" value=0 min=0/>
</div> </div>
</div> </div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "profile.ban_delete_posts"}}</a></div>
<div class="formitem"><select name="delete-posts">
<option value=1>{{lang "option_yes"}}</option>
<option selected value=0>{{lang "option_no"}}</option>
</select></div>
</div>
{{/**<!--<div class="formrow"> {{/**<!--<div class="formrow">
<div class="formitem formlabel"><a>{{lang "profile_ban_user_reason"}}</a></div> <div class="formitem formlabel"><a>{{lang "profile.ban_user_reason"}}</a></div>
<div class="formitem"><textarea name="ban-reason" placeholder="A really horrible person" required></textarea></div> <div class="formitem"><textarea name="ban-reason" placeholder="A really horrible person" required></textarea></div>
</div>-->**/}} </div>-->**/}}
<div class="formrow"> <div class="formrow">
<div class="formitem"><button name="ban-button" class="formbutton form_middle_button">{{lang "profile_ban_user_button"}}</button></div> <div class="formitem"><button name="ban-button" class="formbutton form_middle_button">{{lang "profile.ban_user_button"}}</button></div>
</div>
</div>
</div>
</form>
<div id="delete_posts_head" class="colstack_item colstack_head hash_hide delete_posts_hash" style="display:none;">
<div class="rowitem"><h1><a>{{lang "profile.delete_posts_head"}}</a></h1></div>
</div>
<form id="delete_posts_form" class="hash_hide delete_posts_hash" action="/users/delete-posts/submit/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}" method="post" style="display:none;">
<div class="the_form">
<div class="colline">{{langf "profile.delete_posts_notice" .ProfileOwner.Posts}}</div>
<div class="colstack_item">
<div class="formrow real_first_child">
<div class="formitem"><button name="delete-posts-button" class="formbutton form_middle_button">{{lang "profile.delete_posts_button"}}</button></div>
</div> </div>
</div> </div>
</div> </div>
@ -92,7 +119,7 @@
{{end}} {{end}}
<div id="profile_comments_head" class="colstack_item colstack_head hash_hide"> <div id="profile_comments_head" class="colstack_item colstack_head hash_hide">
<div class="rowitem"><h1><a>{{lang "profile_comments_head"}}</a></h1></div> <div class="rowitem"><h1><a>{{lang "profile.comments_head"}}</a></h1></div>
</div> </div>
<div id="profile_comments" class="colstack_item hash_hide">{{template "profile_comments_row.html" . }}</div> <div id="profile_comments" class="colstack_item hash_hide">{{template "profile_comments_row.html" . }}</div>
@ -102,17 +129,17 @@
<input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" /> <input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" />
<div class="colstack_item topic_reply_form" style="border-top:none;"> <div class="colstack_item topic_reply_form" style="border-top:none;">
<div class="formrow"> <div class="formrow">
<div class="formitem"><textarea class="input_content" name="content" placeholder="{{lang "profile_comments_form_content"}}"></textarea></div> <div class="formitem"><textarea class="input_content" name="content" placeholder="{{lang "profile.comments_form_content"}}"></textarea></div>
</div> </div>
<div class="formrow quick_button_row"> <div class="formrow quick_button_row">
<div class="formitem"><button name="reply-button" class="formbutton">{{lang "profile_comments_form_button"}}</button></div> <div class="formitem"><button name="reply-button" class="formbutton">{{lang "profile.comments_form_button"}}</button></div>
</div> </div>
</div> </div>
</form> </form>
{{end}} {{end}}
{{else}} {{else}}
<div class="colstack_item" style="border-top:none;"> <div class="colstack_item" style="border-top:none;">
<div class="rowitem passive">{{lang "profile_comments_form_guest"}}</div> <div class="rowitem passive">{{lang "profile.comments_form_guest"}}</div>
</div> </div>
{{end}} {{end}}
</div> </div>

View File

@ -6,11 +6,11 @@
<span class="controls"> <span class="controls">
<a href="{{.UserLink}}" class="real_username username">{{.CreatedByName}}</a>&nbsp;&nbsp; <a href="{{.UserLink}}" class="real_username username">{{.CreatedByName}}</a>&nbsp;&nbsp;
{{if $.CurrentUser.IsMod}}<a href="/profile/reply/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_edit_tooltip"}}" aria-label="{{lang "profile_comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a> {{if $.CurrentUser.IsMod}}<a href="/profile/reply/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile.comments_edit_tooltip"}}" aria-label="{{lang "profile.comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a>
<a href="/profile/reply/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_delete_tooltip"}}" aria-label="{{lang "profile_comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a>{{end}} <a href="/profile/reply/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile.comments_delete_tooltip"}}" aria-label="{{lang "profile.comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a>{{end}}
<a class="mod_button" href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item flag_label" title="{{lang "profile_comments_report_tooltip"}}" aria-label="{{lang "profile_comments_report_aria"}}"></button></a> <a class="mod_button" href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item flag_label" title="{{lang "profile.comments_report_tooltip"}}" aria-label="{{lang "profile.comments_report_aria"}}"></button></a>
{{if .Tag}}<a class="username hide_on_mobile user_tag" style="float:right;">{{.Tag}}</a>{{end}} {{if .Tag}}<a class="username hide_on_mobile user_tag" style="float:right;">{{.Tag}}</a>{{end}}
</span> </span>

View File

@ -10,10 +10,10 @@
</div> </div>
<span class="controls"> <span class="controls">
{{if $.CurrentUser.IsMod}} {{if $.CurrentUser.IsMod}}
<a href="/profile/reply/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_edit_tooltip"}}" aria-label="{{lang "profile_comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a> <a href="/profile/reply/edit/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile.comments_edit_tooltip"}}" aria-label="{{lang "profile.comments_edit_aria"}}"><button class="username edit_item edit_label"></button></a>
<a href="/profile/reply/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile_comments_delete_tooltip"}}" aria-label="{{lang "profile_comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a> <a href="/profile/reply/delete/submit/{{.ID}}?s={{$.CurrentUser.Session}}" class="mod_button" title="{{lang "profile.comments_delete_tooltip"}}" aria-label="{{lang "profile.comments_delete_aria"}}"><button class="username delete_item delete_label"></button></a>
{{end}} {{end}}
<a class="mod_button" href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item flag_label" title="{{lang "profile_comments_report_tooltip"}}" aria-label="{{lang "profile_comments_report_aria"}}"></button></a> <a class="mod_button" href="/report/submit/{{.ID}}?s={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item flag_label" title="{{lang "profile.comments_report_tooltip"}}" aria-label="{{lang "profile.comments_report_aria"}}"></button></a>
</span> </span>
</div> </div>
<div class="content_column"> <div class="content_column">