Began work on support for JS Plugins.
Renamed the rrow_assign hook to topic_reply_row_assign and gave it access to more data. Fixed a bug where the topic store wouldn't fetch the last reply time for a topic. Refactored the process of adding and removing topics from and to a *Forum. Fixed a bug where editing the opening post of a topic would yield a vast number of <br>s instead of blank lines. Selecting text in Shadow now has it's own CSS instead of falling back onto the browser defaults. Fixed a bug in Shadow where not all the headers filled up the space they should. Fixed a bug in Shadow where the footer is broken on mobile. Added an ARIA Label to the topic list. Refactored the last poster logic to reduce the number of bugs. Renamed ReplyShort to Reply and Reply to ReplyUser. Added a Copy method to Reply, Group, Forum, User, and Topic. Rewrote Hello World into something slightly more useful for new plugin devs to learn off. Added the GetLength() method to ForumCache.
This commit is contained in:
parent
f5d5f755bb
commit
47963e10a9
|
@ -1,8 +1,8 @@
|
|||
# Gosora [![Azareal's Discord Chat](https://img.shields.io/badge/style-Invite-7289DA.svg?style=flat&label=Discord)](https://discord.gg/eyYvtTf)
|
||||
|
||||
A super fast forum software written in Go.
|
||||
A super fast forum software written in Go. You can talk to us on our Discord chat!
|
||||
|
||||
The initial code-base was forked from one of my side projects, but has now gone far beyond that. We're still fairly early in development, so the code-base might change at an incredible rate. We plan to start stabilising it somewhat once we enter alpha.
|
||||
The initial code-base was forked from one of my side projects, but has now gone far beyond that. We're still fairly early in development, so the code-base might change at an incredible rate. We plan to stop making as many breaking changes once we release the first alpha.
|
||||
|
||||
If you like this software, please give it a star and give us some feedback :)
|
||||
|
||||
|
|
12
database.go
12
database.go
|
@ -25,6 +25,16 @@ func initDatabase() (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// We have to put this here, otherwise LoadForums() won't be able to get the last poster data when building it's forums
|
||||
log.Print("Initialising the user and topic stores")
|
||||
if config.CacheTopicUser == CACHE_STATIC {
|
||||
users = NewMemoryUserStore(config.UserCacheCapacity)
|
||||
topics = NewMemoryTopicStore(config.TopicCacheCapacity)
|
||||
} else {
|
||||
users = NewSQLUserStore()
|
||||
topics = NewSQLTopicStore()
|
||||
}
|
||||
|
||||
log.Print("Loading the forums.")
|
||||
fstore = NewMemoryForumStore()
|
||||
err = fstore.LoadForums()
|
||||
|
@ -45,7 +55,7 @@ func initDatabase() (err error) {
|
|||
}
|
||||
|
||||
log.Print("Loading the plugins.")
|
||||
err = LoadPlugins()
|
||||
err = initExtend()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
/* Copyright Azareal 2016 - 2017 */
|
||||
package main
|
||||
import "github.com/robertkrimen/otto"
|
||||
|
||||
var vm *Otto
|
||||
var js_plugins map[string]*otto.Script = make(map[string]*otto.Script)
|
||||
var js_vars map[string]*otto.Object = make(map[string]*otto.Object)
|
||||
|
||||
func init()
|
||||
{
|
||||
var err error
|
||||
vm = otto.New()
|
||||
js_vars["current_page"], err = vm.Object(`current_page = {}`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func js_add_plugin(plugin string) error
|
||||
{
|
||||
script, err := otto.Compile("./extend/" + plugin + ".js")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vm.Run(script)
|
||||
return nil
|
||||
}
|
||||
|
||||
func js_add_hook(hook string, plugin string)
|
||||
{
|
||||
hooks[hook] = func(data interface{}) interface{} {
|
||||
switch d := data.(type) {
|
||||
case Page:
|
||||
current_page := js_vars["current_page"]
|
||||
current_page.Set("Title", d.Title)
|
||||
case TopicPage:
|
||||
|
||||
case ProfilePage:
|
||||
|
||||
case Reply:
|
||||
|
||||
default:
|
||||
log.Print("Not a valid JS datatype")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
extend.go
20
extend.go
|
@ -6,8 +6,10 @@
|
|||
*/
|
||||
package main
|
||||
|
||||
import "log"
|
||||
import "net/http"
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var plugins = make(map[string]*Plugin)
|
||||
|
||||
|
@ -15,7 +17,6 @@ var plugins = make(map[string]*Plugin)
|
|||
var hooks = map[string][]func(interface{}) interface{}{
|
||||
"forums_frow_assign": nil,
|
||||
"topic_create_frow_assign": nil,
|
||||
"rrow_assign": nil, // TODO: Rename this hook to topic_rrow_assign
|
||||
}
|
||||
|
||||
// Hooks with a variable number of arguments
|
||||
|
@ -26,6 +27,7 @@ var vhooks = map[string]func(...interface{}) interface{}{
|
|||
"forum_trow_assign": nil,
|
||||
"topics_topic_row_assign": nil,
|
||||
//"topics_user_row_assign": nil,
|
||||
"topic_reply_row_assign": nil,
|
||||
"create_group_preappend": nil, // What is this? Investigate!
|
||||
"topic_create_pre_loop": nil,
|
||||
}
|
||||
|
@ -100,6 +102,15 @@ type Plugin struct {
|
|||
Uninstall func() error
|
||||
|
||||
Hooks map[string]int
|
||||
Data interface{} // Usually used for hosting the VMs / reusable elements of non-native plugins
|
||||
}
|
||||
|
||||
func initExtend() (err error) {
|
||||
err = InitPluginLangs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return LoadPlugins()
|
||||
}
|
||||
|
||||
// LoadPlugins polls the database to see which plugins have been activated and which have been installed
|
||||
|
@ -111,8 +122,7 @@ func LoadPlugins() error {
|
|||
defer rows.Close()
|
||||
|
||||
var uname string
|
||||
var active bool
|
||||
var installed bool
|
||||
var active, installed bool
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&uname, &active, &installed)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
current_page.test = true;
|
||||
|
||||
// This shouldn't ever fail
|
||||
var errmsg = "gotcha";
|
||||
errmsg;
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"UName":"heytherejs",
|
||||
"Name":"HeythereJS",
|
||||
"Author":"Azareal",
|
||||
"URL":"https://github.com/Azareal/Gosora",
|
||||
"Main":"main.js"
|
||||
}
|
52
forum.go
52
forum.go
|
@ -28,14 +28,17 @@ type Forum struct {
|
|||
ParentID int
|
||||
ParentType string
|
||||
TopicCount int
|
||||
LastTopicLink string
|
||||
LastTopic string
|
||||
|
||||
LastTopic *Topic
|
||||
LastTopicID int
|
||||
LastReplyer string
|
||||
LastReplyer *User
|
||||
LastReplyerID int
|
||||
LastTopicTime string
|
||||
LastTopicTime string // So that we can re-calculate the relative time on the spot in /forums/
|
||||
|
||||
//LastLock sync.RWMutex // ? - Is this safe to copy? Use a pointer to it? Should we do an fstore.Reload() instead?
|
||||
}
|
||||
|
||||
// ? - What is this for?
|
||||
type ForumSimple struct {
|
||||
ID int
|
||||
Name string
|
||||
|
@ -43,6 +46,36 @@ type ForumSimple struct {
|
|||
Preset string
|
||||
}
|
||||
|
||||
func (forum *Forum) Copy() (fcopy Forum) {
|
||||
//forum.LastLock.RLock()
|
||||
fcopy = *forum
|
||||
//forum.LastLock.RUnlock()
|
||||
return fcopy
|
||||
}
|
||||
|
||||
/*func (forum *Forum) GetLast() (topic *Topic, user *User) {
|
||||
forum.LastLock.RLock()
|
||||
topic = forum.LastTopic
|
||||
if topic == nil {
|
||||
topic = &Topic{ID: 0}
|
||||
}
|
||||
|
||||
user = forum.LastReplyer
|
||||
if user == nil {
|
||||
user = &User{ID: 0}
|
||||
}
|
||||
forum.LastLock.RUnlock()
|
||||
return topic, user
|
||||
}
|
||||
|
||||
func (forum *Forum) SetLast(topic *Topic, user *User) {
|
||||
forum.LastLock.Lock()
|
||||
forum.LastTopic = topic
|
||||
forum.LastReplyer = user
|
||||
forum.LastLock.Unlock()
|
||||
}*/
|
||||
|
||||
// TODO: Write tests for this
|
||||
func (forum *Forum) Update(name string, desc string, active bool, preset string) error {
|
||||
if name == "" {
|
||||
name = forum.Name
|
||||
|
@ -53,9 +86,13 @@ func (forum *Forum) Update(name string, desc string, active bool, preset string)
|
|||
return err
|
||||
}
|
||||
if forum.Preset != preset || preset == "custom" || preset == "" {
|
||||
permmapToQuery(presetToPermmap(preset), forum.ID)
|
||||
err = permmapToQuery(presetToPermmap(preset), forum.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_ = fstore.Reload(forum.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Replace this sorting mechanism with something a lot more efficient
|
||||
|
@ -72,6 +109,11 @@ func (sf SortForum) Less(i, j int) bool {
|
|||
return sf[i].ID < sf[j].ID
|
||||
}
|
||||
|
||||
// ! Don't use this outside of tests and possibly template_init.go
|
||||
func makeDummyForum(fid int, link string, name string, desc string, active bool, preset string, parentID int, parentType string, topicCount int) *Forum {
|
||||
return &Forum{ID: fid, Link: link, Name: name, Desc: desc, Active: active, Preset: preset, ParentID: parentID, ParentType: parentType, TopicCount: topicCount}
|
||||
}
|
||||
|
||||
func buildForumURL(slug string, fid int) string {
|
||||
if slug == "" {
|
||||
return "/forum/" + strconv.Itoa(fid)
|
||||
|
|
169
forum_store.go
169
forum_store.go
|
@ -27,14 +27,13 @@ type ForumStore interface {
|
|||
LoadForums() error
|
||||
DirtyGet(id int) *Forum
|
||||
Get(id int) (*Forum, error)
|
||||
GetCopy(id int) (Forum, error)
|
||||
BypassGet(id int) (*Forum, error)
|
||||
Reload(id int) error // ? - Should we move this to ForumCache? It might require us to do some unnecessary casting though
|
||||
//Update(Forum) error
|
||||
Delete(id int) error
|
||||
IncrementTopicCount(id int) error
|
||||
DecrementTopicCount(id int) error
|
||||
UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error
|
||||
AddTopic(tid int, uid int, fid int) error
|
||||
RemoveTopic(fid int) error
|
||||
UpdateLastTopic(tid int, uid int, fid int) error
|
||||
Exists(id int) bool
|
||||
GetAll() ([]*Forum, error)
|
||||
GetAllIDs() ([]int, error)
|
||||
|
@ -51,6 +50,7 @@ type ForumCache interface {
|
|||
CacheGet(id int) (*Forum, error)
|
||||
CacheSet(forum *Forum) error
|
||||
CacheDelete(id int)
|
||||
GetLength() int
|
||||
}
|
||||
|
||||
// MemoryForumStore is a struct which holds an arbitrary number of forums in memory, usually all of them, although we might introduce functionality to hold a smaller subset in memory for sites with an extremely large number of forums
|
||||
|
@ -58,7 +58,6 @@ type MemoryForumStore struct {
|
|||
forums sync.Map // map[int]*Forum
|
||||
forumView atomic.Value // []*Forum
|
||||
//fids []int
|
||||
forumCount int
|
||||
|
||||
get *sql.Stmt
|
||||
getAll *sql.Stmt
|
||||
|
@ -94,10 +93,6 @@ func NewMemoryForumStore() *MemoryForumStore {
|
|||
|
||||
// TODO: Add support for subforums
|
||||
func (mfs *MemoryForumStore) LoadForums() error {
|
||||
log.Print("Adding the uncategorised forum")
|
||||
forumUpdateMutex.Lock()
|
||||
defer forumUpdateMutex.Unlock()
|
||||
|
||||
var forumView []*Forum
|
||||
addForum := func(forum *Forum) {
|
||||
mfs.forums.Store(forum.ID, forum)
|
||||
|
@ -114,8 +109,8 @@ func (mfs *MemoryForumStore) LoadForums() error {
|
|||
|
||||
var i = 0
|
||||
for ; rows.Next(); i++ {
|
||||
forum := Forum{ID: 0, Active: true, Preset: "all"}
|
||||
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
|
||||
forum := &Forum{ID: 0, Active: true, Preset: "all"}
|
||||
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -129,15 +124,27 @@ func (mfs *MemoryForumStore) LoadForums() error {
|
|||
}
|
||||
|
||||
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
|
||||
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
|
||||
addForum(&forum)
|
||||
|
||||
topic, err := topics.Get(forum.LastTopicID)
|
||||
if err != nil {
|
||||
topic = getDummyTopic()
|
||||
}
|
||||
user, err := users.Get(forum.LastReplyerID)
|
||||
if err != nil {
|
||||
user = getDummyUser()
|
||||
}
|
||||
forum.LastTopic = topic
|
||||
forum.LastReplyer = user
|
||||
//forum.SetLast(topic, user)
|
||||
|
||||
addForum(forum)
|
||||
}
|
||||
mfs.forumCount = i
|
||||
mfs.forumView.Store(forumView)
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// TODO: Hide social groups too
|
||||
// ? - Will this be hit a lot by plugin_socialgroups?
|
||||
func (mfs *MemoryForumStore) rebuildView() {
|
||||
var forumView []*Forum
|
||||
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
|
||||
|
@ -173,46 +180,75 @@ func (mfs *MemoryForumStore) Get(id int) (*Forum, error) {
|
|||
if !ok || fint.(*Forum).Name == "" {
|
||||
var forum = &Forum{ID: id}
|
||||
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
|
||||
if err != nil {
|
||||
return forum, err
|
||||
}
|
||||
|
||||
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
|
||||
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
|
||||
|
||||
topic, err := topics.Get(forum.LastTopicID)
|
||||
if err != nil {
|
||||
topic = getDummyTopic()
|
||||
}
|
||||
user, err := users.Get(forum.LastReplyerID)
|
||||
if err != nil {
|
||||
user = getDummyUser()
|
||||
}
|
||||
forum.LastTopic = topic
|
||||
forum.LastReplyer = user
|
||||
//forum.SetLast(topic, user)
|
||||
|
||||
mfs.CacheSet(forum)
|
||||
return forum, err
|
||||
}
|
||||
return fint.(*Forum), nil
|
||||
}
|
||||
|
||||
func (mfs *MemoryForumStore) GetCopy(id int) (Forum, error) {
|
||||
fint, ok := mfs.forums.Load(id)
|
||||
if !ok || fint.(*Forum).Name == "" {
|
||||
var forum = Forum{ID: id}
|
||||
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
|
||||
|
||||
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
|
||||
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
|
||||
return forum, err
|
||||
}
|
||||
return *fint.(*Forum), nil
|
||||
}
|
||||
|
||||
func (mfs *MemoryForumStore) BypassGet(id int) (*Forum, error) {
|
||||
var forum = Forum{ID: id}
|
||||
var forum = &Forum{ID: id}
|
||||
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
|
||||
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
|
||||
return &forum, err
|
||||
|
||||
topic, err := topics.Get(forum.LastTopicID)
|
||||
if err != nil {
|
||||
topic = getDummyTopic()
|
||||
}
|
||||
user, err := users.Get(forum.LastReplyerID)
|
||||
if err != nil {
|
||||
user = getDummyUser()
|
||||
}
|
||||
forum.LastTopic = topic
|
||||
forum.LastReplyer = user
|
||||
//forum.SetLast(topic, user)
|
||||
|
||||
return forum, err
|
||||
}
|
||||
|
||||
func (mfs *MemoryForumStore) Reload(id int) error {
|
||||
var forum = Forum{ID: id}
|
||||
var forum = &Forum{ID: id}
|
||||
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
|
||||
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
|
||||
|
||||
mfs.CacheSet(&forum)
|
||||
topic, err := topics.Get(forum.LastTopicID)
|
||||
if err != nil {
|
||||
topic = getDummyTopic()
|
||||
}
|
||||
user, err := users.Get(forum.LastReplyerID)
|
||||
if err != nil {
|
||||
user = getDummyUser()
|
||||
}
|
||||
forum.LastTopic = topic
|
||||
forum.LastReplyer = user
|
||||
//forum.SetLast(topic, user)
|
||||
|
||||
mfs.CacheSet(forum)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -281,8 +317,6 @@ func (mfs *MemoryForumStore) Delete(id int) error {
|
|||
if id == 1 {
|
||||
return errors.New("You cannot delete the Reports forum")
|
||||
}
|
||||
forumUpdateMutex.Lock()
|
||||
defer forumUpdateMutex.Unlock()
|
||||
_, err := mfs.delete.Exec(id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -291,53 +325,40 @@ func (mfs *MemoryForumStore) Delete(id int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ! Is this racey?
|
||||
func (mfs *MemoryForumStore) IncrementTopicCount(id int) error {
|
||||
forum, err := mfs.Get(id)
|
||||
func (mfs *MemoryForumStore) AddTopic(tid int, uid int, fid int) error {
|
||||
_, err := updateForumCacheStmt.Exec(tid, uid, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = addTopicsToForumStmt.Exec(1, id)
|
||||
_, err = addTopicsToForumStmt.Exec(1, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forum.TopicCount++
|
||||
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
|
||||
mfs.Reload(fid)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ! Is this racey?
|
||||
func (mfs *MemoryForumStore) DecrementTopicCount(id int) error {
|
||||
forum, err := mfs.Get(id)
|
||||
// TODO: Update the forum cache with the latest topic
|
||||
func (mfs *MemoryForumStore) RemoveTopic(fid int) error {
|
||||
_, err := removeTopicsFromForumStmt.Exec(1, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = removeTopicsFromForumStmt.Exec(1, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forum.TopicCount--
|
||||
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
|
||||
mfs.Reload(fid)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// ! Is this racey?
|
||||
func (mfs *MemoryForumStore) UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error {
|
||||
forum, err := mfs.Get(fid)
|
||||
func (mfs *MemoryForumStore) UpdateLastTopic(tid int, uid int, fid int) error {
|
||||
_, err := updateForumCacheStmt.Exec(tid, uid, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = updateForumCacheStmt.Exec(topicName, tid, username, uid, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
forum.LastTopic = topicName
|
||||
forum.LastTopicID = tid
|
||||
forum.LastReplyer = username
|
||||
forum.LastReplyerID = uid
|
||||
forum.LastTopicTime = time
|
||||
|
||||
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
|
||||
mfs.Reload(fid)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -354,19 +375,25 @@ func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active b
|
|||
}
|
||||
fid := int(fid64)
|
||||
|
||||
mfs.forums.Store(fid, &Forum{fid, buildForumURL(nameToSlug(forumName), fid), forumName, forumDesc, active, preset, 0, "", 0, "", "", 0, "", 0, ""})
|
||||
mfs.forumCount++
|
||||
err = mfs.Reload(fid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// TODO: Add a GroupStore. How would it interact with the ForumStore?
|
||||
permmapToQuery(presetToPermmap(preset), fid)
|
||||
forumCreateMutex.Unlock()
|
||||
|
||||
if active {
|
||||
mfs.rebuildView()
|
||||
}
|
||||
return fid, nil
|
||||
}
|
||||
|
||||
// ! Might be slightly inaccurate, if the sync.Map is constantly shifting and churning, but it'll stabilise eventually. Also, slow. Don't use this on every request x.x
|
||||
func (mfs *MemoryForumStore) GetLength() (length int) {
|
||||
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
|
||||
length++
|
||||
return true
|
||||
})
|
||||
return length
|
||||
}
|
||||
|
||||
// TODO: Get the total count of forums in the forum store minus the blanked forums rather than doing a heavy query for this?
|
||||
// GetGlobalCount returns the total number of forums
|
||||
func (mfs *MemoryForumStore) GetGlobalCount() (fcount int) {
|
||||
|
|
|
@ -186,7 +186,7 @@ func _gen_mysql() (err error) {
|
|||
}
|
||||
|
||||
log.Print("Preparing getForums statement.")
|
||||
getForumsStmt, err = db.Prepare("SELECT `fid`,`name`,`desc`,`active`,`preset`,`parentID`,`parentType`,`topicCount`,`lastTopic`,`lastTopicID`,`lastReplyer`,`lastReplyerID`,`lastTopicTime` FROM `forums` ORDER BY fid ASC")
|
||||
getForumsStmt, err = db.Prepare("SELECT `fid`,`name`,`desc`,`active`,`preset`,`parentID`,`parentType`,`topicCount`,`lastTopicID`,`lastReplyerID` FROM `forums` ORDER BY fid ASC")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -534,7 +534,7 @@ func _gen_mysql() (err error) {
|
|||
}
|
||||
|
||||
log.Print("Preparing updateForumCache statement.")
|
||||
updateForumCacheStmt, err = db.Prepare("UPDATE `forums` SET `lastTopic` = ?,`lastTopicID` = ?,`lastReplyer` = ?,`lastReplyerID` = ?,`lastTopicTime` = UTC_TIMESTAMP() WHERE `fid` = ?")
|
||||
updateForumCacheStmt, err = db.Prepare("UPDATE `forums` SET `lastTopicID` = ?,`lastReplyerID` = ? WHERE `fid` = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ func _gen_pgsql() (err error) {
|
|||
}
|
||||
|
||||
log.Print("Preparing updateForumCache statement.")
|
||||
updateForumCacheStmt, err = db.Prepare("UPDATE `forums` SET `lastTopic` = ?,`lastTopicID` = ?,`lastReplyer` = ?,`lastReplyerID` = ?,`lastTopicTime` = LOCALTIMESTAMP() WHERE `fid` = ?")
|
||||
updateForumCacheStmt, err = db.Prepare("UPDATE `forums` SET `lastTopicID` = ?,`lastReplyerID` = ? WHERE `fid` = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -53,14 +53,6 @@ func gloinit() error {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if config.CacheTopicUser == CACHE_STATIC {
|
||||
users = NewMemoryUserStore(config.UserCacheCapacity)
|
||||
topics = NewMemoryTopicStore(config.TopicCacheCapacity)
|
||||
} else {
|
||||
users = NewSQLUserStore()
|
||||
topics = NewSQLTopicStore()
|
||||
}
|
||||
|
||||
log.Print("Loading the static files.")
|
||||
err = initStaticFiles()
|
||||
if err != nil {
|
||||
|
@ -548,7 +540,7 @@ func BenchmarkQueriesSerial(b *testing.B) {
|
|||
}
|
||||
})
|
||||
|
||||
var replyItem Reply
|
||||
var replyItem ReplyUser
|
||||
var isSuperAdmin bool
|
||||
var group int
|
||||
b.Run("topic_replies_scan", func(b *testing.B) {
|
||||
|
|
5
group.go
5
group.go
|
@ -11,6 +11,7 @@ type GroupAdmin struct {
|
|||
CanDelete bool
|
||||
}
|
||||
|
||||
// ! Fix the data races
|
||||
type Group struct {
|
||||
ID int
|
||||
Name string
|
||||
|
@ -25,3 +26,7 @@ type Group struct {
|
|||
Forums []ForumPerms
|
||||
CanSee []int // The IDs of the forums this group can see
|
||||
}
|
||||
|
||||
func (group *Group) Copy() Group {
|
||||
return *group
|
||||
}
|
||||
|
|
8
main.go
8
main.go
|
@ -82,14 +82,6 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if config.CacheTopicUser == CACHE_STATIC {
|
||||
users = NewMemoryUserStore(config.UserCacheCapacity)
|
||||
topics = NewMemoryTopicStore(config.TopicCacheCapacity)
|
||||
} else {
|
||||
users = NewSQLUserStore()
|
||||
topics = NewSQLTopicStore()
|
||||
}
|
||||
|
||||
log.Print("Loading the static files.")
|
||||
err = initStaticFiles()
|
||||
if err != nil {
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation
|
||||
|
@ -79,7 +78,7 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st
|
|||
// Do a bulk forum fetch, just in case it's the SqlForumStore?
|
||||
forum := fstore.DirtyGet(ffid)
|
||||
if forum.Name != "" && forum.Active {
|
||||
fcopy := *forum
|
||||
fcopy := forum.Copy()
|
||||
if hooks["topic_create_frow_assign"] != nil {
|
||||
// TODO: Add the skip feature to all the other row based hooks?
|
||||
if runHook("topic_create_frow_assign", &fcopy).(bool) {
|
||||
|
@ -144,12 +143,6 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
|
|||
return
|
||||
}
|
||||
|
||||
err = fstore.IncrementTopicCount(fid)
|
||||
if err != nil {
|
||||
InternalError(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = addSubscriptionStmt.Exec(user.ID, lastID, "topic")
|
||||
if err != nil {
|
||||
InternalError(err, w)
|
||||
|
@ -163,7 +156,7 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
|
|||
return
|
||||
}
|
||||
|
||||
err = fstore.UpdateLastTopic(topicName, int(lastID), user.Name, user.ID, time.Now().Format("2006-01-02 15:04:05"), fid)
|
||||
err = fstore.AddTopic(int(lastID), user.ID, fid)
|
||||
if err != nil && err != ErrNoRows {
|
||||
InternalError(err, w)
|
||||
}
|
||||
|
@ -219,7 +212,14 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
|
|||
InternalError(err, w)
|
||||
return
|
||||
}
|
||||
err = fstore.UpdateLastTopic(topic.Title, tid, user.Name, user.ID, time.Now().Format("2006-01-02 15:04:05"), topic.ParentID)
|
||||
|
||||
// Flush the topic out of the cache
|
||||
tcache, ok := topics.(TopicCache)
|
||||
if ok {
|
||||
tcache.CacheRemove(tid)
|
||||
}
|
||||
|
||||
err = fstore.UpdateLastTopic(tid, user.ID, topic.ParentID)
|
||||
if err != nil && err != ErrNoRows {
|
||||
InternalError(err, w)
|
||||
return
|
||||
|
@ -247,12 +247,6 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
|
|||
go notifyWatchers(lastID)
|
||||
}
|
||||
|
||||
// Flush the topic out of the cache
|
||||
tcache, ok := topics.(TopicCache)
|
||||
if ok {
|
||||
tcache.CacheRemove(tid)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
|
||||
err = user.increasePostStats(wcount, false)
|
||||
if err != nil {
|
||||
|
@ -629,7 +623,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI
|
|||
InternalError(err, w)
|
||||
return
|
||||
}
|
||||
err = fstore.UpdateLastTopic(title, int(lastID), user.Name, user.ID, time.Now().Format("2006-01-02 15:04:05"), fid)
|
||||
err = fstore.UpdateLastTopic(int(lastID), user.ID, fid)
|
||||
if err != nil && err != ErrNoRows {
|
||||
InternalError(err, w)
|
||||
return
|
||||
|
|
17
misc_test.go
17
misc_test.go
|
@ -269,19 +269,12 @@ func TestForumStore(t *testing.T) {
|
|||
}
|
||||
|
||||
forum, err = fstore.Get(0)
|
||||
if err == ErrNoRows {
|
||||
t.Error("Couldn't find FID #0")
|
||||
} else if err != nil {
|
||||
if err == nil {
|
||||
t.Error("FID #0 shouldn't exist")
|
||||
} else if err != ErrNoRows {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if forum.ID != 0 {
|
||||
t.Error("forum.ID doesn't not match the requested UID. Got '" + strconv.Itoa(forum.ID) + "' instead.")
|
||||
}
|
||||
if forum.Name != "Uncategorised" {
|
||||
t.Error("FID #0 is named '" + forum.Name + "' and not 'Uncategorised'")
|
||||
}
|
||||
|
||||
forum, err = fstore.Get(1)
|
||||
if err == ErrNoRows {
|
||||
t.Error("Couldn't find FID #1")
|
||||
|
@ -311,8 +304,8 @@ func TestForumStore(t *testing.T) {
|
|||
}
|
||||
|
||||
ok = fstore.Exists(0)
|
||||
if !ok {
|
||||
t.Error("FID #0 should exist")
|
||||
if ok {
|
||||
t.Error("FID #0 shouldn't exist")
|
||||
}
|
||||
|
||||
ok = fstore.Exists(1)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
//"log"
|
||||
//"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -26,7 +27,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
|
|||
return
|
||||
}
|
||||
|
||||
oldTopic, err := topics.Get(tid)
|
||||
topic, err := topics.Get(tid)
|
||||
if err == ErrNoRows {
|
||||
PreErrorJSQ("The topic you tried to edit doesn't exist.", w, r, isJs)
|
||||
return
|
||||
|
@ -36,7 +37,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
|
|||
}
|
||||
|
||||
// TODO: Add hooks to make use of headerLite
|
||||
_, ok := SimpleForumUserCheck(w, r, &user, oldTopic.ParentID)
|
||||
_, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -47,25 +48,20 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
|
|||
|
||||
topicName := r.PostFormValue("topic_name")
|
||||
topicContent := html.EscapeString(r.PostFormValue("topic_content"))
|
||||
log.Print("topicContent ", topicContent)
|
||||
|
||||
// TODO: Move this bit to the TopicStore
|
||||
_, err = editTopicStmt.Exec(topicName, preparseMessage(topicContent), parseMessage(html.EscapeString(preparseMessage(topicContent))), tid)
|
||||
err = topic.Update(topicName, topicContent)
|
||||
if err != nil {
|
||||
InternalErrorJSQ(err, w, r, isJs)
|
||||
return
|
||||
}
|
||||
|
||||
err = fstore.UpdateLastTopic(topicName, tid, user.Name, user.ID, time.Now().Format("2006-01-02 15:04:05"), oldTopic.ParentID)
|
||||
err = fstore.UpdateLastTopic(topic.ID, user.ID, topic.ParentID)
|
||||
if err != nil && err != ErrNoRows {
|
||||
InternalError(err, w)
|
||||
InternalErrorJSQ(err, w, r, isJs)
|
||||
return
|
||||
}
|
||||
|
||||
tcache, ok := topics.(TopicCache)
|
||||
if ok {
|
||||
tcache.CacheRemove(oldTopic.ID)
|
||||
}
|
||||
|
||||
if !isJs {
|
||||
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
*
|
||||
* OttoJS Plugin Module
|
||||
* Copyright Azareal 2016 - 2018
|
||||
*
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
type OttoPluginLang struct {
|
||||
vm *otto.Otto
|
||||
plugins map[string]*otto.Script
|
||||
vars map[string]*otto.Object
|
||||
}
|
||||
|
||||
func init() {
|
||||
pluginLangs["ottojs"] = &OttoPluginLang{
|
||||
plugins: make(map[string]*otto.Script),
|
||||
vars: make(map[string]*otto.Object),
|
||||
}
|
||||
}
|
||||
|
||||
func (js *OttoPluginLang) Init() (err error) {
|
||||
js.vm = otto.New()
|
||||
js.vars["current_page"], err = js.vm.Object(`var current_page = {}`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (js *OttoPluginLang) GetName() string {
|
||||
return "ottojs"
|
||||
}
|
||||
|
||||
func (js *OttoPluginLang) GetExts() []string {
|
||||
return []string{".js"}
|
||||
}
|
||||
|
||||
func (js *OttoPluginLang) AddPlugin(meta PluginMeta) (plugin *Plugin, err error) {
|
||||
script, err := js.vm.Compile("./extend/"+meta.UName+"/"+meta.Main, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pluginInit = func() error {
|
||||
retValue, err := js.vm.Run(script)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if retValue.IsString() {
|
||||
ret, err := retValue.ToString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ret != "" {
|
||||
return errors.New(ret)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var pluginActivate func() error
|
||||
var pluginDeactivate func()
|
||||
var pluginInstall func() error
|
||||
var pluginUninstall func() error
|
||||
|
||||
plugin = NewPlugin(meta.UName, meta.Name, meta.Author, meta.URL, meta.Settings, meta.Tag, "ottojs", pluginInit, pluginActivate, pluginDeactivate, pluginInstall, pluginUninstall)
|
||||
|
||||
plugin.Data = script
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
/*func (js *OttoPluginLang) addHook(hook string, plugin string) {
|
||||
hooks[hook] = func(data interface{}) interface{} {
|
||||
switch d := data.(type) {
|
||||
case Page:
|
||||
currentPage := js.vars["current_page"]
|
||||
currentPage.Set("Title", d.Title)
|
||||
case TopicPage:
|
||||
|
||||
case ProfilePage:
|
||||
|
||||
case Reply:
|
||||
|
||||
default:
|
||||
log.Print("Not a valid JS datatype")
|
||||
}
|
||||
}
|
||||
}*/
|
|
@ -26,11 +26,8 @@ CREATE TABLE `forums`(
|
|||
`preset` varchar(100) DEFAULT '' not null,
|
||||
`parentID` int DEFAULT 0 not null, /* TODO: Add support for subforums */
|
||||
`parentType` varchar(50) DEFAULT '' not null,
|
||||
`lastTopic` varchar(100) DEFAULT '' not null,
|
||||
`lastTopicID` int DEFAULT 0 not null,
|
||||
`lastReplyer` varchar(100) DEFAULT '' not null,
|
||||
`lastReplyerID` int DEFAULT 0 not null,
|
||||
`lastTopicTime` datetime not null,
|
||||
primary key(`fid`)
|
||||
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
|
@ -233,7 +230,7 @@ INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`) VALUES ('Awaiting
|
|||
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`tag`) VALUES ('Not Loggedin','{"ViewTopic":true}','{}','Guest');
|
||||
|
||||
INSERT INTO forums(`name`,`active`) VALUES ('Reports',0);
|
||||
INSERT INTO forums(`name`,`lastTopicTime`,`lastTopicID`,`lastReplyer`,`lastReplyerID`,`lastTopic`) VALUES ('General',UTC_TIMESTAMP(),1,"Admin",1,'Test Topic');
|
||||
INSERT INTO forums(`name`,`lastTopicID`,`lastReplyerID`) VALUES ("General",1,1);
|
||||
|
||||
INSERT INTO forums_permissions(`gid`,`fid`,`permissions`) VALUES (1,1,'{"ViewTopic":true,"CreateReply":true,"CreateTopic":true,"PinTopic":true,"CloseTopic":true}');
|
||||
INSERT INTO forums_permissions(`gid`,`fid`,`permissions`) VALUES (2,1,'{"ViewTopic":true,"CreateReply":true,"CloseTopic":true}');
|
||||
|
|
6
pages.go
6
pages.go
|
@ -54,7 +54,7 @@ type TopicPage struct {
|
|||
Title string
|
||||
CurrentUser User
|
||||
Header *HeaderVars
|
||||
ItemList []Reply
|
||||
ItemList []ReplyUser
|
||||
Topic TopicUser
|
||||
Page int
|
||||
LastPage int
|
||||
|
@ -72,7 +72,7 @@ type ForumPage struct {
|
|||
CurrentUser User
|
||||
Header *HeaderVars
|
||||
ItemList []*TopicsRow
|
||||
Forum Forum
|
||||
Forum *Forum
|
||||
Page int
|
||||
LastPage int
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ type ProfilePage struct {
|
|||
Title string
|
||||
CurrentUser User
|
||||
Header *HeaderVars
|
||||
ItemList []Reply
|
||||
ItemList []ReplyUser
|
||||
ProfileOwner User
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
package main
|
||||
|
||||
func init() {
|
||||
plugins["helloworld"] = NewPlugin("helloworld", "Hello World", "Azareal", "http://github.com/Azareal", "", "", "", initHelloworld, nil, deactivateHelloworld, nil, nil)
|
||||
}
|
||||
|
||||
// init_helloworld is separate from init() as we don't want the plugin to run if the plugin is disabled
|
||||
func initHelloworld() error {
|
||||
plugins["helloworld"].AddHook("rrow_assign", helloworldReply)
|
||||
return nil
|
||||
}
|
||||
|
||||
func deactivateHelloworld() {
|
||||
plugins["helloworld"].RemoveHook("rrow_assign", helloworldReply)
|
||||
}
|
||||
|
||||
func helloworldReply(data interface{}) interface{} {
|
||||
reply := data.(*Reply)
|
||||
reply.Content = "Hello World!"
|
||||
reply.ContentHtml = "Hello World!"
|
||||
reply.Tag = "Auto"
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
func init() {
|
||||
plugins["heythere"] = NewPlugin("heythere", "Hey There", "Azareal", "http://github.com/Azareal", "", "", "", initHeythere, nil, deactivateHeythere, nil, nil)
|
||||
}
|
||||
|
||||
// init_heythere is separate from init() as we don't want the plugin to run if the plugin is disabled
|
||||
func initHeythere() error {
|
||||
plugins["heythere"].AddHook("topic_reply_row_assign", heythereReply)
|
||||
return nil
|
||||
}
|
||||
|
||||
func deactivateHeythere() {
|
||||
plugins["heythere"].RemoveHook("topic_reply_row_assign", heythereReply)
|
||||
}
|
||||
|
||||
func heythereReply(data ...interface{}) interface{} {
|
||||
currentUser := data[0].(*TopicPage).CurrentUser
|
||||
reply := data[1].(*ReplyUser)
|
||||
reply.Content = "Hey there, " + currentUser.Name + "!"
|
||||
reply.ContentHtml = "Hey there, " + currentUser.Name + "!"
|
||||
reply.Tag = "Auto"
|
||||
return nil
|
||||
}
|
|
@ -56,7 +56,7 @@ type SocialGroupPage struct {
|
|||
CurrentUser User
|
||||
Header *HeaderVars
|
||||
ItemList []*TopicsRow
|
||||
Forum Forum
|
||||
Forum *Forum
|
||||
SocialGroup *SocialGroup
|
||||
Page int
|
||||
LastPage int
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var pluginLangs = make(map[string]PluginLang)
|
||||
|
||||
// For non-native plugins to bind JSON files to. E.g. JS and Lua
|
||||
type PluginMeta struct {
|
||||
UName string
|
||||
Name string
|
||||
Author string
|
||||
URL string
|
||||
Settings string
|
||||
Tag string
|
||||
|
||||
Main string // The main file
|
||||
Hooks map[string]string // Hooks mapped to functions
|
||||
}
|
||||
|
||||
type PluginLang interface {
|
||||
GetName() string
|
||||
GetExts() []string
|
||||
|
||||
Init() error
|
||||
AddPlugin(meta PluginMeta) (*Plugin, error)
|
||||
//AddHook(name string, handler interface{}) error
|
||||
//RemoveHook(name string, handler interface{})
|
||||
//RunHook(name string, data interface{}) interface{}
|
||||
//RunVHook(name string data ...interface{}) interface{}
|
||||
}
|
||||
|
||||
/*
|
||||
var ext = filepath.Ext(pluginFile.Name())
|
||||
if ext == ".txt" || ext == ".go" {
|
||||
continue
|
||||
}
|
||||
*/
|
||||
|
||||
func InitPluginLangs() error {
|
||||
for _, pluginLang := range pluginLangs {
|
||||
pluginLang.Init()
|
||||
}
|
||||
|
||||
pluginList, err := GetPluginFiles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pluginItem := range pluginList {
|
||||
pluginFile, err := ioutil.ReadFile("./extend/" + pluginItem + "/plugin.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var plugin PluginMeta
|
||||
err = json.Unmarshal(pluginFile, &plugin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if plugin.UName == "" {
|
||||
return errors.New("The UName field must not be blank on plugin '" + pluginItem + "'")
|
||||
}
|
||||
if plugin.Name == "" {
|
||||
return errors.New("The Name field must not be blank on plugin '" + pluginItem + "'")
|
||||
}
|
||||
if plugin.Author == "" {
|
||||
return errors.New("The Author field must not be blank on plugin '" + pluginItem + "'")
|
||||
}
|
||||
if plugin.Main == "" {
|
||||
return errors.New("Couldn't find a main file for plugin '" + pluginItem + "'")
|
||||
}
|
||||
|
||||
var ext = filepath.Ext(plugin.Main)
|
||||
pluginLang, err := ExtToPluginLang(ext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pplugin, err := pluginLang.AddPlugin(plugin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plugins[plugin.UName] = pplugin
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetPluginFiles() (pluginList []string, err error) {
|
||||
pluginFiles, err := ioutil.ReadDir("./extend")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, pluginFile := range pluginFiles {
|
||||
if !pluginFile.IsDir() {
|
||||
continue
|
||||
}
|
||||
pluginList = append(pluginList, pluginFile.Name())
|
||||
}
|
||||
return pluginList, nil
|
||||
}
|
||||
|
||||
func ExtToPluginLang(ext string) (PluginLang, error) {
|
||||
for _, pluginLang := range pluginLangs {
|
||||
for _, registeredExt := range pluginLang.GetExts() {
|
||||
if registeredExt == ext {
|
||||
return pluginLang, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("No plugin lang handlers are capable of handling extension '" + ext + "'")
|
||||
}
|
|
@ -215,7 +215,7 @@ func write_selects(adapter qgen.DB_Adapter) error {
|
|||
|
||||
adapter.SimpleSelect("getGroups", "users_groups", "gid, name, permissions, plugin_perms, is_mod, is_admin, is_banned, tag", "", "", "")
|
||||
|
||||
adapter.SimpleSelect("getForums", "forums", "fid, name, desc, active, preset, parentID, parentType, topicCount, lastTopic, lastTopicID, lastReplyer, lastReplyerID, lastTopicTime", "", "fid ASC", "")
|
||||
adapter.SimpleSelect("getForums", "forums", "fid, name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID", "", "fid ASC", "")
|
||||
|
||||
adapter.SimpleSelect("getForumsPermissions", "forums_permissions", "gid, fid, permissions", "", "gid ASC, fid ASC", "")
|
||||
|
||||
|
@ -358,7 +358,7 @@ func write_updates(adapter qgen.DB_Adapter) error {
|
|||
|
||||
adapter.SimpleUpdate("removeTopicsFromForum", "forums", "topicCount = topicCount - ?", "fid = ?")
|
||||
|
||||
adapter.SimpleUpdate("updateForumCache", "forums", "lastTopic = ?, lastTopicID = ?, lastReplyer = ?, lastReplyerID = ?, lastTopicTime = UTC_TIMESTAMP()", "fid = ?")
|
||||
adapter.SimpleUpdate("updateForumCache", "forums", "lastTopicID = ?, lastReplyerID = ?", "fid = ?")
|
||||
|
||||
adapter.SimpleUpdate("addLikesToTopic", "topics", "likeCount = likeCount + ?", "tid = ?")
|
||||
|
||||
|
|
17
reply.go
17
reply.go
|
@ -8,8 +8,7 @@ package main
|
|||
|
||||
// ? - Should we add a reply store to centralise all the reply logic? Would this cover profile replies too or would that be seperate?
|
||||
|
||||
type Reply struct /* Should probably rename this to ReplyUser and rename ReplyShort to Reply */
|
||||
{
|
||||
type ReplyUser struct {
|
||||
ID int
|
||||
ParentID int
|
||||
Content string
|
||||
|
@ -36,7 +35,7 @@ type Reply struct /* Should probably rename this to ReplyUser and rename ReplySh
|
|||
ActionIcon string
|
||||
}
|
||||
|
||||
type ReplyShort struct {
|
||||
type Reply struct {
|
||||
ID int
|
||||
ParentID int
|
||||
Content string
|
||||
|
@ -51,14 +50,18 @@ type ReplyShort struct {
|
|||
LikeCount int
|
||||
}
|
||||
|
||||
func getReply(id int) (*ReplyShort, error) {
|
||||
reply := ReplyShort{ID: id}
|
||||
func (reply *Reply) Copy() Reply {
|
||||
return *reply
|
||||
}
|
||||
|
||||
func getReply(id int) (*Reply, error) {
|
||||
reply := Reply{ID: id}
|
||||
err := getReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress, &reply.LikeCount)
|
||||
return &reply, err
|
||||
}
|
||||
|
||||
func getUserReply(id int) (*ReplyShort, error) {
|
||||
reply := ReplyShort{ID: id}
|
||||
func getUserReply(id int) (*Reply, error) {
|
||||
reply := Reply{ID: id}
|
||||
err := getUserReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress)
|
||||
return &reply, err
|
||||
}
|
||||
|
|
38
routes.go
38
routes.go
|
@ -159,6 +159,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO: Make CanSee a method on *Group with a canSee field?
|
||||
var canSee []int
|
||||
if user.IsSuperAdmin {
|
||||
canSee, err = fstore.GetAllVisibleIDs()
|
||||
|
@ -379,7 +380,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user User, sfid string)
|
|||
topicItem.LastUser = userList[topicItem.LastReplyBy]
|
||||
}
|
||||
|
||||
pi := ForumPage{forum.Name, user, headerVars, topicList, *forum, page, lastPage}
|
||||
pi := ForumPage{forum.Name, user, headerVars, topicList, forum, page, lastPage}
|
||||
if preRenderHooks["pre_render_view_forum"] != nil {
|
||||
if runPreRenderHook("pre_render_view_forum", w, r, &user, &pi) {
|
||||
return
|
||||
|
@ -417,16 +418,22 @@ func routeForums(w http.ResponseWriter, r *http.Request, user User) {
|
|||
}
|
||||
|
||||
for _, fid := range canSee {
|
||||
//log.Print(forums[fid])
|
||||
var forum = *fstore.DirtyGet(fid)
|
||||
// Avoid data races by copying the struct into something we can freely mold without worrying about breaking something somewhere else
|
||||
var forum = fstore.DirtyGet(fid).Copy()
|
||||
if forum.ParentID == 0 && forum.Name != "" && forum.Active {
|
||||
if forum.LastTopicID != 0 {
|
||||
forum.LastTopicTime, err = relativeTime(forum.LastTopicTime)
|
||||
//topic, user := forum.GetLast()
|
||||
//if topic.ID != 0 && user.ID != 0 {
|
||||
if forum.LastTopic.ID != 0 && forum.LastReplyer.ID != 0 {
|
||||
forum.LastTopicTime, err = relativeTime(forum.LastTopic.LastReplyAt)
|
||||
if err != nil {
|
||||
InternalError(err, w)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
forum.LastTopicTime = ""
|
||||
}
|
||||
} else {
|
||||
forum.LastTopic = "None"
|
||||
forum.LastTopicTime = ""
|
||||
}
|
||||
if hooks["forums_frow_assign"] != nil {
|
||||
|
@ -448,7 +455,7 @@ func routeForums(w http.ResponseWriter, r *http.Request, user User) {
|
|||
func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
|
||||
var err error
|
||||
var page, offset int
|
||||
var replyList []Reply
|
||||
var replyList []ReplyUser
|
||||
|
||||
page, _ = strconv.Atoi(r.FormValue("page"))
|
||||
|
||||
|
@ -465,7 +472,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
|
|||
}
|
||||
|
||||
// Get the topic...
|
||||
topic, err := getTopicuser(tid)
|
||||
topic, err := getTopicUser(tid)
|
||||
if err == ErrNoRows {
|
||||
NotFound(w, r)
|
||||
return
|
||||
|
@ -488,7 +495,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
|
|||
|
||||
BuildWidgets("view_topic", &topic, headerVars, r)
|
||||
|
||||
topic.Content = parseMessage(topic.Content)
|
||||
topic.ContentHTML = parseMessage(topic.Content)
|
||||
topic.ContentLines = strings.Count(topic.Content, "\n")
|
||||
|
||||
// We don't want users posting in locked topics...
|
||||
|
@ -543,6 +550,8 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
|
|||
page = 1
|
||||
}
|
||||
|
||||
tpage := TopicPage{topic.Title, user, headerVars, replyList, topic, page, lastPage}
|
||||
|
||||
// Get the replies..
|
||||
rows, err := getTopicRepliesOffsetStmt.Query(topic.ID, offset, config.ItemsPerPage)
|
||||
if err == ErrNoRows {
|
||||
|
@ -554,7 +563,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
replyItem := Reply{ClassName: ""}
|
||||
replyItem := ReplyUser{ClassName: ""}
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType)
|
||||
if err != nil {
|
||||
|
@ -628,9 +637,8 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
|
|||
}
|
||||
replyItem.Liked = false
|
||||
|
||||
// TODO: Rename this to topic_rrow_assign
|
||||
if hooks["rrow_assign"] != nil {
|
||||
runHook("rrow_assign", &replyItem)
|
||||
if vhooks["topic_reply_row_assign"] != nil {
|
||||
runVhook("topic_reply_row_assign", &tpage, &replyItem)
|
||||
}
|
||||
replyList = append(replyList, replyItem)
|
||||
}
|
||||
|
@ -640,7 +648,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
|
|||
return
|
||||
}
|
||||
|
||||
tpage := TopicPage{topic.Title, user, headerVars, replyList, topic, page, lastPage}
|
||||
tpage.ItemList = replyList
|
||||
if preRenderHooks["pre_render_view_topic"] != nil {
|
||||
if runPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) {
|
||||
return
|
||||
|
@ -658,7 +666,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) {
|
|||
var err error
|
||||
var replyContent, replyCreatedByName, replyCreatedAt, replyAvatar, replyTag, replyClassName string
|
||||
var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int
|
||||
var replyList []Reply
|
||||
var replyList []ReplyUser
|
||||
|
||||
// SEO URLs...
|
||||
halves := strings.Split(r.URL.Path[len("/user/"):], ".")
|
||||
|
@ -736,7 +744,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) {
|
|||
|
||||
// TODO: Add a hook here
|
||||
|
||||
replyList = append(replyList, Reply{rid, puser.ID, replyContent, parseMessage(replyContent), replyCreatedBy, buildProfileURL(nameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
|
||||
replyList = append(replyList, ReplyUser{rid, puser.ID, replyContent, parseMessage(replyContent), replyCreatedBy, buildProfileURL(nameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
|
|
|
@ -73,7 +73,7 @@ w.Write(forums_0)
|
|||
if len(tmpl_forums_vars.ItemList) != 0 {
|
||||
for _, item := range tmpl_forums_vars.ItemList {
|
||||
w.Write(forums_1)
|
||||
if item.Desc != "" || item.LastTopicTime != "" {
|
||||
if item.Desc != "" || item.LastTopic.Title != "" {
|
||||
w.Write(forums_2)
|
||||
}
|
||||
w.Write(forums_3)
|
||||
|
@ -93,21 +93,25 @@ w.Write([]byte(item.Name))
|
|||
w.Write(forums_10)
|
||||
}
|
||||
w.Write(forums_11)
|
||||
w.Write([]byte(item.LastTopicLink))
|
||||
w.Write([]byte(item.LastTopic.Link))
|
||||
w.Write(forums_12)
|
||||
w.Write([]byte(item.LastTopic))
|
||||
if item.LastTopic.Title != "" {
|
||||
w.Write([]byte(item.LastTopic.Title))
|
||||
} else {
|
||||
w.Write(forums_13)
|
||||
if item.LastTopicTime != "" {
|
||||
w.Write(forums_14)
|
||||
w.Write([]byte(item.LastTopicTime))
|
||||
w.Write(forums_15)
|
||||
}
|
||||
w.Write(forums_14)
|
||||
if item.LastTopicTime != "" {
|
||||
w.Write(forums_15)
|
||||
w.Write([]byte(item.LastTopicTime))
|
||||
w.Write(forums_16)
|
||||
}
|
||||
} else {
|
||||
w.Write(forums_17)
|
||||
}
|
||||
} else {
|
||||
w.Write(forums_18)
|
||||
}
|
||||
w.Write(forums_19)
|
||||
w.Write(footer_0)
|
||||
if len(tmpl_forums_vars.Header.Themes) != 0 {
|
||||
for _, item := range tmpl_forums_vars.Header.Themes {
|
||||
|
|
|
@ -106,9 +106,9 @@ func compileTemplates() error {
|
|||
|
||||
log.Print("Compiling the templates")
|
||||
|
||||
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, "Date", "Date", 0, "", "127.0.0.1", 0, 1, "classname", "weird-data", buildProfileURL("fake-user", 62), "Fake User", config.DefaultGroup, "", 0, "", "", "", "", 58, false}
|
||||
var replyList []Reply
|
||||
replyList = append(replyList, Reply{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", config.DefaultGroup, "", 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
|
||||
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, "Date", "Date", 0, "", "127.0.0.1", 0, 1, "classname", "weird-data", buildProfileURL("fake-user", 62), "Fake User", config.DefaultGroup, "", 0, "", "", "", "", "", 58, false}
|
||||
var replyList []ReplyUser
|
||||
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", config.DefaultGroup, "", 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
|
||||
|
||||
var varList = make(map[string]VarItem)
|
||||
tpage := TopicPage{"Title", user, headerVars, replyList, topic, 1, 1}
|
||||
|
@ -135,6 +135,7 @@ func compileTemplates() error {
|
|||
}
|
||||
|
||||
for _, forum := range forums {
|
||||
//log.Printf("*forum %+v\n", *forum)
|
||||
forumList = append(forumList, *forum)
|
||||
}
|
||||
varList = make(map[string]VarItem)
|
||||
|
@ -154,7 +155,7 @@ func compileTemplates() error {
|
|||
|
||||
//var topicList []TopicUser
|
||||
//topicList = append(topicList,TopicUser{1,"topic-title","Topic Title","The topic content.",1,false,false,"Date","Date",1,"","127.0.0.1",0,1,"classname","","admin-fred","Admin Fred",config.DefaultGroup,"",0,"","","","",58,false})
|
||||
forumItem := Forum{1, "general", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0, "", "", 0, "", 0, ""}
|
||||
forumItem := makeDummyForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0)
|
||||
forumPage := ForumPage{"General Forum", user, headerVars, topicsList, forumItem, 1, 1}
|
||||
forumTmpl, err := c.compileTemplate("forum.html", "templates/", "ForumPage", forumPage, varList)
|
||||
if err != nil {
|
||||
|
|
|
@ -185,50 +185,52 @@ var topic_63 = []byte(`), url(/static/post-avatar-bg.jpg);background-position: 0
|
|||
var topic_64 = []byte(`-1`)
|
||||
var topic_65 = []byte(`0px;background-repeat:no-repeat, repeat-y;`)
|
||||
var topic_66 = []byte(`">
|
||||
`)
|
||||
var topic_67 = []byte(`
|
||||
<p class="editable_block user_content" style="margin:0;padding:0;">`)
|
||||
var topic_67 = []byte(`</p>
|
||||
var topic_68 = []byte(`</p>
|
||||
|
||||
<span class="controls">
|
||||
|
||||
<a href="`)
|
||||
var topic_68 = []byte(`" class="username real_username">`)
|
||||
var topic_69 = []byte(`</a>
|
||||
var topic_69 = []byte(`" class="username real_username">`)
|
||||
var topic_70 = []byte(`</a>
|
||||
`)
|
||||
var topic_70 = []byte(`<a href="/reply/like/submit/`)
|
||||
var topic_71 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
|
||||
var topic_72 = []byte(` style="background-color:#D6FFD6;"`)
|
||||
var topic_73 = []byte(`></button></a>`)
|
||||
var topic_74 = []byte(`<a href="/reply/edit/submit/`)
|
||||
var topic_75 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
|
||||
var topic_76 = []byte(`<a href="/reply/delete/submit/`)
|
||||
var topic_77 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
|
||||
var topic_78 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
|
||||
var topic_79 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
|
||||
var topic_80 = []byte(`
|
||||
var topic_71 = []byte(`<a href="/reply/like/submit/`)
|
||||
var topic_72 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
|
||||
var topic_73 = []byte(` style="background-color:#D6FFD6;"`)
|
||||
var topic_74 = []byte(`></button></a>`)
|
||||
var topic_75 = []byte(`<a href="/reply/edit/submit/`)
|
||||
var topic_76 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
|
||||
var topic_77 = []byte(`<a href="/reply/delete/submit/`)
|
||||
var topic_78 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
|
||||
var topic_79 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
|
||||
var topic_80 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
|
||||
var topic_81 = []byte(`
|
||||
<a href="/report/submit/`)
|
||||
var topic_81 = []byte(`?session=`)
|
||||
var topic_82 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
|
||||
var topic_82 = []byte(`?session=`)
|
||||
var topic_83 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
|
||||
|
||||
`)
|
||||
var topic_83 = []byte(`<a class="username hide_on_micro like_count">`)
|
||||
var topic_84 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
|
||||
var topic_85 = []byte(`<a class="username hide_on_micro user_tag">`)
|
||||
var topic_86 = []byte(`</a>`)
|
||||
var topic_87 = []byte(`<a class="username hide_on_micro level">`)
|
||||
var topic_88 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
|
||||
var topic_89 = []byte(`
|
||||
var topic_84 = []byte(`<a class="username hide_on_micro like_count">`)
|
||||
var topic_85 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
|
||||
var topic_86 = []byte(`<a class="username hide_on_micro user_tag">`)
|
||||
var topic_87 = []byte(`</a>`)
|
||||
var topic_88 = []byte(`<a class="username hide_on_micro level">`)
|
||||
var topic_89 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
|
||||
var topic_90 = []byte(`
|
||||
|
||||
</span>
|
||||
</article>
|
||||
`)
|
||||
var topic_90 = []byte(`</div>
|
||||
var topic_91 = []byte(`</div>
|
||||
|
||||
`)
|
||||
var topic_91 = []byte(`
|
||||
var topic_92 = []byte(`
|
||||
<div class="rowblock topic_reply_form">
|
||||
<form action="/reply/create/" method="post">
|
||||
<input name="tid" value='`)
|
||||
var topic_92 = []byte(`' type="hidden" />
|
||||
var topic_93 = []byte(`' type="hidden" />
|
||||
<div class="formrow real_first_child">
|
||||
<div class="formitem"><textarea name="reply-content" placeholder="Insert reply here" required></textarea></div>
|
||||
</div>
|
||||
|
@ -238,7 +240,7 @@ var topic_92 = []byte(`' type="hidden" />
|
|||
</form>
|
||||
</div>
|
||||
`)
|
||||
var topic_93 = []byte(`
|
||||
var topic_94 = []byte(`
|
||||
|
||||
</main>
|
||||
|
||||
|
@ -632,17 +634,18 @@ var forums_11 = []byte(`
|
|||
<span style="float: right;">
|
||||
<a href="`)
|
||||
var forums_12 = []byte(`" style="float: right;font-size: 14px;">`)
|
||||
var forums_13 = []byte(`</a>
|
||||
var forums_13 = []byte(`None`)
|
||||
var forums_14 = []byte(`</a>
|
||||
`)
|
||||
var forums_14 = []byte(`<br /><span class="rowsmall">`)
|
||||
var forums_15 = []byte(`</span>`)
|
||||
var forums_16 = []byte(`
|
||||
var forums_15 = []byte(`<br /><span class="rowsmall">`)
|
||||
var forums_16 = []byte(`</span>`)
|
||||
var forums_17 = []byte(`
|
||||
</span>
|
||||
<div style="clear: both;"></div>
|
||||
</div>
|
||||
`)
|
||||
var forums_17 = []byte(`<div class="rowitem passive">You don't have access to any forums.</div>`)
|
||||
var forums_18 = []byte(`
|
||||
var forums_18 = []byte(`<div class="rowitem passive">You don't have access to any forums.</div>`)
|
||||
var forums_19 = []byte(`
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
@ -653,7 +656,7 @@ var topics_0 = []byte(`
|
|||
<div class="rowblock rowhead">
|
||||
<div class="rowitem"><h1>Topic List</h1></div>
|
||||
</div>
|
||||
<div id="topic_list" class="rowblock topic_list">
|
||||
<div id="topic_list" class="rowblock topic_list" aria-label="The main topic list">
|
||||
`)
|
||||
var topics_1 = []byte(`<div class="rowitem topic_left passive datarow `)
|
||||
var topics_2 = []byte(`topic_sticky`)
|
||||
|
|
|
@ -123,7 +123,7 @@ w.Write(topic_22)
|
|||
w.Write(topic_23)
|
||||
}
|
||||
w.Write(topic_24)
|
||||
w.Write([]byte(tmpl_topic_vars.Topic.Content))
|
||||
w.Write([]byte(tmpl_topic_vars.Topic.ContentHTML))
|
||||
w.Write(topic_25)
|
||||
w.Write([]byte(tmpl_topic_vars.Topic.Content))
|
||||
w.Write(topic_26)
|
||||
|
@ -219,66 +219,67 @@ w.Write(topic_64)
|
|||
w.Write(topic_65)
|
||||
}
|
||||
w.Write(topic_66)
|
||||
w.Write([]byte(item.ContentHtml))
|
||||
w.Write(topic_67)
|
||||
w.Write([]byte(item.UserLink))
|
||||
w.Write([]byte(item.ContentHtml))
|
||||
w.Write(topic_68)
|
||||
w.Write([]byte(item.CreatedByName))
|
||||
w.Write([]byte(item.UserLink))
|
||||
w.Write(topic_69)
|
||||
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
|
||||
w.Write([]byte(item.CreatedByName))
|
||||
w.Write(topic_70)
|
||||
w.Write([]byte(strconv.Itoa(item.ID)))
|
||||
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
|
||||
w.Write(topic_71)
|
||||
if item.Liked {
|
||||
w.Write([]byte(strconv.Itoa(item.ID)))
|
||||
w.Write(topic_72)
|
||||
}
|
||||
if item.Liked {
|
||||
w.Write(topic_73)
|
||||
}
|
||||
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
|
||||
w.Write(topic_74)
|
||||
w.Write([]byte(strconv.Itoa(item.ID)))
|
||||
}
|
||||
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
|
||||
w.Write(topic_75)
|
||||
w.Write([]byte(strconv.Itoa(item.ID)))
|
||||
w.Write(topic_76)
|
||||
}
|
||||
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
|
||||
w.Write(topic_76)
|
||||
w.Write([]byte(strconv.Itoa(item.ID)))
|
||||
w.Write(topic_77)
|
||||
w.Write([]byte(strconv.Itoa(item.ID)))
|
||||
w.Write(topic_78)
|
||||
}
|
||||
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
|
||||
w.Write(topic_78)
|
||||
w.Write([]byte(item.IPAddress))
|
||||
w.Write(topic_79)
|
||||
}
|
||||
w.Write([]byte(item.IPAddress))
|
||||
w.Write(topic_80)
|
||||
w.Write([]byte(strconv.Itoa(item.ID)))
|
||||
}
|
||||
w.Write(topic_81)
|
||||
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
|
||||
w.Write([]byte(strconv.Itoa(item.ID)))
|
||||
w.Write(topic_82)
|
||||
if item.LikeCount > 0 {
|
||||
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
|
||||
w.Write(topic_83)
|
||||
w.Write([]byte(strconv.Itoa(item.LikeCount)))
|
||||
if item.LikeCount > 0 {
|
||||
w.Write(topic_84)
|
||||
w.Write([]byte(strconv.Itoa(item.LikeCount)))
|
||||
w.Write(topic_85)
|
||||
}
|
||||
if item.Tag != "" {
|
||||
w.Write(topic_85)
|
||||
w.Write([]byte(item.Tag))
|
||||
w.Write(topic_86)
|
||||
} else {
|
||||
w.Write([]byte(item.Tag))
|
||||
w.Write(topic_87)
|
||||
w.Write([]byte(strconv.Itoa(item.Level)))
|
||||
} else {
|
||||
w.Write(topic_88)
|
||||
}
|
||||
w.Write([]byte(strconv.Itoa(item.Level)))
|
||||
w.Write(topic_89)
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Write(topic_90)
|
||||
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
|
||||
w.Write(topic_91)
|
||||
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
|
||||
w.Write(topic_92)
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Write(topic_91)
|
||||
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
|
||||
w.Write(topic_92)
|
||||
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
|
||||
w.Write(topic_93)
|
||||
}
|
||||
w.Write(topic_94)
|
||||
w.Write(footer_0)
|
||||
if len(tmpl_topic_vars.Header.Themes) != 0 {
|
||||
for _, item := range tmpl_topic_vars.Header.Themes {
|
||||
|
|
|
@ -126,7 +126,7 @@ w.Write([]byte(strconv.Itoa(tmpl_topic_alt_vars.Topic.Level)))
|
|||
w.Write(topic_alt_24)
|
||||
}
|
||||
w.Write(topic_alt_25)
|
||||
w.Write([]byte(tmpl_topic_alt_vars.Topic.Content))
|
||||
w.Write([]byte(tmpl_topic_alt_vars.Topic.ContentHTML))
|
||||
w.Write(topic_alt_26)
|
||||
w.Write([]byte(tmpl_topic_alt_vars.Topic.Content))
|
||||
w.Write(topic_alt_27)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="rowitem"><a>Forums</a></div>
|
||||
</div>
|
||||
<div class="rowblock">
|
||||
{{range .ItemList}}<div class="rowitem {{if (.Desc) or (.LastTopicTime)}}datarow{{end}}">
|
||||
{{range .ItemList}}<div class="rowitem {{if (.Desc) or (.LastTopic.Title)}}datarow{{end}}">
|
||||
{{if .Desc}}<span style="float: left;">
|
||||
<a href="{{.Link}}" style="">{{.Name}}</a>
|
||||
<br /><span class="rowsmall">{{.Desc}}</span>
|
||||
|
@ -15,7 +15,7 @@
|
|||
</span>{{end}}
|
||||
|
||||
<span style="float: right;">
|
||||
<a href="{{.LastTopicLink}}" style="float: right;font-size: 14px;">{{.LastTopic}}</a>
|
||||
<a href="{{.LastTopic.Link}}" style="float: right;font-size: 14px;">{{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}None{{end}}</a>
|
||||
{{if .LastTopicTime}}<br /><span class="rowsmall">{{.LastTopicTime}}</span>{{end}}
|
||||
</span>
|
||||
<div style="clear: both;"></div>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<article class="rowblock post_container top_post">
|
||||
<div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="{{if .Topic.Avatar}}background-image:url({{.Topic.Avatar}}), url(/static/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
|
||||
<p class="hide_on_edit topic_content user_content" style="margin:0;padding:0;">{{.Topic.Content}}</p>
|
||||
<p class="hide_on_edit topic_content user_content" style="margin:0;padding:0;">{{.Topic.ContentHTML}}</p>
|
||||
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
|
||||
|
||||
<span class="controls">
|
||||
|
@ -56,6 +56,7 @@
|
|||
</article>
|
||||
{{else}}
|
||||
<article class="rowitem passive deletable_block editable_parent post_item {{.ClassName}}" style="{{if .Avatar}}background-image:url({{.Avatar}}), url(/static/post-avatar-bg.jpg);background-position: 0px {{if le .ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
|
||||
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
|
||||
<p class="editable_block user_content" style="margin:0;padding:0;">{{.ContentHtml}}</p>
|
||||
|
||||
<span class="controls">
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
{{if .Topic.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Topic.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">Level {{.Topic.Level}}</div><div class="tag_post"></div></div>{{end}}
|
||||
</div>
|
||||
<div class="content_container">
|
||||
<div class="hide_on_edit topic_content user_content">{{.Topic.Content}}</div>
|
||||
<div class="hide_on_edit topic_content user_content">{{.Topic.ContentHTML}}</div>
|
||||
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
|
||||
<div class="button_container">
|
||||
{{if .CurrentUser.Loggedin}}
|
||||
|
@ -58,6 +58,7 @@
|
|||
<span class="action_icon" style="font-size: 18px;padding-right: 5px;">{{.ActionIcon}}</span>
|
||||
<span>{{.ActionType}}</span>
|
||||
{{else}}
|
||||
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
|
||||
<div class="editable_block user_content">{{.ContentHtml}}</div>
|
||||
<div class="button_container">
|
||||
{{if $.CurrentUser.Loggedin}}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="rowblock rowhead">
|
||||
<div class="rowitem"><h1>Topic List</h1></div>
|
||||
</div>
|
||||
<div id="topic_list" class="rowblock topic_list">
|
||||
<div id="topic_list" class="rowblock topic_list" aria-label="The main topic list">
|
||||
{{range .ItemList}}<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}" style="{{if .Creator.Avatar}}background-image: url({{.Creator.Avatar}});background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;{{end}}">
|
||||
<span class="topic_inner_right rowsmall" style="float: right;">
|
||||
<span class="replyCount">{{.PostCount}} replies</span><br />
|
||||
|
|
|
@ -7,6 +7,11 @@ body {
|
|||
background-color: #222222;
|
||||
margin: 0;
|
||||
}
|
||||
p::selection, span::selection, a::selection {
|
||||
background-color: hsl(0,0%,75%);
|
||||
color: hsl(0,0%,20%);
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
#back {
|
||||
margin-left: auto;
|
||||
|
@ -133,7 +138,7 @@ a {
|
|||
}
|
||||
|
||||
.rowblock:not(.opthead):not(.colstack_head):not(.rowhead) .rowitem {
|
||||
font-size: 15px;
|
||||
font-size: 15px; /*16px*/
|
||||
}
|
||||
|
||||
.rowblock:last-child, .colstack_item:last-child {
|
||||
|
@ -479,7 +484,7 @@ input, select, textarea {
|
|||
}
|
||||
|
||||
/* Forum View */
|
||||
.rowhead, .opthead, .colstack_head {
|
||||
.rowhead, .opthead, .colstack_head, .rowhead .rowitem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
@ -797,6 +802,9 @@ input, select, textarea {
|
|||
.topic_list .topic_right {
|
||||
display: none;
|
||||
}
|
||||
#poweredBy span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 470px) {
|
||||
|
|
39
topic.go
39
topic.go
|
@ -7,8 +7,13 @@
|
|||
package main
|
||||
|
||||
//import "fmt"
|
||||
import "strconv"
|
||||
import "html/template"
|
||||
import (
|
||||
"html"
|
||||
"html/template"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ? - Add a TopicMeta struct for *Forums?
|
||||
|
||||
type Topic struct {
|
||||
ID int
|
||||
|
@ -54,6 +59,7 @@ type TopicUser struct {
|
|||
Group int
|
||||
Avatar string
|
||||
ContentLines int
|
||||
ContentHTML string
|
||||
Tag string
|
||||
URL string
|
||||
URLPrefix string
|
||||
|
@ -138,6 +144,18 @@ func (topic *Topic) RemoveLike(uid int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (topic *Topic) Update(name string, content string) error {
|
||||
content = preparseMessage(content)
|
||||
parsed_content := parseMessage(html.EscapeString(content))
|
||||
_, err := editTopicStmt.Exec(name, content, parsed_content, topic.ID)
|
||||
|
||||
tcache, ok := topics.(TopicCache)
|
||||
if ok {
|
||||
tcache.CacheRemove(topic.ID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (topic *Topic) CreateActionReply(action string, ipaddress string, user User) (err error) {
|
||||
_, err = createActionReplyStmt.Exec(topic.ID, action, ipaddress, user.ID)
|
||||
if err != nil {
|
||||
|
@ -152,8 +170,12 @@ func (topic *Topic) CreateActionReply(action string, ipaddress string, user User
|
|||
return err
|
||||
}
|
||||
|
||||
func (topic *Topic) Copy() Topic {
|
||||
return *topic
|
||||
}
|
||||
|
||||
// TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser
|
||||
func getTopicuser(tid int) (TopicUser, error) {
|
||||
func getTopicUser(tid int) (TopicUser, error) {
|
||||
tcache, tok := topics.(TopicCache)
|
||||
ucache, uok := users.(UserCache)
|
||||
if tok && uok {
|
||||
|
@ -165,7 +187,7 @@ func getTopicuser(tid int) (TopicUser, error) {
|
|||
}
|
||||
|
||||
// We might be better off just passing seperate topic and user structs to the caller?
|
||||
return copyTopicToTopicuser(topic, user), nil
|
||||
return copyTopicToTopicUser(topic, user), nil
|
||||
} else if ucache.GetLength() < ucache.GetCapacity() {
|
||||
topic, err = topics.Get(tid)
|
||||
if err != nil {
|
||||
|
@ -175,7 +197,7 @@ func getTopicuser(tid int) (TopicUser, error) {
|
|||
if err != nil {
|
||||
return TopicUser{ID: tid}, err
|
||||
}
|
||||
return copyTopicToTopicuser(topic, user), nil
|
||||
return copyTopicToTopicUser(topic, user), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,7 +215,7 @@ func getTopicuser(tid int) (TopicUser, error) {
|
|||
return tu, err
|
||||
}
|
||||
|
||||
func copyTopicToTopicuser(topic *Topic, user *User) (tu TopicUser) {
|
||||
func copyTopicToTopicUser(topic *Topic, user *User) (tu TopicUser) {
|
||||
tu.UserLink = user.Link
|
||||
tu.CreatedByName = user.Name
|
||||
tu.Group = user.Group
|
||||
|
@ -220,6 +242,11 @@ func copyTopicToTopicuser(topic *Topic, user *User) (tu TopicUser) {
|
|||
return tu
|
||||
}
|
||||
|
||||
// For use in tests and for generating blank topics for forums which don't have a last poster
|
||||
func getDummyTopic() *Topic {
|
||||
return &Topic{ID: 0, Title: ""}
|
||||
}
|
||||
|
||||
func getTopicByReply(rid int) (*Topic, error) {
|
||||
topic := Topic{ID: 0}
|
||||
err := getTopicByReplyStmt.QueryRow(rid).Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
|
|
|
@ -61,7 +61,7 @@ type MemoryTopicStore struct {
|
|||
|
||||
// NewMemoryTopicStore gives you a new instance of MemoryTopicStore
|
||||
func NewMemoryTopicStore(capacity int) *MemoryTopicStore {
|
||||
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
|
||||
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ func (mts *MemoryTopicStore) Get(id int) (*Topic, error) {
|
|||
}
|
||||
|
||||
topic = &Topic{ID: id}
|
||||
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
if err == nil {
|
||||
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
||||
_ = mts.CacheAdd(topic)
|
||||
|
@ -125,14 +125,14 @@ func (mts *MemoryTopicStore) Get(id int) (*Topic, error) {
|
|||
// BypassGet will always bypass the cache and pull the topic directly from the database
|
||||
func (mts *MemoryTopicStore) BypassGet(id int) (*Topic, error) {
|
||||
topic := &Topic{ID: id}
|
||||
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
||||
return topic, err
|
||||
}
|
||||
|
||||
func (mts *MemoryTopicStore) Reload(id int) error {
|
||||
topic := &Topic{ID: id}
|
||||
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
if err == nil {
|
||||
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
||||
_ = mts.CacheSet(topic)
|
||||
|
@ -160,7 +160,7 @@ func (mts *MemoryTopicStore) Delete(id int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = fstore.DecrementTopicCount(topic.ParentID)
|
||||
err = fstore.RemoveTopic(topic.ParentID)
|
||||
if err != nil && err != ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ type SQLTopicStore struct {
|
|||
}
|
||||
|
||||
func NewSQLTopicStore() *SQLTopicStore {
|
||||
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
|
||||
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -297,7 +297,7 @@ func NewSQLTopicStore() *SQLTopicStore {
|
|||
|
||||
func (sts *SQLTopicStore) Get(id int) (*Topic, error) {
|
||||
topic := Topic{ID: id}
|
||||
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
||||
return &topic, err
|
||||
}
|
||||
|
@ -305,7 +305,7 @@ func (sts *SQLTopicStore) Get(id int) (*Topic, error) {
|
|||
// BypassGet is an alias of Get(), as we don't have a cache for SQLTopicStore
|
||||
func (sts *SQLTopicStore) BypassGet(id int) (*Topic, error) {
|
||||
topic := &Topic{ID: id}
|
||||
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
|
||||
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
|
||||
return topic, err
|
||||
}
|
||||
|
@ -332,7 +332,7 @@ func (sts *SQLTopicStore) Delete(id int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = fstore.DecrementTopicCount(topic.ParentID)
|
||||
err = fstore.RemoveTopic(topic.ParentID)
|
||||
if err != nil && err != ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
|
9
user.go
9
user.go
|
@ -201,6 +201,10 @@ func (user *User) decreasePostStats(wcount int, topic bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (user *User) Copy() User {
|
||||
return *user
|
||||
}
|
||||
|
||||
// TODO: Write unit tests for this
|
||||
func (user *User) initPerms() {
|
||||
if user.TempGroup != 0 {
|
||||
|
@ -290,6 +294,11 @@ func wordsToScore(wcount int, topic bool) (score int) {
|
|||
return score
|
||||
}
|
||||
|
||||
// For use in tests and to help generate dummy users for forums which don't have last posters
|
||||
func getDummyUser() *User {
|
||||
return &User{ID: 0, Name: ""}
|
||||
}
|
||||
|
||||
// TODO: Write unit tests for this
|
||||
func buildProfileURL(slug string, uid int) string {
|
||||
if slug == "" {
|
||||
|
|
Loading…
Reference in New Issue