Starting on Achievements
A few cleanup things too.
This commit is contained in:
parent
278d409ad9
commit
0b202e63fa
@ -38,3 +38,31 @@ func getAllLevelUpChannelXp(user string) map[string]int {
|
||||
closeDatabase()
|
||||
return ret
|
||||
}
|
||||
|
||||
func getAllNonLevelUpStats(user string) map[string]int {
|
||||
openDatabase()
|
||||
ret := make(map[string]int)
|
||||
// First, get a list of all levelup stats
|
||||
db.Update(func(tx *bolt.Tx) error {
|
||||
var b, uB, uSB *bolt.Bucket
|
||||
var err error
|
||||
|
||||
b = tx.Bucket([]byte("users"))
|
||||
if b == nil {
|
||||
return fmt.Errorf("Unable to open 'users' bucket")
|
||||
}
|
||||
if uB = b.Bucket([]byte(user)); uB != nil {
|
||||
if uSB = uB.Bucket([]byte("stats")); uSB != nil {
|
||||
return uSB.ForEach(func(k, v []byte) error {
|
||||
if !strings.HasPrefix(string(k), "levelup-") {
|
||||
ret[string(k)], _ = strconv.Atoi(string(v))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
closeDatabase()
|
||||
return ret
|
||||
}
|
||||
|
@ -275,41 +275,45 @@ func (wm *generalWebModule) handleStats(w http.ResponseWriter, req *http.Request
|
||||
sc := "var stats = {totalchannelmessages:"
|
||||
sc = fmt.Sprintf("%s%d,", sc, s.TotalChannelMessages)
|
||||
sc = sc + "channels:["
|
||||
for _, k := range s.ChannelStats {
|
||||
sc = fmt.Sprintf("%s{name:\"%s\",member_count:%d,message_count:%d},",
|
||||
sc,
|
||||
k.Name,
|
||||
k.MemberCount,
|
||||
k.MessageCount,
|
||||
)
|
||||
if len(s.ChannelStats) > 0 {
|
||||
for _, k := range s.ChannelStats {
|
||||
sc = fmt.Sprintf("%s{name:\"%s\",member_count:%d,message_count:%d},",
|
||||
sc,
|
||||
k.Name,
|
||||
k.MemberCount,
|
||||
k.MessageCount,
|
||||
)
|
||||
}
|
||||
// Trim the last ,
|
||||
sc = sc[:len(sc)-1]
|
||||
}
|
||||
// Trim the last ,
|
||||
sc = sc[:len(sc)-1]
|
||||
sc = sc + "],"
|
||||
sc = sc + "users:["
|
||||
for _, usr := range s.UserStats {
|
||||
sc = fmt.Sprintf("%s{name:\"%s\",message_count:%d,",
|
||||
sc,
|
||||
usr.Name,
|
||||
usr.Messages,
|
||||
)
|
||||
sc = sc + "messages:{"
|
||||
sc = sc + "hours:{"
|
||||
for i, k := range usr.Hours {
|
||||
sc = sc + fmt.Sprintf("%d:%d,", i, k)
|
||||
if len(s.UserStats) > 0 {
|
||||
for _, usr := range s.UserStats {
|
||||
sc = fmt.Sprintf("%s{name:\"%s\",message_count:%d,",
|
||||
sc,
|
||||
usr.Name,
|
||||
usr.Messages,
|
||||
)
|
||||
sc = sc + "messages:{"
|
||||
sc = sc + "hours:{"
|
||||
for i, k := range usr.Hours {
|
||||
sc = sc + fmt.Sprintf("%d:%d,", i, k)
|
||||
}
|
||||
sc = sc[:len(sc)-1]
|
||||
sc = sc + "},"
|
||||
sc = sc + "dow:{"
|
||||
for _, k := range []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} {
|
||||
sc = sc + fmt.Sprintf("%s:%d,", k, usr.Dow[k])
|
||||
}
|
||||
sc = sc[:len(sc)-1]
|
||||
sc = sc + "}}"
|
||||
sc = sc + "},"
|
||||
}
|
||||
// Trim the last ,
|
||||
sc = sc[:len(sc)-1]
|
||||
sc = sc + "},"
|
||||
sc = sc + "dow:{"
|
||||
for _, k := range []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} {
|
||||
sc = sc + fmt.Sprintf("%s:%d,", k, usr.Dow[k])
|
||||
}
|
||||
sc = sc[:len(sc)-1]
|
||||
sc = sc + "}}"
|
||||
sc = sc + "},"
|
||||
}
|
||||
// Trim the last ,
|
||||
sc = sc[:len(sc)-1]
|
||||
sc = sc + "],"
|
||||
// Get all of the message stats
|
||||
sc = sc + "messages:{"
|
||||
|
@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type levelUpStatProcessor struct{}
|
||||
@ -56,6 +58,7 @@ func (wm *levelUpWebModule) GetName() string {
|
||||
func (wm *levelUpWebModule) GetRoutes() map[string]func(http.ResponseWriter, *http.Request) {
|
||||
ret := make(map[string]func(http.ResponseWriter, *http.Request))
|
||||
ret["/levelup/"] = wm.handleLevelUpGeneral
|
||||
ret["/levelup/profile/{username}"] = wm.handleLevelUpProfile
|
||||
return ret
|
||||
}
|
||||
func (wm *levelUpWebModule) Register() {
|
||||
@ -106,23 +109,49 @@ func (wm *levelUpWebModule) handleLevelUpGeneral(w http.ResponseWriter, req *htt
|
||||
|
||||
sc := "var levelUpStats = {"
|
||||
sc = sc + "users:["
|
||||
for _, k := range userStats {
|
||||
sc = sc + "{"
|
||||
sc = sc + "name:\"" + k.Name + "\","
|
||||
sc = fmt.Sprintf("%sxp:%d,", sc, k.Xp)
|
||||
sc = sc + "channels:["
|
||||
if len(k.ChannelStats) > 0 {
|
||||
for chK, chV := range k.ChannelStats {
|
||||
sc = fmt.Sprintf("%s{name:\"%s\",xp:%d},", sc, chK, chV)
|
||||
if len(userStats) > 0 {
|
||||
for _, k := range userStats {
|
||||
sc = sc + "{"
|
||||
sc = sc + "name:\"" + k.Name + "\","
|
||||
sc = fmt.Sprintf("%sxp:%d,", sc, k.Xp)
|
||||
sc = sc + "channels:["
|
||||
if len(k.ChannelStats) > 0 {
|
||||
for chK, chV := range k.ChannelStats {
|
||||
sc = fmt.Sprintf("%s{name:\"%s\",xp:%d},", sc, chK, chV)
|
||||
}
|
||||
sc = sc[:len(sc)-1]
|
||||
}
|
||||
sc = sc[:len(sc)-1]
|
||||
sc = sc + "]},"
|
||||
}
|
||||
sc = sc + "]},"
|
||||
sc = sc[:len(sc)-1]
|
||||
}
|
||||
sc = sc[:len(sc)-1]
|
||||
sc = sc + "]};"
|
||||
addToInlineScript(sc)
|
||||
site.Scripts = append(site.Scripts, "/assets/js/levelup_main.js")
|
||||
|
||||
showPage("levelup-main.html", site, w)
|
||||
}
|
||||
|
||||
func (wm *levelUpWebModule) handleLevelUpProfile(w http.ResponseWriter, req *http.Request) {
|
||||
type UserLevelUpStats struct {
|
||||
Name string
|
||||
Xp int
|
||||
ChannelStats map[string]int
|
||||
OtherStats map[string]int
|
||||
}
|
||||
var u *User
|
||||
var err error
|
||||
vars := mux.Vars(req)
|
||||
if u, err = getUserInfoFromName(vars["username"]); err != nil {
|
||||
pageNotFound(fmt.Errorf(fmt.Sprintf("Couldn't find a profile for user "+vars["username"])+"\n%s\n", err), w)
|
||||
return
|
||||
}
|
||||
p := UserLevelUpStats{Name: vars["username"]}
|
||||
p.Xp, _ = getUserStat(u.ID, "levelup-xp")
|
||||
p.ChannelStats = getAllLevelUpChannelXp(u.ID)
|
||||
p.OtherStats = getAllNonLevelUpStats(u.ID)
|
||||
site.TemplateData = p
|
||||
|
||||
initRequest(w, req)
|
||||
showPage("levelup-profile.html", site, w)
|
||||
}
|
||||
|
95
processor_levelupachieve.go
Normal file
95
processor_levelupachieve.go
Normal file
@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type levelUpAchieveStatProcessor struct {
|
||||
Achievements []levelUpAchievement
|
||||
}
|
||||
|
||||
func (p *levelUpAchieveStatProcessor) GetName() string {
|
||||
return "LevelUp Achievements"
|
||||
}
|
||||
|
||||
func (p *levelUpAchieveStatProcessor) GetStatKeys() []string {
|
||||
return []string{
|
||||
"levelupachievement-*",
|
||||
}
|
||||
}
|
||||
|
||||
type levelUpAchievement interface {
|
||||
GetName() string
|
||||
GetText() string
|
||||
// Returns whether the user already has this achievement
|
||||
DoesUserHave(uID string) bool
|
||||
// Processes the message, returns true if the achievement was triggered
|
||||
ProcessMessage(m *Message) bool
|
||||
}
|
||||
|
||||
func (p *levelUpAchieveStatProcessor) ProcessMessage(m *Message) {
|
||||
type UserLevelUpStats struct {
|
||||
Name string
|
||||
Xp int
|
||||
ChannelStats map[string]int
|
||||
OtherStats map[string]int
|
||||
}
|
||||
var u *User
|
||||
var err error
|
||||
vars := mux.Vars(req)
|
||||
if u, err = getUserInfo(m.User); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p := UserLevelUpStats{Name: u.Name}
|
||||
p.Xp, _ = getUserStat(u.ID, "levelup-xp")
|
||||
p.ChannelStats = getAllLevelUpChannelXp(u.ID)
|
||||
p.OtherStats = getAllNonLevelUpStats(u.ID)
|
||||
|
||||
}
|
||||
func (p *levelUpAchieveStatProcessor) ProcessAdminMessage(m *Message) {}
|
||||
func (p *levelUpAchieveStatProcessor) ProcessBotMessage(m *Message) {}
|
||||
|
||||
func (p *levelUpAchieveStatProcessor) ProcessUserMessage(m *Message) {}
|
||||
func (p *levelUpAchieveStatProcessor) ProcessAdminUserMessage(m *Message) {}
|
||||
func (p *levelUpAchieveStatProcessor) ProcessBotUserMessage(m *Message) {}
|
||||
|
||||
func (p *levelUpAchieveStatProcessor) ProcessChannelMessage(m *Message) {
|
||||
}
|
||||
func (p *levelUpAchieveStatProcessor) ProcessAdminChannelMessage(m *Message) {}
|
||||
func (p *levelUpAchieveStatProcessor) ProcessBotChannelMessage(m *Message) {}
|
||||
|
||||
/*
|
||||
* Web Site Module
|
||||
*/
|
||||
type levelUpAchieveWebModule struct{}
|
||||
|
||||
func (wm *levelUpAchieveWebModule) GetName() string {
|
||||
return "LevelUp Achievement Web Module"
|
||||
}
|
||||
func (wm *levelUpAchieveWebModule) GetRoutes() map[string]func(http.ResponseWriter, *http.Request) {
|
||||
ret := make(map[string]func(http.ResponseWriter, *http.Request))
|
||||
ret["/levelup/achieve"] = wm.handleLevelUpAchieveGeneral
|
||||
return ret
|
||||
}
|
||||
func (wm *levelUpAchieveWebModule) Register() {
|
||||
for k, v := range wm.GetRoutes() {
|
||||
r.HandleFunc(k, v)
|
||||
}
|
||||
}
|
||||
func (wm *levelUpAchieveWebModule) GetMenuEntries() []menuItem {
|
||||
var ret []menuItem
|
||||
ret = append(ret, menuItem{Text: "Achieve GET!", Link: "/levelup/achieve"})
|
||||
return ret
|
||||
}
|
||||
func (wm *levelUpAchieveWebModule) GetBottomMenuEntries() []menuItem {
|
||||
var ret []menuItem
|
||||
return ret
|
||||
}
|
||||
|
||||
func (wm *levelUpAchieveWebModule) handleLevelUpAchieveGeneral(w http.ResponseWriter, req *http.Request) {
|
||||
initRequest(w, req)
|
||||
setMenuItemActive("Achieve GET!")
|
||||
}
|
@ -65,6 +65,9 @@ func statBotMain(slack *Slack) {
|
||||
registerStatProcessor(new(levelUpStatProcessor))
|
||||
registerStatProcessor(new(generalStatProcessor))
|
||||
|
||||
levelUpAchievements := new(levelUpAchieveStatProcessor)
|
||||
// Register Achievements
|
||||
|
||||
registerMessageProcessor(new(generalProcessor))
|
||||
|
||||
fmt.Println("statbot ready, ^C exits")
|
||||
@ -74,6 +77,7 @@ func statBotMain(slack *Slack) {
|
||||
// read each incoming message
|
||||
m, err := slack.getMessage()
|
||||
if err == nil {
|
||||
writeToLog(" " + time.Now().Format(time.RFC3339) + " - Received Message\n")
|
||||
processMessage(slack, &m)
|
||||
}
|
||||
}
|
||||
|
178
statbot_model.go
178
statbot_model.go
@ -553,54 +553,56 @@ func getUserInfo(usr string) (*User, error) {
|
||||
err := db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("users"))
|
||||
if uB := b.Bucket([]byte(usr)); uB != nil {
|
||||
var err error
|
||||
if ret.Name, err = bktGetString(uB, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Deleted, err = bktGetBool(uB, "deleted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Status, err = bktGetString(uB, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Color, err = bktGetString(uB, "color"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.RealName, err = bktGetString(uB, "real_name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZ, err = bktGetString(uB, "tz"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZLabel, err = bktGetString(uB, "tz_label"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZOffset, err = bktGetInt(uB, "tz_offset"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsAdmin, err = bktGetBool(uB, "is_admin"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsOwner, err = bktGetBool(uB, "is_owner"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsPrimaryOwner, err = bktGetBool(uB, "is_primary_owner"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsRestricted, err = bktGetBool(uB, "is_restricted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsUltraRestricted, err = bktGetBool(uB, "is_ultra_restricted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsBot, err = bktGetBool(uB, "is_bot"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.HasFiles, err = bktGetBool(uB, "has_files"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.LastUpdated, err = bktGetTime(uB, "last_updated"); err != nil {
|
||||
return err
|
||||
if uIB := uB.Bucket([]byte("info")); uIB != nil {
|
||||
var err error
|
||||
if ret.Name, err = bktGetString(uIB, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Deleted, err = bktGetBool(uIB, "deleted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Status, err = bktGetString(uIB, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Color, err = bktGetString(uIB, "color"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.RealName, err = bktGetString(uIB, "real_name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZ, err = bktGetString(uIB, "tz"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZLabel, err = bktGetString(uIB, "tz_label"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZOffset, err = bktGetInt(uIB, "tz_offset"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsAdmin, err = bktGetBool(uIB, "is_admin"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsOwner, err = bktGetBool(uIB, "is_owner"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsPrimaryOwner, err = bktGetBool(uIB, "is_primary_owner"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsRestricted, err = bktGetBool(uIB, "is_restricted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsUltraRestricted, err = bktGetBool(uIB, "is_ultra_restricted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsBot, err = bktGetBool(uIB, "is_bot"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.HasFiles, err = bktGetBool(uIB, "has_files"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.LastUpdated, err = bktGetTime(uB, "last_updated"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -610,6 +612,84 @@ func getUserInfo(usr string) (*User, error) {
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
func getUserInfoFromName(usrName string) (*User, error) {
|
||||
openDatabase()
|
||||
ret := User{}
|
||||
err := db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("users"))
|
||||
c := b.Cursor()
|
||||
var tstName string
|
||||
var uB, uIB *bolt.Bucket
|
||||
var err error
|
||||
var userID string
|
||||
for k, v := c.First(); k != nil && strings.ToLower(tstName) != strings.ToLower(usrName); k, v = c.Next() {
|
||||
if v == nil {
|
||||
if uB = b.Bucket(k); uB != nil {
|
||||
if uIB = uB.Bucket([]byte("info")); uIB != nil {
|
||||
if tstName, err = bktGetString(uIB, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
userID = string(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
if tstName != usrName {
|
||||
return fmt.Errorf("Requested user (" + usrName + ") not found")
|
||||
}
|
||||
ret.ID = userID
|
||||
ret.Name = tstName
|
||||
if ret.Deleted, err = bktGetBool(uIB, "deleted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Status, err = bktGetString(uIB, "status"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Color, err = bktGetString(uIB, "color"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.RealName, err = bktGetString(uIB, "real_name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZ, err = bktGetString(uIB, "tz"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZLabel, err = bktGetString(uIB, "tz_label"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.TZOffset, err = bktGetInt(uIB, "tz_offset"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsAdmin, err = bktGetBool(uIB, "is_admin"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsOwner, err = bktGetBool(uIB, "is_owner"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsPrimaryOwner, err = bktGetBool(uIB, "is_primary_owner"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsRestricted, err = bktGetBool(uIB, "is_restricted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsUltraRestricted, err = bktGetBool(uIB, "is_ultra_restricted"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.IsBot, err = bktGetBool(uIB, "is_bot"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.HasFiles, err = bktGetBool(uIB, "has_files"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.LastUpdated, err = bktGetTime(uIB, "last_updated"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
})
|
||||
closeDatabase()
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
func saveUserInfo(usr *User) error {
|
||||
openDatabase()
|
||||
err := db.Update(func(tx *bolt.Tx) error {
|
||||
@ -670,7 +750,7 @@ func saveUserInfo(usr *User) error {
|
||||
if err = bktPutBool(uIB, "has_files", usr.HasFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
err = bktPutTime(uIB, "last_udpated", usr.LastUpdated)
|
||||
err = bktPutTime(uIB, "last_updated", usr.LastUpdated)
|
||||
return err
|
||||
})
|
||||
closeDatabase()
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
"github.com/gorilla/mux"
|
||||
@ -72,6 +73,7 @@ func statWebMain(slack *Slack) {
|
||||
|
||||
registerWebModule(new(generalWebModule))
|
||||
registerWebModule(new(levelUpWebModule))
|
||||
registerWebModule(new(levelUpAchieveWebModule))
|
||||
|
||||
http.Handle("/", r)
|
||||
go func() {
|
||||
@ -80,6 +82,7 @@ func statWebMain(slack *Slack) {
|
||||
}
|
||||
|
||||
func initRequest(w http.ResponseWriter, req *http.Request) {
|
||||
writeToLog(" " + time.Now().Format(time.RFC3339) + " >> Web Request Received\n")
|
||||
site.SubTitle = ""
|
||||
site.Stylesheets = make([]string, 0, 0)
|
||||
site.Stylesheets = append(site.Stylesheets, "/assets/css/pure-min.css")
|
||||
@ -190,6 +193,13 @@ func addToInlineScript(s string) {
|
||||
site.InlineScript = fmt.Sprintf("%s%s", site.InlineScript, s)
|
||||
}
|
||||
|
||||
func pageNotFound(err error, w http.ResponseWriter) {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("Page Not Found")
|
||||
}
|
||||
http.Error(w, err.Error(), 404)
|
||||
}
|
||||
|
||||
func assertError(err error, w http.ResponseWriter) bool {
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
|
12
templates/levelup-profile.html
Normal file
12
templates/levelup-profile.html
Normal file
@ -0,0 +1,12 @@
|
||||
<h1>{{ .TemplateData.Name }}</h1>
|
||||
<div>{{ .TemplateData.Xp }}</div>
|
||||
<ul>
|
||||
{{ range $i, $v := .TemplateData.ChannelStats }}
|
||||
<li>{{ $i }} - {{ $v }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul>
|
||||
{{ range $i, $v := .TemplateData.OtherStats }}
|
||||
<li>{{ $i }} - {{ $v }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
Loading…
Reference in New Issue
Block a user