util.go (6268B)
1 package util 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "encoding/hex" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "html/template" 11 "log" 12 "net/http" 13 "net/url" 14 "os" 15 "path/filepath" 16 "regexp" 17 "strconv" 18 "strings" 19 "time" 20 21 "github.com/gomarkdown/markdown" 22 "github.com/gomarkdown/markdown/parser" 23 "github.com/komkom/toml" 24 "github.com/microcosm-cc/bluemonday" 25 "golang.org/x/exp/utf8string" 26 27 "cerca/defaults" 28 "cerca/types" 29 ) 30 31 /* util.Eout example invocations 32 if err != nil { 33 return util.Eout(err, "reading data") 34 } 35 if err = util.Eout(err, "reading data"); err != nil { 36 return nil, err 37 } 38 */ 39 40 type ErrorDescriber struct { 41 environ string // the basic context that is potentially generating errors (like a GetThread function, the environ would be "get thread") 42 } 43 44 // parametrize Eout/Check such that error messages contain a defined context/environ 45 func Describe(environ string) ErrorDescriber { 46 return ErrorDescriber{environ} 47 } 48 49 func (ed ErrorDescriber) Eout(err error, msg string, args ...interface{}) error { 50 msg = fmt.Sprintf("%s: %s", ed.environ, msg) 51 return Eout(err, msg, args...) 52 } 53 54 func (ed ErrorDescriber) Check(err error, msg string, args ...interface{}) { 55 msg = fmt.Sprintf("%s: %s", ed.environ, msg) 56 Check(err, msg, args...) 57 } 58 59 // format all errors consistently, and provide context for the error using the string `msg` 60 func Eout(err error, msg string, args ...interface{}) error { 61 if err != nil { 62 // received an invocation of e.g. format: 63 // Eout(err, "reading data for %s and %s", "database item", "weird user") 64 if len(args) > 0 { 65 return fmt.Errorf("%s (%w)", fmt.Sprintf(msg, args...), err) 66 } 67 return fmt.Errorf("%s (%w)", msg, err) 68 } 69 return nil 70 } 71 72 func Check(err error, msg string, args ...interface{}) { 73 if len(args) > 0 { 74 err = Eout(err, msg, args...) 75 } else { 76 err = Eout(err, msg) 77 } 78 if err != nil { 79 log.Fatalln(err) 80 } 81 } 82 83 func Contains(slice []string, s string) bool { 84 for _, item := range slice { 85 if item == s { 86 return true 87 } 88 } 89 return false 90 } 91 92 var contentGuardian = bluemonday.UGCPolicy() 93 var strictContentGuardian = bluemonday.StrictPolicy() 94 95 // Turns Markdown input into HTML 96 func Markup(md string) template.HTML { 97 mdBytes := []byte(string(md)) 98 // fix newlines 99 mdBytes = markdown.NormalizeNewlines(mdBytes) 100 mdParser := parser.NewWithExtensions(parser.CommonExtensions ^ parser.MathJax) 101 maybeUnsafeHTML := markdown.ToHTML(mdBytes, mdParser, nil) 102 // guard against malicious code being embedded 103 html := contentGuardian.SanitizeBytes(maybeUnsafeHTML) 104 // lazy load images 105 pattern := regexp.MustCompile("<img") 106 if pattern.Match(html) { 107 html = pattern.ReplaceAll(html, []byte(`<img loading="lazy"`)) 108 } 109 return template.HTML(html) 110 } 111 112 func SanitizeStringStrict(s string) string { 113 return strictContentGuardian.Sanitize(s) 114 } 115 116 func VerificationPrefix(name string) string { 117 pattern := regexp.MustCompile("A|E|O|U|I|Y") 118 upper := strings.ToUpper(name) 119 replaced := string(pattern.ReplaceAll([]byte(upper), []byte(""))) 120 if len(replaced) < 3 { 121 replaced += "XYZ" 122 } 123 return replaced[0:3] 124 } 125 126 func GetThreadSlug(threadid int, title string, threadLen int) string { 127 return fmt.Sprintf("/thread/%d/%s-%d/", threadid, SanitizeURL(title), threadLen) 128 } 129 130 func Hex2Base64(s string) (string, error) { 131 b, err := hex.DecodeString(s) 132 if err != nil { 133 return "", err 134 } 135 b64 := base64.StdEncoding.EncodeToString(b) 136 return b64, nil 137 } 138 139 // make a string be suitable for use as part of a url 140 func SanitizeURL(input string) string { 141 input = strings.ReplaceAll(input, " ", "-") 142 input = url.PathEscape(input) 143 // TODO(2022-01-08): evaluate use of strict content guardian? 144 return strings.ToLower(input) 145 } 146 147 // returns an id from a url path, and a boolean. the boolean is true if we're returning what we expect; false if the 148 // operation failed 149 func GetURLPortion(req *http.Request, index int) (int, bool) { 150 var desiredID int 151 parts := strings.Split(strings.TrimSpace(req.URL.Path), "/") 152 if len(parts) < index || parts[index] == "" { 153 return -1, false 154 } 155 desiredID, err := strconv.Atoi(parts[index]) 156 if err != nil { 157 return -1, false 158 } 159 return desiredID, true 160 } 161 162 func Capitalize(s string) string { 163 // utf8 safe capitalization 164 str := utf8string.NewString(s) 165 first := string(str.At(0)) 166 rest := string(str.Slice(1, str.RuneCount())) 167 return strings.ToUpper(first) + rest 168 } 169 170 func CreateIfNotExist(docpath, content string) (bool, error) { 171 err := os.MkdirAll(filepath.Dir(docpath), 0750) 172 if err != nil { 173 return false, err 174 } 175 _, err = os.Stat(docpath) 176 if err != nil { 177 // if the file doesn't exist, create it 178 if errors.Is(err, os.ErrNotExist) { 179 err = os.WriteFile(docpath, []byte(content), 0777) 180 if err != nil { 181 return false, err 182 } 183 // created file successfully 184 return true, nil 185 } else { 186 return false, err 187 } 188 } 189 return false, nil 190 } 191 192 const solarYearSecs = 31556926 193 194 func RelativeTime(t time.Time) string { 195 d := time.Since(t) 196 var metric string 197 var amount int 198 if d.Seconds() < 60 { 199 amount = int(d.Seconds()) 200 metric = "second" 201 } else if d.Minutes() < 60 { 202 amount = int(d.Minutes()) 203 metric = "minute" 204 } else if d.Hours() < 24 { 205 amount = int(d.Hours()) 206 metric = "hour" 207 } else if d.Seconds() < solarYearSecs { 208 amount = int(d.Hours()) / 24 209 metric = "day" 210 } else { 211 amount = int(d.Seconds()) / solarYearSecs 212 metric = "year" 213 } 214 if amount == 1 { 215 return fmt.Sprintf("%d %s ago", amount, metric) 216 } else { 217 return fmt.Sprintf("%d %ss ago", amount, metric) 218 } 219 } 220 221 func ReadConfig(confpath string) types.Config { 222 ed := Describe("config") 223 _, err := CreateIfNotExist(confpath, defaults.DEFAULT_CONFIG) 224 ed.Check(err, "create default config") 225 226 data, err := os.ReadFile(confpath) 227 ed.Check(err, "read file") 228 229 var conf types.Config 230 decoder := json.NewDecoder(toml.New(bytes.NewBuffer(data))) 231 232 err = decoder.Decode(&conf) 233 ed.Check(err, "decode toml with json decoder") 234 235 return conf 236 } 237 238 func LoadFile(key, docpath, defaultContent string) ([]byte, error) { 239 ed := Describe("load file") 240 _, err := CreateIfNotExist(docpath, defaultContent) 241 err = ed.Eout(err, "create if not exist (%s) %s", key, docpath) 242 if err != nil { 243 return nil, err 244 } 245 data, err := os.ReadFile(docpath) 246 err = ed.Eout(err, "read %s", docpath) 247 if err != nil { 248 return nil, err 249 } 250 return data, nil 251 }