hanayo/funcmap.go

613 lines
16 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"html/template"
"io/ioutil"
"math"
"math/rand"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"github.com/russross/blackfriday"
"github.com/thehowl/qsql"
"golang.org/x/oauth2"
"zxq.co/ripple/go-discord-oauth"
"github.com/osuYozora/hanayo/modules/bbcode"
"github.com/osuYozora/hanayo/modules/btcaddress"
"github.com/osuYozora/hanayo/modules/doc"
"github.com/osuYozora/hanayo/modules/fa-semantic-mappings"
"zxq.co/ripple/playstyle"
"zxq.co/ripple/rippleapi/common"
)
// funcMap contains useful functions for the various templates.
var funcMap = template.FuncMap{
// html disables HTML escaping on the values it is given.
"html": func(value interface{}) template.HTML {
return template.HTML(fmt.Sprint(value))
},
// avatars is a function returning the configuration constant AvatarURL
"config": func(key string) interface{} {
return configMap[key]
},
// navbarItem is a function to generate an item in the navbar.
// The reason why this exists is that I wanted to have the currently
// selected element in the navbar having the "active" class.
"navbarItem": func(currentPath, name, path string) template.HTML {
var act string
if path == currentPath {
act = "active "
}
return template.HTML(fmt.Sprintf(`<a class="%sitem" href="%s">%s</a>`, act, path, name))
},
// curryear returns the current year.
"curryear": func() int {
return time.Now().Year()
},
// hasAdmin returns, based on the user's privileges, whether they should be
// able to see the RAP button (aka AdminPrivilegeAccessRAP).
"hasAdmin": func(privs common.UserPrivileges) bool {
return privs&common.AdminPrivilegeAccessRAP > 0
},
// isRAP returns whether the current page is in RAP.
"isRAP": func(p string) bool {
parts := strings.Split(p, "/")
return len(parts) > 1 && parts[1] == "admin"
},
// favMode is just a helper function for user profiles. Basically checks
// whether a float and an int are ==, and if they are it will return "active ",
// so that the element in the mode menu of a user profile can be marked as
// the current active element.
"favMode": func(favMode float64, current int) string {
if int(favMode) == current {
return "active "
}
return ""
},
// slice generates a []interface{} with the elements it is given.
// useful to iterate over some elements, like this:
// {{ range slice 1 2 3 }}{{ . }}{{ end }}
"slice": func(els ...interface{}) []interface{} {
return els
},
// int converts a float/int to an int.
"int": func(f interface{}) int {
if f == nil {
return 0
}
switch f := f.(type) {
case int:
return f
case float64:
return int(f)
case float32:
return int(f)
}
return 0
},
// float converts an int to a float.
"float": func(i int) float64 {
return float64(i)
},
// atoi converts a string to an int and then a float64.
// If s is not an actual int, it returns nil.
"atoi": func(s string) interface{} {
i, err := strconv.Atoi(s)
if err != nil {
return nil
}
return float64(i)
},
// atoint is like atoi but returns always an int.
"atoint": func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
// parseUserpage compiles BBCode to HTML.
"parseUserpage": func(s string) template.HTML {
return template.HTML(bbcode.Compile(s))
},
// time converts a RFC3339 timestamp to the HTML element <time>.
"time": func(s string) template.HTML {
t, _ := time.Parse(time.RFC3339, s)
return _time(s, t)
},
// time generates a time from a native Go time.Time
"timeFromTime": func(t time.Time) template.HTML {
return _time(t.Format(time.RFC3339), t)
},
// timeAddDay is basically time but adds a day.
"timeAddDay": func(s string) template.HTML {
t, _ := time.Parse(time.RFC3339, s)
t = t.Add(time.Hour * 24)
return _time(t.Format(time.RFC3339), t)
},
// nativeTime creates a native Go time.Time from a RFC3339 timestamp.
"nativeTime": func(s string) time.Time {
t, _ := time.Parse(time.RFC3339, s)
return t
},
// band is a bitwise AND.
"band": func(i1 int, i ...int) int {
for _, el := range i {
i1 &= el
}
return i1
},
// countryReadable converts a country's ISO name to its full name.
"countryReadable": countryReadable,
"country": func(s string, name bool) template.HTML {
var c string
if name {
c = countryReadable(s)
if c == "" {
return ""
}
}
return template.HTML(fmt.Sprintf(`<i class="%s flag"></i>%s`, strings.ToLower(s), c))
},
// humanize pretty-prints a float, e.g.
// humanize(1000) == "1,000"
"humanize": func(f float64) string {
return humanize.Commaf(f)
},
// levelPercent basically does this:
// levelPercent(56.23215) == "23"
"levelPercent": func(l float64) string {
_, f := math.Modf(l)
f *= 100
return fmt.Sprintf("%.0f", f)
},
// level removes the decimal part from a float.
"level": func(l float64) string {
i, _ := math.Modf(l)
return fmt.Sprintf("%.0f", i)
},
// faIcon converts a fontawesome icon to a semantic ui icon.
"faIcon": func(i string) string {
classes := strings.Split(i, " ")
for i, class := range classes {
if v, ok := fasuimappings.Mappings[class]; ok {
classes[i] = v
}
}
return strings.Join(classes, " ")
},
// log fmt.Printf's something
"log": fmt.Printf,
// has returns whether priv1 has all 1 bits of priv2, aka priv1 & priv2 == priv2
"has": func(priv1 interface{}, priv2 float64) bool {
var p1 uint64
switch priv1 := priv1.(type) {
case common.UserPrivileges:
p1 = uint64(priv1)
case float64:
p1 = uint64(priv1)
case int:
p1 = uint64(priv1)
}
return p1&uint64(priv2) == uint64(priv2)
},
// _range is like python range's.
// If it is given 1 argument, it returns a []int containing numbers from 0
// to x.
// If it is given 2 arguments, it returns a []int containing numers from x
// to y if x < y, from y to x if y < x.
"_range": func(x int, y ...int) ([]int, error) {
switch len(y) {
case 0:
r := make([]int, x)
for i := range r {
r[i] = i
}
return r, nil
case 1:
nums, up := pos(y[0] - x)
r := make([]int, nums)
for i := range r {
if up {
r[i] = i + x + 1
} else {
r[i] = i + y[0]
}
}
if !up {
// reverse r
sort.Sort(sort.Reverse(sort.IntSlice(r)))
}
return r, nil
}
return nil, errors.New("y must be at maximum 1 parameter")
},
// blackfriday passes some markdown through blackfriday.
"blackfriday": func(m string) template.HTML {
// The reason of m[strings.Index...] is to remove the "header", where
// there is the information about the file (namely, title, old_id and
// reference_version)
return template.HTML(
blackfriday.Run(
[]byte(
m[strings.Index(m, "\n---\n")+5:],
),
blackfriday.WithExtensions(blackfriday.CommonExtensions),
),
)
},
// i is an inline if.
// i (cond) (true) (false)
"i": func(a bool, x, y interface{}) interface{} {
if a {
return x
}
return y
},
// modes returns an array containing all the modes (in their string representation).
"modes": func() []string {
return []string{
"osu! standard",
"Taiko",
"Catch the Beat",
"osu!mania",
}
},
// _or is like or, but has only false and nil as its "falsey" values
"_or": func(args ...interface{}) interface{} {
for _, a := range args {
if a != nil && a != false {
return a
}
}
return nil
},
// unixNano returns the UNIX timestamp of when hanayo was started in nanoseconds.
"unixNano": func() string {
return strconv.FormatInt(hanayoStarted, 10)
},
// playstyle returns the string representation of a playstyle.
"playstyle": func(i float64, f *profileData) string {
var parts []string
p := int(i)
for k, v := range playstyle.Styles {
if p&(1<<uint(k)) > 0 {
parts = append(parts, f.T(v))
}
}
return strings.Join(parts, ", ")
},
// arithmetic plus/minus
"plus": func(i ...float64) float64 {
var sum float64
for _, i := range i {
sum += i
}
return sum
},
"minus": func(i1 float64, i ...float64) float64 {
for _, i := range i {
i1 -= i
}
return i1
},
// rsin - Return Slice If Nil
"rsin": func(i interface{}) interface{} {
if i == nil {
return []struct{}{}
}
return i
},
// loadjson loads a json file.
"loadjson": func(jsonfile string) interface{} {
f, err := ioutil.ReadFile(jsonfile)
if err != nil {
return nil
}
var x interface{}
err = json.Unmarshal(f, &x)
if err != nil {
return nil
}
return x
},
// loadChangelog loads the changelog.
"loadChangelog": loadChangelog,
// teamJSON returns the data of team.json
"teamJSON": func() map[string]interface{} {
f, err := ioutil.ReadFile("team.json")
if err != nil {
return nil
}
var m map[string]interface{}
json.Unmarshal(f, &m)
return m
},
// in returns whether the first argument is in one of the following
"in": func(a1 interface{}, as ...interface{}) bool {
for _, a := range as {
if a == a1 {
return true
}
}
return false
},
"capitalise": strings.Title,
// servicePrefix gets the prefix of a service, like github.
"servicePrefix": func(s string) string { return servicePrefixes[s] },
// randomLogoColour picks a "random" colour for ripple's logo.
"randomLogoColour": func() string {
if rand.Int()%4 == 0 {
return logoColours[rand.Int()%len(logoColours)]
}
return "pink"
},
// after checks whether a certain time is after time.Now()
"after": func(s string) bool {
t, _ := time.Parse(time.RFC3339, s)
return t.After(time.Now())
},
// qsql functions
"qb": func(q string, p ...interface{}) map[string]qsql.String {
r, err := qb.QueryRow(q, p...)
if err != nil {
fmt.Println(err)
}
if r == nil {
return make(map[string]qsql.String, 0)
}
return r
},
"qba": func(q string, p ...interface{}) []map[string]qsql.String {
r, err := qb.Query(q, p...)
if err != nil {
fmt.Println(err)
}
return r
},
"qbe": func(q string, p ...interface{}) int {
i, _, err := qb.Exec(q, p...)
if err != nil {
fmt.Println(err)
}
return i
},
// bget makes a request to the bancho api
// https://docs.ripple.moe/docs/banchoapi/v1
"bget": func(ept string, qs ...interface{}) map[string]interface{} {
d, err := http.Get(fmt.Sprintf(config.BanchoAPI+"/api/v1/"+ept, qs...))
if err != nil {
return nil
}
x := make(map[string]interface{})
data, _ := ioutil.ReadAll(d.Body)
json.Unmarshal(data, &x)
return x
},
// styles returns playstyle.Styles
"styles": func() []string {
return playstyle.Styles[:]
},
// shift shifts n1 by n2
"shift": func(n1, n2 int) int {
return n1 << uint(n2)
},
// calculateDonorPrice calculates the price of x donor months in euros.
"calculateDonorPrice": func(a float64) string {
return fmt.Sprintf("%.2f", math.Pow(a*30*0.2, 0.7))
},
// is2faEnabled checks 2fa is enabled for an user
"is2faEnabled": is2faEnabled,
// get2faConfirmationToken retrieves the current confirmation token for a certain user.
"get2faConfirmationToken": get2faConfirmationToken,
// csrfGenerate creates a csrf token input
"csrfGenerate": func(u int) template.HTML {
return template.HTML(`<input type="hidden" name="csrf" value="` + mustCSRFGenerate(u) + `">`)
},
// csrfURL creates a CSRF token for GET requests.
"csrfURL": func(u int) template.URL {
return template.URL("csrf=" + mustCSRFGenerate(u))
},
// systemSetting retrieves some information from the table system_settings
"systemSettings": systemSettings,
// authCodeURL gets the auth code for discord
"authCodeURL": func(u int) string {
return getDiscord().AuthCodeURL(mustCSRFGenerate(u))
},
// perc returns a percentage
"perc": func(i, total float64) string {
return fmt.Sprintf("%.0f", i/total*100)
},
// atLeastOne returns 1 if i < 1, or i otherwise.
"atLeastOne": func(i int) int {
if i < 1 {
i = 1
}
return i
},
// ieForm fixes forms in IE/Trident being immensely fucked up. I hate microsoft.
"ieForm": func(c *gin.Context) template.HTML {
if !isIE(c.Request.UserAgent()) {
return ""
}
return ieUnfucker
},
// version gets what's the current Hanayo version.
"version": func() string {
return version
},
"generateKey": generateKey,
// getKeys gets the recovery 2fa keys for an user
"getKeys": func(id int) []string {
var keyRaw string
db.Get(&keyRaw, "SELECT recovery FROM 2fa_totp WHERE userid = ?", id)
s := make([]string, 0, 8)
json.Unmarshal([]byte(keyRaw), &s)
return s
},
// rediget retrieves a value from redis.
"rediget": func(k string) string {
x := rd.Get(k)
if x == nil {
return ""
}
if err := x.Err(); err != nil {
fmt.Println(err)
}
return x.Val()
},
"getBitcoinAddress": btcaddress.Get,
"languageInformation": func() []langInfo {
return languageInformation
},
"languageInformationByNameShort": func(s string) langInfo {
for _, lang := range languageInformation {
if lang.NameShort == s {
return lang
}
}
return langInfo{}
},
"countryList": func(n int64) []string {
return rd.ZRevRange("hanayo:country_list", 0, n-1).Val()
},
"documentationFiles": doc.GetDocs,
"documentationData": func(slug string, language string) doc.File {
if i, err := strconv.Atoi(slug); err == nil {
slug = doc.SlugFromOldID(i)
}
return doc.GetFile(slug, language)
},
"privilegesToString": func(privs float64) string {
return common.Privileges(privs).String()
},
"htmlescaper": template.HTMLEscaper,
}
var localeLanguages = []string{"de", "pl", "it", "es", "ru", "fr", "nl", "ro", "fi", "sv", "vi", "ko"}
var hanayoStarted = time.Now().UnixNano()
var servicePrefixes = map[string]string{
"github": "https://github.com/",
"twitter": "https://twitter.com/",
"mail": "mailto:",
}
var logoColours = [...]string{
"blue",
"green",
"orange",
"red",
}
// we still haven't got jquery when the script is here, so well shit.
const ieUnfucker = `<input type="submit" class="ie" name="submit" value="submit">
<script>
var deferredToPageLoad = function() {
$("button[form]").click(function() {
$("form#" + $(this).attr("form") + " input.ie").click();
});
};
</script>`
func pos(x int) (int, bool) {
if x > 0 {
return x, true
}
return x * -1, false
}
func _time(s string, t time.Time) template.HTML {
return template.HTML(fmt.Sprintf(`<time class="timeago" datetime="%s">%v</time>`, s, t))
}
// Fantastic IEs And Where To Find Them
var ieUserAgentsContain = []string{
"MSIE ",
"Trident/",
"Edge/",
}
func isIE(s string) bool {
for _, v := range ieUserAgentsContain {
if strings.Contains(s, v) {
return true
}
}
return false
}
type systemSetting struct {
Name string
Int int
String string
}
func systemSettings(names ...string) map[string]systemSetting {
var settingsRaw []systemSetting
q, p, _ := sqlx.In("SELECT name, value_int as `int`, value_string as `string` FROM system_settings WHERE name IN (?)", names)
err := db.Select(&settingsRaw, q, p...)
if err != nil {
fmt.Println(err)
return nil
}
settings := make(map[string]systemSetting, len(names))
for _, s := range settingsRaw {
settings[s.Name] = s
}
return settings
}
func getDiscord() *oauth2.Config {
return &oauth2.Config{
ClientID: config.DiscordOAuthID,
ClientSecret: config.DiscordOAuthSecret,
RedirectURL: config.BaseURL + "/settings/discord/finish",
Endpoint: discordoauth.Endpoint,
Scopes: []string{"identify"},
}
}
func getLanguageFromGin(c *gin.Context) string {
for _, l := range getLang(c) {
if in(l, localeLanguages) {
return l
}
}
return ""
}
func init() {
rand.Seed(time.Now().UnixNano())
}
type langInfo struct {
Name, CountryShort, NameShort string
}
var languageInformation = []langInfo{
{"Deutsch", "de", "de"},
{"English (UK)", "gb", "en"},
{"Español", "es", "es"},
{"Français", "fr", "fr"},
{"Italiano", "it", "it"},
{"Nederlands", "nl", "nl"},
{"Polski", "pl", "pl"},
{"Русский", "ru", "ru"},
{"Română", "ro", "ro"},
{"Suomi", "fi", "fi"},
{"Svenska", "se", "sv"},
{"Tiếng Việt Nam", "vn", "vi"},
{"한국어", "kr", "ko"},
}