Merge pull request #277 from garyburd/import

Import garyburd/talksapp and garyburd/lintapp
diff --git a/lintapp/README.md b/lintapp/README.md
new file mode 100644
index 0000000..0edff47
--- /dev/null
+++ b/lintapp/README.md
@@ -0,0 +1,11 @@
+lintapp
+=======
+
+This directory contains the source for [go-lint.appspot.com](http://go-lint.appspot.com).
+
+Development Environment Setup
+-----------------------------
+
+- Copy config.go.template to config.go and edit the file as described in the comments.
+- Install Go App Engine SDK 
+- Run the server using the dev_appserver command.
diff --git a/lintapp/app.yaml b/lintapp/app.yaml
new file mode 100644
index 0000000..9894f1b
--- /dev/null
+++ b/lintapp/app.yaml
@@ -0,0 +1,16 @@
+application: go-lint
+version: 1
+runtime: go
+api_version: go1
+
+handlers:
+- url: /favicon\.ico
+  static_files: assets/favicon.ico
+  upload: assets/favicon\.ico
+
+- url: /robots\.txt
+  static_files: assets/robots.txt
+  upload: assets/robots\.txt
+
+- url: /.*
+  script: _go_app
diff --git a/lintapp/assets/favicon.ico b/lintapp/assets/favicon.ico
new file mode 100644
index 0000000..f19c04d
--- /dev/null
+++ b/lintapp/assets/favicon.ico
Binary files differ
diff --git a/lintapp/assets/robots.txt b/lintapp/assets/robots.txt
new file mode 100644
index 0000000..6ffbc30
--- /dev/null
+++ b/lintapp/assets/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Disallow: /
+
diff --git a/lintapp/assets/templates/common.html b/lintapp/assets/templates/common.html
new file mode 100644
index 0000000..b494dd8
--- /dev/null
+++ b/lintapp/assets/templates/common.html
@@ -0,0 +1,9 @@
+{{define "commonHead"}}
+  <meta charset="utf-8" />
+  <link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.3.0/base-min.css">
+  <style>body { padding: 15px; }</style> 
+{{end}}
+
+{{define "commonFooter"}}
+<p><a href="/">Home</a> | <a href="mailto:{{contactEmail}}">Feedback</a> | <a href="https://github.com/golang/gddo/issues">Website Issues</a>
+{{end}}
diff --git a/lintapp/assets/templates/error.html b/lintapp/assets/templates/error.html
new file mode 100644
index 0000000..4ad7724
--- /dev/null
+++ b/lintapp/assets/templates/error.html
@@ -0,0 +1,10 @@
+{{define "ROOT"}}
+<!DOCTYPE html>
+<html> 
+<head> 
+  {{template "commonHead"}}
+  <title>{{.}}</title>
+<html><body>
+    <p>{{.}}
+</body></html>
+{{end}}
diff --git a/lintapp/assets/templates/index.html b/lintapp/assets/templates/index.html
new file mode 100644
index 0000000..9f3d18f
--- /dev/null
+++ b/lintapp/assets/templates/index.html
@@ -0,0 +1,20 @@
+{{define "ROOT"}}
+<!DOCTYPE html>
+<html> 
+<head> 
+  {{template "commonHead"}}
+  <title>go-lint</title>
+</head>
+<body>
+  <h3>Go Lint</h3>
+  <p>Go Lint lints <a href="http://golang.org/">Go</a> source files on GitHub,
+  Bitbucket and Google Project Hosting using the <a
+    href="https://github.com/golang/lint">lint package</a>.
+  <form method="POST" action="/-/refresh">
+    <input type="text" size=60 name="importPath" autofocus="autofocus" placeholder="Package import path">
+    <input value="Lint" type="submit">
+  </form>
+  {{template "commonFooter"}}
+</body>
+</html>
+{{end}}
diff --git a/lintapp/assets/templates/package.html b/lintapp/assets/templates/package.html
new file mode 100644
index 0000000..57f4a86
--- /dev/null
+++ b/lintapp/assets/templates/package.html
@@ -0,0 +1,21 @@
+{{define "ROOT"}}
+<!DOCTYPE html>
+<html> 
+<head> 
+  {{template "commonHead"}}
+  <title>Lint {{.Path}}</title>
+</head>
+<body>
+  <h3>Lint for {{if .URL}}<a href="{{.URL}}">{{.Path}}<a/>{{else}}{{.Path}}{{end}}</h3>
+  <form method="POST" action="/-/refresh">
+    <input type="hidden" name="importPath" value="{{.Path}}">
+    This report was generated {{.Updated|timeago}}. <input type="submit" value="Refresh">
+  </form>
+  {{range $f := .Files}}{{range .Problems}}
+    <p>{{if .Line}}<a href="{{printf $.LineFmt $f.URL .Line}}" title="{{.LineText}}">{{$f.Name}}:{{.Line}}</a>{{else}}{{$f.Name}}{{end}}: 
+      {{.Text}}
+      {{if .Link}} <a href="{{.Link}}">☞</a>{{end}}
+  {{end}}{{end}}
+  {{template "commonFooter"}}
+</body></html>
+{{end}}
diff --git a/lintapp/config.go.template b/lintapp/config.go.template
new file mode 100644
index 0000000..e3579fa
--- /dev/null
+++ b/lintapp/config.go.template
@@ -0,0 +1,10 @@
+package lintapp
+
+func init() {
+	// Register an application at https://github.com/settings/applications/new
+	// and enter the client ID and client secret here.
+	gitHubCredentials = "client_id=<id>&client_secret=<secret>"
+
+	// Set contact email for /-/bot.html
+	contactEmail = "example@example.com"
+}
diff --git a/lintapp/main.go b/lintapp/main.go
new file mode 100644
index 0000000..b969d5b
--- /dev/null
+++ b/lintapp/main.go
@@ -0,0 +1,313 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd.
+
+// Package lintapp implements the go-lint.appspot.com server.
+package lintapp
+
+import (
+	"bytes"
+	"encoding/gob"
+	"fmt"
+	"html/template"
+	"net/http"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"appengine"
+	"appengine/datastore"
+	"appengine/urlfetch"
+
+	"github.com/golang/gddo/gosrc"
+	"github.com/golang/lint"
+)
+
+func init() {
+	http.Handle("/", handlerFunc(serveRoot))
+	http.Handle("/-/bot", handlerFunc(serveBot))
+	http.Handle("/-/refresh", handlerFunc(serveRefresh))
+}
+
+var (
+	contactEmail    = "unknown@example.com"
+	homeTemplate    = parseTemplate("common.html", "index.html")
+	packageTemplate = parseTemplate("common.html", "package.html")
+	errorTemplate   = parseTemplate("common.html", "error.html")
+	templateFuncs   = template.FuncMap{
+		"timeago":      timeagoFn,
+		"contactEmail": contactEmailFn,
+	}
+	gitHubCredentials = ""
+)
+
+func parseTemplate(fnames ...string) *template.Template {
+	paths := make([]string, len(fnames))
+	for i := range fnames {
+		paths[i] = filepath.Join("assets/templates", fnames[i])
+	}
+	t, err := template.New("").Funcs(templateFuncs).ParseFiles(paths...)
+	if err != nil {
+		panic(err)
+	}
+	t = t.Lookup("ROOT")
+	if t == nil {
+		panic(fmt.Sprintf("ROOT template not found in %v", fnames))
+	}
+	return t
+}
+
+func contactEmailFn() string {
+	return contactEmail
+}
+
+func timeagoFn(t time.Time) string {
+	d := time.Since(t)
+	switch {
+	case d < time.Second:
+		return "just now"
+	case d < 2*time.Second:
+		return "one second ago"
+	case d < time.Minute:
+		return fmt.Sprintf("%d seconds ago", d/time.Second)
+	case d < 2*time.Minute:
+		return "one minute ago"
+	case d < time.Hour:
+		return fmt.Sprintf("%d minutes ago", d/time.Minute)
+	case d < 2*time.Hour:
+		return "one hour ago"
+	case d < 48*time.Hour:
+		return fmt.Sprintf("%d hours ago", d/time.Hour)
+	default:
+		return fmt.Sprintf("%d days ago", d/(time.Hour*24))
+	}
+}
+
+func writeResponse(w http.ResponseWriter, status int, t *template.Template, v interface{}) error {
+	var buf bytes.Buffer
+	if err := t.Execute(&buf, v); err != nil {
+		return err
+	}
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+	w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
+	w.WriteHeader(status)
+	_, err := w.Write(buf.Bytes())
+	return err
+}
+
+func writeErrorResponse(w http.ResponseWriter, status int) error {
+	return writeResponse(w, status, errorTemplate, http.StatusText(status))
+}
+
+type transport struct {
+	rt http.RoundTripper
+	ua string
+}
+
+func (t transport) RoundTrip(r *http.Request) (*http.Response, error) {
+	r.Header.Set("User-Agent", t.ua)
+	if r.URL.Host == "api.github.com" && gitHubCredentials != "" {
+		if r.URL.RawQuery == "" {
+			r.URL.RawQuery = gitHubCredentials
+		} else {
+			r.URL.RawQuery += "&" + gitHubCredentials
+		}
+	}
+	return t.rt.RoundTrip(r)
+}
+
+func httpClient(r *http.Request) *http.Client {
+	c := appengine.NewContext(r)
+	return &http.Client{
+		Transport: &transport{
+			rt: &urlfetch.Transport{Context: c, Deadline: 10 * time.Second},
+			ua: fmt.Sprintf("%s (+http://%s/-/bot)", appengine.AppID(c), r.Host),
+		},
+	}
+}
+
+const version = 1
+
+type storePackage struct {
+	Data    []byte
+	Version int
+}
+
+type lintPackage struct {
+	Files   []*lintFile
+	Path    string
+	Updated time.Time
+	LineFmt string
+	URL     string
+}
+
+type lintFile struct {
+	Name     string
+	Problems []*lintProblem
+	URL      string
+}
+
+type lintProblem struct {
+	Line       int
+	Text       string
+	LineText   string
+	Confidence float64
+	Link       string
+}
+
+func putPackage(c appengine.Context, importPath string, pkg *lintPackage) error {
+	var buf bytes.Buffer
+	if err := gob.NewEncoder(&buf).Encode(pkg); err != nil {
+		return err
+	}
+	_, err := datastore.Put(c,
+		datastore.NewKey(c, "Package", importPath, 0, nil),
+		&storePackage{Data: buf.Bytes(), Version: version})
+	return err
+}
+
+func getPackage(c appengine.Context, importPath string) (*lintPackage, error) {
+	var spkg storePackage
+	if err := datastore.Get(c, datastore.NewKey(c, "Package", importPath, 0, nil), &spkg); err != nil {
+		if err == datastore.ErrNoSuchEntity {
+			err = nil
+		}
+		return nil, err
+	}
+	if spkg.Version != version {
+		return nil, nil
+	}
+	var pkg lintPackage
+	if err := gob.NewDecoder(bytes.NewReader(spkg.Data)).Decode(&pkg); err != nil {
+		return nil, err
+	}
+	return &pkg, nil
+}
+
+func runLint(r *http.Request, importPath string) (*lintPackage, error) {
+	dir, err := gosrc.Get(httpClient(r), importPath, "")
+	if err != nil {
+		return nil, err
+	}
+
+	pkg := lintPackage{
+		Path:    importPath,
+		Updated: time.Now(),
+		LineFmt: dir.LineFmt,
+		URL:     dir.BrowseURL,
+	}
+	linter := lint.Linter{}
+	for _, f := range dir.Files {
+		if !strings.HasSuffix(f.Name, ".go") {
+			continue
+		}
+		problems, err := linter.Lint(f.Name, f.Data)
+		if err == nil && len(problems) == 0 {
+			continue
+		}
+		file := lintFile{Name: f.Name, URL: f.BrowseURL}
+		if err != nil {
+			file.Problems = []*lintProblem{{Text: err.Error()}}
+		} else {
+			for _, p := range problems {
+				file.Problems = append(file.Problems, &lintProblem{
+					Line:       p.Position.Line,
+					Text:       p.Text,
+					LineText:   p.LineText,
+					Confidence: p.Confidence,
+					Link:       p.Link,
+				})
+			}
+		}
+		if len(file.Problems) > 0 {
+			pkg.Files = append(pkg.Files, &file)
+		}
+	}
+
+	if err := putPackage(appengine.NewContext(r), importPath, &pkg); err != nil {
+		return nil, err
+	}
+
+	return &pkg, nil
+}
+
+func filterByConfidence(r *http.Request, pkg *lintPackage) {
+	minConfidence, err := strconv.ParseFloat(r.FormValue("minConfidence"), 64)
+	if err != nil {
+		minConfidence = 0.8
+	}
+	for _, f := range pkg.Files {
+		j := 0
+		for i := range f.Problems {
+			if f.Problems[i].Confidence >= minConfidence {
+				f.Problems[j] = f.Problems[i]
+				j += 1
+			}
+		}
+		f.Problems = f.Problems[:j]
+	}
+}
+
+type handlerFunc func(http.ResponseWriter, *http.Request) error
+
+func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	c := appengine.NewContext(r)
+	err := f(w, r)
+	if err == nil {
+		return
+	} else if gosrc.IsNotFound(err) {
+		writeErrorResponse(w, 404)
+	} else if e, ok := err.(*gosrc.RemoteError); ok {
+		c.Infof("Remote error %s: %v", e.Host, e)
+		writeResponse(w, 500, errorTemplate, fmt.Sprintf("Error accessing %s.", e.Host))
+	} else if err != nil {
+		c.Errorf("Internal error %v", err)
+		writeErrorResponse(w, 500)
+	}
+}
+
+func serveRoot(w http.ResponseWriter, r *http.Request) error {
+	switch {
+	case r.Method != "GET" && r.Method != "HEAD":
+		return writeErrorResponse(w, 405)
+	case r.URL.Path == "/":
+		return writeResponse(w, 200, homeTemplate, nil)
+	default:
+		importPath := r.URL.Path[1:]
+		if !gosrc.IsValidPath(importPath) {
+			return gosrc.NotFoundError{Message: "bad path"}
+		}
+		c := appengine.NewContext(r)
+		pkg, err := getPackage(c, importPath)
+		if pkg == nil && err == nil {
+			pkg, err = runLint(r, importPath)
+		}
+		if err != nil {
+			return err
+		}
+		filterByConfidence(r, pkg)
+		return writeResponse(w, 200, packageTemplate, pkg)
+	}
+}
+
+func serveRefresh(w http.ResponseWriter, r *http.Request) error {
+	if r.Method != "POST" {
+		return writeErrorResponse(w, 405)
+	}
+	importPath := r.FormValue("importPath")
+	pkg, err := runLint(r, importPath)
+	if err != nil {
+		return err
+	}
+	http.Redirect(w, r, "/"+pkg.Path, 301)
+	return nil
+}
+
+func serveBot(w http.ResponseWriter, r *http.Request) error {
+	c := appengine.NewContext(r)
+	_, err := fmt.Fprintf(w, "Contact %s for help with the %s bot.", contactEmail, appengine.AppID(c))
+	return err
+}
diff --git a/talksapp/.gitignore b/talksapp/.gitignore
new file mode 100644
index 0000000..0026861
--- /dev/null
+++ b/talksapp/.gitignore
@@ -0,0 +1,22 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
diff --git a/talksapp/README.md b/talksapp/README.md
new file mode 100644
index 0000000..5e77c6f
--- /dev/null
+++ b/talksapp/README.md
@@ -0,0 +1,12 @@
+talksapp
+========
+
+This directory contains the source for [go-talks.appspot.com](http://go-talks.appspot.com).
+
+Development Environment Setup
+-----------------------------
+
+- Copy config.go.template to config.go and edit the file as described in the comments.
+- Install Go App Engine SDK 
+- $ sh setup.sh 
+- Run the server using the dev_appserver command.
diff --git a/talksapp/app.yaml b/talksapp/app.yaml
new file mode 100644
index 0000000..2e9b1d6
--- /dev/null
+++ b/talksapp/app.yaml
@@ -0,0 +1,19 @@
+application: go-talks
+version: 1
+runtime: go
+api_version: go1
+
+handlers:
+- url: /robots\.txt
+  static_files: assets/robots.txt
+  upload: assets/robots.txt
+- url: /favicon\.ico
+  static_files: present/static/favicon.ico
+  upload: present/static/favicon.ico
+- url: /static
+  static_dir: present/static
+- url: /play\.js
+  static_files: present/play.js
+  upload: present/play.js
+- url: /.*
+  script: _go_app
diff --git a/talksapp/assets/home.article b/talksapp/assets/home.article
new file mode 100644
index 0000000..21d5078
--- /dev/null
+++ b/talksapp/assets/home.article
@@ -0,0 +1,27 @@
+go-talks.appspot.org
+
+Francesc Campoy (maintainer)
+@francesc
+
+Gary Burd
+@gburd
+
+* Introduction
+
+This site plays slide presentations and articles stored on GitHub in the 
+[[http://godoc.org/code.google.com/p/go.tools/present][present format]].
+
+The syntax for URLs is:
+
+    http://go-talks.appspot.com/github.com/owner/project/file.ext
+    http://go-talks.appspot.com/github.com/owner/project/sub/directory/file.ext
+
+The supported file extensions (.ext) are .slide and .article.
+
+The .html command is not supported.
+
+This page is [[/github.com/golang/gddo/talksapp/assets/home.article][an article]].
+
+* Feedback
+
+Report bugs and request features using the [[https://github.com/golang/gddo/issues/new][GitHub Issue Tracker]]. 
diff --git a/talksapp/assets/robots.txt b/talksapp/assets/robots.txt
new file mode 100644
index 0000000..1f53798
--- /dev/null
+++ b/talksapp/assets/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/talksapp/config.go.template b/talksapp/config.go.template
new file mode 100644
index 0000000..0dd1796
--- /dev/null
+++ b/talksapp/config.go.template
@@ -0,0 +1,10 @@
+package talksapp
+
+func init() {
+    // Register an application at https://github.com/settings/applications/new
+    // and enter the client ID and client secret here.
+    gitHubCredentials = "client_id=<id>&client_secret=<secret>"
+
+    // Set contact email for /bot.html
+    contactEmail = "example@example.com"
+}
diff --git a/talksapp/main.go b/talksapp/main.go
new file mode 100644
index 0000000..5ed7dbb
--- /dev/null
+++ b/talksapp/main.go
@@ -0,0 +1,249 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd.
+
+// Package talksapp implements the go-talks.appspot.com server.
+package talksapp
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"html/template"
+	"io"
+	"net/http"
+	"os"
+	"path"
+	"time"
+
+	"appengine"
+	"appengine/memcache"
+	"appengine/urlfetch"
+
+	"code.google.com/p/go.tools/present"
+	"github.com/golang/gddo/gosrc"
+)
+
+var (
+	presentTemplates = map[string]*template.Template{
+		".article": parsePresentTemplate("article.tmpl"),
+		".slide":   parsePresentTemplate("slides.tmpl"),
+	}
+	homeArticle       = loadHomeArticle()
+	contactEmail      = "unknown@example.com"
+	gitHubCredentials = ""
+)
+
+func init() {
+	http.Handle("/", handlerFunc(serveRoot))
+	http.Handle("/compile", handlerFunc(serveCompile))
+	http.Handle("/bot.html", handlerFunc(serveBot))
+	present.PlayEnabled = true
+}
+
+func playable(c present.Code) bool {
+	return present.PlayEnabled && c.Play && c.Ext == ".go"
+}
+
+func parsePresentTemplate(name string) *template.Template {
+	t := present.Template()
+	t = t.Funcs(template.FuncMap{"playable": playable})
+	if _, err := t.ParseFiles("present/templates/"+name, "present/templates/action.tmpl"); err != nil {
+		panic(err)
+	}
+	t = t.Lookup("root")
+	if t == nil {
+		panic("root template not found for " + name)
+	}
+	return t
+}
+
+func loadHomeArticle() []byte {
+	const fname = "assets/home.article"
+	f, err := os.Open(fname)
+	if err != nil {
+		panic(err)
+	}
+	defer f.Close()
+	doc, err := present.Parse(f, fname, 0)
+	if err != nil {
+		panic(err)
+	}
+	var buf bytes.Buffer
+	if err := renderPresentation(&buf, fname, doc); err != nil {
+		panic(err)
+	}
+	return buf.Bytes()
+}
+
+func renderPresentation(w io.Writer, fname string, doc *present.Doc) error {
+	t := presentTemplates[path.Ext(fname)]
+	if t == nil {
+		return errors.New("unknown template extension")
+	}
+	data := struct {
+		*present.Doc
+		Template    *template.Template
+		PlayEnabled bool
+	}{
+		doc,
+		t,
+		true,
+	}
+	return t.Execute(w, &data)
+}
+
+type presFileNotFoundError string
+
+func (s presFileNotFoundError) Error() string { return fmt.Sprintf("File %s not found.", string(s)) }
+
+func writeHTMLHeader(w http.ResponseWriter, status int) {
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+	w.WriteHeader(status)
+}
+
+func writeTextHeader(w http.ResponseWriter, status int) {
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	w.WriteHeader(status)
+}
+
+type transport struct {
+	rt http.RoundTripper
+	ua string
+}
+
+func (t transport) RoundTrip(r *http.Request) (*http.Response, error) {
+	r.Header.Set("User-Agent", t.ua)
+	if r.URL.Host == "api.github.com" && gitHubCredentials != "" {
+		if r.URL.RawQuery == "" {
+			r.URL.RawQuery = gitHubCredentials
+		} else {
+			r.URL.RawQuery += "&" + gitHubCredentials
+		}
+	}
+	return t.rt.RoundTrip(r)
+}
+
+func httpClient(r *http.Request) *http.Client {
+	c := appengine.NewContext(r)
+	return &http.Client{
+		Transport: &transport{
+			rt: &urlfetch.Transport{Context: c, Deadline: 10 * time.Second},
+			ua: fmt.Sprintf("%s (+http://%s/bot.html)", appengine.AppID(c), r.Host),
+		},
+	}
+}
+
+type handlerFunc func(http.ResponseWriter, *http.Request) error
+
+func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	c := appengine.NewContext(r)
+	err := f(w, r)
+	if err == nil {
+		return
+	} else if gosrc.IsNotFound(err) {
+		writeTextHeader(w, 400)
+		io.WriteString(w, "Not Found.")
+	} else if e, ok := err.(*gosrc.RemoteError); ok {
+		writeTextHeader(w, 500)
+		fmt.Fprintf(w, "Error accessing %s.\n%v", e.Host, e)
+		c.Infof("Remote error %s: %v", e.Host, e)
+	} else if e, ok := err.(presFileNotFoundError); ok {
+		writeTextHeader(w, 200)
+		io.WriteString(w, e.Error())
+	} else if err != nil {
+		writeTextHeader(w, 500)
+		io.WriteString(w, "Internal server error.")
+		c.Errorf("Internal error %v", err)
+	}
+}
+
+func serveRoot(w http.ResponseWriter, r *http.Request) error {
+	switch {
+	case r.Method != "GET" && r.Method != "HEAD":
+		writeTextHeader(w, 405)
+		_, err := io.WriteString(w, "Method not supported.")
+		return err
+	case r.URL.Path == "/":
+		writeHTMLHeader(w, 200)
+		_, err := w.Write(homeArticle)
+		return err
+	default:
+		return servePresentation(w, r)
+	}
+}
+
+func servePresentation(w http.ResponseWriter, r *http.Request) error {
+	c := appengine.NewContext(r)
+	importPath := r.URL.Path[1:]
+
+	item, err := memcache.Get(c, importPath)
+	if err == nil {
+		writeHTMLHeader(w, 200)
+		w.Write(item.Value)
+		return nil
+	} else if err != memcache.ErrCacheMiss {
+		return err
+	}
+
+	c.Infof("Fetching presentation %s.", importPath)
+	pres, err := gosrc.GetPresentation(httpClient(r), importPath)
+	if err != nil {
+		return err
+	}
+
+	ctx := &present.Context{
+		ReadFile: func(name string) ([]byte, error) {
+			if p, ok := pres.Files[name]; ok {
+				return p, nil
+			}
+			return nil, presFileNotFoundError(name)
+		},
+	}
+
+	doc, err := ctx.Parse(bytes.NewReader(pres.Files[pres.Filename]), pres.Filename, 0)
+	if err != nil {
+		return err
+	}
+
+	var buf bytes.Buffer
+	if err := renderPresentation(&buf, importPath, doc); err != nil {
+		return err
+	}
+
+	if err := memcache.Add(c, &memcache.Item{
+		Key:        importPath,
+		Value:      buf.Bytes(),
+		Expiration: time.Hour,
+	}); err != nil {
+		return err
+	}
+
+	writeHTMLHeader(w, 200)
+	_, err = w.Write(buf.Bytes())
+	return err
+}
+
+func serveCompile(w http.ResponseWriter, r *http.Request) error {
+	client := urlfetch.Client(appengine.NewContext(r))
+	if err := r.ParseForm(); err != nil {
+		return err
+	}
+	resp, err := client.PostForm("http://play.golang.org/compile", r.Form)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
+	_, err = io.Copy(w, resp.Body)
+	return err
+}
+
+func serveBot(w http.ResponseWriter, r *http.Request) error {
+	c := appengine.NewContext(r)
+	writeTextHeader(w, 200)
+	_, err := fmt.Fprintf(w, "Contact %s for help with the %s bot.", contactEmail, appengine.AppID(c))
+	return err
+}
diff --git a/talksapp/setup.sh b/talksapp/setup.sh
new file mode 100755
index 0000000..bbac4a7
--- /dev/null
+++ b/talksapp/setup.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+go get golang.org/x/tools/cmd/present
+go get golang.org/x/tools/godoc
+present=`go list -f '{{.Dir}}' golang.org/x/tools/cmd/present`
+godoc=`go list -f '{{.Dir}}' golang.org/x/tools/godoc`
+mkdir -p present
+
+(cat $godoc/static/jquery.js $godoc/static/playground.js $godoc/static/play.js && echo "initPlayground(new HTTPTransport());") > present/play.js
+
+cd ./present
+for i in templates static
+do
+    ln -is $present/$i
+done