Skip to content

Commit 49b70e3

Browse files
committed
Extract library, use gorilla HTTP. Closes #64. Closes #66
1 parent ad587d8 commit 49b70e3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+375
-7212
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ _bin/
66
pkg/
77
src/tinderizer/tinderizer
88
src/code.google.com/
9+
*.key
10+
*.crt

.godir

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
tinderizer
1+
ForrestFire

.ruby-version

-1
This file was deleted.

.rvmrc

-2
This file was deleted.

app.go

+367
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"github.com/darkhelmet/ForrestFire/bookmarklet"
10+
"github.com/darkhelmet/ForrestFire/looper"
11+
"github.com/darkhelmet/env"
12+
"github.com/darkhelmet/postmark"
13+
"github.com/darkhelmet/stat"
14+
"github.com/darkhelmet/tinderizer"
15+
J "github.com/darkhelmet/tinderizer/job"
16+
"github.com/darkhelmet/webutil"
17+
"github.com/gorilla/mux"
18+
"html/template"
19+
"io"
20+
"log"
21+
"net/http"
22+
"os"
23+
"os/exec"
24+
"os/signal"
25+
"regexp"
26+
"runtime"
27+
"strings"
28+
)
29+
30+
const (
31+
HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin"
32+
QueueSize = 10
33+
34+
HttpRedirect = "http.redirect"
35+
36+
SubmitOld = "submit.old"
37+
SubmitSuccess = "submit.success"
38+
SubmitError = "submit.error"
39+
SubmitEmail = "submit.email"
40+
PostmarkBounce = "postmark.bounce"
41+
42+
ContentType = "Content-Type"
43+
Location = "Location"
44+
ContentTypeHTML = "text/html; charset=utf-8"
45+
ContentTypePlain = "text/plain; charset=utf-8"
46+
ContentTypeJavascript = "application/javascript; charset=utf-8"
47+
ContentTypeJSON = "application/json; charset=utf-8"
48+
)
49+
50+
var (
51+
doneRegex = regexp.MustCompile("(?i:done|failed|limited|invalid|error|sorry)")
52+
port = env.IntDefault("PORT", 8080)
53+
canonicalHost = env.StringDefaultF("CANONICAL_HOST", func() string { return fmt.Sprintf("tinderizer.dev:%d", port) })
54+
logger = log.New(os.Stdout, "[server] ", env.IntDefault("LOG_FLAGS", log.LstdFlags|log.Lmicroseconds))
55+
templates = template.Must(template.ParseGlob("views/*.tmpl"))
56+
app *tinderizer.App
57+
)
58+
59+
type JSON map[string]interface{}
60+
61+
func init() {
62+
stat.Prefix = "[Tinderizer]"
63+
64+
rdbToken := env.String("READABILITY_TOKEN")
65+
pmToken := env.String("POSTMARK_TOKEN")
66+
from := env.String("FROM")
67+
binary, _ := exec.LookPath(fmt.Sprintf("kindlegen-%s", runtime.GOOS))
68+
69+
tlogger := log.New(os.Stdout, "[tinderizer] ", env.IntDefault("LOG_FLAGS", log.LstdFlags|log.Lmicroseconds))
70+
71+
app = tinderizer.New(rdbToken, pmToken, from, binary, tlogger)
72+
app.Run(QueueSize)
73+
74+
// TODO: handle SIGINT
75+
c := make(chan os.Signal, 1)
76+
signal.Notify(c, os.Interrupt)
77+
go shutdown(c)
78+
}
79+
80+
func shutdown(c chan os.Signal) {
81+
<-c
82+
logger.Println("shutting down...")
83+
app.Shutdown()
84+
os.Exit(0)
85+
}
86+
87+
type Response struct {
88+
http.ResponseWriter
89+
}
90+
91+
func (r Response) HTML() http.ResponseWriter {
92+
h := r.Header()
93+
h.Set(ContentType, ContentTypeHTML)
94+
r.WriteHeader(http.StatusOK)
95+
return r.ResponseWriter
96+
}
97+
98+
func (r Response) Javascript() http.ResponseWriter {
99+
h := r.Header()
100+
h.Set(ContentType, ContentTypeJavascript)
101+
r.WriteHeader(http.StatusOK)
102+
return r.ResponseWriter
103+
}
104+
105+
func (r Response) JSON() http.ResponseWriter {
106+
h := r.Header()
107+
h.Set(ContentType, ContentTypeJSON)
108+
r.WriteHeader(http.StatusOK)
109+
return r.ResponseWriter
110+
}
111+
112+
func (r Response) Plain() http.ResponseWriter {
113+
h := r.Header()
114+
h.Set(ContentType, ContentTypePlain)
115+
r.WriteHeader(http.StatusOK)
116+
return r.ResponseWriter
117+
}
118+
119+
func H(f func(Response, *http.Request)) func(http.ResponseWriter, *http.Request) {
120+
return func(w http.ResponseWriter, req *http.Request) {
121+
f(Response{w}, req)
122+
}
123+
}
124+
125+
func RenderPage(w io.Writer, page, host string) error {
126+
var buffer bytes.Buffer
127+
if err := templates.ExecuteTemplate(&buffer, page, nil); err != nil {
128+
return err
129+
}
130+
return templates.ExecuteTemplate(w, "layout.tmpl", JSON{
131+
"host": host,
132+
"yield": template.HTML(buffer.String()),
133+
})
134+
}
135+
136+
func HandleBookmarklet(res Response, req *http.Request) {
137+
w := res.Javascript()
138+
w.Write(bookmarklet.Javascript())
139+
}
140+
141+
func PageHandler(res Response, req *http.Request) {
142+
w := res.HTML()
143+
vars := mux.Vars(req)
144+
tmpl := fmt.Sprintf("%s.tmpl", vars["page"])
145+
if err := RenderPage(w, tmpl, canonicalHost); err != nil {
146+
logger.Printf("failed rendering page: %s", err)
147+
}
148+
}
149+
150+
func ChunkHandler(res Response, req *http.Request) {
151+
w := res.HTML()
152+
vars := mux.Vars(req)
153+
tmpl := fmt.Sprintf("%s.tmpl", vars["chunk"])
154+
if err := templates.ExecuteTemplate(w, tmpl, nil); err != nil {
155+
logger.Printf("failed rendering chunk: %s", err)
156+
}
157+
}
158+
159+
func HomeHandler(res Response, req *http.Request) {
160+
w := res.HTML()
161+
if err := RenderPage(w, "index.tmpl", canonicalHost); err != nil {
162+
logger.Printf("failed rendering index: %s", err)
163+
}
164+
}
165+
166+
type EmailHeader struct {
167+
Name, Value string
168+
}
169+
170+
type EmailToFull struct {
171+
Email, Name string
172+
}
173+
174+
type InboundEmail struct {
175+
From, To, CC, ReplyTo, Subject string
176+
ToFull []EmailToFull
177+
MessageId, Date, MailboxHash string
178+
TextBody, HtmlBody string
179+
Tag string
180+
Headers []EmailHeader
181+
}
182+
183+
func ExtractParts(e *InboundEmail) (email string, url string, err error) {
184+
parts := strings.Split(e.ToFull[0].Email, "@")
185+
if len(parts) == 0 {
186+
return "", "", errors.New("failed splitting email on '@'")
187+
}
188+
emailBytes, err := hex.DecodeString(parts[0])
189+
if err != nil {
190+
return "", "", fmt.Errorf("failed decoding email from hex: %s", err)
191+
}
192+
email = string(emailBytes)
193+
buffer := bytes.NewBufferString(strings.TrimSpace(e.TextBody))
194+
url, err = buffer.ReadString('\n')
195+
if len(url) == 0 && err != nil {
196+
return "", "", fmt.Errorf("failed reading line from email body: %s", err)
197+
}
198+
err = nil
199+
url = strings.TrimSpace(url)
200+
return
201+
}
202+
203+
func InboundHandler(res Response, req *http.Request) {
204+
decoder := json.NewDecoder(req.Body)
205+
var inbound InboundEmail
206+
err := decoder.Decode(&inbound)
207+
if err != nil {
208+
logger.Printf("failed decoding inbound email: %s", err)
209+
} else {
210+
email, url, err := ExtractParts(&inbound)
211+
if err != nil {
212+
logger.Printf("failed extracting needed parts from email: %s", err)
213+
} else {
214+
logger.Printf("email submission of %#v to %#v", url, email)
215+
if job, err := J.New(email, url, ""); err == nil {
216+
app.Queue(*job)
217+
stat.Count(SubmitEmail, 1)
218+
}
219+
}
220+
}
221+
w := res.Plain()
222+
io.WriteString(w, "ok")
223+
}
224+
225+
func BounceHandler(res Response, req *http.Request) {
226+
decoder := json.NewDecoder(req.Body)
227+
var bounce postmark.Bounce
228+
err := decoder.Decode(&bounce)
229+
if err != nil {
230+
logger.Printf("failed decoding bounce: %s", err)
231+
return
232+
}
233+
234+
if looper.AlreadyResent(bounce.MessageID, bounce.Email) {
235+
logger.Printf("skipping resend of message ID %s", bounce.MessageID)
236+
} else {
237+
err = app.Reactivate(bounce)
238+
if err != nil {
239+
logger.Printf("failed reactivating bounce: %s", err)
240+
return
241+
}
242+
uri := looper.MarkResent(bounce.MessageID, bounce.Email)
243+
if job, err := J.New(bounce.Email, uri, ""); err != nil {
244+
logger.Printf("bounced email failed to validate as a job: %s", err)
245+
} else {
246+
app.Queue(*job)
247+
logger.Printf("resending %#v to %#v after bounce", uri, bounce.Email)
248+
stat.Count(PostmarkBounce, 1)
249+
}
250+
}
251+
w := res.Plain()
252+
io.WriteString(w, "ok")
253+
}
254+
255+
type Submission struct {
256+
Url string `json:"url"`
257+
Email string `json:"email"`
258+
Content string `json:"content"`
259+
}
260+
261+
func SubmitHandler(res Response, req *http.Request) {
262+
decoder := json.NewDecoder(req.Body)
263+
var submission Submission
264+
err := decoder.Decode(&submission)
265+
if err != nil {
266+
logger.Printf("failed decoding submission: %s", err)
267+
}
268+
logger.Printf("submission of %#v to %#v", submission.Url, submission.Email)
269+
270+
w := res.JSON()
271+
encoder := json.NewEncoder(w)
272+
Submit(encoder, submission.Email, submission.Url, submission.Content)
273+
}
274+
275+
func OldSubmitHandler(res Response, req *http.Request) {
276+
w := res.JSON()
277+
encoder := json.NewEncoder(w)
278+
Submit(encoder, req.URL.Query().Get("email"), req.URL.Query().Get("url"), "")
279+
stat.Count(SubmitOld, 1)
280+
}
281+
282+
func HandleSubmitError(encoder *json.Encoder, err error) {
283+
stat.Count(SubmitError, 1)
284+
encoder.Encode(JSON{"message": err.Error()})
285+
}
286+
287+
func Submit(encoder *json.Encoder, email, url, content string) {
288+
job, err := J.New(email, url, content)
289+
if err != nil {
290+
HandleSubmitError(encoder, err)
291+
return
292+
}
293+
294+
job.Progress("Working...")
295+
app.Queue(*job)
296+
encoder.Encode(JSON{
297+
"message": "Submitted! Hang tight...",
298+
"id": job.Key.String(),
299+
})
300+
stat.Count(SubmitSuccess, 1)
301+
}
302+
303+
func StatusHandler(res Response, req *http.Request) {
304+
vars := mux.Vars(req)
305+
w := res.JSON()
306+
message := "No job with that ID found."
307+
done := true
308+
if v, err := app.Status(vars["id"]); err == nil {
309+
message = v
310+
done = doneRegex.MatchString(message)
311+
}
312+
encoder := json.NewEncoder(w)
313+
encoder.Encode(JSON{
314+
"message": message,
315+
"done": done,
316+
})
317+
}
318+
319+
type CanonicalHostHandler struct {
320+
http.Handler
321+
}
322+
323+
func (c CanonicalHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
324+
prefix := strings.HasPrefix(r.URL.Path, "/ajax")
325+
get := r.Method == "GET"
326+
chost := r.Host == canonicalHost
327+
if prefix || !get || chost {
328+
c.Handler.ServeHTTP(w, r)
329+
} else {
330+
stat.Count(HttpRedirect, 1)
331+
r.URL.Host = canonicalHost
332+
r.URL.Scheme = "http:"
333+
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
334+
}
335+
}
336+
337+
func main() {
338+
submitRoute := "/ajax/submit.json"
339+
statusRoute := "/ajax/status/{id:[^.]+}.json"
340+
341+
r := mux.NewRouter()
342+
r.HandleFunc("/", H(HomeHandler)).Methods("GET")
343+
r.HandleFunc("/inbound", H(InboundHandler)).Methods("POST")
344+
r.HandleFunc("/bounce", H(BounceHandler)).Methods("POST")
345+
r.HandleFunc("/static/bookmarklet.js", H(HandleBookmarklet)).Methods("GET")
346+
r.HandleFunc("/{page:(faq|bugs|contact)}", H(PageHandler)).Methods("GET")
347+
r.HandleFunc("/{chunk:(firefox|safari|chrome|ie|ios|kindle-email)}", H(ChunkHandler)).Methods("GET")
348+
r.HandleFunc(submitRoute, H(SubmitHandler)).Methods("POST")
349+
r.HandleFunc(submitRoute, H(OldSubmitHandler)).Methods("GET")
350+
r.HandleFunc(statusRoute, H(StatusHandler)).Methods("GET")
351+
r.PathPrefix("/").Handler(http.FileServer(http.Dir("public")))
352+
353+
var handler http.Handler = r
354+
handler = webutil.AlwaysHeaderHandler{handler, http.Header{HeaderAccessControlAllowOrigin: {"*"}}}
355+
handler = webutil.GzipHandler{handler}
356+
handler = webutil.LoggerHandler{handler, logger}
357+
handler = CanonicalHostHandler{handler}
358+
handler = webutil.EnsureRequestBodyClosedHandler{handler}
359+
360+
http.Handle("/", handler)
361+
362+
logger.Printf("Tinderizer is starting on 0.0.0.0:%d", port)
363+
err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", port), nil)
364+
if err != nil {
365+
logger.Fatalf("failed to serve: %s", err)
366+
}
367+
}
File renamed without changes.

src/bookmarklet/bookmarklet.go renamed to bookmarklet/bookmarklet.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import (
1515
)
1616

1717
const (
18-
CoffeeScriptPath = "src/bookmarklet/bookmarklet.coffee"
19-
LessPath = "src/bookmarklet/bookmarklet.less"
18+
CoffeeScriptPath = "bookmarklet/bookmarklet.coffee"
19+
LessPath = "bookmarklet/bookmarklet.less"
2020
)
2121

2222
var (
File renamed without changes.

0 commit comments

Comments
 (0)