M go.mod => go.mod +1 -1
@@ 18,7 18,7 @@ require (
github.com/tdewolff/test v1.0.6 // indirect
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.0.1
- github.com/urfave/cli/v2 v2.1.1 // indirect
+ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect
golang.org/x/exp v0.0.0-20191129062945-2f5052295587
golang.org/x/sync v0.0.0-20190423024810-112230192c58
google.golang.org/api v0.14.0
M go.sum => go.sum +2 -9
@@ 30,8 30,6 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ 123,12 121,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sfreiberg/gotwilio v0.0.0-20191120211240-38187998ae52 h1:eSEqKfQ/c2IiETwCvVyhJfkrv50zgWFSe0Pr6X1n/II=
github.com/sfreiberg/gotwilio v0.0.0-20191120211240-38187998ae52/go.mod h1:dhtsjtHOWmTLjCOyNloce1diOIs9H1mvVmcOG7qmZUc=
-github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ 152,9 146,6 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
-github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
-github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ 163,6 154,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
M internal/api/api.go => internal/api/api.go +102 -28
@@ 7,7 7,9 @@ import (
"net/http"
"net/url"
"os"
+ "path/filepath"
"strconv"
+ "strings"
"time"
"git.evanjon.es/minieggs/errors"
@@ 53,6 55,9 @@ type dataLayer interface {
NoteGetLatest(ctx context.Context, user common.User) (common.Note, error)
NoteGetLatestWithTime(ctx context.Context, user common.User, t time.Duration) (common.Note, error)
NoteCreate(ctx context.Context, user common.User, text string) (common.Note, error)
+ // media
+ MediaCreate(ctx context.Context, user common.User, URL string) (common.Media, error)
+ MediaGet(ctx context.Context, user common.User, URL string) (common.Media, error)
// special gdpr
UserAll(context.Context, common.User) ([]common.Note, error)
UserDel(context.Context, common.User) error
@@ 63,7 68,7 @@ type csvLayer interface {
}
type smsLayer interface {
- Send(number, text string) error
+ Send(number, text, mediaURL string) error
Hook(c *gin.Context) (number, text string, medias []string, err error)
}
@@ 108,34 113,38 @@ func (app App) HookSMS(c *gin.Context) {
return
}
- // Create note for each media.
- eg := errgroup.Group{}
+ if user.Admin() {
+ // Create all medias.
+ eg := errgroup.Group{}
+
+ for _, media := range medias {
+ func(fromURL string) {
+ eg.Go(func() error {
+ url, err := app.file.TwilioUpload(fromURL)
+ if err != nil {
+ return err
+ }
+
+ if _, err := app.data.MediaCreate(c, user, url); err != nil {
+ return err
+ }
- for _, media := range medias {
- func(fromURL string) {
- eg.Go(func() error {
- toURL, err := app.file.TwilioUpload(fromURL)
- if err != nil {
return nil
- }
- if _, err := app.data.NoteCreate(c, user, toURL); err != nil {
- return err
- }
- return nil
- })
- }(media)
- }
+ })
+ }(media)
+ }
- if err := eg.Wait(); err != nil {
- app.error(c, err)
- return
+ if err := eg.Wait(); err != nil {
+ app.errorCLI(c, err)
+ return
+ }
}
// If there is text in the received message we always want to create that as
// the latest note.
if text != "" {
if _, err := app.data.NoteCreate(c, user, text); err != nil {
- app.error(c, err)
+ app.errorCLI(c, err)
return
}
}
@@ 148,15 157,46 @@ func (app App) NoteCreate(c *gin.Context) {
Text string
}
- err := c.Bind(&payload)
+ user, err := app.currentUser(c)
if err != nil {
- app.error(c, err)
+ app.error(c, errors.New("not logged in; or something else terribly wrong"))
return
}
- user, err := app.currentUser(c)
- if err != nil {
- app.error(c, errors.New("not logged in; or something else terribly wrong"))
+ var mediaURL string
+
+ file, header, err := c.Request.FormFile("File")
+ if err == nil {
+ // We have a file! Let's upload it.
+ bytes, err := ioutil.ReadAll(file)
+ if err != nil {
+ app.error(c, err)
+ return
+ }
+
+ url, err := app.file.Upload(bytes, header.Filename)
+ if err != nil {
+ app.error(c, err)
+ return
+ }
+
+ media, err := app.data.MediaCreate(c, user, url)
+ if err != nil {
+ app.error(c, err)
+ return
+ }
+
+ // TODO: Should we really send this as a text? Or send with reg SMS message?
+ // if err := app.sms.Send(user.Phone(), media.Text()); err != nil {
+ // app.error(c, err)
+ // return
+ // }
+
+ mediaURL = fmt.Sprintf("%s?usertoken=%s", media.Text(), user.Token())
+ }
+
+ if err := c.Bind(&payload); err != nil {
+ app.error(c, err)
return
}
@@ 166,7 206,7 @@ func (app App) NoteCreate(c *gin.Context) {
return
}
- if err := app.sms.Send(user.Phone(), note.Text()); err != nil {
+ if err := app.sms.Send(user.Phone(), note.Text(), mediaURL); err != nil {
app.error(c, err)
return
}
@@ 179,6 219,8 @@ func (app App) NoteCreate(c *gin.Context) {
}
func (app App) NoteCreateCLI(c *gin.Context) {
+ // TODO: File support?
+
var payload struct {
Token, Text string
}
@@ 201,7 243,7 @@ func (app App) NoteCreateCLI(c *gin.Context) {
return
}
- if err := app.sms.Send(user.Phone(), note.Text()); err != nil {
+ if err := app.sms.Send(user.Phone(), note.Text(), ""); err != nil {
app.errorCLI(c, err)
return
}
@@ 641,7 683,7 @@ func (app App) UserForgotPassword(c *gin.Context) {
return
}
- err = app.sms.Send(user.Phone(), passwordResetForeword+fmt.Sprintf(app.cfg.resetPassswordLink, tokenID))
+ err = app.sms.Send(user.Phone(), passwordResetForeword+fmt.Sprintf(app.cfg.resetPassswordLink, tokenID), "")
if err != nil {
app.formError(c, common.NewUserForgotFormError(errors.Wrap(err, "failed to send sms")))
return
@@ 702,3 744,35 @@ func (app App) UserForgotPasswordNewPassword(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, "/")
}
+
+func (app App) MediaGet(c *gin.Context) {
+ user, err := app.currentUser(c)
+ if err != nil {
+ // Or try it from query param (this is how Twilio receives image).
+ user, err = app.currentUserFromToken(c, c.Query("usertoken"))
+ if err != nil {
+ app.error(c, errors.New("not logged in; or something else terribly wrong"))
+ return
+ }
+ }
+
+ filename := c.Param("id")
+ id := strings.TrimSuffix(filename, filepath.Ext(filename))
+
+ media, err := app.data.MediaGet(c, user, id)
+ if err != nil {
+ app.error(c, err)
+ return
+ }
+
+ // Raw is the URL on e3's side.
+ bytes, err := app.file.Get(media.Raw())
+ if err != nil {
+ app.error(c, err)
+ return
+ }
+
+ c.Writer.Header().Add("Cache-Control", "maxage=31536000, s-maxage=31536000") // Cache for one year.
+ c.Writer.WriteHeader(http.StatusOK)
+ c.Writer.Write(bytes)
+}
M internal/common/common.go => internal/common/common.go +6 -0
@@ 6,6 6,7 @@ import (
type User interface {
ID() string
+ Admin() bool
Username() string
Phone() string
Token() string /* Stored in session, secret, unique per session. */
@@ 19,9 20,14 @@ type Note interface {
ID() string
Short() string
Text() string
+ Raw() string
Token() string /* Unique per note (i.e. like an ID), only let author see. */
}
+type Media interface {
+ Note
+}
+
// Reset is used for password resets.
type Reset interface {
ID() string
M internal/e3/e3.go => internal/e3/e3.go +34 -7
@@ 6,6 6,7 @@ import (
"io/ioutil"
"mime/multipart"
"net/http"
+ "path/filepath"
"strings"
"git.evanjon.es/minieggs/errors"
@@ 25,7 26,7 @@ func New(username, password string) *E3 {
func (e3 E3) TwilioUpload(fromURL string) (toURL string, err error) {
// Get image from Twilio.
- twilioResp, err := http.Get(fromURL)
+ twilioResp, err := http.Get(strings.TrimSuffix(fromURL, filepath.Ext(fromURL)))
if err != nil {
return "", errors.Wrap(err, "failed to contact twilio servers")
}
@@ 48,8 49,11 @@ func (e3 *E3) Upload(imgBytes []byte, imageName string) (url string, err error)
buf := bytes.Buffer{}
formWriter := multipart.NewWriter(&buf)
formFile, err := formWriter.CreateFormFile("data", imageName)
+ if err != nil {
+ return "", err
+ }
io.Copy(formFile, fileReader)
- defer formWriter.Close()
+ formWriter.Close() // NO DEFER! It messed this up. File won't be sent to e3.
// Upload image to e3.
req, err := http.NewRequest("POST", baseURL, &buf)
@@ 68,17 72,40 @@ func (e3 *E3) Upload(imgBytes []byte, imageName string) (url string, err error)
// Receive final URL and return.
body, err := ioutil.ReadAll(e3Resp.Body)
if err != nil {
- return "", errors.Wrap(err, "failed to read results from e3 object storage")
+ return "", errors.Wrap(err, "failed to read result from e3 object storage")
}
if e3Resp.StatusCode != http.StatusOK {
return "", errors.New(string(body))
}
- // TODO: Proxy this url.
return strings.TrimSpace(string(body)), nil
}
-func (e3 *E3) Get(url string) ([]byte, error) {
- // TODO: Not complete.
- return []byte(""), errors.New("not complete")
+func (e3 *E3) Get(url string) (_image []byte, err error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ err = errors.Wrap(err, "failed to create request to e3 object storage")
+ return
+ }
+ req.SetBasicAuth(e3.username, e3.password)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ err = errors.Wrap(err, "failed to complete request to e3 object storage")
+ return
+ }
+ defer resp.Body.Close()
+
+ // Receive image bytes and return.
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ err = errors.Wrap(err, "failed to read result from e3 object storage")
+ return
+ }
+ if resp.StatusCode != http.StatusOK {
+ err = errors.New(string(body))
+ return
+ }
+
+ return body, nil
}
M internal/fs/fs.go => internal/fs/fs.go +80 -4
@@ 2,8 2,10 @@ package fs
import (
"context"
+ "fmt"
"log"
"os"
+ "path/filepath"
"time"
"cloud.google.com/go/firestore"
@@ 60,6 62,23 @@ func (fs FS) snaptouser(ctx context.Context, doc *firestore.DocumentSnapshot) (c
return &user, nil
}
+func (fs FS) snaptonote(ctx context.Context, doc *firestore.DocumentSnapshot) (common.Note, error) {
+ note := Note{ref: doc.Ref}
+ if err := doc.DataTo(¬e); err != nil {
+ return nil, errors.Wrap(err, "note value corrupted")
+ }
+
+ token, err := fs.sec.TokenCreate(security.TokenMap{"NoteID": note.ID()})
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to create unique token for note")
+ }
+
+ note.token = token
+ note.fs = fs
+
+ return ¬e, nil
+}
+
func (fs FS) itertouser(ctx context.Context, iter *firestore.DocumentIterator) (common.User, error) {
doc, err := iter.Next()
if err != nil {
@@ 158,10 177,10 @@ func (fs FS) NoteGetLatest(ctx context.Context, user common.User) (common.Note,
doc, err := iter.Next()
if err == iterator.Done {
- return nil, nil
+ return nil, errors.New("no note")
}
if err != nil {
- return nil, errors.Wrap(err, "failed to find note")
+ return nil, errors.New("failed to find note")
}
note := Note{ref: doc.Ref}
@@ 194,7 213,7 @@ func (fs FS) NoteGetLatestWithTime(ctx context.Context, user common.User, t time
return nil, nil
}
if err != nil {
- return nil, errors.Wrap(err, "failed to find note")
+ return nil, errors.New("failed to find note")
}
note := Note{ref: doc.Ref}
@@ 257,10 276,11 @@ func (fs FS) NoteGetList(ctx context.Context, user common.User, page, count int)
return ret, hasMore, nil
}
-func (fs FS) NoteCreate(ctx context.Context, user common.User, text string) (common.Note, error) {
+func (fs FS) noteCreate(ctx context.Context, user common.User, text, raw string) (common.Note, error) {
note := Note{
ref: fs.conn.Collection("notes").NewDoc(),
NoteText: text,
+ NoteRaw: raw,
NoteShort: fs.toshort(text),
NoteCreatedAt: time.Now().UTC().Unix(),
UserID: user.ID(),
@@ 282,6 302,10 @@ func (fs FS) NoteCreate(ctx context.Context, user common.User, text string) (com
return ¬e, nil
}
+func (fs FS) NoteCreate(ctx context.Context, user common.User, text string) (common.Note, error) {
+ return fs.noteCreate(ctx, user, text, "")
+}
+
func (fs FS) UserGet(ctx context.Context, token string) (common.User, error) {
claims, err := fs.sec.TokenFrom(token)
if err != nil {
@@ 404,6 428,7 @@ type User struct {
err error
}
+func (user *User) Admin() bool { return user.UserUsername == "minieggs" }
func (user *User) Username() string { return user.UserUsername }
func (user *User) Phone() string { return user.UserPhone }
func (user *User) ID() string { return user.ref.ID }
@@ 468,6 493,7 @@ func (user *User) Save(ctx context.Context) error {
type Note struct {
ref *firestore.DocumentRef
NoteText string
+ NoteRaw string
NoteShort string
NoteCreatedAt int64
@@ 481,5 507,55 @@ type Note struct {
func (Note Note) Short() string { return Note.NoteShort }
func (Note Note) Text() string { return Note.NoteText }
+func (Note Note) Raw() string { return Note.NoteRaw }
func (Note Note) ID() string { return Note.ref.ID }
func (Note Note) Token() string { return Note.token }
+
+// media has the same backend as note. This is on purpose.
+
+type Media struct {
+ Note
+}
+
+func (fs FS) MediaCreate(ctx context.Context, user common.User, fromURL string) (common.Media, error) {
+ if !user.Admin() {
+ return nil, errors.New("you are not allowed to do that")
+ }
+
+ // TODO: Non optimal way to do this. FIX.
+ cMedia, err := fs.noteCreate(ctx, user, fromURL, fromURL)
+ if err != nil {
+ return nil, err
+ }
+
+ media, ok := cMedia.(*Note)
+ if !ok {
+ return nil, errors.New("media has became corrupted")
+ }
+
+ // TODO: Real URL.
+ toURL := fmt.Sprintf("https://smscp.xyz/media/%s%s", media.ID(), filepath.Ext(fromURL))
+ media.NoteText = toURL
+ media.NoteShort = fs.toshort(toURL)
+
+ if _, err := fs.conn.Collection("notes").Doc(media.ID()).Set(ctx, media); err != nil {
+ return nil, err
+ }
+
+ return media, nil
+}
+
+func (fs FS) MediaGet(ctx context.Context, user common.User, id string) (common.Media, error) {
+ if !user.Admin() {
+ return nil, errors.New("you are not allowed to do that")
+ }
+
+ snap, err := fs.conn.Collection("notes").Doc(id).Get(ctx)
+ if err != nil {
+ return nil, errors.New("failed to find media")
+ }
+
+ media, err := fs.snaptonote(ctx, snap)
+
+ return media, err
+}
M internal/sms/twilio/twilio.go => internal/sms/twilio/twilio.go +78 -23
@@ 2,6 2,7 @@ package twilio
import (
"fmt"
+ "mime"
"strings"
"git.evanjon.es/minieggs/errors"
@@ 21,14 22,27 @@ func Default(id, secret, from string) SMS { return SMS{id, secret, from} }
// Send pushes the `text` to `to` via Twilio. May return an error from
// `twilio.SendMMS`.
-func (sms SMS) Send(to, text string) error {
+func (sms SMS) Send(to, text, mediaURL string) error {
twilio := gotwilio.NewTwilioClient(sms.id, sms.secret)
- if _, _, err := twilio.SendMMS(sms.from, to, text, "", "", ""); err != nil {
+ if _, _, err := twilio.SendMMS(sms.from, to, text, mediaURL, "", ""); err != nil {
return errors.Wrap(err, "failed to send message")
}
return nil
}
+func getmedia(fn, ext string) (string, error) {
+ exts, err := mime.ExtensionsByType(ext)
+ if err != nil {
+ return "", err
+ }
+
+ if len(exts) < 1 {
+ return "", errors.New("no mine found")
+ }
+
+ return fmt.Sprintf("%s%s", fn, exts[0]), nil
+}
+
// Hook handles webhooks from Twilio. Used to receive text messages from users.
// Returns user phone number, user text, and potential error from reading and
// parsing payload from Twilio.
@@ 38,51 52,92 @@ func (sms SMS) Hook(c *gin.Context) (_number, _text string, _medias []string, _e
var payload struct {
Body, From, FromCountry string
// Yep, nice API you got there, Twilio.
- MediaUrl0 string
- MediaUrl1 string
- MediaUrl2 string
- MediaUrl3 string
- MediaUrl4 string
- MediaUrl5 string
- MediaUrl6 string
- MediaUrl7 string
- MediaUrl8 string
- MediaUrl9 string
+ MediaContentType0, MediaUrl0 string
+ MediaContentType1, MediaUrl1 string
+ MediaContentType2, MediaUrl2 string
+ MediaContentType3, MediaUrl3 string
+ MediaContentType4, MediaUrl4 string
+ MediaContentType5, MediaUrl5 string
+ MediaContentType6, MediaUrl6 string
+ MediaContentType7, MediaUrl7 string
+ MediaContentType8, MediaUrl8 string
+ MediaContentType9, MediaUrl9 string
}
if err := c.Bind(&payload); err != nil {
return "", "", medias, err
}
- // Annoying.
+ // Annoying. My Lord.
+
if payload.MediaUrl0 != "" {
- medias = append(medias, payload.MediaUrl0)
+ media, err := getmedia(payload.MediaUrl0, payload.MediaContentType0)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl1 != "" {
- medias = append(medias, payload.MediaUrl1)
+ media, err := getmedia(payload.MediaUrl1, payload.MediaContentType1)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl2 != "" {
- medias = append(medias, payload.MediaUrl2)
+ media, err := getmedia(payload.MediaUrl2, payload.MediaContentType2)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl3 != "" {
- medias = append(medias, payload.MediaUrl3)
+ media, err := getmedia(payload.MediaUrl3, payload.MediaContentType3)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl4 != "" {
- medias = append(medias, payload.MediaUrl4)
+ media, err := getmedia(payload.MediaUrl4, payload.MediaContentType4)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl5 != "" {
- medias = append(medias, payload.MediaUrl5)
+ media, err := getmedia(payload.MediaUrl5, payload.MediaContentType5)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl6 != "" {
- medias = append(medias, payload.MediaUrl6)
+ media, err := getmedia(payload.MediaUrl6, payload.MediaContentType6)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl7 != "" {
- medias = append(medias, payload.MediaUrl7)
+ media, err := getmedia(payload.MediaUrl7, payload.MediaContentType7)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl8 != "" {
- medias = append(medias, payload.MediaUrl8)
+ media, err := getmedia(payload.MediaUrl8, payload.MediaContentType8)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
if payload.MediaUrl9 != "" {
- medias = append(medias, payload.MediaUrl9)
+ media, err := getmedia(payload.MediaUrl9, payload.MediaContentType9)
+ if err != nil {
+ return "", "", medias, err
+ }
+ medias = append(medias, media)
}
phone, err := libphonenumber.Parse(payload.From, payload.FromCountry)
M pkg/builder/builder.go => pkg/builder/builder.go +3 -0
@@ 59,6 59,9 @@ func Build() *gin.Engine {
router.POST("/note/create", app.NoteCreate)
router.GET("/note/list/:page", app.NoteListJSON)
+ // media
+ router.GET("/media/:id", app.MediaGet)
+
// cli
router.POST("/cli/user/login", app.UserLoginCLI)
router.POST("/cli/user/create", app.UserCreateCLI)
M web/html/_main.js => web/html/_main.js +22 -1
@@ 14,8 14,29 @@
}
})();
+// for fake and real file input
+(function() {
+ var fake = document.querySelector('input[id="fake-file"]')
+ var real = document.querySelector('input[id="real-file"]')
+ if (!fake || !real) return;
+
+ fake.addEventListener('click', moveInput)
+ fake.addEventListener('focus', moveInput)
+ function moveInput() {
+ fake.blur()
+ real.click()
+ }
+
+ real.addEventListener('input', onInput)
+ real.addEventListener('change', onInput)
+ function onInput() {
+ if (real.files.length > 0) fake.value = real.files[0].name
+ else fake.value = ''
+ }
+})();
+
// form errors, scroll to
-(function () {
+(function() {
// get first form error and scroll to it
window.addEventListener('load', function() {
var input = document.querySelector('input.border-red-500')
M web/html/main.html => web/html/main.html +11 -1
@@ 46,7 46,7 @@
<div class='max-w-4xl mx-auto w-full rounded'>
<div class='bg-white shadow-xl rounded p-5 pt-0'>
- <form id='create' action='/note/create' method='POST' class='py-5'>
+ <form id='create' action='/note/create' method='POST' class='py-5' enctype='multipart/form-data'>
<fieldset>
<legend class='block text-gray-700 text-xl font-bold mb-5'>
Send to phone
@@ 69,6 69,16 @@
class='appearance-none border w-full py-2 px-3
rounded-l text-gray-700 leading-tight focus:outline-none text-md' />
+ {{ if .User.Admin }}
+ <input class="bg-gray-200 hover:bg-gray-300 text-gray-500 hover:text-gray-600
+ font-bold py-2 px-4 text-md cursor-pointer"
+ id='fake-file'
+ value='file'
+ type="button"/>
+
+ <input class='hidden' id='real-file' type=file name=File multiple=false>
+ {{ end }}
+
<input class="bg-gray-200 hover:bg-gray-300 text-gray-500 hover:text-gray-600
font-bold py-2 px-4 text-md cursor-pointer"
id='large'