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 }