Revamped Tempra Conflux.

Updated the readme.
Added more comments.
The forumView cache for MemoryForumStore now excludes Social Groups.
Moved the word filter logic into it's own file.
You can now change the language via the configuration file.
Moved the inline CSS into the CSS files.
This commit is contained in:
Azareal 2017-09-13 16:09:13 +01:00
parent 01bef7a320
commit ce9195c841
30 changed files with 443 additions and 204 deletions

View File

@ -152,15 +152,11 @@ We're looking for ways to clean-up the plugin system so that all of them (except
![Tempra Conflux Mobile](https://github.com/Azareal/Gosora/blob/master/images/tempra-conflux-control-panel.png)
![Cosmo Conflux Theme](https://github.com/Azareal/Gosora/blob/master/images/cosmo-conflux.png)
![Cosmo Theme](https://github.com/Azareal/Gosora/blob/master/images/cosmo.png)
More images in the /images/ folder. Beware though, some of them are *really* outdated.
# Dependencies
* Go 1.8
* Go 1.9
* MariaDB (or any other MySQL compatible database engine). We'll allow other database engines in the future.

View File

@ -1,4 +1,9 @@
/* Work in progress. Check back later! */
/*
*
* Gosora Forum Store
* Copyright Azareal 2017 - 2018
*
*/
package main
import (
@ -15,6 +20,7 @@ var forumCreateMutex sync.Mutex
var forumPerms map[int]map[int]ForumPerms // [gid][fid]Perms
var fstore ForumStore
// ForumStore is an interface for accessing the forums and the metadata stored on them
type ForumStore interface {
LoadForums() error
DirtyGet(id int) *Forum
@ -43,9 +49,9 @@ type ForumStore interface {
GetGlobalCount() 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
type MemoryForumStore struct {
//forums map[int]*Forum
forums sync.Map
forums sync.Map // map[int]*Forum
forumView atomic.Value // []*Forum
//fids []int
forumCount int
@ -56,6 +62,7 @@ type MemoryForumStore struct {
getForumCount *sql.Stmt
}
// NewMemoryForumStore gives you a new instance of MemoryForumStore
func NewMemoryForumStore() *MemoryForumStore {
getStmt, err := qgen.Builder.SimpleSelect("forums", "name, desc, active, preset, parentID, parentType, topicCount, lastTopic, lastTopicID, lastReplyer, lastReplyerID, lastTopicTime", "fid = ?", "", "")
if err != nil {
@ -81,6 +88,7 @@ func NewMemoryForumStore() *MemoryForumStore {
}
}
// TODO: Add support for subforums
func (mfs *MemoryForumStore) LoadForums() error {
log.Print("Adding the uncategorised forum")
forumUpdateMutex.Lock()
@ -89,7 +97,7 @@ func (mfs *MemoryForumStore) LoadForums() error {
var forumView []*Forum
addForum := func(forum *Forum) {
mfs.forums.Store(forum.ID, forum)
if forum.Active && forum.Name != "" {
if forum.Active && forum.Name != "" && forum.ParentType == "" {
forumView = append(forumView, forum)
}
}
@ -132,7 +140,8 @@ func (mfs *MemoryForumStore) rebuildView() {
var forumView []*Forum
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
forum := value.(*Forum)
if forum.Active && forum.Name != "" {
// ? - ParentType blank means that it doesn't have a parent
if forum.Active && forum.Name != "" && forum.ParentType == "" {
forumView = append(forumView, forum)
}
return true

View File

@ -124,7 +124,7 @@ func BenchmarkTopicAdminRouteParallel(b *testing.B) {
for pb.Next() {
topicW.Body.Reset()
route_topic_id(topicW, topicReqAdmin, user)
routeTopicID(topicW, topicReqAdmin, user)
}
})
}
@ -143,7 +143,7 @@ func BenchmarkTopicGuestRouteParallel(b *testing.B) {
topicReq := httptest.NewRequest("get", "/topic/1", bytes.NewReader(nil))
for pb.Next() {
topicW.Body.Reset()
route_topic_id(topicW, topicReq, guestUser)
routeTopicID(topicW, topicReq, guestUser)
}
})
}

3
group_store.go Normal file
View File

@ -0,0 +1,3 @@
package main
// TODO: Coming Soon. Probably in the next commit or two

View File

@ -146,22 +146,23 @@ func main() {
func init() {
// Site Info
site.Name = "` + siteName + `" // Should be a setting in the database
site.Email = "" // Should be a setting in the database
site.Url = "` + siteURL + `"
site.Name = "` + siteName + `"
site.Email = ""
site.URL = "` + siteURL + `"
site.Port = "` + serverPort + `"
site.EnableSsl = false
site.EnableEmails = false
site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle
config.SslPrivkey = ""
config.SslFullchain = ""
site.Language = "english"
// Database details
db_config.Host = "` + dbHost + `"
db_config.Username = "` + dbUsername + `"
db_config.Password = "` + dbPassword + `"
db_config.Dbname = "` + dbName + `"
db_config.Port = "` + dbPort + `" // You probably won't need to change this
dbConfig.Host = "` + dbHost + `"
dbConfig.Username = "` + dbUsername + `"
dbConfig.Password = "` + dbPassword + `"
dbConfig.Dbname = "` + dbName + `"
dbConfig.Port = "` + dbPort + `" // You probably won't need to change this
// Limiters
config.MaxRequestSize = 5 * megabyte

View File

@ -53,7 +53,7 @@ func _initMysql() (err error) {
if err == sql.ErrNoRows {
fmt.Println("Unable to find the database. Attempting to create it")
_, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + "")
_, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + dbName)
if err != nil {
return err
}

View File

@ -39,5 +39,9 @@
},
"SettingLabels": {
"activation_type": "Activate All,Email Activation,Admin Approval"
},
"Accounts": {
"VerifyEmailSubject": "Validate Your Email @ {{name}}",
"VerifyEmailBody": "Dear {{username}}, following your registration on our forums, we ask you to validate your email, so that we can confirm that this email actually belongs to you.\n\nClick on the following link to do so. {{schema}}://{{url}}/user/edit/token/{{token}}\n\nIf you haven't created an account here, then please feel free to ignore this email.\nWe're sorry for the inconvenience this may have caused."
}
}

62
main.go
View File

@ -1,4 +1,9 @@
/* Copyright Azareal 2016 - 2018 */
/*
*
* Gosora Main File
* Copyright Azareal 2016 - 2018
*
*/
package main
import (
@ -8,7 +13,6 @@ import (
"net/http"
"os"
"strings"
"sync/atomic"
"time"
//"runtime/pprof"
)
@ -27,8 +31,6 @@ const terabyte int = gigabyte * 1024
const saltLength int = 32
const sessionLength int = 80
var enableWebsockets = false // Don't change this, the value is overwritten by an initialiser
var router *GenRouter
var startTime time.Time
@ -41,55 +43,13 @@ var groupCapCount int
var staticFiles = make(map[string]SFile)
var logWriter = io.MultiWriter(os.Stderr)
type WordFilter struct {
ID int
Find string
Replacement string
}
type WordFilterBox map[int]WordFilter
var wordFilterBox atomic.Value // An atomic value holding a WordFilterBox
func init() {
wordFilterBox.Store(WordFilterBox(make(map[int]WordFilter)))
}
func LoadWordFilters() error {
rows, err := get_word_filters_stmt.Query()
if err != nil {
return err
}
defer rows.Close()
var wordFilters = WordFilterBox(make(map[int]WordFilter))
var wfid int
var find string
var replacement string
for rows.Next() {
err := rows.Scan(&wfid, &find, &replacement)
if err != nil {
return err
}
wordFilters[wfid] = WordFilter{ID: wfid, Find: find, Replacement: replacement}
}
wordFilterBox.Store(wordFilters)
return rows.Err()
}
func addWordFilter(id int, find string, replacement string) {
wordFilters := wordFilterBox.Load().(WordFilterBox)
wordFilters[id] = WordFilter{ID: id, Find: find, Replacement: replacement}
wordFilterBox.Store(wordFilters)
}
func processConfig() {
config.Noavatar = strings.Replace(config.Noavatar, "{site_url}", site.Url, -1)
config.Noavatar = strings.Replace(config.Noavatar, "{site_url}", site.URL, -1)
if site.Port != "80" && site.Port != "443" {
site.Url = strings.TrimSuffix(site.Url, "/")
site.Url = strings.TrimSuffix(site.Url, "\\")
site.Url = strings.TrimSuffix(site.Url, ":")
site.Url = site.Url + ":" + site.Port
site.URL = strings.TrimSuffix(site.URL, "/")
site.URL = strings.TrimSuffix(site.URL, "\\")
site.URL = strings.TrimSuffix(site.URL, ":")
site.URL = site.URL + ":" + site.Port
}
}

View File

@ -137,7 +137,7 @@ func TestForumStore(t *testing.T) {
func TestSlugs(t *testing.T) {
var res string
var msgList []ME_Pair
var msgList []MEPair
msgList = addMEPair(msgList, "Unknown", "unknown")
msgList = addMEPair(msgList, "Unknown2", "unknown2")

View File

@ -27,12 +27,12 @@ func init() {
func _initDatabase() (err error) {
var _dbpassword string
if db_config.Password != "" {
_dbpassword = ":" + db_config.Password
if dbConfig.Password != "" {
_dbpassword = ":" + dbConfig.Password
}
// Open the database connection
db, err = sql.Open("mysql", db_config.Username+_dbpassword+"@tcp("+db_config.Host+":"+db_config.Port+")/"+db_config.Dbname+"?collation="+dbCollation)
db, err = sql.Open("mysql", dbConfig.Username+_dbpassword+"@tcp("+dbConfig.Host+":"+dbConfig.Port+")/"+dbConfig.Dbname+"?collation="+dbCollation)
if err != nil {
return err
}

View File

@ -5,33 +5,36 @@ package main
import "errors"
import "net/http"
var wsHub WS_Hub
// TODO: Disable WebSockets on high load? Add a Control Panel interface for disabling it?
var enableWebsockets = false // Put this in caps for consistency with the other constants?
var wsHub WSHub
var errWsNouser = errors.New("This user isn't connected via WebSockets")
type WS_Hub struct {
type WSHub struct {
}
func (_ *WS_Hub) guestCount() int {
func (_ *WSHub) guestCount() int {
return 0
}
func (_ *WS_Hub) userCount() int {
func (_ *WSHub) userCount() int {
return 0
}
func (hub *WS_Hub) broadcastMessage(_ string) error {
func (hub *WSHub) broadcastMessage(_ string) error {
return nil
}
func (hub *WS_Hub) pushMessage(_ int, _ string) error {
func (hub *WSHub) pushMessage(_ int, _ string) error {
return errWsNouser
}
func (hub *WS_Hub) pushAlert(_ int, _ int, _ string, _ string, _ int, _ int, _ int) error {
func (hub *WSHub) pushAlert(_ int, _ int, _ string, _ string, _ int, _ int, _ int) error {
return errWsNouser
}
func (hub *WS_Hub) pushAlerts(_ []int, _ int, _ string, _ string, _ int, _ int, _ int) error {
func (hub *WSHub) pushAlerts(_ []int, _ int, _ string, _ string, _ int, _ int, _ int) error {
return errWsNouser
}

View File

@ -207,10 +207,10 @@ type PanelEditForumPage struct {
Groups []GroupForumPermPreset
}
type NameLangPair struct {
/*type NameLangPair struct {
Name string
LangStr string
}
}*/
type NameLangToggle struct {
Name string

View File

@ -19,7 +19,6 @@ import (
// TODO: Let the admin edit phrases from inside the Control Panel? How should we persist these? Should we create a copy of the langpack or edit the primaries? Use the changeLangpack mutex for this?
// nolint Be quiet megacheck, this *is* used
var currentLanguage = "english"
var currentLangPack atomic.Value
var langpackCount int // TODO: Use atomics for this
@ -40,6 +39,7 @@ type LanguagePack struct {
GlobalPerms map[string]string
LocalPerms map[string]string
SettingLabels map[string]string
Accounts map[string]string // TODO: Apply these phrases in the software proper
}
// TODO: Add the ability to edit language JSON files from the Control Panel and automatically scan the files for changes
@ -86,9 +86,9 @@ func initPhrases() error {
return errors.New("You don't have any language packs")
}
langPack, ok := langpacks.Load(currentLanguage)
langPack, ok := langpacks.Load(site.Language)
if !ok {
return errors.New("Couldn't find the " + currentLanguage + " language pack")
return errors.New("Couldn't find the " + site.Language + " language pack")
}
currentLangPack.Store(langPack)
return nil
@ -139,6 +139,14 @@ func GetAllSettingLabels() map[string]string {
return currentLangPack.Load().(*LanguagePack).SettingLabels
}
func GetAccountPhrase(name string) string {
res, ok := currentLangPack.Load().(*LanguagePack).Accounts[name]
if !ok {
return "{name}"
}
return res
}
// ? - Use runtime reflection for updating phrases?
// TODO: Implement these
func AddPhrase() {

View File

@ -3,22 +3,21 @@ package main
import "strconv"
import "testing"
// TODO: Replace the soft tabs with hard ones
// go test -v
type ME_Pair struct {
type MEPair struct {
Msg string
Expects string
}
func addMEPair(msgList []ME_Pair, msg string, expects string) []ME_Pair {
return append(msgList, ME_Pair{msg, expects})
func addMEPair(msgList []MEPair, msg string, expects string) []MEPair {
return append(msgList, MEPair{msg, expects})
}
func TestBBCodeRender(t *testing.T) {
//t.Skip()
var res string
var msgList []ME_Pair
var msgList []MEPair
msgList = addMEPair(msgList, "hi", "hi")
msgList = addMEPair(msgList, "😀", "😀")
msgList = addMEPair(msgList, "[b]😀[/b]", "<b>😀</b>")
@ -190,7 +189,7 @@ func TestBBCodeRender(t *testing.T) {
func TestMarkdownRender(t *testing.T) {
//t.Skip()
var res string
var msgList []ME_Pair
var msgList []MEPair
msgList = addMEPair(msgList, "hi", "hi")
msgList = addMEPair(msgList, "**hi**", "<b>hi</b>")
msgList = addMEPair(msgList, "_hi_", "<u>hi</u>")

21
site.go
View File

@ -2,24 +2,23 @@ package main
import "net/http"
// TODO: Add a langPack configuration item or setting
var site = &Site{Name: "Magical Fairy Land"}
var db_config = DB_Config{Host: "localhost"}
var site = &Site{Name: "Magical Fairy Land", Language: "english"}
var dbConfig = DBConfig{Host: "localhost"}
var config Config
var dev DevConfig
type Site struct {
Name string
Email string
Url string
Name string // ? - Move this into the settings table?
Email string // ? - Move this into the settings table?
URL string
Port string
EnableSsl bool
EnableEmails bool
HasProxy bool
Language string // ? - Move this into the settings table?
}
type DB_Config struct {
type DBConfig struct {
Host string
Username string
Password string
@ -44,13 +43,13 @@ type Config struct {
DefaultRoute func(http.ResponseWriter, *http.Request, User)
DefaultGroup int
ActivationGroup int
StaffCss string
StaffCss string // ? - Move this into the settings table? Might be better to implement this as Group CSS
UncategorisedForumVisible bool
MinifyTemplates bool
MultiServer bool
Noavatar string
ItemsPerPage int
Noavatar string // ? - Move this into the settings table?
ItemsPerPage int // ? - Move this into the settings table?
}
type DevConfig struct {

View File

@ -1,3 +1,9 @@
/*
*
* Gosora Task System
* Copyright Azareal 2017 - 2018
*
*/
package main
import "time"

View File

@ -246,7 +246,7 @@ var topic_91 = []byte(`
`)
var footer_0 = []byte(`<div class="footer">
<div id="poweredBy" style="float: left;margin-top: 4px;">Powered by Gosora - <span style="font-size: 12px;">Made with love by Azareal</span></div>
<div id="poweredBy">Powered by Gosora - <span>Made with love by Azareal</span></div>
<form action="/theme/" method="post">
<div id="themeSelector" style="float: right;">
<select id="themeSelectorSelect" name="themeSelector" aria-label="Change the site's appearance">
@ -312,10 +312,8 @@ var topic_alt_19 = []byte(`
</form>
</div>
<!-- Stop inling this x.x -->
<style type="text/css">.rowitem:last-child .content_container { margin-bottom: 5px !important; }</style>
<div class="rowblock post_container" style="border-top: none;">
<article class="rowitem passive deletable_block editable_parent post_item top_post" style="background-color: #eaeaea;padding-top: 4px;padding-left: 5px;clear: both;border-bottom: none;padding-right: 4px;padding-bottom: 2px;">
<div class="rowblock post_container">
<article class="rowitem passive deletable_block editable_parent post_item top_post">
<div class="userinfo">
<div class="avatar_item" style="background-image: url(`)
var topic_alt_20 = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
@ -426,7 +424,7 @@ var topic_alt_84 = []byte(`
var topic_alt_85 = []byte(`</div>
`)
var topic_alt_86 = []byte(`
<div class="rowblock topic_reply_form" style="border-top: none;">
<div class="rowblock topic_reply_form">
<form action="/reply/create/" method="post">
<input name="tid" value='`)
var topic_alt_87 = []byte(`' type="hidden" />

View File

@ -1,5 +1,5 @@
<div class="footer">
<div id="poweredBy" style="float: left;margin-top: 4px;">Powered by Gosora - <span style="font-size: 12px;">Made with love by Azareal</span></div>
<div id="poweredBy">Powered by Gosora - <span>Made with love by Azareal</span></div>
<form action="/theme/" method="post">
<div id="themeSelector" style="float: right;">
<select id="themeSelectorSelect" name="themeSelector" aria-label="Change the site's appearance">

View File

@ -22,10 +22,8 @@
</form>
</div>
<!-- Stop inling this x.x -->
<style type="text/css">.rowitem:last-child .content_container { margin-bottom: 5px !important; }</style>
<div class="rowblock post_container" style="border-top: none;">
<article class="rowitem passive deletable_block editable_parent post_item top_post" style="background-color: #eaeaea;padding-top: 4px;padding-left: 5px;clear: both;border-bottom: none;padding-right: 4px;padding-bottom: 2px;">
<div class="rowblock post_container">
<article class="rowitem passive deletable_block editable_parent post_item top_post">
<div class="userinfo">
<div class="avatar_item" style="background-image: url({{.Topic.Avatar}}), url(/static/white-dot.jpg);background-position: 0px -10px;">&nbsp;</div>
<a href="{{.Topic.UserLink}}" class="the_name">{{.Topic.CreatedByName}}</a>
@ -79,7 +77,7 @@
</article>
{{end}}</div>
{{if .CurrentUser.Perms.CreateReply}}
<div class="rowblock topic_reply_form" style="border-top: none;">
<div class="rowblock topic_reply_form">
<form action="/reply/create/" method="post">
<input name="tid" value='{{.Topic.ID}}' type="hidden" />
<div class="formrow">

View File

@ -460,6 +460,13 @@ input, select, textarea {
font-size: 13px;
padding: 4px;
}
#poweredBy {
float: left;
margin-top: 4px;
}
#poweredBy span {
font-size: 12px;
}
/* Forum View */
.rowhead {

View File

@ -7,21 +7,29 @@
body {
font-family: arial;
padding-bottom: 8px;
/* TODO: Redo the background */
background-image: url('/static/test_bg2.svg');
background-size: cover;
background: white;
}
/* Patch for Edge, until they fix emojis in arial x.x */
@supports (-ms-ime-align:auto) { .user_content { font-family: Segoe UI Emoji, arial; } }
@supports (-ms-ime-align:auto) {
.user_content { font-family: Segoe UI Emoji, arial; }
}
ul {
padding-left: 0px;
padding-right: 0px;
height: 36px;
list-style-type: none;
border: 1px solid #ccc;
background-color: white;
border: 1px solid hsl(0,0%,80%);
background: hsl(0, 0%, 97%);
margin-bottom: 12px;
margin-top: 0px;
border-bottom: 1.5px inset hsl(0,0%,80%);
margin-left: -8px;
margin-right: -8px;
}
li {
height: 35px;
@ -29,7 +37,9 @@ li {
padding-top: 8px;
padding-bottom: 8px;
}
li:hover { background: rgb(250,250,250); }
li:hover {
background: rgb(250,250,250);
}
li a {
text-decoration: none;
/*color: #515151;*/
@ -38,12 +48,14 @@ li a {
}
.menu_left {
float: left;
border-right: 1px solid #ccc;
border-right: 1px solid hsl(0,0%,80%);
border-bottom: 1.5px inset hsl(0,0%,80%);
padding-right: 10px;
background: hsl(0, 0%, 98%);
}
.menu_right {
float: right;
border-left: 1px solid #ccc;
border-left: 1px solid hsl(0,0%,80%);
padding-right: 10px;
}
@ -85,8 +97,9 @@ li a {
color: black;
font-weight: bold;
}
.selectedAlert .alert_counter { display: none; }
.menu_alerts .alertList { display: none; }
.selectedAlert .alert_counter, .menu_alerts .alertList {
display: none;
}
.selectedAlert .alertList {
position: absolute;
top: 51px;
@ -96,10 +109,10 @@ li a {
line-height: 16px;
width: 300px;
right: calc(5% + 7px);
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-top: 1px solid hsl(0,0%,80%);
border-left: 1px solid hsl(0,0%,80%);
border-right: 1px solid hsl(0,0%,80%);
border-bottom: 1px solid hsl(0,0%,80%);
margin-bottom: 10px;
}
.alertItem {
@ -136,19 +149,47 @@ li a {
padding: 0px;
margin-left: auto;
margin-right: auto;
background: hsl(0, 0%, 98%);
border: 1px solid hsl(0,0%,80%);
border-top: none;
}
#back {
padding: 12px;
padding-top: 0px;
}
/* Explict declaring each border direction to fix a bug in Chrome where an override to .rowhead was also applying to .rowblock in some cases */
.rowblock {
border: 1px solid #ccc;
border: 1px solid hsl(0,0%,80%);
width: 100%;
padding: 0px;
padding-top: 0px;
border-top: 1px solid hsl(0,0%,80%);
border-left: 1px solid hsl(0,0%,80%);
border-right: 1px solid hsl(0,0%,80%);
border-bottom: 1.5px inset hsl(0,0%,80%);
}
.rowblock:empty {
display: none;
}
.rowblock:empty { display: none; }
.rowsmall {
font-size:12px;
}
/* Firefox specific CSS */
@supports (-moz-appearance: none) {
ul, .menu_left, .rowblock {
border-bottom: 2px inset hsl(0,0%,40%);
}
}
/* Edge... We can't get the exact shade here, because of how they implemented it x.x */
@supports (-ms-ime-align:auto) {
ul, .menu_left, .rowblock {
border-bottom: 1.5px inset hsl(0,0%,100%);
}
}
.rowlist {
font-size: 14px;
}
@ -162,25 +203,6 @@ li a {
padding-left: 48px;
}
.colblock_left {
border: 1px solid #ccc;
padding: 0px;
padding-top: 0px;
width: 30%;
float: left;
margin-right: 8px;
}
.colblock_right {
border: 1px solid #ccc;
padding: 0px;
padding-top: 0px;
width: 65%;
overflow: hidden;
word-wrap: break-word;
}
.colblock_left:empty, .colblock_right:empty { display: none; }
/* The new method of doing columns layouts, colblock is now deprecated :( */
.colstack_left {
float: left;
width: 30%;
@ -192,7 +214,7 @@ li a {
width: calc(70% - 15px);
}
.colstack_item {
border: 1px solid #ccc;
border: 1px solid hsl(0,0%,80%);
padding: 0px;
padding-top: 0px;
width: 100%;
@ -211,7 +233,7 @@ li a {
margin-top: 2px;
}
.grid_item {
border: 1px solid #ccc;
border: 1px solid hsl(0,0%,80%);
word-wrap: break-word;
background-color: white;
width: 100%;
@ -223,21 +245,33 @@ li a {
padding-bottom: 12px;
font-size: 16px;
}
.grid_istat { margin-bottom: 5px; }
.grid_istat {
margin-bottom: 5px;
}
.stat_green { background-color: lightgreen; border-color: green; }
.stat_orange { background-color: #ffe4b3; border-color: orange; }
.stat_red { background-color: #ffb2b2; border-color: red; }
.stat_disabled { background-color: lightgray; border-color: gray; }
.stat_green {
background-color: lightgreen;
border-color: green;
}
.stat_orange {
background-color: #ffe4b3;
border-color: orange;
}
.stat_red {
background-color: #ffb2b2;
border-color: red;
}
.stat_disabled {
background-color: lightgray;
border-color: gray;
}
.rowitem {
width: 100%;
/*padding-left: 8px;
padding-right: 8px;
padding-top: 17px;
padding-bottom: 12px;
font-weight: bold;
text-transform: uppercase;*/
padding-bottom: 12px;*/
padding-left: 10px;
padding-top: 14px;
padding-bottom: 12px;
@ -249,13 +283,16 @@ li a {
text-transform: none;
}
.rowitem:not(:last-child) {
border-bottom: 1px dotted #ccc;
border-bottom: 1px solid hsl(0,0%,85%);
}
.rowitem a {
text-decoration: none;
color: black;
}
.rowitem a:hover { color: silver; }
.rowitem a:hover {
color: hsl(0,0%,40%);
}
.opthead { display: none; }
.datarow {
padding-top: 10px;
@ -272,8 +309,12 @@ li a {
content: " ";
display: table;
}
.formrow:after { clear: both; }
.formrow:not(:last-child) { border-bottom: 1px dotted #ccc; }
.formrow:after {
clear: both;
}
.formrow:not(:last-child) {
border-bottom: 1px dotted hsl(0,0%,80%);
}
.formitem {
float: left;
@ -282,8 +323,12 @@ li a {
/*font-size: 17px;*/
font-weight: normal;
}
.formitem:not(:last-child) { border-right: 1px dotted #ccc; }
.formitem.invisible_border { border: none; }
.formitem:not(:last-child) {
border-right: 1px dotted hsl(0,0%,80%);
}
.formitem.invisible_border {
border: none;
}
/* Mostly for textareas */
.formitem:only-child { width: 100%; }
@ -296,7 +341,9 @@ li a {
margin: 0 auto;
float: none;
}
.formitem:not(:only-child) input, .formitem:not(:only-child) select { padding: 3px;/*5px;*/ }
.formitem:not(:only-child) input, .formitem:not(:only-child) select {
padding: 3px;/*5px;*/
}
.formitem:not(:only-child).formlabel {
padding-top: 15px;/*18px;*/
padding-bottom: 12px;/*16px;*/
@ -308,7 +355,7 @@ li a {
margin-left: auto;
margin-right: auto;
font-size: 15px;
border-color: #ccc;
border-color: hsl(0,0%,80%);
}
button {
@ -318,6 +365,33 @@ button {
/* Topics */
.topic_list {
display: grid;
grid-template-columns: calc(100% - 204px) 204px;
}
.topic_list .topic_inner_right {
display: none;
}
.topic_list .lastReplyAt {
white-space: nowrap;
}
@supports not (display: grid) {
.topic_list .rowitem {
float: left;
overflow: hidden;
}
.topic_list .topic_left {
width: calc(100% - 204px);
}
.topic_list .topic_right {
width: 204px;
}
}
.topic_status_sticky {
display: none;
}
.topic_sticky {
background-color: rgb(255,255,234);
}
@ -338,8 +412,22 @@ button {
}
.topic_status:empty { display: none; }
.rowhead, .colstack_head {
border-bottom: none;
}
.rowhead .rowitem, .opthead .rowitem, .colstack_head .rowitem {
background: linear-gradient(to bottom, white, hsl(0, 0%, 93%));
/*background: linear-gradient(to bottom, white, hsl(0, 0%, 93%));*/
background: hsl(0, 0%, 98%);
}
.rowhead h1, .colstack_head h1 {
-webkit-margin-before: 0; /* Opera / Chrome Implicit padding */
-webkit-margin-after: 0;
font-weight: normal;
font-size: 16px;
margin-block-start: 0; /* Firefox Implicit padding */
margin-block-end: 0;
margin-top: 0;
margin-bottom: 0; /* Edge Implicit padding */
}
.topic_sticky_head {
background-color: #FFFFEA;
@ -360,7 +448,7 @@ button {
color: #505050; /* 80,80,80 */
background-color: #FFFFFF;
border-style: solid;
border-color: #ccc;
border-color: hsl(0,0%,80%);
border-width: 1px;
font-size: 15px;
}
@ -396,7 +484,7 @@ button.username {
display: block;
padding: 5px;
margin-bottom: 10px;
border: 1px solid #ccc;
border: 1px solid hsl(0,0%,80%);
background-color: white;
}
.alert_success {
@ -415,16 +503,19 @@ button.username {
}
/* Tempra Conflux */
.user_content {
padding: 5px;
margin-top: 3px;
margin-top: 2px;
margin-bottom: 0;
background: white;
min-height: 145px;
padding-bottom: 0;
width: 100%;
}
.user_content.nobuttons { min-height: 168px; }
.user_content.nobuttons {
min-height: 168px;
}
.button_container {
border-top: solid 1px #eaeaea;
@ -515,7 +606,8 @@ button.username {
float: left;
position: sticky;
top: 4px;
box-shadow:0 1px 2px rgba(0,0,0,.1);
/*box-shadow: 0 1px 2px rgba(0,0,0,.1);*/
border-bottom: 1px inset hsl(0,0%,80%);
}
.userinfo .avatar_item {
background-repeat: no-repeat, repeat-y;
@ -534,7 +626,8 @@ button.username {
min-height: 128px;
margin-bottom: 0;
margin-right: 3px;
box-shadow:0 1px 2px rgba(0,0,0,.1);
/*box-shadow: 0 1px 2px rgba(0,0,0,.1);*/
border-bottom: 1px inset hsl(0,0%,80%);
}
.action_item .userinfo { display: none; }
@ -551,7 +644,7 @@ button.username {
border-width: 1px;
background-color: #FFFFFF;
border-style: solid;
border-color: #ccc;
border-color: hsl(0,0%,80%);
padding: 0px;
padding-left: 5px;
padding-right: 5px;
@ -574,4 +667,62 @@ button.username {
top: -2px;
}
.post_container {
border-bottom: none;
}
.topic_reply_form {
border-top: 1px solid hsl(0,0%,80%);
}
.post_container .post_item {
background-color: #eaeaea;
padding-top: 4px;
padding-left: 5px;
clear: both;
border-bottom: none;
padding-right: 4px;
padding-bottom: 2px;
}
.post_container .post_item:first-child {
padding-top: 6px;
}
.post_container .post_item:last-child .content_container {
margin-bottom: 6px !important;
}
.footer {
border: 1px solid hsl(0,0%,80%);
margin-top: 12px;
clear: both;
height: 40px;
padding: 6px;
padding-left: 10px;
padding-right: 10px;
background-color: white;
border-bottom: 1.5px inset hsl(0,0%,80%);
}
.footer select {
padding: 2px;
margin-top: 1px;
}
#poweredBy {
float: left;
margin-top: 4px;
}
#poweredBy span {
font-size: 12px;
}
/* Firefox specific CSS */
@supports (-moz-appearance: none) {
.footer {
border-bottom: 2px inset hsl(0,0%,40%);
}
}
/* Edge... We can't get the exact shade here, because of how they implemented it x.x */
@supports (-ms-ime-align:auto) {
.footer {
border-bottom: 1.5px inset hsl(0,0%,100%);
}
}
{{template "media.partial.css" }}

View File

@ -373,9 +373,6 @@ button {
.topic_list .topic_inner_right {
display: none;
}
.topic_list .rowitem:last-child {
margin-bottom: 10px;
}
.topic_list .lastReplyAt {
white-space: nowrap;
}
@ -598,6 +595,27 @@ button.username {
top: -2px;
}
.footer {
border: 1px solid #ccc;
margin-top: 12px;
clear: both;
height: 40px;
padding: 6px;
padding-left: 10px;
padding-right: 10px;
}
.footer select {
padding: 2px;
margin-top: 1px;
}
#poweredBy {
float: left;
margin-top: 4px;
}
#poweredBy span {
font-size: 12px;
}
#profile_comments .rowitem {
background-repeat: no-repeat, repeat-y;
background-size: 128px;

View File

@ -1,3 +1,9 @@
/*
*
* Gosora Topic File
* Copyright Azareal 2017 - 2018
*
*/
package main
//import "fmt"

View File

@ -1,3 +1,9 @@
/*
*
* Gosora Topic Store
* Copyright Azareal 2017 - 2018
*
*/
package main
import "log"
@ -32,6 +38,7 @@ type MemoryTopicStore struct {
sync.RWMutex
}
// NewMemoryTopicStore gives you a new instance of MemoryTopicStore
func NewMemoryTopicStore(capacity int) *MemoryTopicStore {
stmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
if err != nil {

View File

@ -1,3 +1,9 @@
/*
*
* Gosora User File
* Copyright Azareal 2017 - 2018
*
*/
package main
import (
@ -141,7 +147,7 @@ func SendValidationEmail(username string, email string, token string) bool {
// TODO: Move these to the phrase system
subject := "Validate Your Email @ " + site.Name
msg := "Dear " + username + ", following your registration on our forums, we ask you to validate your email, so that we can confirm that this email actually belongs to you.\n\nClick on the following link to do so. " + schema + "://" + site.Url + "/user/edit/token/" + token + "\n\nIf you haven't created an account here, then please feel free to ignore this email.\nWe're sorry for the inconvenience this may have caused."
msg := "Dear " + username + ", following your registration on our forums, we ask you to validate your email, so that we can confirm that this email actually belongs to you.\n\nClick on the following link to do so. " + schema + "://" + site.URL + "/user/edit/token/" + token + "\n\nIf you haven't created an account here, then please feel free to ignore this email.\nWe're sorry for the inconvenience this may have caused."
return SendEmail(email, subject, msg)
}

View File

@ -46,6 +46,7 @@ type MemoryUserStore struct {
sync.RWMutex
}
// NewMemoryUserStore gives you a new instance of MemoryUserStore
func NewMemoryUserStore(capacity int) *MemoryUserStore {
getStmt, err := qgen.Builder.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", "")
if err != nil {

View File

@ -1,3 +1,9 @@
/*
*
* Utility Functions And Stuff
* Copyright Azareal 2017 - 2018
*
*/
package main
import (

View File

@ -1,5 +1,11 @@
// +build !no_ws
/*
*
* Gosora WebSocket Subsystem
* Copyright Azareal 2017 - 2018
*
*/
package main
import (
@ -17,44 +23,46 @@ import (
"github.com/gorilla/websocket"
)
type WS_User struct {
type WSUser struct {
conn *websocket.Conn
User *User
}
type WS_Hub struct {
onlineUsers map[int]*WS_User
onlineGuests map[*WS_User]bool
type WSHub struct {
onlineUsers map[int]*WSUser
onlineGuests map[*WSUser]bool
guests sync.RWMutex
users sync.RWMutex
}
var wsHub WS_Hub
// TODO: Disable WebSockets on high load? Add a Control Panel interface for disabling it?
var enableWebsockets = true // Put this in caps for consistency with the other constants?
var wsHub WSHub
var wsUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
var errWsNouser = errors.New("This user isn't connected via WebSockets")
func init() {
enableWebsockets = true
adminStatsWatchers = make(map[*WS_User]bool)
wsHub = WS_Hub{
onlineUsers: make(map[int]*WS_User),
onlineGuests: make(map[*WS_User]bool),
adminStatsWatchers = make(map[*WSUser]bool)
wsHub = WSHub{
onlineUsers: make(map[int]*WSUser),
onlineGuests: make(map[*WSUser]bool),
}
}
func (hub *WS_Hub) guestCount() int {
func (hub *WSHub) guestCount() int {
defer hub.guests.RUnlock()
hub.guests.RLock()
return len(hub.onlineGuests)
}
func (hub *WS_Hub) userCount() int {
func (hub *WSHub) userCount() int {
defer hub.users.RUnlock()
hub.users.RLock()
return len(hub.onlineUsers)
}
func (hub *WS_Hub) broadcastMessage(msg string) error {
func (hub *WSHub) broadcastMessage(msg string) error {
hub.users.RLock()
for _, wsUser := range hub.onlineUsers {
w, err := wsUser.conn.NextWriter(websocket.TextMessage)
@ -67,7 +75,7 @@ func (hub *WS_Hub) broadcastMessage(msg string) error {
return nil
}
func (hub *WS_Hub) pushMessage(targetUser int, msg string) error {
func (hub *WSHub) pushMessage(targetUser int, msg string) error {
hub.users.RLock()
wsUser, ok := hub.onlineUsers[targetUser]
hub.users.RUnlock()
@ -85,7 +93,7 @@ func (hub *WS_Hub) pushMessage(targetUser int, msg string) error {
return nil
}
func (hub *WS_Hub) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
func (hub *WSHub) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
//log.Print("In pushAlert")
hub.users.RLock()
wsUser, ok := hub.onlineUsers[targetUser]
@ -111,9 +119,9 @@ func (hub *WS_Hub) pushAlert(targetUser int, asid int, event string, elementType
return nil
}
func (hub *WS_Hub) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
func (hub *WSHub) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
//log.Print("In pushAlerts")
var wsUsers []*WS_User
var wsUsers []*WSUser
hub.users.RLock()
// We don't want to keep a lock on this for too long, so we'll accept some nil pointers
for _, uid := range users {
@ -165,7 +173,7 @@ func routeWebsockets(w http.ResponseWriter, r *http.Request, user User) {
return
}
wsUser := &WS_User{conn, userptr}
wsUser := &WSUser{conn, userptr}
if user.ID == 0 {
wsHub.guests.Lock()
wsHub.onlineGuests[wsUser] = true
@ -224,7 +232,7 @@ func routeWebsockets(w http.ResponseWriter, r *http.Request, user User) {
conn.Close()
}
func wsPageResponses(wsUser *WS_User, page []byte) {
func wsPageResponses(wsUser *WSUser, page []byte) {
switch string(page) {
case "/panel/":
//log.Print("/panel/ WS Route")
@ -255,7 +263,7 @@ func wsPageResponses(wsUser *WS_User, page []byte) {
}
}
func wsLeavePage(wsUser *WS_User, page []byte) {
func wsLeavePage(wsUser *WSUser, page []byte) {
switch string(page) {
case "/panel/":
adminStatsMutex.Lock()
@ -264,7 +272,7 @@ func wsLeavePage(wsUser *WS_User, page []byte) {
}
}
var adminStatsWatchers map[*WS_User]bool
var adminStatsWatchers map[*WSUser]bool
var adminStatsMutex sync.RWMutex
func adminStatsTicker() {

45
word_filters.go Normal file
View File

@ -0,0 +1,45 @@
package main
import "sync/atomic"
type WordFilter struct {
ID int
Find string
Replacement string
}
type WordFilterBox map[int]WordFilter
var wordFilterBox atomic.Value // An atomic value holding a WordFilterBox
func init() {
wordFilterBox.Store(WordFilterBox(make(map[int]WordFilter)))
}
func LoadWordFilters() error {
rows, err := get_word_filters_stmt.Query()
if err != nil {
return err
}
defer rows.Close()
var wordFilters = WordFilterBox(make(map[int]WordFilter))
var wfid int
var find string
var replacement string
for rows.Next() {
err := rows.Scan(&wfid, &find, &replacement)
if err != nil {
return err
}
wordFilters[wfid] = WordFilter{ID: wfid, Find: find, Replacement: replacement}
}
wordFilterBox.Store(wordFilters)
return rows.Err()
}
func addWordFilter(id int, find string, replacement string) {
wordFilters := wordFilterBox.Load().(WordFilterBox)
wordFilters[id] = WordFilter{ID: id, Find: find, Replacement: replacement}
wordFilterBox.Store(wordFilters)
}