diff --git a/assets/button.png b/assets/button.png new file mode 100644 index 0000000..c94631b Binary files /dev/null and b/assets/button.png differ diff --git a/config/cfg.go b/config/cfg.go index 59082f4..7048cc4 100644 --- a/config/cfg.go +++ b/config/cfg.go @@ -4,26 +4,28 @@ import "os" type ( Config struct { - DatabaseURL string - SessionKey string - Env string - CertFile string - KeyFile string - AssetsDir string - EmailUsername string - EmailPassword string - EmailHost string - EmailHostAddr string + DatabaseURL string + SessionKey string + Env string + CertFile string + KeyFile string + AssetsDir string + EmailUsername string + EmailPassword string + EmailHost string + EmailHostAddr string + ManualRegistration bool } ) func New() *Config { return &Config{ - DatabaseURL: os.Getenv("DATABASE_URL"), - SessionKey: os.Getenv("SESSION_KEY"), - Env: os.Getenv("ENV"), - CertFile: os.Getenv("CERT_FILE"), - KeyFile: os.Getenv("CERT_KEY_FILE"), - AssetsDir: os.Getenv("ASSETS_DIR"), + DatabaseURL: os.Getenv("DATABASE_URL"), + SessionKey: os.Getenv("SESSION_KEY"), + Env: os.Getenv("ENV"), + CertFile: os.Getenv("CERT_FILE"), + KeyFile: os.Getenv("CERT_KEY_FILE"), + AssetsDir: os.Getenv("ASSETS_DIR"), + ManualRegistration: len(os.Getenv("MANUAL_REGISTRATION")) > 0, } } diff --git a/storage/user.go b/storage/user.go index e1b29f2..ad45da0 100644 --- a/storage/user.go +++ b/storage/user.go @@ -41,12 +41,12 @@ func (s *Storage) CreateUser(user model.User) error { if err != nil { return err } - insertUser := `INSERT INTO users (name, hash, email, signup_email, signup_msg) VALUES (lower($1), $2, $3, $4, $5)` + insertUser := `INSERT INTO users (name, hash, email, signup_email, signup_msg, active) VALUES (lower($1), $2, $3, $4, $5, $6)` statement, err := s.db.Prepare(insertUser) if err != nil { return err } - _, err = statement.Exec(user.Name, hash, user.Email, user.SignupEmail, user.SignupMsg) + _, err = statement.Exec(user.Name, hash, user.Email, user.SignupEmail, user.SignupMsg, user.Active) return err } diff --git a/web/handler/form/register.go b/web/handler/form/register.go index 82e7d94..507bbb4 100644 --- a/web/handler/form/register.go +++ b/web/handler/form/register.go @@ -24,6 +24,9 @@ func (f *RegisterForm) Validate() error { if len(f.Username) < 3 { return errors.New("username needs to be at least 3 characters") } + if len(f.Username) > 20 { + return errors.New("username should be 20 characters or less") + } match, _ := regexp.MatchString("^[a-z0-9-_]+$", f.Username) if !match { return errors.New("only lowercase letters and digits are accepted for username") diff --git a/web/handler/handler.go b/web/handler/handler.go index 6eaf24e..8726542 100644 --- a/web/handler/handler.go +++ b/web/handler/handler.go @@ -82,8 +82,8 @@ func New(cfg *config.Config, sess *session.Session, data *storage.Storage) (http router.HandleFunc("/users/{user}/status", h.showUserStatusView).Methods(http.MethodGet) router.HandleFunc("/users/{user}/status.json", h.showUserStatusJSONView).Methods(http.MethodGet) - router.HandleFunc("/users/{user}/status.png", h.showUserStatusImageView).Methods(http.MethodGet) - router.HandleFunc("/users/{user}/badge.png", h.showUserStatusBadgeView).Methods(http.MethodGet) + //router.HandleFunc("/users/{user}/status.png", h.showUserStatusImageViewEmoji).Methods(http.MethodGet) + //router.HandleFunc("/users/{user}/badge.png", h.showUserStatusBadgeView).Methods(http.MethodGet) router.PathPrefix("/assets/").Handler( http.StripPrefix("/assets/", http.FileServer( diff --git a/web/handler/html.go b/web/handler/html.go index d7f80da..303d524 100644 --- a/web/handler/html.go +++ b/web/handler/html.go @@ -165,6 +165,7 @@ var TplMap = map[string]string{

status.cafe is a place to share your current status.

Register now!

{{ end }} +

Status stream

@@ -257,47 +258,37 @@ var TplMap = map[string]string{ {{ end }}`, "register": `{{ define "content" }}
-
-
-

Register

-
-

Registrations are manually approved to prevent spam and keep this little cafe a cool place to hang out.

-

You should receive a confirmation within a few hours at the email you have provided. Make sure to enter a valid address!

-
+

Register

+ {{ if .form.Error }} +

{{ .form.Error }}

+ {{ end }} +
+
+ +
-
- {{ if .form.Error }} -

{{ .form.Error }}

- {{ end }} - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - +
+ +
-
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
{{ end }}`, "register-success": `{{ define "content" }} diff --git a/web/handler/html/index.html b/web/handler/html/index.html index 2a4405e..402a4eb 100644 --- a/web/handler/html/index.html +++ b/web/handler/html/index.html @@ -17,6 +17,7 @@

status.cafe is a place to share your current status.

Register now!

{{ end }} +

Status stream

diff --git a/web/handler/html/register.html b/web/handler/html/register.html index 8b89125..770e9b6 100644 --- a/web/handler/html/register.html +++ b/web/handler/html/register.html @@ -1,45 +1,35 @@ {{ define "content" }}
-
-
-

Register

-
-

Registrations are manually approved to prevent spam and keep this little cafe a cool place to hang out.

-

You should receive a confirmation within a few hours at the email you have provided. Make sure to enter a valid address!

-
+

Register

+ {{ if .form.Error }} +

{{ .form.Error }}

+ {{ end }} +
+
+ +
-
- {{ if .form.Error }} -

{{ .form.Error }}

- {{ end }} - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - +
+ +
-
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
{{ end }} \ No newline at end of file diff --git a/web/handler/register.go b/web/handler/register.go index d5ed583..d444a04 100644 --- a/web/handler/register.go +++ b/web/handler/register.go @@ -109,13 +109,24 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) { showError(errors.New("username already exists")) return } + if !h.cfg.ManualRegistration { + user.Active = true + } if err := h.storage.CreateUser(user); err != nil { showError(err) return } - h.renderLayout(w, "register-success", map[string]interface{}{ - "name": user.Name, - "email": user.SignupEmail, - }, "") + if h.cfg.ManualRegistration { + h.renderLayout(w, "register-success", map[string]interface{}{ + "name": user.Name, + "email": user.SignupEmail, + }, "") + } else { + if err := h.sess.Save(r, w, r.FormValue("name")); err != nil { + serverError(w, err) + return + } + http.Redirect(w, r, "/", http.StatusFound) + } } } diff --git a/web/handler/user_show.go b/web/handler/user_show.go index facf25a..2a6a819 100644 --- a/web/handler/user_show.go +++ b/web/handler/user_show.go @@ -1,28 +1,207 @@ package handler import ( + "bytes" "encoding/json" "encoding/xml" + "errors" "fmt" "github.com/fogleman/gg" "github.com/golang/freetype" "github.com/golang/freetype/truetype" "github.com/gorilla/mux" + "golang.org/x/image/draw" "golang.org/x/image/font" + _ "golang.org/x/image/font/gofont/gobold" "golang.org/x/image/font/gofont/goregular" "golang.org/x/image/math/fixed" "html/template" "image" "image/color" - "image/draw" + + "image/jpeg" + "io" + "os" "status/model" "strconv" + "strings" "time" //"image/png" "net/http" ) +const ( + exitSuccess = 0 + exitFailure = 1 +) + +const ( + imagePath = "/home/m15o/Downloads/noto-emoji-main/png/128" + aliasFile = "/home/m15o/Downloads/noto-emoji-main/emoji_aliases.txt" + fontSize = 64 // point + imageWidth = 640 // pixel + imageHeight = 120 // pixel + textTopMargin = 80 // fixed.I + lineHeight = 70 // fixed.I +) + +// emoji alias +var alias = map[string]string{} + +// +//func init() { +// fp, err := os.Open(aliasFile) +// if err != nil { +// panic(err) +// } +// +// s := bufio.NewScanner(fp) +// for s.Scan() { +// line := s.Text() +// +// /* +// alias file format: +// # 'fe0f' is not in these sequences +// 1f3c3;1f3c3_200d_2642 # RUNNER -> man running +// */ +// +// if strings.HasPrefix(line, "#") { +// continue +// } +// +// s := strings.Split(line, ";") +// if len(s) != 2 { +// continue +// } +// +// i := strings.Index(s[1], " ") +// if i < 0 { +// continue +// } +// +// from := s[0] +// to := s[1][:i] +// +// alias[from] = to +// } +// +// if err := s.Err(); err != nil { +// fmt.Fprintln(os.Stderr, "reading alias file:", err) +// } +// +//} + +func exist(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// GetPath if unicode codepoint is included in list and file exists return file path +func getPath(r rune) (string, error) { + if r > 47 && r < 58 { + return "", errors.New("numeric char") + } + name := fmt.Sprintf("%.4x", r) + var path string + + if v, ok := alias[name]; ok { + path = fmt.Sprintf("%s/emoji_u%s.png", imagePath, v) + } else { + path = fmt.Sprintf("%s/emoji_u%s.png", imagePath, name) + } + + if !exist(path) { + return "", fmt.Errorf("%s does NOT exist", path) + } + + return path, nil +} + +func loadEmoji(r rune, size int) (image.Image, bool) { + var img image.Image + + path, err := getPath(r) + if err != nil { + //fmt.Fprintln(os.Stderr, err) + return img, false + } + + fp, err := os.Open(path) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return img, false + } + defer fp.Close() + + img, _, err = image.Decode(fp) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return img, false + } + + rect := image.Rect(0, 0, size, size) + dst := image.NewRGBA(rect) + draw.ApproxBiLinear.Scale(dst, rect, img, img.Bounds(), draw.Over, nil) + + return dst, true +} + +func renderLine(img draw.Image, dr *font.Drawer, s string) { + size := dr.Face.Metrics().Ascent.Floor() + dr.Face.Metrics().Descent.Floor() + + for _, r := range s { + emoji, ok := loadEmoji(r, size) + + if ok { + // Drawer.Dot is glyph baseline of next glyph + // get left/top coordinates for draw.Draw(). + p := image.Pt(dr.Dot.X.Floor(), dr.Dot.Y.Floor()-dr.Face.Metrics().Ascent.Floor()) + rect := image.Rect(0, 0, size, size).Add(p) + + // draw emoji and ascend baseline + draw.Draw(img, rect, emoji, image.ZP, draw.Over) + dr.Dot.X += fixed.I(size) + } else { + // fallback: use normal glyph + dr.DrawString(string(r)) + } + } +} + +func renderText(img draw.Image, face font.Face, text string) error { + dr := &font.Drawer{ + Dst: img, + Src: image.White, + Face: face, + Dot: fixed.Point26_6{}, + } + + for i, s := range strings.Split(text, "\n") { + dr.Dot.X = (fixed.I(imageWidth) - dr.MeasureString(s)) / 2 + dr.Dot.Y = fixed.I(textTopMargin + i*lineHeight) + + renderLine(img, dr, s) + } + + return nil +} + +func outputJPEG(img image.Image, w io.Writer) error { + buf := new(bytes.Buffer) + err := jpeg.Encode(buf, img, nil) + if err != nil { + return err + } + + _, err = io.Copy(w, buf) + if err != nil { + return err + } + + return nil +} + func (h *Handler) showManageView(w http.ResponseWriter, r *http.Request) { logged, err := h.sess.Get(r) if err != nil { @@ -220,6 +399,52 @@ func (h *Handler) showUserStatusBadgeView(w http.ResponseWriter, r *http.Request dc.EncodePNG(w) } +// +//func (h *Handler) showUserStatusImageViewEmoji(w http.ResponseWriter, r *http.Request) { +// user := mux.Vars(r)["user"] +// if !h.storage.UserExists(user) { +// notFound(w) +// return +// } +// statuses, _, err := h.storage.StatusByUsername(mux.Vars(r)["user"], 1, 0) +// if err != nil { +// serverError(w, err) +// return +// } +// +// var username string +// var face string +// if len(statuses) > 0 { +// username = statuses[0].User +// face = statuses[0].Face +// } +// +// img := image.NewRGBA(image.Rect(0, 0, imageWidth, imageHeight)) +// +// ft, err := truetype.Parse(gobold.TTF) +// if err != nil { +// fmt.Fprintln(os.Stderr, err) +// os.Exit(exitFailure) +// } +// +// opt := truetype.Options{ +// Size: fontSize, +// DPI: 0, +// Hinting: 0, +// GlyphCacheEntries: 0, +// SubPixelsX: 0, +// SubPixelsY: 0, +// } +// +// renderText(img, truetype.NewFace(ft, &opt), fmt.Sprintf("%s %s", username, face)) +// +// err = outputJPEG(img, w) +// if err != nil { +// fmt.Fprintln(os.Stderr, err) +// os.Exit(exitFailure) +// } +//} + func (h *Handler) showUserStatusImageView(w http.ResponseWriter, r *http.Request) { user := mux.Vars(r)["user"] if !h.storage.UserExists(user) {