lots of stuff

This commit is contained in:
m15o 2021-12-22 10:21:14 +01:00
parent 79d73d12f5
commit 4a9e788f29
10 changed files with 326 additions and 103 deletions

BIN
assets/button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -4,26 +4,28 @@ import "os"
type ( type (
Config struct { Config struct {
DatabaseURL string DatabaseURL string
SessionKey string SessionKey string
Env string Env string
CertFile string CertFile string
KeyFile string KeyFile string
AssetsDir string AssetsDir string
EmailUsername string EmailUsername string
EmailPassword string EmailPassword string
EmailHost string EmailHost string
EmailHostAddr string EmailHostAddr string
ManualRegistration bool
} }
) )
func New() *Config { func New() *Config {
return &Config{ return &Config{
DatabaseURL: os.Getenv("DATABASE_URL"), DatabaseURL: os.Getenv("DATABASE_URL"),
SessionKey: os.Getenv("SESSION_KEY"), SessionKey: os.Getenv("SESSION_KEY"),
Env: os.Getenv("ENV"), Env: os.Getenv("ENV"),
CertFile: os.Getenv("CERT_FILE"), CertFile: os.Getenv("CERT_FILE"),
KeyFile: os.Getenv("CERT_KEY_FILE"), KeyFile: os.Getenv("CERT_KEY_FILE"),
AssetsDir: os.Getenv("ASSETS_DIR"), AssetsDir: os.Getenv("ASSETS_DIR"),
ManualRegistration: len(os.Getenv("MANUAL_REGISTRATION")) > 0,
} }
} }

View file

@ -41,12 +41,12 @@ func (s *Storage) CreateUser(user model.User) error {
if err != nil { if err != nil {
return err 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) statement, err := s.db.Prepare(insertUser)
if err != nil { if err != nil {
return err 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 return err
} }

View file

@ -24,6 +24,9 @@ func (f *RegisterForm) Validate() error {
if len(f.Username) < 3 { if len(f.Username) < 3 {
return errors.New("username needs to be at least 3 characters") 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) match, _ := regexp.MatchString("^[a-z0-9-_]+$", f.Username)
if !match { if !match {
return errors.New("only lowercase letters and digits are accepted for username") return errors.New("only lowercase letters and digits are accepted for username")

View file

@ -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", h.showUserStatusView).Methods(http.MethodGet)
router.HandleFunc("/users/{user}/status.json", h.showUserStatusJSONView).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}/status.png", h.showUserStatusImageViewEmoji).Methods(http.MethodGet)
router.HandleFunc("/users/{user}/badge.png", h.showUserStatusBadgeView).Methods(http.MethodGet) //router.HandleFunc("/users/{user}/badge.png", h.showUserStatusBadgeView).Methods(http.MethodGet)
router.PathPrefix("/assets/").Handler( router.PathPrefix("/assets/").Handler(
http.StripPrefix("/assets/", http.StripPrefix("/assets/",
http.FileServer( http.FileServer(

View file

@ -165,6 +165,7 @@ var TplMap = map[string]string{
<p>status.cafe is a place to share your current status.</p> <p>status.cafe is a place to share your current status.</p>
<p><a href="/register">Register now!</a></p> <p><a href="/register">Register now!</a></p>
{{ end }} {{ end }}
<p><img src="/assets/button.png"/></p>
</section> </section>
<section> <section>
<h2>Status stream</h2> <h2>Status stream</h2>
@ -257,47 +258,37 @@ var TplMap = map[string]string{
{{ end }}`, {{ end }}`,
"register": `{{ define "content" }} "register": `{{ define "content" }}
<section> <section>
<div class="cols"> <h1>Register</h1>
<div> {{ if .form.Error }}
<h1>Register</h1> <p>{{ .form.Error }}</p>
<div class="info"> {{ end }}
<p>Registrations are manually approved to prevent spam and keep this little cafe a cool place to hang out.</p> <form action="/register" method="post" class="auth-form">
<p>You should receive a confirmation within a few hours at the email you have provided. Make sure to enter a valid address!</p> <div class="field">
</div> <label for="name">Username</label>
<input type="text" id="name" name="name" autocomplete="off" required maxlength="20" autofocus/>
</div> </div>
<div> <div class="field">
{{ if .form.Error }} <label for="email">Email</label>
<p>{{ .form.Error }}</p> <input type="email" id="email" name="email" autocomplete="off" required/>
{{ end }}
<form action="/register" method="post" class="auth-form">
<div class="field">
<label for="name">Username</label>
<input type="text" id="name" name="name" autocomplete="off" required autofocus/>
</div>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="off" required autofocus/>
</div>
<div class="field">
<label for="show-email">Show e-mail</label>
<input type="checkbox" name="show-email" value="1" id="show-email" style="width: inherit;">
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" name="password" required/>
</div>
<div class="field">
<label for="password-confirm">Confirm password</label>
<input type="password" id="password-confirm" name="password-confirm" required/>
</div>
<div class="field">
<label for="answer">How did you discover status.cafe?</label>
<textarea id="answer" name="answer" required></textarea>
</div>
<input type="submit" value="Submit">
</form>
</div> </div>
</div> <div class="field">
<label for="show-email">Show e-mail</label>
<input type="checkbox" name="show-email" value="1" id="show-email" style="width: inherit;">
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" name="password" required/>
</div>
<div class="field">
<label for="password-confirm">Confirm password</label>
<input type="password" id="password-confirm" name="password-confirm" required/>
</div>
<div class="field">
<label for="answer">How did you discover status.cafe?</label>
<textarea id="answer" name="answer" required></textarea>
</div>
<input type="submit" value="Submit">
</form>
</section> </section>
{{ end }}`, {{ end }}`,
"register-success": `{{ define "content" }} "register-success": `{{ define "content" }}

View file

@ -17,6 +17,7 @@
<p>status.cafe is a place to share your current status.</p> <p>status.cafe is a place to share your current status.</p>
<p><a href="/register">Register now!</a></p> <p><a href="/register">Register now!</a></p>
{{ end }} {{ end }}
<p><img src="/assets/button.png"/></p>
</section> </section>
<section> <section>
<h2>Status stream</h2> <h2>Status stream</h2>

View file

@ -1,45 +1,35 @@
{{ define "content" }} {{ define "content" }}
<section> <section>
<div class="cols"> <h1>Register</h1>
<div> {{ if .form.Error }}
<h1>Register</h1> <p>{{ .form.Error }}</p>
<div class="info"> {{ end }}
<p>Registrations are manually approved to prevent spam and keep this little cafe a cool place to hang out.</p> <form action="/register" method="post" class="auth-form">
<p>You should receive a confirmation within a few hours at the email you have provided. Make sure to enter a valid address!</p> <div class="field">
</div> <label for="name">Username</label>
<input type="text" id="name" name="name" autocomplete="off" required maxlength="20" autofocus/>
</div> </div>
<div> <div class="field">
{{ if .form.Error }} <label for="email">Email</label>
<p>{{ .form.Error }}</p> <input type="email" id="email" name="email" autocomplete="off" required/>
{{ end }}
<form action="/register" method="post" class="auth-form">
<div class="field">
<label for="name">Username</label>
<input type="text" id="name" name="name" autocomplete="off" required autofocus/>
</div>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="off" required autofocus/>
</div>
<div class="field">
<label for="show-email">Show e-mail</label>
<input type="checkbox" name="show-email" value="1" id="show-email" style="width: inherit;">
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" name="password" required/>
</div>
<div class="field">
<label for="password-confirm">Confirm password</label>
<input type="password" id="password-confirm" name="password-confirm" required/>
</div>
<div class="field">
<label for="answer">How did you discover status.cafe?</label>
<textarea id="answer" name="answer" required></textarea>
</div>
<input type="submit" value="Submit">
</form>
</div> </div>
</div> <div class="field">
<label for="show-email">Show e-mail</label>
<input type="checkbox" name="show-email" value="1" id="show-email" style="width: inherit;">
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" name="password" required/>
</div>
<div class="field">
<label for="password-confirm">Confirm password</label>
<input type="password" id="password-confirm" name="password-confirm" required/>
</div>
<div class="field">
<label for="answer">How did you discover status.cafe?</label>
<textarea id="answer" name="answer" required></textarea>
</div>
<input type="submit" value="Submit">
</form>
</section> </section>
{{ end }} {{ end }}

View file

@ -109,13 +109,24 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) {
showError(errors.New("username already exists")) showError(errors.New("username already exists"))
return return
} }
if !h.cfg.ManualRegistration {
user.Active = true
}
if err := h.storage.CreateUser(user); err != nil { if err := h.storage.CreateUser(user); err != nil {
showError(err) showError(err)
return return
} }
h.renderLayout(w, "register-success", map[string]interface{}{ if h.cfg.ManualRegistration {
"name": user.Name, h.renderLayout(w, "register-success", map[string]interface{}{
"email": user.SignupEmail, "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)
}
} }
} }

View file

@ -1,28 +1,207 @@
package handler package handler
import ( import (
"bytes"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"github.com/fogleman/gg" "github.com/fogleman/gg"
"github.com/golang/freetype" "github.com/golang/freetype"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"golang.org/x/image/draw"
"golang.org/x/image/font" "golang.org/x/image/font"
_ "golang.org/x/image/font/gofont/gobold"
"golang.org/x/image/font/gofont/goregular" "golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
"html/template" "html/template"
"image" "image"
"image/color" "image/color"
"image/draw"
"image/jpeg"
"io"
"os"
"status/model" "status/model"
"strconv" "strconv"
"strings"
"time" "time"
//"image/png" //"image/png"
"net/http" "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) { func (h *Handler) showManageView(w http.ResponseWriter, r *http.Request) {
logged, err := h.sess.Get(r) logged, err := h.sess.Get(r)
if err != nil { if err != nil {
@ -220,6 +399,52 @@ func (h *Handler) showUserStatusBadgeView(w http.ResponseWriter, r *http.Request
dc.EncodePNG(w) 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) { func (h *Handler) showUserStatusImageView(w http.ResponseWriter, r *http.Request) {
user := mux.Vars(r)["user"] user := mux.Vars(r)["user"]
if !h.storage.UserExists(user) { if !h.storage.UserExists(user) {