PageRenderTime 2455ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/helper.go

https://bitbucket.org/anzellai/kitty
Go | 525 lines | 421 code | 67 blank | 37 comment | 94 complexity | ea22e06aef27a359f7714490da8e968c MD5 | raw file
Possible License(s): BSD-3-Clause, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-2-Clause
  1. package kitty
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "html"
  9. "html/template"
  10. "math/rand"
  11. "net/http"
  12. "os"
  13. "regexp"
  14. "sort"
  15. "strings"
  16. "time"
  17. "github.com/gobuffalo/uuid"
  18. "github.com/gorilla/mux"
  19. "github.com/microcosm-cc/bluemonday"
  20. "google.golang.org/appengine"
  21. "google.golang.org/appengine/log"
  22. "google.golang.org/appengine/mail"
  23. "google.golang.org/appengine/urlfetch"
  24. )
  25. var (
  26. sanitizer = bluemonday.UGCPolicy()
  27. isBase64Regex = regexp.MustCompile("^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$")
  28. // PostcodeRegex to match 2 parts of a valid UK postcode
  29. PostcodeRegex = regexp.MustCompile(`^([A-Z]{1,2}\d{1,2}[A-Z]?)\s*(\d[A-Z]{2})$`)
  30. )
  31. // AppContext instantiate appengine context from request
  32. func AppContext(r *http.Request) context.Context {
  33. return appengine.NewContext(r)
  34. }
  35. // ValidateUID checks if uid is in uuid.v4 format
  36. func ValidateUID(uid string) error {
  37. uuid4, err := uuid.FromString(uid)
  38. if err != nil {
  39. return fmt.Errorf("invalid uid: %s", err.Error())
  40. }
  41. if uuid4.Version() != 4 {
  42. return fmt.Errorf("invalid uuid.v4 format: %s", uuid4.String())
  43. }
  44. return nil
  45. }
  46. // StructToMap converts struct to a map interface
  47. func StructToMap(ds interface{}) map[string]interface{} {
  48. var structMap map[string]interface{}
  49. result, err := json.Marshal(ds)
  50. if err != nil {
  51. return nil
  52. }
  53. err = json.Unmarshal(result, &structMap)
  54. if err != nil {
  55. return nil
  56. }
  57. return structMap
  58. }
  59. // MapKeys converts a map interface to slice of string keys
  60. func MapKeys(input map[string]interface{}) (keys []string) {
  61. for key := range input {
  62. keys = append(keys, key)
  63. }
  64. sort.Strings(keys)
  65. return
  66. }
  67. // HasStringInSlice checks if string element is in slice
  68. func HasStringInSlice(elements []string, s string) bool {
  69. for _, element := range elements {
  70. if element == s {
  71. return true
  72. }
  73. }
  74. return false
  75. }
  76. // Cleanse returns cleansed and html escapedstring
  77. func Cleanse(text string) string {
  78. return html.EscapeString(strings.TrimSpace(text))
  79. }
  80. // Sanitise returns sanitised html string allowing safe string
  81. func Sanitise(text string) string {
  82. return html.UnescapeString(sanitizer.Sanitize(strings.TrimSpace(text)))
  83. }
  84. // SanitiseInChunks returns sanitised base64 encoded string allowing safe string in 1000 length for each chunk
  85. func SanitiseInChunks(texts []string) []string {
  86. completedText := strings.TrimSpace(strings.Join(texts, ""))
  87. if !isBase64Regex.MatchString(completedText) {
  88. completedText = Sanitise(completedText)
  89. }
  90. slices := []string{}
  91. count := 0
  92. lastIndex := 0
  93. if len(completedText) > 1000 {
  94. for index := range completedText {
  95. count++
  96. if count%1001 == 0 {
  97. slices = append(slices, completedText[lastIndex:index])
  98. lastIndex = index
  99. }
  100. }
  101. } else {
  102. slices = append(slices, completedText)
  103. }
  104. return slices
  105. }
  106. // ToTitleCase parse input to title cased string separated with single space
  107. func ToTitleCase(input interface{}) string {
  108. textInput := fmt.Sprintf("%s", input)
  109. output := make([]string, len(textInput))
  110. for _, charText := range strings.Split(textInput, "") {
  111. if charText == strings.ToUpper(charText) {
  112. output = append(output, " ")
  113. }
  114. output = append(output, charText)
  115. }
  116. return strings.Join(output, "")
  117. }
  118. // MarshalStruct renders JSON string of a struct instance
  119. func MarshalStruct(ds interface{}) (message string) {
  120. response, err := json.MarshalIndent(ds, "", " ")
  121. if err != nil {
  122. return
  123. }
  124. message = string(response)
  125. return
  126. }
  127. type jsonResponse struct {
  128. Trace string `json:"trace"`
  129. Code int `json:"code"`
  130. Status string `json:"status"`
  131. Error string `json:"error,omitempty"`
  132. Data interface{} `json:"data,omitempty"`
  133. }
  134. // jsonmarshal will marshal input with indent on DEV env
  135. func jsonmarshal(input interface{}) (response []byte, err error) {
  136. if GetEnv("ENV") == "DEV" {
  137. response, err = json.MarshalIndent(input, "", " ")
  138. } else {
  139. response, err = json.Marshal(input)
  140. }
  141. return
  142. }
  143. // RenderJSON renders standardised json response body
  144. func RenderJSON(w http.ResponseWriter, r *http.Request, code int, err error, data interface{}) {
  145. ctx := AppContext(r)
  146. resp := &jsonResponse{
  147. Trace: appengine.RequestID(ctx),
  148. Code: code,
  149. Status: "ok",
  150. }
  151. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  152. if err != nil {
  153. resp.Status = "error"
  154. resp.Error = fmt.Sprintf("%+v", err)
  155. } else {
  156. resp.Data = data
  157. }
  158. response, err := jsonmarshal(&resp)
  159. if err != nil {
  160. resp.Code = http.StatusUnprocessableEntity
  161. resp.Status = "error"
  162. resp.Error = fmt.Sprintf("%+v", err)
  163. resp.Data = nil
  164. // retry and fallback with simple error without body
  165. response, err = jsonmarshal(&resp)
  166. if err != nil {
  167. http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  168. return
  169. }
  170. }
  171. w.WriteHeader(code)
  172. _, err = w.Write(response)
  173. if err != nil {
  174. panic(err)
  175. }
  176. }
  177. // GetEnv returns requested key from environment or nil
  178. func GetEnv(key string) string {
  179. return os.Getenv(strings.ToUpper(strings.TrimSpace(key)))
  180. }
  181. // StructFromJSON parse json file to struct instance
  182. func StructFromJSON(i interface{}, fpath string) {
  183. f, err := os.Open(fpath)
  184. if err != nil {
  185. return
  186. }
  187. defer f.Close()
  188. err = json.NewDecoder(f).Decode(&i)
  189. if err != nil {
  190. return
  191. }
  192. }
  193. // Template is a struct proxy to all available templates
  194. type Template struct {
  195. // templates are pointers to Globs parsed from "public/templates/"
  196. templates *template.Template
  197. }
  198. // Templates store named and parsed template within map
  199. type Templates map[string]*template.Template
  200. // App proxy with mux.Router and functions
  201. type App struct {
  202. t Templates
  203. d interface{}
  204. }
  205. // NewApp returns new Kitty App instance
  206. func NewApp() *App {
  207. return &App{
  208. t: make(Templates),
  209. }
  210. }
  211. // IsDevelopment checks ENV environment variable
  212. func (app *App) IsDevelopment() bool {
  213. return strings.ToUpper(GetEnv("ENV")) == "DEV"
  214. }
  215. // IsProduction checks ENV environment vairable
  216. func (app *App) IsProduction() bool {
  217. return !app.IsDevelopment()
  218. }
  219. // Use middleware proxy to mux.Router
  220. func (app *App) Use(router *mux.Router, mw func(http.Handler) http.Handler) {
  221. router.Use(mw)
  222. }
  223. // SetData set data on Router instance
  224. func (app *App) SetData(data interface{}) {
  225. app.d = data
  226. }
  227. // Static serves static over prefix path
  228. func (app *App) Static(router *mux.Router, prefix, dir string) {
  229. router.PathPrefix(prefix).Handler(
  230. http.StripPrefix(prefix, http.FileServer(http.Dir(dir))),
  231. )
  232. }
  233. // Mount path to serve router
  234. func (app *App) Mount(router *mux.Router, path string) {
  235. http.Handle(path, router)
  236. }
  237. // RegisterView registers the route and handle view template rendering
  238. func (app *App) RegisterView(router *mux.Router, view string) {
  239. view = strings.TrimPrefix(view, "/")
  240. viewNames := strings.Split(view, ".")
  241. viewName := viewNames[len(viewNames)-1]
  242. if viewName == "" {
  243. viewName = "home"
  244. }
  245. router.HandleFunc(
  246. fmt.Sprintf("/%s", view),
  247. func(w http.ResponseWriter, r *http.Request) {
  248. app.RenderTemplate(w, viewName)
  249. },
  250. )
  251. }
  252. // Register404View renders 404 view
  253. func (app *App) Register404View(router *mux.Router) {
  254. app.RegisterView(router, "/404")
  255. router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  256. app.RenderTemplate(w, "404")
  257. })
  258. }
  259. // RegisterRedirect registers the route and handle redirect
  260. func (app *App) RegisterRedirect(router *mux.Router, view, redirectURI string, redirectCode int) {
  261. router.HandleFunc(
  262. view,
  263. func(w http.ResponseWriter, r *http.Request) {
  264. http.Redirect(w, r, redirectURI, redirectCode)
  265. },
  266. )
  267. }
  268. // RenderTemplate renders base and content template
  269. func (app *App) RenderTemplate(w http.ResponseWriter, name string) error {
  270. tplRoot := GetEnv("TEMPLATE_ROOT")
  271. if tplRoot == "" {
  272. tplRoot = "./public/templates/"
  273. }
  274. app.t[name] = template.Must(template.New("").Funcs(template.FuncMap{
  275. "getenv": func(key string) string {
  276. return GetEnv(key)
  277. },
  278. "random": func(min, max int) int {
  279. rand.Seed(time.Now().Unix())
  280. return rand.Intn(max-min) + min
  281. },
  282. "today": func() time.Time {
  283. return time.Now()
  284. },
  285. "dict": func(values ...interface{}) (map[string]interface{}, error) {
  286. if len(values)%2 != 0 {
  287. return nil, errors.New("invalid dict call")
  288. }
  289. dict := make(map[string]interface{}, len(values)/2)
  290. for i := 0; i < len(values); i += 2 {
  291. key, ok := values[i].(string)
  292. if !ok {
  293. return nil, errors.New("dict keys must be strings")
  294. }
  295. dict[key] = values[i+1]
  296. }
  297. return dict, nil
  298. },
  299. "js": func(s string) template.JS {
  300. return template.JS(s)
  301. },
  302. "safe": func(s string) template.HTML {
  303. return template.HTML(s)
  304. },
  305. }).ParseFiles(tplRoot+name+".html", tplRoot+"base.html"))
  306. return app.t[name].ExecuteTemplate(w, "base", app.d)
  307. }
  308. // Notify is a function to send datatore interface summary and a short sentence to Notification channel or Bot
  309. func Notify(ctx context.Context, title string, ds Datastore) {
  310. notification := &map[string]interface{}{
  311. "chat_id": GetEnv("BOT_CHAT_ID"),
  312. "parse_mode": "HTML",
  313. "text": fmt.Sprintf(`
  314. <b>Notification</b>: %s
  315. <pre>%s</pre>`, title, ds.Summary()),
  316. }
  317. payload, err := json.MarshalIndent(notification, "", " ")
  318. if err != nil {
  319. log.Errorf(ctx, "Notification cannot be marshalled: %s", err.Error())
  320. return
  321. }
  322. botAPI := GetEnv("BOT_API")
  323. if botAPI == "" {
  324. log.Infof(ctx, "Notification sent OK to console:\n%s", string(payload))
  325. return
  326. }
  327. req, err := http.NewRequest("POST", botAPI, bytes.NewBuffer(payload))
  328. if err != nil {
  329. log.Errorf(ctx, "Notification request cannot be prepared: %s", err.Error())
  330. return
  331. }
  332. req.Header.Set("Content-Type", "application/json")
  333. client := urlfetch.Client(ctx)
  334. resp, err := client.Do(req)
  335. if err != nil {
  336. log.Errorf(ctx, "Notification request cannot be made: %s", err.Error())
  337. return
  338. }
  339. defer resp.Body.Close()
  340. log.Infof(ctx, "Notification sent OK with response body: %s", resp.Body)
  341. }
  342. // NotifyAdmin send an email to designated admin so technical issues can be resolved
  343. func NotifyAdmin(ctx context.Context, topic string, entity Datastore, errSource error) {
  344. sender := GetEnv("EMAIL_SENDER")
  345. errorMessage := "No error"
  346. if errSource != nil {
  347. errorMessage = errSource.Error()
  348. }
  349. msg := &mail.Message{
  350. Sender: sender,
  351. To: []string{"anzel.lai@gmail.com", sender},
  352. Subject: fmt.Sprintf("%s for <%s>", topic, entity.Summary()),
  353. Body: fmt.Sprintf(
  354. "Details:\n\nTime: %s\n\nError: %s\n\nEntity: %s",
  355. time.Now(),
  356. errorMessage,
  357. MarshalStruct(entity),
  358. ),
  359. }
  360. if sender == "" {
  361. log.Infof(ctx, "no email sender is configured.")
  362. return
  363. }
  364. log.Infof(ctx, "Email to be sent: %+v", msg)
  365. if err := mail.Send(ctx, msg); err != nil {
  366. log.Errorf(ctx, "Couldn't send email: %v", err)
  367. }
  368. }
  369. // FormatPhoneE164 is a function to format phone number to be E164 specific format
  370. func FormatPhoneE164(phone string) (formatted string) {
  371. formatted = strings.TrimSpace(phone)
  372. if formatted == "" {
  373. return
  374. }
  375. if formatted[:3] == "+44" {
  376. formatted = formatted[3:]
  377. } else if formatted[:2] == "44" {
  378. formatted = formatted[2:]
  379. } else if formatted[0] == '0' {
  380. formatted = formatted[1:]
  381. }
  382. formatted = "+44" + formatted
  383. return
  384. }
  385. // ValidatePhone checks if mobile phone number can be used
  386. func ValidatePhone(
  387. ctx context.Context,
  388. phone string,
  389. phoneTypes []string,
  390. ) error {
  391. phone = FormatPhoneE164(phone)
  392. if phone == "" {
  393. return errors.New("phone number cannot be empty")
  394. }
  395. msgConfig := &struct {
  396. MsgAPILive bool
  397. MsgAPISender string
  398. MsgAPIVerifyMobileEndpoint string
  399. MsgAPIID string
  400. MsgAPIAuthToken string
  401. MsgWebhookSid string
  402. }{
  403. MsgAPILive: GetEnv("MSG_API_LIVE") == "true",
  404. MsgAPIVerifyMobileEndpoint: GetEnv("MSG_API_VERIFY_MOBILE_ENDPOINT"),
  405. MsgAPISender: GetEnv("MSG_API_SENDER"),
  406. MsgAPIID: GetEnv("MSG_API_ID"),
  407. MsgAPIAuthToken: GetEnv("MSG_API_AUTHTOKEN"),
  408. MsgWebhookSid: GetEnv("MSG_WEBHOOK_SID"),
  409. }
  410. env := GetEnv("ENV")
  411. if env == "PRODUCTION" {
  412. StructFromJSON(msgConfig, "./config.json")
  413. }
  414. if phone == msgConfig.MsgAPISender {
  415. return errors.New("phone number cannot be the same as the sender")
  416. }
  417. // Verify number is a mobile phone SMS capable
  418. phoneWithoutSign := phone[1:]
  419. // If SMS API is not live, just bypass and validate it
  420. if env != "PRODUCTION" {
  421. log.Infof(ctx, "ValidatePhone %s bypassed: %s", phoneWithoutSign, env)
  422. return nil
  423. }
  424. endpoint := fmt.Sprintf(msgConfig.MsgAPIVerifyMobileEndpoint, phoneWithoutSign)
  425. req, err := http.NewRequest("GET", endpoint, nil)
  426. if err != nil {
  427. log.Errorf(ctx, "ValidatePhone %s request error: %s", phoneWithoutSign, err.Error())
  428. return err
  429. }
  430. req.SetBasicAuth(msgConfig.MsgAPIID, msgConfig.MsgAPIAuthToken)
  431. client := urlfetch.Client(ctx)
  432. resp, err := client.Do(req)
  433. if err != nil {
  434. log.Errorf(ctx, "ValidatePhone %s error: %s", phoneWithoutSign, err.Error())
  435. return err
  436. }
  437. defer resp.Body.Close()
  438. var verifyPhoneResponse map[string]interface{}
  439. decoder := json.NewDecoder(resp.Body)
  440. err = decoder.Decode(&verifyPhoneResponse)
  441. if err != nil {
  442. log.Errorf(ctx, "ValidatePhone %s response error: %s", phoneWithoutSign, err.Error())
  443. return err
  444. }
  445. log.Infof(ctx, "ValidatePhone %s response body: %s", phoneWithoutSign, verifyPhoneResponse)
  446. carrier, ok := verifyPhoneResponse["carrier"].(map[string]interface{})
  447. if !ok {
  448. return errors.New("phone number cannot be verified")
  449. }
  450. carrierType, ok := carrier["type"].(string)
  451. if !ok {
  452. return errors.New("phone number cannot be verified")
  453. }
  454. if len(phoneTypes) > 0 && !HasStringInSlice(phoneTypes, carrierType) {
  455. return errors.New("phone number cannot be verified")
  456. }
  457. return nil
  458. }