The new Ripple frontend.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

funcmap.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. package main
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "html/template"
  7. "io/ioutil"
  8. "math"
  9. "math/rand"
  10. "net/http"
  11. "sort"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/dustin/go-humanize"
  16. "github.com/gin-gonic/gin"
  17. "github.com/jmoiron/sqlx"
  18. "github.com/russross/blackfriday"
  19. "github.com/thehowl/qsql"
  20. "golang.org/x/oauth2"
  21. "zxq.co/ripple/go-discord-oauth"
  22. "zxq.co/ripple/hanayo/modules/bbcode"
  23. "zxq.co/ripple/hanayo/modules/btcaddress"
  24. "zxq.co/ripple/hanayo/modules/doc"
  25. "zxq.co/ripple/hanayo/modules/fa-semantic-mappings"
  26. "zxq.co/ripple/playstyle"
  27. "zxq.co/ripple/rippleapi/common"
  28. )
  29. // funcMap contains useful functions for the various templates.
  30. var funcMap = template.FuncMap{
  31. // html disables HTML escaping on the values it is given.
  32. "html": func(value interface{}) template.HTML {
  33. return template.HTML(fmt.Sprint(value))
  34. },
  35. // avatars is a function returning the configuration constant AvatarURL
  36. "config": func(key string) interface{} {
  37. return configMap[key]
  38. },
  39. // navbarItem is a function to generate an item in the navbar.
  40. // The reason why this exists is that I wanted to have the currently
  41. // selected element in the navbar having the "active" class.
  42. "navbarItem": func(currentPath, name, path string) template.HTML {
  43. var act string
  44. if path == currentPath {
  45. act = "active "
  46. }
  47. return template.HTML(fmt.Sprintf(`<a class="%sitem" href="%s">%s</a>`, act, path, name))
  48. },
  49. // curryear returns the current year.
  50. "curryear": func() int {
  51. return time.Now().Year()
  52. },
  53. // hasAdmin returns, based on the user's privileges, whether they should be
  54. // able to see the RAP button (aka AdminPrivilegeAccessRAP).
  55. "hasAdmin": func(privs common.UserPrivileges) bool {
  56. return privs&common.AdminPrivilegeAccessRAP > 0
  57. },
  58. // isRAP returns whether the current page is in RAP.
  59. "isRAP": func(p string) bool {
  60. parts := strings.Split(p, "/")
  61. return len(parts) > 1 && parts[1] == "admin"
  62. },
  63. // favMode is just a helper function for user profiles. Basically checks
  64. // whether a float and an int are ==, and if they are it will return "active ",
  65. // so that the element in the mode menu of a user profile can be marked as
  66. // the current active element.
  67. "favMode": func(favMode float64, current int) string {
  68. if int(favMode) == current {
  69. return "active "
  70. }
  71. return ""
  72. },
  73. // slice generates a []interface{} with the elements it is given.
  74. // useful to iterate over some elements, like this:
  75. // {{ range slice 1 2 3 }}{{ . }}{{ end }}
  76. "slice": func(els ...interface{}) []interface{} {
  77. return els
  78. },
  79. // int converts a float/int to an int.
  80. "int": func(f interface{}) int {
  81. if f == nil {
  82. return 0
  83. }
  84. switch f := f.(type) {
  85. case int:
  86. return f
  87. case float64:
  88. return int(f)
  89. case float32:
  90. return int(f)
  91. }
  92. return 0
  93. },
  94. // float converts an int to a float.
  95. "float": func(i int) float64 {
  96. return float64(i)
  97. },
  98. // atoi converts a string to an int and then a float64.
  99. // If s is not an actual int, it returns nil.
  100. "atoi": func(s string) interface{} {
  101. i, err := strconv.Atoi(s)
  102. if err != nil {
  103. return nil
  104. }
  105. return float64(i)
  106. },
  107. // atoint is like atoi but returns always an int.
  108. "atoint": func(s string) int {
  109. i, _ := strconv.Atoi(s)
  110. return i
  111. },
  112. // parseUserpage compiles BBCode to HTML.
  113. "parseUserpage": func(s string) template.HTML {
  114. return template.HTML(bbcode.Compile(s))
  115. },
  116. // time converts a RFC3339 timestamp to the HTML element <time>.
  117. "time": func(s string) template.HTML {
  118. t, _ := time.Parse(time.RFC3339, s)
  119. return _time(s, t)
  120. },
  121. // time generates a time from a native Go time.Time
  122. "timeFromTime": func(t time.Time) template.HTML {
  123. return _time(t.Format(time.RFC3339), t)
  124. },
  125. // timeAddDay is basically time but adds a day.
  126. "timeAddDay": func(s string) template.HTML {
  127. t, _ := time.Parse(time.RFC3339, s)
  128. t = t.Add(time.Hour * 24)
  129. return _time(t.Format(time.RFC3339), t)
  130. },
  131. // nativeTime creates a native Go time.Time from a RFC3339 timestamp.
  132. "nativeTime": func(s string) time.Time {
  133. t, _ := time.Parse(time.RFC3339, s)
  134. return t
  135. },
  136. // band is a bitwise AND.
  137. "band": func(i1 int, i ...int) int {
  138. for _, el := range i {
  139. i1 &= el
  140. }
  141. return i1
  142. },
  143. // countryReadable converts a country's ISO name to its full name.
  144. "countryReadable": countryReadable,
  145. "country": func(s string, name bool) template.HTML {
  146. var c string
  147. if name {
  148. c = countryReadable(s)
  149. if c == "" {
  150. return ""
  151. }
  152. }
  153. return template.HTML(fmt.Sprintf(`<i class="%s flag"></i>%s`, strings.ToLower(s), c))
  154. },
  155. // humanize pretty-prints a float, e.g.
  156. // humanize(1000) == "1,000"
  157. "humanize": func(f float64) string {
  158. return humanize.Commaf(f)
  159. },
  160. // levelPercent basically does this:
  161. // levelPercent(56.23215) == "23"
  162. "levelPercent": func(l float64) string {
  163. _, f := math.Modf(l)
  164. f *= 100
  165. return fmt.Sprintf("%.0f", f)
  166. },
  167. // level removes the decimal part from a float.
  168. "level": func(l float64) string {
  169. i, _ := math.Modf(l)
  170. return fmt.Sprintf("%.0f", i)
  171. },
  172. // faIcon converts a fontawesome icon to a semantic ui icon.
  173. "faIcon": func(i string) string {
  174. classes := strings.Split(i, " ")
  175. for i, class := range classes {
  176. if v, ok := fasuimappings.Mappings[class]; ok {
  177. classes[i] = v
  178. }
  179. }
  180. return strings.Join(classes, " ")
  181. },
  182. // log fmt.Printf's something
  183. "log": fmt.Printf,
  184. // has returns whether priv1 has all 1 bits of priv2, aka priv1 & priv2 == priv2
  185. "has": func(priv1 interface{}, priv2 float64) bool {
  186. var p1 uint64
  187. switch priv1 := priv1.(type) {
  188. case common.UserPrivileges:
  189. p1 = uint64(priv1)
  190. case float64:
  191. p1 = uint64(priv1)
  192. case int:
  193. p1 = uint64(priv1)
  194. }
  195. return p1&uint64(priv2) == uint64(priv2)
  196. },
  197. // _range is like python range's.
  198. // If it is given 1 argument, it returns a []int containing numbers from 0
  199. // to x.
  200. // If it is given 2 arguments, it returns a []int containing numers from x
  201. // to y if x < y, from y to x if y < x.
  202. "_range": func(x int, y ...int) ([]int, error) {
  203. switch len(y) {
  204. case 0:
  205. r := make([]int, x)
  206. for i := range r {
  207. r[i] = i
  208. }
  209. return r, nil
  210. case 1:
  211. nums, up := pos(y[0] - x)
  212. r := make([]int, nums)
  213. for i := range r {
  214. if up {
  215. r[i] = i + x + 1
  216. } else {
  217. r[i] = i + y[0]
  218. }
  219. }
  220. if !up {
  221. // reverse r
  222. sort.Sort(sort.Reverse(sort.IntSlice(r)))
  223. }
  224. return r, nil
  225. }
  226. return nil, errors.New("y must be at maximum 1 parameter")
  227. },
  228. // blackfriday passes some markdown through blackfriday.
  229. "blackfriday": func(m string) template.HTML {
  230. // The reason of m[strings.Index...] is to remove the "header", where
  231. // there is the information about the file (namely, title, old_id and
  232. // reference_version)
  233. return template.HTML(
  234. blackfriday.Run(
  235. []byte(
  236. m[strings.Index(m, "\n---\n")+5:],
  237. ),
  238. blackfriday.WithExtensions(blackfriday.CommonExtensions),
  239. ),
  240. )
  241. },
  242. // i is an inline if.
  243. // i (cond) (true) (false)
  244. "i": func(a bool, x, y interface{}) interface{} {
  245. if a {
  246. return x
  247. }
  248. return y
  249. },
  250. // modes returns an array containing all the modes (in their string representation).
  251. "modes": func() []string {
  252. return []string{
  253. "osu! standard",
  254. "Taiko",
  255. "Catch the Beat",
  256. "osu!mania",
  257. }
  258. },
  259. // _or is like or, but has only false and nil as its "falsey" values
  260. "_or": func(args ...interface{}) interface{} {
  261. for _, a := range args {
  262. if a != nil && a != false {
  263. return a
  264. }
  265. }
  266. return nil
  267. },
  268. // unixNano returns the UNIX timestamp of when hanayo was started in nanoseconds.
  269. "unixNano": func() string {
  270. return strconv.FormatInt(hanayoStarted, 10)
  271. },
  272. // playstyle returns the string representation of a playstyle.
  273. "playstyle": func(i float64, f *profileData) string {
  274. var parts []string
  275. p := int(i)
  276. for k, v := range playstyle.Styles {
  277. if p&(1<<uint(k)) > 0 {
  278. parts = append(parts, f.T(v))
  279. }
  280. }
  281. return strings.Join(parts, ", ")
  282. },
  283. // arithmetic plus/minus
  284. "plus": func(i ...float64) float64 {
  285. var sum float64
  286. for _, i := range i {
  287. sum += i
  288. }
  289. return sum
  290. },
  291. "minus": func(i1 float64, i ...float64) float64 {
  292. for _, i := range i {
  293. i1 -= i
  294. }
  295. return i1
  296. },
  297. // rsin - Return Slice If Nil
  298. "rsin": func(i interface{}) interface{} {
  299. if i == nil {
  300. return []struct{}{}
  301. }
  302. return i
  303. },
  304. // loadjson loads a json file.
  305. "loadjson": func(jsonfile string) interface{} {
  306. f, err := ioutil.ReadFile(jsonfile)
  307. if err != nil {
  308. return nil
  309. }
  310. var x interface{}
  311. err = json.Unmarshal(f, &x)
  312. if err != nil {
  313. return nil
  314. }
  315. return x
  316. },
  317. // loadChangelog loads the changelog.
  318. "loadChangelog": loadChangelog,
  319. // teamJSON returns the data of team.json
  320. "teamJSON": func() map[string]interface{} {
  321. f, err := ioutil.ReadFile("team.json")
  322. if err != nil {
  323. return nil
  324. }
  325. var m map[string]interface{}
  326. json.Unmarshal(f, &m)
  327. return m
  328. },
  329. // in returns whether the first argument is in one of the following
  330. "in": func(a1 interface{}, as ...interface{}) bool {
  331. for _, a := range as {
  332. if a == a1 {
  333. return true
  334. }
  335. }
  336. return false
  337. },
  338. "capitalise": strings.Title,
  339. // servicePrefix gets the prefix of a service, like github.
  340. "servicePrefix": func(s string) string { return servicePrefixes[s] },
  341. // randomLogoColour picks a "random" colour for ripple's logo.
  342. "randomLogoColour": func() string {
  343. if rand.Int()%4 == 0 {
  344. return logoColours[rand.Int()%len(logoColours)]
  345. }
  346. return "pink"
  347. },
  348. // after checks whether a certain time is after time.Now()
  349. "after": func(s string) bool {
  350. t, _ := time.Parse(time.RFC3339, s)
  351. return t.After(time.Now())
  352. },
  353. // qsql functions
  354. "qb": func(q string, p ...interface{}) map[string]qsql.String {
  355. r, err := qb.QueryRow(q, p...)
  356. if err != nil {
  357. fmt.Println(err)
  358. }
  359. if r == nil {
  360. return make(map[string]qsql.String, 0)
  361. }
  362. return r
  363. },
  364. "qba": func(q string, p ...interface{}) []map[string]qsql.String {
  365. r, err := qb.Query(q, p...)
  366. if err != nil {
  367. fmt.Println(err)
  368. }
  369. return r
  370. },
  371. "qbe": func(q string, p ...interface{}) int {
  372. i, _, err := qb.Exec(q, p...)
  373. if err != nil {
  374. fmt.Println(err)
  375. }
  376. return i
  377. },
  378. // bget makes a request to the bancho api
  379. // https://docs.ripple.moe/docs/banchoapi/v1
  380. "bget": func(ept string, qs ...interface{}) map[string]interface{} {
  381. d, err := http.Get(fmt.Sprintf(config.BanchoAPI+"/api/v1/"+ept, qs...))
  382. if err != nil {
  383. return nil
  384. }
  385. x := make(map[string]interface{})
  386. data, _ := ioutil.ReadAll(d.Body)
  387. json.Unmarshal(data, &x)
  388. return x
  389. },
  390. // styles returns playstyle.Styles
  391. "styles": func() []string {
  392. return playstyle.Styles[:]
  393. },
  394. // shift shifts n1 by n2
  395. "shift": func(n1, n2 int) int {
  396. return n1 << uint(n2)
  397. },
  398. // calculateDonorPrice calculates the price of x donor months in euros.
  399. "calculateDonorPrice": func(a float64) string {
  400. return fmt.Sprintf("%.2f", math.Pow(a*30*0.2, 0.7))
  401. },
  402. // is2faEnabled checks 2fa is enabled for an user
  403. "is2faEnabled": is2faEnabled,
  404. // get2faConfirmationToken retrieves the current confirmation token for a certain user.
  405. "get2faConfirmationToken": get2faConfirmationToken,
  406. // csrfGenerate creates a csrf token input
  407. "csrfGenerate": func(u int) template.HTML {
  408. return template.HTML(`<input type="hidden" name="csrf" value="` + mustCSRFGenerate(u) + `">`)
  409. },
  410. // csrfURL creates a CSRF token for GET requests.
  411. "csrfURL": func(u int) template.URL {
  412. return template.URL("csrf=" + mustCSRFGenerate(u))
  413. },
  414. // systemSetting retrieves some information from the table system_settings
  415. "systemSettings": systemSettings,
  416. // authCodeURL gets the auth code for discord
  417. "authCodeURL": func(u int) string {
  418. return getDiscord().AuthCodeURL(mustCSRFGenerate(u))
  419. },
  420. // perc returns a percentage
  421. "perc": func(i, total float64) string {
  422. return fmt.Sprintf("%.0f", i/total*100)
  423. },
  424. // atLeastOne returns 1 if i < 1, or i otherwise.
  425. "atLeastOne": func(i int) int {
  426. if i < 1 {
  427. i = 1
  428. }
  429. return i
  430. },
  431. // ieForm fixes forms in IE/Trident being immensely fucked up. I hate microsoft.
  432. "ieForm": func(c *gin.Context) template.HTML {
  433. if !isIE(c.Request.UserAgent()) {
  434. return ""
  435. }
  436. return ieUnfucker
  437. },
  438. // version gets what's the current Hanayo version.
  439. "version": func() string {
  440. return version
  441. },
  442. "generateKey": generateKey,
  443. // getKeys gets the recovery 2fa keys for an user
  444. "getKeys": func(id int) []string {
  445. var keyRaw string
  446. db.Get(&keyRaw, "SELECT recovery FROM 2fa_totp WHERE userid = ?", id)
  447. s := make([]string, 0, 8)
  448. json.Unmarshal([]byte(keyRaw), &s)
  449. return s
  450. },
  451. // rediget retrieves a value from redis.
  452. "rediget": func(k string) string {
  453. x := rd.Get(k)
  454. if x == nil {
  455. return ""
  456. }
  457. if err := x.Err(); err != nil {
  458. fmt.Println(err)
  459. }
  460. return x.Val()
  461. },
  462. "getBitcoinAddress": btcaddress.Get,
  463. "languageInformation": func() []langInfo {
  464. return languageInformation
  465. },
  466. "languageInformationByNameShort": func(s string) langInfo {
  467. for _, lang := range languageInformation {
  468. if lang.NameShort == s {
  469. return lang
  470. }
  471. }
  472. return langInfo{}
  473. },
  474. "countryList": func(n int64) []string {
  475. return rd.ZRevRange("hanayo:country_list", 0, n-1).Val()
  476. },
  477. "documentationFiles": doc.GetDocs,
  478. "documentationData": func(slug string, language string) doc.File {
  479. if i, err := strconv.Atoi(slug); err == nil {
  480. slug = doc.SlugFromOldID(i)
  481. }
  482. return doc.GetFile(slug, language)
  483. },
  484. "privilegesToString": func(privs float64) string {
  485. return common.Privileges(privs).String()
  486. },
  487. "htmlescaper": template.HTMLEscaper,
  488. }
  489. var localeLanguages = []string{"de", "pl", "it", "es", "ru", "fr", "nl", "ro", "fi", "sv", "vi", "ko"}
  490. var hanayoStarted = time.Now().UnixNano()
  491. var servicePrefixes = map[string]string{
  492. "github": "https://github.com/",
  493. "twitter": "https://twitter.com/",
  494. "mail": "mailto:",
  495. }
  496. var logoColours = [...]string{
  497. "blue",
  498. "green",
  499. "orange",
  500. "red",
  501. }
  502. // we still haven't got jquery when the script is here, so well shit.
  503. const ieUnfucker = `<input type="submit" class="ie" name="submit" value="submit">
  504. <script>
  505. var deferredToPageLoad = function() {
  506. $("button[form]").click(function() {
  507. $("form#" + $(this).attr("form") + " input.ie").click();
  508. });
  509. };
  510. </script>`
  511. func pos(x int) (int, bool) {
  512. if x > 0 {
  513. return x, true
  514. }
  515. return x * -1, false
  516. }
  517. func _time(s string, t time.Time) template.HTML {
  518. return template.HTML(fmt.Sprintf(`<time class="timeago" datetime="%s">%v</time>`, s, t))
  519. }
  520. // Fantastic IEs And Where To Find Them
  521. var ieUserAgentsContain = []string{
  522. "MSIE ",
  523. "Trident/",
  524. "Edge/",
  525. }
  526. func isIE(s string) bool {
  527. for _, v := range ieUserAgentsContain {
  528. if strings.Contains(s, v) {
  529. return true
  530. }
  531. }
  532. return false
  533. }
  534. type systemSetting struct {
  535. Name string
  536. Int int
  537. String string
  538. }
  539. func systemSettings(names ...string) map[string]systemSetting {
  540. var settingsRaw []systemSetting
  541. q, p, _ := sqlx.In("SELECT name, value_int as `int`, value_string as `string` FROM system_settings WHERE name IN (?)", names)
  542. err := db.Select(&settingsRaw, q, p...)
  543. if err != nil {
  544. fmt.Println(err)
  545. return nil
  546. }
  547. settings := make(map[string]systemSetting, len(names))
  548. for _, s := range settingsRaw {
  549. settings[s.Name] = s
  550. }
  551. return settings
  552. }
  553. func getDiscord() *oauth2.Config {
  554. return &oauth2.Config{
  555. ClientID: config.DiscordOAuthID,
  556. ClientSecret: config.DiscordOAuthSecret,
  557. RedirectURL: config.BaseURL + "/settings/discord/finish",
  558. Endpoint: discordoauth.Endpoint,
  559. Scopes: []string{"identify"},
  560. }
  561. }
  562. func getLanguageFromGin(c *gin.Context) string {
  563. for _, l := range getLang(c) {
  564. if in(l, localeLanguages) {
  565. return l
  566. }
  567. }
  568. return ""
  569. }
  570. func init() {
  571. rand.Seed(time.Now().UnixNano())
  572. }
  573. type langInfo struct {
  574. Name, CountryShort, NameShort string
  575. }
  576. var languageInformation = []langInfo{
  577. {"Deutsch", "de", "de"},
  578. {"English (UK)", "gb", "en"},
  579. {"Español", "es", "es"},
  580. {"Français", "fr", "fr"},
  581. {"Italiano", "it", "it"},
  582. {"Nederlands", "nl", "nl"},
  583. {"Polski", "pl", "pl"},
  584. {"Русский", "ru", "ru"},
  585. {"Română", "ro", "ro"},
  586. {"Suomi", "fi", "fi"},
  587. {"Svenska", "se", "sv"},
  588. {"Tiếng Việt Nam", "vn", "vi"},
  589. {"한국어", "kr", "ko"},
  590. }