You can now search for whatever IP you want in the IP Searcher.
Removed the Uncategorised Forum. Added the Backup Page for super admins. Not quite functional yet. Forums are now sorted properly again. Fixed a bug in DirtyGet() where invalid IDs would trigger a panic. Fixed a bug where alternate themes wouldn't work without setting them as default first and restarting Gosora.
This commit is contained in:
parent
d869b87aa1
commit
11c60b3cbe
|
@ -7,6 +7,7 @@ brun.bat
|
|||
|
||||
uploads/avatar_*
|
||||
uploads/socialgroup_*
|
||||
backups/*.sql
|
||||
bin/*
|
||||
out/*
|
||||
*.exe
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
This file is here so that Git will include this folder in the repository.
|
|
@ -39,7 +39,7 @@ func init() {
|
|||
config.DefaultGroup = 3 // Should be a setting in the database
|
||||
config.ActivationGroup = 5 // Should be a setting in the database
|
||||
config.StaffCSS = "staff_post"
|
||||
config.UncategorisedForumVisible = true
|
||||
config.DefaultForum = 2
|
||||
config.MinifyTemplates = false
|
||||
config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features
|
||||
|
||||
|
|
14
forum.go
14
forum.go
|
@ -39,6 +39,20 @@ type ForumSimple struct {
|
|||
Preset string
|
||||
}
|
||||
|
||||
// TODO: Replace this sorting mechanism with something a lot more efficient
|
||||
// ? - Use sort.Slice instead?
|
||||
type SortForum []*Forum
|
||||
|
||||
func (sf SortForum) Len() int {
|
||||
return len(sf)
|
||||
}
|
||||
func (sf SortForum) Swap(i, j int) {
|
||||
sf[i], sf[j] = sf[j], sf[i]
|
||||
}
|
||||
func (sf SortForum) Less(i, j int) bool {
|
||||
return sf[i].ID < sf[j].ID
|
||||
}
|
||||
|
||||
func buildForumURL(slug string, fid int) string {
|
||||
if slug == "" {
|
||||
return "/forum/" + strconv.Itoa(fid)
|
||||
|
|
|
@ -9,6 +9,7 @@ package main
|
|||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
|
@ -104,8 +105,6 @@ func (mfs *MemoryForumStore) LoadForums() error {
|
|||
}
|
||||
}
|
||||
|
||||
addForum(&Forum{0, buildForumURL(nameToSlug("Uncategorised"), 0), "Uncategorised", "", config.UncategorisedForumVisible, "all", 0, "", 0, "", "", 0, "", 0, ""})
|
||||
|
||||
rows, err := getForumsStmt.Query()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -148,16 +147,16 @@ func (mfs *MemoryForumStore) rebuildView() {
|
|||
}
|
||||
return true
|
||||
})
|
||||
sort.Sort(SortForum(forumView))
|
||||
mfs.forumView.Store(forumView)
|
||||
}
|
||||
|
||||
func (mfs *MemoryForumStore) DirtyGet(id int) *Forum {
|
||||
fint, ok := mfs.forums.Load(id)
|
||||
forum := fint.(*Forum)
|
||||
if !ok || forum.Name == "" {
|
||||
if !ok || fint.(*Forum).Name == "" {
|
||||
return &Forum{ID: -1, Name: ""}
|
||||
}
|
||||
return forum
|
||||
return fint.(*Forum)
|
||||
}
|
||||
|
||||
func (mfs *MemoryForumStore) CacheGet(id int) (*Forum, error) {
|
||||
|
@ -225,24 +224,29 @@ func (mfs *MemoryForumStore) CacheSet(forum *Forum) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ! Has a randomised order
|
||||
func (mfs *MemoryForumStore) GetAll() (forumView []*Forum, err error) {
|
||||
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
|
||||
forumView = append(forumView, value.(*Forum))
|
||||
return true
|
||||
})
|
||||
sort.Sort(SortForum(forumView))
|
||||
return forumView, nil
|
||||
}
|
||||
|
||||
// ? - Can we optimise the sorting?
|
||||
func (mfs *MemoryForumStore) GetAllIDs() (ids []int, err error) {
|
||||
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
|
||||
ids = append(ids, value.(*Forum).ID)
|
||||
return true
|
||||
})
|
||||
sort.Ints(ids)
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (mfs *MemoryForumStore) GetAllVisible() ([]*Forum, error) {
|
||||
return mfs.forumView.Load().([]*Forum), nil
|
||||
func (mfs *MemoryForumStore) GetAllVisible() (forumView []*Forum, err error) {
|
||||
forumView = mfs.forumView.Load().([]*Forum)
|
||||
return forumView, nil
|
||||
}
|
||||
|
||||
func (mfs *MemoryForumStore) GetAllVisibleIDs() ([]int, error) {
|
||||
|
@ -285,6 +289,7 @@ func (mfs *MemoryForumStore) Delete(id int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ! Is this racey?
|
||||
func (mfs *MemoryForumStore) IncrementTopicCount(id int) error {
|
||||
forum, err := mfs.Get(id)
|
||||
if err != nil {
|
||||
|
@ -298,6 +303,7 @@ func (mfs *MemoryForumStore) IncrementTopicCount(id int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ! Is this racey?
|
||||
func (mfs *MemoryForumStore) DecrementTopicCount(id int) error {
|
||||
forum, err := mfs.Get(id)
|
||||
if err != nil {
|
||||
|
@ -312,6 +318,7 @@ func (mfs *MemoryForumStore) DecrementTopicCount(id int) error {
|
|||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
|
|
|
@ -214,6 +214,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
case "/panel/groups/create/":
|
||||
routePanelGroupsCreateSubmit(w,req,user)
|
||||
return
|
||||
case "/panel/backups/":
|
||||
routePanelBackups(w,req,user,extra_data)
|
||||
return
|
||||
case "/panel/logs/mod/":
|
||||
routePanelLogsMod(w,req,user)
|
||||
return
|
||||
|
|
|
@ -183,7 +183,7 @@ config.DefaultRoute = routeTopics
|
|||
config.DefaultGroup = 3 // Should be a setting in the database
|
||||
config.ActivationGroup = 5 // Should be a setting in the database
|
||||
config.StaffCSS = "staff_post"
|
||||
config.UncategorisedForumVisible = true
|
||||
config.DefaultForum = 2
|
||||
config.MinifyTemplates = true
|
||||
config.MultiServer = false // Experimental: Enable Cross-Server Synchronisation and several other features
|
||||
|
||||
|
|
17
main.go
17
main.go
|
@ -12,7 +12,6 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
//"runtime/pprof"
|
||||
)
|
||||
|
@ -41,16 +40,6 @@ var externalSites = map[string]string{
|
|||
var staticFiles = make(map[string]SFile)
|
||||
var logWriter = io.MultiWriter(os.Stderr)
|
||||
|
||||
func processConfig() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// TODO: Have a file for each run with the time/date the server started as the file name?
|
||||
// TODO: Log panics with recover()
|
||||
|
@ -121,6 +110,11 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = verifyConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Run this goroutine once a second
|
||||
secondTicker := time.NewTicker(1 * time.Second)
|
||||
fifteenMinuteTicker := time.NewTicker(15 * time.Minute)
|
||||
|
@ -149,6 +143,7 @@ func main() {
|
|||
// TODO: Manage the TopicStore, UserStore, and ForumStore
|
||||
// TODO: Alert the admin, if CPU usage, RAM usage, or the number of posts in the past second are too high
|
||||
// TODO: Clean-up alerts with no unread matches which are over two weeks old. Move this to a 24 hour task?
|
||||
// TODO: Rescan the static files for changes
|
||||
|
||||
// TODO: Add a plugin hook here
|
||||
case <-fifteenMinuteTicker.C:
|
||||
|
|
|
@ -28,6 +28,9 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st
|
|||
return
|
||||
}
|
||||
}
|
||||
if fid == 0 {
|
||||
fid = config.DefaultForum
|
||||
}
|
||||
|
||||
headerVars, ok := ForumUserCheck(w, r, &user, fid)
|
||||
if !ok {
|
||||
|
|
|
@ -574,7 +574,7 @@ func routeIps(w http.ResponseWriter, r *http.Request, user User) {
|
|||
return
|
||||
}
|
||||
|
||||
ip := html.EscapeString(r.URL.Path[len("/users/ips/"):])
|
||||
ip := r.FormValue("ip")
|
||||
var uid int
|
||||
var reqUserList = make(map[int]bool)
|
||||
|
||||
|
|
30
pages.go
30
pages.go
|
@ -8,6 +8,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HeaderVars struct {
|
||||
|
@ -227,7 +228,23 @@ type PanelEditGroupPermsPage struct {
|
|||
GlobalPerms []NameLangToggle
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
type backupItem struct {
|
||||
SQLURL string
|
||||
|
||||
// TODO: Add an easier to parse format here for Gosora to be able to more easily reimport portions of the dump and to strip unneccesary data (e.g. table defs and parsed post data)
|
||||
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type PanelBackupPage struct {
|
||||
Title string
|
||||
CurrentUser User
|
||||
Header *HeaderVars
|
||||
Stats PanelStats
|
||||
Backups []backupItem
|
||||
}
|
||||
|
||||
type logItem struct {
|
||||
Action template.HTML
|
||||
IPAddress string
|
||||
DoneAt string
|
||||
|
@ -238,7 +255,7 @@ type PanelLogsPage struct {
|
|||
CurrentUser User
|
||||
Header *HeaderVars
|
||||
Stats PanelStats
|
||||
Logs []Log
|
||||
Logs []logItem
|
||||
PageList []int
|
||||
Page int
|
||||
LastPage int
|
||||
|
@ -283,6 +300,7 @@ func init() {
|
|||
urlReg = regexp.MustCompile(urlpattern)
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func shortcodeToUnicode(msg string) string {
|
||||
//re := regexp.MustCompile(":(.):")
|
||||
msg = strings.Replace(msg, ":grinning:", "😀", -1)
|
||||
|
@ -421,6 +439,7 @@ func preparseMessage(msg string) string {
|
|||
return shortcodeToUnicode(msg)
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func parseMessage(msg string /*, user User*/) string {
|
||||
msg = strings.Replace(msg, ":)", "😀", -1)
|
||||
msg = strings.Replace(msg, ":(", "😞", -1)
|
||||
|
@ -634,6 +653,7 @@ func parseMessage(msg string /*, user User*/) string {
|
|||
return msg
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func regexParseMessage(msg string) string {
|
||||
msg = strings.Replace(msg, ":)", "😀", -1)
|
||||
msg = strings.Replace(msg, ":D", "😃", -1)
|
||||
|
@ -648,6 +668,7 @@ func regexParseMessage(msg string) string {
|
|||
|
||||
// 6, 7, 8, 6, 7
|
||||
// ftp://, http://, https:// git://, mailto: (not a URL, just here for length comparison purposes)
|
||||
// TODO: Write a test for this
|
||||
func validateURLBytes(data []byte) bool {
|
||||
datalen := len(data)
|
||||
i := 0
|
||||
|
@ -670,6 +691,7 @@ func validateURLBytes(data []byte) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func validatedURLBytes(data []byte) (url []byte) {
|
||||
datalen := len(data)
|
||||
i := 0
|
||||
|
@ -694,6 +716,7 @@ func validatedURLBytes(data []byte) (url []byte) {
|
|||
return url
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func partialURLBytes(data []byte) (url []byte) {
|
||||
datalen := len(data)
|
||||
i := 0
|
||||
|
@ -719,6 +742,7 @@ func partialURLBytes(data []byte) (url []byte) {
|
|||
return url
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func partialURLBytesLen(data []byte) int {
|
||||
datalen := len(data)
|
||||
i := 0
|
||||
|
@ -745,6 +769,7 @@ func partialURLBytesLen(data []byte) int {
|
|||
return datalen
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func parseMediaBytes(data []byte) (protocol []byte, url []byte) {
|
||||
datalen := len(data)
|
||||
i := 0
|
||||
|
@ -774,6 +799,7 @@ func parseMediaBytes(data []byte) (protocol []byte, url []byte) {
|
|||
return protocol, data[i:]
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func coerceIntBytes(data []byte) (res int, length int) {
|
||||
if !(data[0] > 47 && data[0] < 58) {
|
||||
return 0, 1
|
||||
|
|
|
@ -6,8 +6,11 @@ import (
|
|||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -190,6 +193,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user User) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO: Paginate this?
|
||||
var forumList []interface{}
|
||||
forums, err := fstore.GetAll()
|
||||
if err != nil {
|
||||
|
@ -197,6 +201,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user User) {
|
|||
return
|
||||
}
|
||||
|
||||
// ? - Should we generate something similar to the forumView? It might be a little overkill for a page which is rarely loaded in comparison to /forums/
|
||||
for _, forum := range forums {
|
||||
if forum.Name != "" && forum.ParentID == 0 {
|
||||
fadmin := ForumAdmin{forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, forum.TopicCount, presetToLang(forum.Preset)}
|
||||
|
@ -1940,6 +1945,59 @@ func routePanelThemesSetDefault(w http.ResponseWriter, r *http.Request, user Use
|
|||
http.Redirect(w, r, "/panel/themes/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func routePanelBackups(w http.ResponseWriter, r *http.Request, user User, backupURL string) {
|
||||
headerVars, stats, ok := PanelUserCheck(w, r, &user)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !user.IsSuperAdmin {
|
||||
NoPermissions(w, r, user)
|
||||
return
|
||||
}
|
||||
|
||||
if backupURL != "" {
|
||||
// We don't want them trying to break out of this directory, it shouldn't hurt since it's a super admin, but it's always good to practice good security hygiene, especially if this is one of many instances on a managed server not controlled by the superadmin/s
|
||||
backupURL = Stripslashes(backupURL)
|
||||
|
||||
var ext = filepath.Ext("./backups/" + backupURL)
|
||||
if ext == ".sql" {
|
||||
info, err := os.Stat("./backups/" + backupURL)
|
||||
if err != nil {
|
||||
NotFound(w, r)
|
||||
return
|
||||
}
|
||||
// TODO: Change the served filename to gosora_backup_%timestamp%.sql, the time the file was generated, not when it was modified aka what the name of it should be
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=gosora_backup.sql")
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10))
|
||||
// TODO: Fix the problem where non-existent files aren't greeted with custom 404s on ServeFile()'s side
|
||||
http.ServeFile(w, r, "./backups/"+backupURL)
|
||||
return
|
||||
}
|
||||
NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
var backupList []backupItem
|
||||
backupFiles, err := ioutil.ReadDir("./backups")
|
||||
if err != nil {
|
||||
InternalError(err, w)
|
||||
return
|
||||
}
|
||||
for _, backupFile := range backupFiles {
|
||||
var ext = filepath.Ext(backupFile.Name())
|
||||
if ext != ".sql" {
|
||||
continue
|
||||
}
|
||||
backupList = append(backupList, backupItem{backupFile.Name(), backupFile.ModTime()})
|
||||
}
|
||||
|
||||
pi := PanelBackupPage{"Backups", user, headerVars, stats, backupList}
|
||||
err = templates.ExecuteTemplate(w, "panel-backups.html", pi)
|
||||
if err != nil {
|
||||
InternalError(err, w)
|
||||
}
|
||||
}
|
||||
|
||||
func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) {
|
||||
headerVars, stats, ok := PanelUserCheck(w, r, &user)
|
||||
if !ok {
|
||||
|
@ -1964,7 +2022,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) {
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []Log
|
||||
var logs []logItem
|
||||
var action, elementType, ipaddress, doneAt string
|
||||
var elementID, actorID int
|
||||
for rows.Next() {
|
||||
|
@ -2035,7 +2093,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) {
|
|||
default:
|
||||
action = "Unknown action '" + action + "' by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
|
||||
}
|
||||
logs = append(logs, Log{Action: template.HTML(action), IPAddress: ipaddress, DoneAt: doneAt})
|
||||
logs = append(logs, logItem{Action: template.HTML(action), IPAddress: ipaddress, DoneAt: doneAt})
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
|
|
|
@ -81,6 +81,7 @@ func routes() {
|
|||
Route{"routePanelGroupsEditPermsSubmit", "/panel/groups/edit/perms/submit/", "", []string{"extra_data"}},
|
||||
Route{"routePanelGroupsCreateSubmit", "/panel/groups/create/", "", []string{}},
|
||||
|
||||
Route{"routePanelBackups", "/panel/backups/", "", []string{"extra_data"}},
|
||||
Route{"routePanelLogsMod", "/panel/logs/mod/", "", []string{}},
|
||||
Route{"routePanelDebug", "/panel/debug/", "", []string{}},
|
||||
)
|
||||
|
|
10
routes.go
10
routes.go
|
@ -45,6 +45,9 @@ func routeStatic(w http.ResponseWriter, r *http.Request) {
|
|||
//log.Print("Outputting static file '" + r.URL.Path + "'")
|
||||
file, ok := staticFiles[r.URL.Path]
|
||||
if !ok {
|
||||
if dev.DebugMode {
|
||||
log.Print("Failed to find '" + r.URL.Path + "'")
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
@ -75,14 +78,9 @@ func routeStatic(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Deprecated: Test route for stopping the server during a performance analysis
|
||||
/*func route_exit(w http.ResponseWriter, r *http.Request){
|
||||
/*func routeExit(w http.ResponseWriter, r *http.Request, user User){
|
||||
db.Close()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Deprecated: Test route to see which file serving method is faster
|
||||
func route_fstatic(w http.ResponseWriter, r *http.Request){
|
||||
http.ServeFile(w,r,r.URL.Path)
|
||||
}*/
|
||||
|
||||
// TODO: Make this a static file somehow? Is it possible for us to put this file somewhere else?
|
||||
|
|
27
site.go
27
site.go
|
@ -1,6 +1,10 @@
|
|||
package main
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var site = &Site{Name: "Magical Fairy Land", Language: "english"}
|
||||
var dbConfig = DBConfig{Host: "localhost"}
|
||||
|
@ -8,7 +12,7 @@ var config Config
|
|||
var dev DevConfig
|
||||
|
||||
type Site struct {
|
||||
Name string // ? - Move this into the settings table?
|
||||
Name string // ? - Move this into the settings table? Should we make a second version of this for the abbreviation shown in the navbar?
|
||||
Email string // ? - Move this into the settings table?
|
||||
URL string
|
||||
Port string
|
||||
|
@ -44,7 +48,7 @@ type Config struct {
|
|||
DefaultGroup int
|
||||
ActivationGroup int
|
||||
StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS
|
||||
UncategorisedForumVisible bool
|
||||
DefaultForum int // The forum posts go in by default, this used to be covered by the Uncategorised Forum, but we want to replace it with a more robust solution. Make this a setting?
|
||||
MinifyTemplates bool
|
||||
MultiServer bool
|
||||
|
||||
|
@ -57,3 +61,20 @@ type DevConfig struct {
|
|||
SuperDebug bool
|
||||
Profiling bool
|
||||
}
|
||||
|
||||
func processConfig() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func verifyConfig() error {
|
||||
if !fstore.Exists(config.DefaultForum) {
|
||||
return errors.New("Invalid default forum")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -134,9 +134,9 @@ var topic_28 = []byte(`</a>
|
|||
`)
|
||||
var topic_29 = []byte(`<a href="/topic/like/submit/`)
|
||||
var topic_30 = []byte(`" class="mod_button" title="Love it" style="color:#202020;">
|
||||
<button class="username like_label" style="`)
|
||||
var topic_31 = []byte(`background-color:/*#eaffea*/#D6FFD6;`)
|
||||
var topic_32 = []byte(`"></button></a>`)
|
||||
<button class="username like_label"`)
|
||||
var topic_31 = []byte(` style="background-color:#D6FFD6;"`)
|
||||
var topic_32 = []byte(`></button></a>`)
|
||||
var topic_33 = []byte(`<a href='/topic/edit/`)
|
||||
var topic_34 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>`)
|
||||
var topic_35 = []byte(`<a href='/topic/delete/submit/`)
|
||||
|
@ -149,7 +149,7 @@ var topic_41 = []byte(`<a class="mod_button" href='/topic/unstick/submit/`)
|
|||
var topic_42 = []byte(`' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>`)
|
||||
var topic_43 = []byte(`<a href='/topic/stick/submit/`)
|
||||
var topic_44 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>`)
|
||||
var topic_45 = []byte(`<a class="mod_button" href='/users/ips/`)
|
||||
var topic_45 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
|
||||
var topic_46 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
|
||||
var topic_47 = []byte(`
|
||||
<a href="/report/submit/`)
|
||||
|
@ -195,14 +195,14 @@ var topic_68 = []byte(`" class="username real_username">`)
|
|||
var topic_69 = []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" style="`)
|
||||
var topic_72 = []byte(`background-color:/*#eaffea*/#D6FFD6;`)
|
||||
var topic_73 = []byte(`"></button></a>`)
|
||||
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/`)
|
||||
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(`
|
||||
<a href="/report/submit/`)
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
{{template "header.html" . }}
|
||||
<main>
|
||||
|
||||
<div class="rowblock opthead">
|
||||
<div class="rowitem"><a>IP Search</a></div>
|
||||
<div class="rowblock rowhead">
|
||||
<div class="rowitem">
|
||||
<h1>IP Search</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rowblock">
|
||||
<div class="rowitem passive">Searching for {{.IP}}</div>
|
||||
|
||||
<form action="/users/ips/" method="get" id="ip-search-form"></form>
|
||||
<div class="rowblock ip_search_block">
|
||||
<div class="rowitem passive">
|
||||
<input form="ip-search-form" name="ip" class="ip_search_input" type="search" placeholder="🔍︎" {{if .IP}}value="{{.IP}}"{{end}}/>
|
||||
<input form="ip-search-form" class="ip_search_search" type="submit" value="Search" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="rowblock rowlist bgavatars">
|
||||
{{range .ItemList}}<div class="rowitem" style="{{if .Avatar}}background-image: url('{{.Avatar}}');{{end}}">
|
||||
|
||||
{{if .IP}}
|
||||
<div class="rowblock bgavatars">
|
||||
{{range .ItemList}}<div class="rowitem"{{if .Avatar}} style="background-image: url('{{.Avatar}}');"{{end}}>
|
||||
<a href="{{.Link}}">{{.Name}}</a>
|
||||
</div>
|
||||
{{else}}<div class="rowitem passive">No users found.</div>{{end}}
|
||||
{{else}}<div class="rowitem">No users found.</div>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
</main>
|
||||
{{template "footer.html" . }}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{{template "header.html" . }}
|
||||
{{template "panel-menu.html" . }}
|
||||
<main class="colstack_right">
|
||||
<div class="colstack_item colstack_head">
|
||||
<div class="rowitem"><h1>Backups</h1></div>
|
||||
</div>
|
||||
<div id="panel_backups" class="colstack_item rowlist">
|
||||
{{range .Backups}}
|
||||
<div class="rowitem panel_compactrow">
|
||||
<span>{{.SQLURL}}</span>
|
||||
<span class="panel_floater">
|
||||
<a href="/panel/backups/{{.SQLURL}}" class="panel_tag panel_right_button">Download</a>
|
||||
</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="rowitem">There aren't any backups available at this time.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer.html" . }}
|
|
@ -11,14 +11,14 @@
|
|||
</div>
|
||||
<div id="panel_forums" class="colstack_item rowlist">
|
||||
{{range .ItemList}}
|
||||
<div class="rowitem editable_parent{{if eq .ID 1}} builtin_forum_divider{{end}}">
|
||||
<div class="rowitem editable_parent">
|
||||
<span class="panel_floater">
|
||||
<span data-field="forum_active" data-type="list" class="panel_tag editable_block forum_active {{if .Active}}forum_active_Show" data-value="1{{else}}forum_active_Hide" data-value="0{{end}}" title="Hidden"></span>
|
||||
<span data-field="forum_preset" data-type="list" data-value="{{.Preset}}" class="panel_tag editable_block forum_preset forum_preset_{{.Preset}}" title="{{.PresetLang}}"></span>
|
||||
|
||||
<span class="panel_buttons">
|
||||
{{if gt .ID 0}}<a class="panel_tag edit_fields hide_on_edit panel_right_button">Edit</a>
|
||||
<a class="panel_right_button" href="/panel/forums/edit/submit/{{.ID}}"><button class='panel_tag submit_edit show_on_edit' type='submit'>Update</button></a>{{end}}
|
||||
<a class="panel_tag edit_fields hide_on_edit panel_right_button">Edit</a>
|
||||
<a class="panel_right_button" href="/panel/forums/edit/submit/{{.ID}}"><button class='panel_tag submit_edit show_on_edit' type='submit'>Update</button></a>
|
||||
{{if gt .ID 1}}<a href="/panel/forums/delete/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button hide_on_edit">Delete</a>{{end}}
|
||||
<a href="/panel/forums/edit/{{.ID}}" class="panel_tag panel_right_button show_on_edit">Full Edit</a>
|
||||
</span>
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
{{if .CurrentUser.Perms.ManagePlugins}}<div class="rowitem passive">
|
||||
<a href="/panel/plugins/">Plugins</a>
|
||||
</div>{{end}}
|
||||
{{if .CurrentUser.IsSuperAdmin}}<div class="rowitem passive">
|
||||
<a href="/panel/backups/">Backups</a>
|
||||
</div>{{end}}
|
||||
<div class="rowitem passive">
|
||||
<a href="/panel/logs/mod/">Logs</a>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
<a href="{{.Topic.UserLink}}" class="username real_username">{{.Topic.CreatedByName}}</a>
|
||||
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}" class="mod_button" title="Love it" style="color:#202020;">
|
||||
<button class="username like_label" style="{{if .Topic.Liked}}background-color:/*#eaffea*/#D6FFD6;{{end}}"></button></a>{{end}}
|
||||
<button class="username like_label"{{if .Topic.Liked}} style="background-color:#D6FFD6;"{{end}}></button></a>{{end}}
|
||||
|
||||
{{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>{{end}}
|
||||
|
||||
|
@ -39,7 +39,7 @@
|
|||
{{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unlock Topic"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Lock Topic"><button class="username lock_label"></button></a>{{end}}{{end}}
|
||||
|
||||
{{if .CurrentUser.Perms.PinTopic}}{{if .Topic.Sticky}}<a class="mod_button" href='/topic/unstick/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>{{end}}{{end}}
|
||||
{{if .CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/{{.Topic.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}}
|
||||
{{if .CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.Topic.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}}
|
||||
<a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag Topic"><button class="username flag_label"></button></a>
|
||||
|
||||
{{if .Topic.LikeCount}}<a class="username hide_on_micro like_count">{{.Topic.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}}
|
||||
|
@ -61,12 +61,12 @@
|
|||
<span class="controls">
|
||||
|
||||
<a href="{{.UserLink}}" class="username real_username">{{.CreatedByName}}</a>
|
||||
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label" style="{{if .Liked}}background-color:/*#eaffea*/#D6FFD6;{{end}}"></button></a>{{end}}
|
||||
{{if $.CurrentUser.Perms.LikeItem}}<a href="/reply/like/submit/{{.ID}}" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"{{if .Liked}} style="background-color:#D6FFD6;"{{end}}></button></a>{{end}}
|
||||
|
||||
{{if $.CurrentUser.Perms.EditReply}}<a href="/reply/edit/submit/{{.ID}}" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>{{end}}
|
||||
|
||||
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>{{end}}
|
||||
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/{{.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}}
|
||||
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}}
|
||||
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
|
||||
|
||||
{{if .LikeCount}}<a class="username hide_on_micro like_count">{{.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}}
|
||||
|
|
45
themes.go
45
themes.go
|
@ -70,6 +70,11 @@ type ThemeResource struct {
|
|||
Location string
|
||||
}
|
||||
|
||||
func init() {
|
||||
defaultThemeBox.Store(fallbackTheme)
|
||||
}
|
||||
|
||||
// ? - Delete themes which no longer exist in the themes folder from the database?
|
||||
func LoadThemes() error {
|
||||
changeDefaultThemeMutex.Lock()
|
||||
rows, err := getThemesStmt.Query()
|
||||
|
@ -92,32 +97,16 @@ func LoadThemes() error {
|
|||
continue
|
||||
}
|
||||
|
||||
theme.TemplatesMap = make(map[string]string)
|
||||
theme.TmplPtr = make(map[string]interface{})
|
||||
if theme.Templates != nil {
|
||||
for _, themeTmpl := range theme.Templates {
|
||||
theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source
|
||||
theme.TmplPtr[themeTmpl.Name] = tmplPtrMap["o_"+themeTmpl.Source]
|
||||
}
|
||||
}
|
||||
|
||||
theme.ResourceTemplates = template.New("")
|
||||
template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + uname + "/public/*.css"))
|
||||
|
||||
if defaultThemeSwitch {
|
||||
log.Print("Loading the theme '" + theme.Name + "'")
|
||||
log.Print("Loading the default theme '" + theme.Name + "'")
|
||||
theme.Active = true
|
||||
defaultThemeBox.Store(uname)
|
||||
defaultThemeBox.Store(theme.Name)
|
||||
mapThemeTemplates(theme)
|
||||
} else {
|
||||
log.Print("Loading the theme '" + theme.Name + "'")
|
||||
theme.Active = false
|
||||
}
|
||||
|
||||
// It should be safe for us to load the files for all the themes in memory, as-long as the admin hasn't setup a ridiculous number of themes
|
||||
err = addThemeStaticFiles(theme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
themes[uname] = theme
|
||||
}
|
||||
changeDefaultThemeMutex.Unlock()
|
||||
|
@ -160,6 +149,24 @@ func initThemes() error {
|
|||
}
|
||||
}
|
||||
|
||||
theme.TemplatesMap = make(map[string]string)
|
||||
theme.TmplPtr = make(map[string]interface{})
|
||||
if theme.Templates != nil {
|
||||
for _, themeTmpl := range theme.Templates {
|
||||
theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source
|
||||
theme.TmplPtr[themeTmpl.Name] = tmplPtrMap["o_"+themeTmpl.Source]
|
||||
}
|
||||
}
|
||||
|
||||
theme.ResourceTemplates = template.New("")
|
||||
template.Must(theme.ResourceTemplates.ParseGlob("./themes/" + theme.Name + "/public/*.css"))
|
||||
|
||||
// It should be safe for us to load the files for all the themes in memory, as-long as the admin hasn't setup a ridiculous number of themes
|
||||
err = addThemeStaticFiles(theme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
themes[theme.Name] = theme
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -146,7 +146,6 @@ a {
|
|||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.rowitem h1 {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
|
@ -154,7 +153,6 @@ a {
|
|||
-webkit-margin-after: 0;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.rowsmall {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@ -181,6 +179,11 @@ a {
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Algin to right in a flex head */
|
||||
.to_right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Topic View */
|
||||
|
||||
/* TODO: How should we handle the sticky headers? */
|
||||
|
@ -223,7 +226,6 @@ a {
|
|||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.mod_button button {
|
||||
border: none;
|
||||
background: none;
|
||||
|
@ -288,7 +290,6 @@ a {
|
|||
color: rgb(205,205,205);
|
||||
float: right;
|
||||
}
|
||||
|
||||
.level {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
@ -408,7 +409,11 @@ textarea.large {
|
|||
background-size: 40px;
|
||||
padding-left: 46px;
|
||||
}
|
||||
|
||||
.bgavatars:not(.rowlist) .rowitem {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 40px;
|
||||
padding-left: 46px;
|
||||
}
|
||||
.rowlist .formrow, .rowlist .formrow:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
@ -475,7 +480,7 @@ input, select, textarea {
|
|||
}
|
||||
|
||||
/* Forum View */
|
||||
.rowhead {
|
||||
.rowhead, .opthead, .colstack_head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
@ -533,9 +538,6 @@ input, select, textarea {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topic_item {
|
||||
display: flex;
|
||||
}
|
||||
.topic_name_input {
|
||||
width: 100%;
|
||||
margin-right: 10px;
|
||||
|
@ -596,6 +598,32 @@ input, select, textarea {
|
|||
padding-left: 136px;
|
||||
}
|
||||
|
||||
.ip_search_block .rowitem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.ip_search_block input {
|
||||
background-color: #444444;
|
||||
border: 1px solid #555555;
|
||||
color: #999999;
|
||||
margin-top: -3px;
|
||||
margin-bottom: -3px;
|
||||
padding: 4px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.ip_search_input {
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.ip_search_search {
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.colstack_grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
|
22
utils.go
22
utils.go
|
@ -29,6 +29,7 @@ type Version struct {
|
|||
TagID int
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func (version *Version) String() (out string) {
|
||||
out = strconv.Itoa(version.Major) + "." + strconv.Itoa(version.Minor) + "." + strconv.Itoa(version.Patch)
|
||||
if version.Tag != "" {
|
||||
|
@ -40,7 +41,8 @@ func (version *Version) String() (out string) {
|
|||
return
|
||||
}
|
||||
|
||||
// GenerateSafeString is for generating a cryptographically secure set of random bytes..
|
||||
// GenerateSafeString is for generating a cryptographically secure set of random bytes...
|
||||
// TODO: Write a test for this
|
||||
func GenerateSafeString(length int) (string, error) {
|
||||
rb := make([]byte, length)
|
||||
_, err := rand.Read(rb)
|
||||
|
@ -50,6 +52,7 @@ func GenerateSafeString(length int) (string, error) {
|
|||
return base64.URLEncoding.EncodeToString(rb), nil
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func relativeTime(in string) (string, error) {
|
||||
if in == "" {
|
||||
return "", nil
|
||||
|
@ -97,6 +100,7 @@ func relativeTime(in string) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func convertByteUnit(bytes float64) (float64, string) {
|
||||
switch {
|
||||
case bytes >= float64(terabyte):
|
||||
|
@ -112,6 +116,7 @@ func convertByteUnit(bytes float64) (float64, string) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func convertByteInUnit(bytes float64, unit string) (count float64) {
|
||||
switch unit {
|
||||
case "TB":
|
||||
|
@ -132,6 +137,7 @@ func convertByteInUnit(bytes float64, unit string) (count float64) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func convertUnit(num int) (int, string) {
|
||||
switch {
|
||||
case num >= 1000000000000:
|
||||
|
@ -147,6 +153,7 @@ func convertUnit(num int) (int, string) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func convertFriendlyUnit(num int) (int, string) {
|
||||
switch {
|
||||
case num >= 1000000000000:
|
||||
|
@ -231,6 +238,7 @@ func SendEmail(email string, subject string, msg string) (res bool) {
|
|||
return true
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func weakPassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
return errors.New("your password needs to be at-least eight characters long")
|
||||
|
@ -283,6 +291,7 @@ func weakPassword(password string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func createFile(name string) error {
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
|
@ -291,6 +300,7 @@ func createFile(name string) error {
|
|||
return f.Close()
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func writeFile(name string, content string) (err error) {
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
|
@ -307,6 +317,13 @@ func writeFile(name string, content string) (err error) {
|
|||
return f.Close()
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func Stripslashes(text string) string {
|
||||
text = strings.Replace(text, "/", "", -1)
|
||||
return strings.Replace(text, "\\", "", -1)
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func wordCount(input string) (count int) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
|
@ -326,6 +343,7 @@ func wordCount(input string) (count int) {
|
|||
return count + 1
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func getLevel(score int) (level int) {
|
||||
var base float64 = 25
|
||||
var current, prev float64
|
||||
|
@ -346,6 +364,7 @@ func getLevel(score int) (level int) {
|
|||
return level
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func getLevelScore(getLevel int) (score int) {
|
||||
var base float64 = 25
|
||||
var current, prev float64
|
||||
|
@ -367,6 +386,7 @@ func getLevelScore(getLevel int) (score int) {
|
|||
return int(math.Ceil(current))
|
||||
}
|
||||
|
||||
// TODO: Write a test for this
|
||||
func getLevels(maxLevel int) []float64 {
|
||||
var base float64 = 25
|
||||
var current, prev float64 // = 0
|
||||
|
|
Loading…
Reference in New Issue