github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/utils.go (about)

     1  /*
     2  *
     3  *	Utility Functions And Stuff
     4  *	Copyright Azareal 2017 - 2020
     5  *
     6   */
     7  package common
     8  
     9  import (
    10  	"crypto/rand"
    11  	"encoding/base32"
    12  	"encoding/base64"
    13  	"encoding/json"
    14  	"errors"
    15  	"fmt"
    16  	"html"
    17  	"io/ioutil"
    18  	"math"
    19  	"os"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  	"unicode"
    24  )
    25  
    26  // Version stores a Gosora version
    27  type Version struct {
    28  	Major int
    29  	Minor int
    30  	Patch int
    31  	Tag   string
    32  	TagID int
    33  }
    34  
    35  // TODO: Write a test for this
    36  func (ver *Version) String() (out string) {
    37  	out = strconv.Itoa(ver.Major) + "." + strconv.Itoa(ver.Minor) + "." + strconv.Itoa(ver.Patch)
    38  	if ver.Tag != "" {
    39  		out += "-" + ver.Tag
    40  		if ver.TagID != 0 {
    41  			out += strconv.Itoa(ver.TagID)
    42  		}
    43  	}
    44  	return
    45  }
    46  
    47  // GenerateSafeString is for generating a cryptographically secure set of random bytes which is base64 encoded and safe for URLs
    48  // TODO: Write a test for this
    49  func GenerateSafeString(len int) (string, error) {
    50  	rb := make([]byte, len)
    51  	_, err := rand.Read(rb)
    52  	if err != nil {
    53  		return "", err
    54  	}
    55  	return base64.URLEncoding.EncodeToString(rb), nil
    56  }
    57  
    58  // GenerateStd32SafeString is for generating a cryptographically secure set of random bytes which is base32 encoded
    59  // ? - Safe for URLs? Mostly likely due to the small range of characters
    60  func GenerateStd32SafeString(len int) (string, error) {
    61  	rb := make([]byte, len)
    62  	_, err := rand.Read(rb)
    63  	if err != nil {
    64  		return "", err
    65  	}
    66  	return base32.StdEncoding.EncodeToString(rb), nil
    67  }
    68  
    69  // TODO: Write a test for this
    70  func RelativeTimeFromString(in string) (string, error) {
    71  	if in == "" {
    72  		return "", nil
    73  	}
    74  
    75  	t, err := time.Parse("2006-01-02 15:04:05", in)
    76  	if err != nil {
    77  		return "", err
    78  	}
    79  
    80  	return RelativeTime(t), nil
    81  }
    82  
    83  // TODO: Write a test for this
    84  func RelativeTime(t time.Time) string {
    85  	diff := time.Since(t)
    86  	hours := diff.Hours()
    87  	secs := diff.Seconds()
    88  	weeks := int(hours / 24 / 7)
    89  	months := int(hours / 24 / 31)
    90  	switch {
    91  	case months > 3:
    92  		if t.Year() != time.Now().Year() {
    93  			//return t.Format("Mon Jan 2 2006")
    94  			return t.Format("Jan 2 2006")
    95  		}
    96  		return t.Format("Jan 2")
    97  	case months > 1:
    98  		return fmt.Sprintf("%d months ago", months)
    99  	case months == 1:
   100  		return "a month ago"
   101  	case weeks > 1:
   102  		return fmt.Sprintf("%d weeks ago", weeks)
   103  	case int(hours/24) == 7:
   104  		return "a week ago"
   105  	case int(hours/24) == 1:
   106  		return "1 day ago"
   107  	case int(hours/24) > 1:
   108  		return fmt.Sprintf("%d days ago", int(hours/24))
   109  	case secs <= 1:
   110  		return "a moment ago"
   111  	case secs < 60:
   112  		return fmt.Sprintf("%d seconds ago", int(secs))
   113  	case secs < 120:
   114  		return "a minute ago"
   115  	case secs < 3600:
   116  		return fmt.Sprintf("%d minutes ago", int(secs/60))
   117  	case secs < 7200:
   118  		return "an hour ago"
   119  	}
   120  	return fmt.Sprintf("%d hours ago", int(secs/60/60))
   121  }
   122  
   123  // TODO: Finish this faster and more localised version of RelativeTime
   124  /*
   125  // TODO: Write a test for this
   126  // ! Experimental
   127  func RelativeTimeBytes(t time.Time, lang int) []byte {
   128  	diff := time.Since(t)
   129  	hours := diff.Hours()
   130  	secs := diff.Seconds()
   131  	weeks := int(hours / 24 / 7)
   132  	months := int(hours / 24 / 31)
   133  	switch {
   134  	case months > 3:
   135  		if t.Year() != time.Now().Year() {
   136  			return []byte(t.Format(phrases.RTime.MultiYear(lang)))
   137  		}
   138  		return []byte(t.Format(phrases.RTime.SingleYear(lang)))
   139  	case months > 1:
   140  		return phrases.RTime.Months(lang, months)
   141  	case months == 1:
   142  		return phrases.RTime.Month(lang)
   143  	case weeks > 1:
   144  		return phrases.RTime.Weeks(lang, weeks)
   145  	case int(hours/24) == 7:
   146  		return phrases.RTime.Week(lang)
   147  	case int(hours/24) == 1:
   148  		return phrases.RTime.Day(lang)
   149  	case int(hours/24) > 1:
   150  		return phrases.RTime.Days(lang, int(hours/24))
   151  	case secs <= 1:
   152  		return phrases.RTime.Moment(lang)
   153  	case secs < 60:
   154  		return phrases.RTime.Seconds(lang, int(secs))
   155  	case secs < 120:
   156  		return phrases.RTime.Minute(lang)
   157  	case secs < 3600:
   158  		return phrases.RTime.Minutes(lang, int(secs/60))
   159  	case secs < 7200:
   160  		return phrases.RTime.Hour(lang)
   161  	}
   162  	return phrases.RTime.Hours(lang, int(secs/60/60))
   163  }
   164  */
   165  
   166  var pMs = 1000
   167  var pSec = pMs * 1000
   168  var pMin = pSec * 60
   169  var pHour = pMin * 60
   170  var pDay = pHour * 24
   171  
   172  func ConvertPerfUnit(quan float64) (out float64, unit string) {
   173  	f := func() (float64, string) {
   174  		switch {
   175  		case quan >= float64(pDay):
   176  			return quan / float64(pDay), "d"
   177  		case quan >= float64(pHour):
   178  			return quan / float64(pHour), "h"
   179  		case quan >= float64(pMin):
   180  			return quan / float64(pMin), "m"
   181  		case quan >= float64(pSec):
   182  			return quan / float64(pSec), "s"
   183  		case quan >= float64(pMs):
   184  			return quan / float64(pMs), "ms"
   185  		}
   186  		return quan, "μs"
   187  	}
   188  	out, unit = f()
   189  	return math.Ceil(out), unit
   190  }
   191  
   192  // TODO: Write a test for this
   193  func ConvertByteUnit(bytes float64) (float64, string) {
   194  	switch {
   195  	case bytes >= float64(Petabyte):
   196  		return bytes / float64(Petabyte), "PB"
   197  	case bytes >= float64(Terabyte):
   198  		return bytes / float64(Terabyte), "TB"
   199  	case bytes >= float64(Gigabyte):
   200  		return bytes / float64(Gigabyte), "GB"
   201  	case bytes >= float64(Megabyte):
   202  		return bytes / float64(Megabyte), "MB"
   203  	case bytes >= float64(Kilobyte):
   204  		return bytes / float64(Kilobyte), "KB"
   205  	}
   206  	return bytes, " bytes"
   207  }
   208  
   209  // TODO: Write a test for this
   210  func ConvertByteInUnit(bytes float64, unit string) (count float64) {
   211  	switch unit {
   212  	case "PB":
   213  		count = bytes / float64(Petabyte)
   214  	case "TB":
   215  		count = bytes / float64(Terabyte)
   216  	case "GB":
   217  		count = bytes / float64(Gigabyte)
   218  	case "MB":
   219  		count = bytes / float64(Megabyte)
   220  	case "KB":
   221  		count = bytes / float64(Kilobyte)
   222  	default:
   223  		count = 0.1
   224  	}
   225  
   226  	if count < 0.1 {
   227  		count = 0.1
   228  	}
   229  	return
   230  }
   231  
   232  // TODO: Write a test for this
   233  // TODO: Localise this?
   234  func FriendlyUnitToBytes(quantity int, unit string) (bytes int, err error) {
   235  	switch unit {
   236  	case "PB":
   237  		bytes = quantity * Petabyte
   238  	case "TB":
   239  		bytes = quantity * Terabyte
   240  	case "GB":
   241  		bytes = quantity * Gigabyte
   242  	case "MB":
   243  		bytes = quantity * Megabyte
   244  	case "KB":
   245  		bytes = quantity * Kilobyte
   246  	case "":
   247  		// Do nothing
   248  	default:
   249  		return bytes, errors.New("Unknown unit")
   250  	}
   251  	return bytes, nil
   252  }
   253  
   254  // TODO: Write a test for this
   255  // TODO: Re-add T as int64
   256  func ConvertUnit(num int) (int, string) {
   257  	switch {
   258  	case num >= 1000000000000:
   259  		return num / 1000000000000, "T"
   260  	case num >= 1000000000:
   261  		return num / 1000000000, "B"
   262  	case num >= 1000000:
   263  		return num / 1000000, "M"
   264  	case num >= 1000:
   265  		return num / 1000, "K"
   266  	}
   267  	return num, ""
   268  }
   269  
   270  // TODO: Write a test for this
   271  // TODO: Re-add quadrillion as int64
   272  // TODO: Re-add trillion as int64
   273  func ConvertFriendlyUnit(num int) (int, string) {
   274  	switch {
   275  	case num >= 1000000000000000:
   276  		return 0, " quadrillion"
   277  	case num >= 1000000000000:
   278  		return 0, " trillion"
   279  	case num >= 1000000000:
   280  		return num / 1000000000, " billion"
   281  	case num >= 1000000:
   282  		return num / 1000000, " million"
   283  	case num >= 1000:
   284  		return num / 1000, " thousand"
   285  	}
   286  	return num, ""
   287  }
   288  
   289  // TODO: Make slugs optional for certain languages across the entirety of Gosora?
   290  // TODO: Let plugins replace NameToSlug and the URL building logic with their own
   291  /*func NameToSlug(name string) (slug string) {
   292  	// TODO: Do we want this reliant on config file flags? This might complicate tests and oddball uses
   293  	if !Config.BuildSlugs {
   294  		return ""
   295  	}
   296  	name = strings.TrimSpace(name)
   297  	name = strings.Replace(name, "  ", " ", -1)
   298  
   299  	for _, char := range name {
   300  		if unicode.IsLower(char) || unicode.IsNumber(char) {
   301  			slug += string(char)
   302  		} else if unicode.IsUpper(char) {
   303  			slug += string(unicode.ToLower(char))
   304  		} else if unicode.IsSpace(char) {
   305  			slug += "-"
   306  		}
   307  	}
   308  
   309  	if slug == "" {
   310  		slug = "untitled"
   311  	}
   312  	return slug
   313  }*/
   314  
   315  // TODO: Make slugs optional for certain languages across the entirety of Gosora?
   316  // TODO: Let plugins replace NameToSlug and the URL building logic with their own
   317  func NameToSlug(name string) (slug string) {
   318  	// TODO: Do we want this reliant on config file flags? This might complicate tests and oddball uses
   319  	if !Config.BuildSlugs {
   320  		return ""
   321  	}
   322  	name = strings.TrimSpace(name)
   323  	name = strings.Replace(name, "  ", " ", -1)
   324  
   325  	var sb strings.Builder
   326  	for _, char := range name {
   327  		if unicode.IsLower(char) || unicode.IsNumber(char) {
   328  			sb.WriteRune(char)
   329  		} else if unicode.IsUpper(char) {
   330  			sb.WriteRune(unicode.ToLower(char))
   331  		} else if unicode.IsSpace(char) {
   332  			sb.WriteByte('-')
   333  		}
   334  	}
   335  
   336  	if sb.Len() == 0 {
   337  		return "untitled"
   338  	}
   339  	return sb.String()
   340  }
   341  
   342  // TODO: Write a test for this
   343  func HasSuspiciousEmail(email string) bool {
   344  	if email == "" {
   345  		return false
   346  	}
   347  	lowEmail := strings.ToLower(email)
   348  	// TODO: Use a more flexible blacklist, perhaps with a similar mechanism to the HTML tag registration system in PreparseMessage()
   349  	if !strings.Contains(lowEmail, "@") || strings.Contains(lowEmail, "casino") || strings.Contains(lowEmail, "viagra") || strings.Contains(lowEmail, "pharma") || strings.Contains(lowEmail, "pill") {
   350  		return true
   351  	}
   352  
   353  	var dotCount, shortBits, currentSegmentLength int
   354  	for _, char := range lowEmail {
   355  		if char == '.' {
   356  			dotCount++
   357  			if currentSegmentLength < 3 {
   358  				shortBits++
   359  			}
   360  			currentSegmentLength = 0
   361  		} else {
   362  			currentSegmentLength++
   363  		}
   364  	}
   365  
   366  	return dotCount > 7 || shortBits > 2
   367  }
   368  
   369  func unmarshalJsonFile(name string, in interface{}) error {
   370  	data, err := ioutil.ReadFile(name)
   371  	if err != nil {
   372  		return err
   373  	}
   374  	return json.Unmarshal(data, in)
   375  }
   376  
   377  func unmarshalJsonFileIgnore404(name string, in interface{}) error {
   378  	data, err := ioutil.ReadFile(name)
   379  	if err == os.ErrPermission || err == os.ErrClosed {
   380  		return err
   381  	} else if err != nil {
   382  		return nil
   383  	}
   384  	return json.Unmarshal(data, in)
   385  }
   386  
   387  func CanonEmail(email string) string {
   388  	email = strings.ToLower(email)
   389  
   390  	// Gmail emails are equivalent without the dots
   391  	espl := strings.Split(email, "@")
   392  	if len(espl) >= 2 && espl[1] == "gmail.com" {
   393  		return strings.Replace(espl[0], ".", "", -1) + "@" + espl[1]
   394  	}
   395  
   396  	return email
   397  }
   398  
   399  // TODO: Write a test for this
   400  func createFile(name string) error {
   401  	f, err := os.Create(name)
   402  	if err != nil {
   403  		return err
   404  	}
   405  	return f.Close()
   406  }
   407  
   408  // TODO: Write a test for this
   409  func writeFile(name, content string) (err error) {
   410  	f, err := os.Create(name)
   411  	if err != nil {
   412  		return err
   413  	}
   414  	_, err = f.WriteString(content)
   415  	if err != nil {
   416  		return err
   417  	}
   418  	err = f.Sync()
   419  	if err != nil {
   420  		return err
   421  	}
   422  	return f.Close()
   423  }
   424  
   425  // TODO: Write a test for this
   426  func Stripslashes(text string) string {
   427  	text = strings.Replace(text, "/", "", -1)
   428  	return strings.Replace(text, "\\", "", -1)
   429  }
   430  
   431  // The word counter might run into problems with some languages where words aren't as obviously demarcated, I would advise turning it off in those cases, or if it becomes annoying in general, really.
   432  func WordCount(input string) (count int) {
   433  	input = strings.TrimSpace(input)
   434  	if input == "" {
   435  		return 0
   436  	}
   437  
   438  	var inSpace bool
   439  	for _, value := range input {
   440  		if unicode.IsSpace(value) || unicode.IsPunct(value) {
   441  			if !inSpace {
   442  				inSpace = true
   443  			}
   444  		} else if inSpace {
   445  			count++
   446  			inSpace = false
   447  		}
   448  	}
   449  
   450  	return count + 1
   451  }
   452  
   453  // TODO: Write a test for this
   454  func GetLevel(score int) (level int) {
   455  	var base float64 = 25
   456  	var current, prev float64
   457  	var expFactor = 2.8
   458  
   459  	for i := 1; ; i++ {
   460  		_, bit := math.Modf(float64(i) / 10)
   461  		if bit == 0 {
   462  			expFactor += 0.1
   463  		}
   464  		current = base + math.Pow(float64(i), expFactor) + (prev / 3)
   465  		prev = current
   466  		if float64(score) < current {
   467  			break
   468  		}
   469  		level++
   470  	}
   471  	return level
   472  }
   473  
   474  // TODO: Write a test for this
   475  func GetLevelScore(getLevel int) (score int) {
   476  	var base float64 = 25
   477  	var current float64
   478  	var expFactor = 2.8
   479  
   480  	for i := 1; i <= getLevel; i++ {
   481  		_, bit := math.Modf(float64(i) / 10)
   482  		if bit == 0 {
   483  			expFactor += 0.1
   484  		}
   485  		current = base + math.Pow(float64(i), expFactor) + (current / 3)
   486  		//fmt.Println("level: ", i)
   487  		//fmt.Println("current: ", current)
   488  	}
   489  	return int(math.Ceil(current))
   490  }
   491  
   492  // TODO: Write a test for this
   493  func GetLevels(maxLevel int) []float64 {
   494  	var base float64 = 25
   495  	var current, prev float64 // = 0
   496  	var expFactor = 2.8
   497  	var out []float64
   498  	out = append(out, 0)
   499  
   500  	for i := 1; i <= maxLevel; i++ {
   501  		_, bit := math.Modf(float64(i) / 10)
   502  		if bit == 0 {
   503  			expFactor += 0.1
   504  		}
   505  		current = base + math.Pow(float64(i), expFactor) + (prev / 3)
   506  		prev = current
   507  		out = append(out, current)
   508  	}
   509  	return out
   510  }
   511  
   512  // TODO: Write a test for this
   513  // SanitiseSingleLine is a generic function for escaping html entities and removing silly characters from usernames and topic titles. It also strips newline characters
   514  func SanitiseSingleLine(in string) string {
   515  	in = strings.Replace(in, "\n", "", -1)
   516  	in = strings.Replace(in, "\r", "", -1)
   517  	return SanitiseBody(in)
   518  }
   519  
   520  // TODO: Write a test for this
   521  // TODO: Add more strange characters
   522  // TODO: Strip all sub-32s minus \r and \n?
   523  // SanitiseBody is the same as SanitiseSingleLine, but it doesn't strip newline characters
   524  func SanitiseBody(in string) string {
   525  	in = strings.Replace(in, "​", "", -1) // Strip Zero length space
   526  	in = html.EscapeString(in)
   527  	return strings.TrimSpace(in)
   528  }
   529  
   530  func BuildSlug(slug string, id int) string {
   531  	if slug == "" || !Config.BuildSlugs {
   532  		return strconv.Itoa(id)
   533  	}
   534  	return slug + "." + strconv.Itoa(id)
   535  }