You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

253 rivejä
6.4 KiB

  1. // Copyright 2013 The Go Authors. All rights reserved.
  2. //
  3. // Use of this source code is governed by a BSD-style
  4. // license that can be found in the LICENSE file or at
  5. // https://developers.google.com/open-source/licenses/bsd.
  6. // Package talksapp implements the go-talks.appspot.com server.
  7. package talksapp
  8. import (
  9. "bytes"
  10. "errors"
  11. "fmt"
  12. "html/template"
  13. "io"
  14. "net/http"
  15. "os"
  16. "path"
  17. "time"
  18. "google.golang.org/appengine"
  19. "google.golang.org/appengine/log"
  20. "google.golang.org/appengine/memcache"
  21. "google.golang.org/appengine/urlfetch"
  22. "github.com/golang/gddo/gosrc"
  23. "github.com/golang/gddo/httputil"
  24. "golang.org/x/net/context"
  25. "golang.org/x/tools/present"
  26. )
  27. var (
  28. presentTemplates = map[string]*template.Template{
  29. ".article": parsePresentTemplate("article.tmpl"),
  30. ".slide": parsePresentTemplate("slides.tmpl"),
  31. }
  32. homeArticle = loadHomeArticle()
  33. contactEmail = "golang-dev@googlegroups.com"
  34. // used for mocking in tests
  35. getPresentation = gosrc.GetPresentation
  36. playCompileURL = "https://play.golang.org/compile"
  37. )
  38. func init() {
  39. http.Handle("/", handlerFunc(serveRoot))
  40. http.Handle("/compile", handlerFunc(serveCompile))
  41. http.Handle("/bot.html", handlerFunc(serveBot))
  42. present.PlayEnabled = true
  43. if s := os.Getenv("CONTACT_EMAIL"); s != "" {
  44. contactEmail = s
  45. }
  46. if appengine.IsDevAppServer() {
  47. return
  48. }
  49. github := httputil.NewAuthTransportFromEnvironment(nil)
  50. if github.Token == "" || github.ClientID == "" || github.ClientSecret == "" {
  51. panic("missing GitHub metadata, follow the instructions on README.md")
  52. }
  53. }
  54. func playable(c present.Code) bool {
  55. return present.PlayEnabled && c.Play && c.Ext == ".go"
  56. }
  57. func parsePresentTemplate(name string) *template.Template {
  58. t := present.Template()
  59. t = t.Funcs(template.FuncMap{"playable": playable})
  60. if _, err := t.ParseFiles("present/templates/"+name, "present/templates/action.tmpl"); err != nil {
  61. panic(err)
  62. }
  63. t = t.Lookup("root")
  64. if t == nil {
  65. panic("root template not found for " + name)
  66. }
  67. return t
  68. }
  69. func loadHomeArticle() []byte {
  70. const fname = "assets/home.article"
  71. f, err := os.Open(fname)
  72. if err != nil {
  73. panic(err)
  74. }
  75. defer f.Close()
  76. doc, err := present.Parse(f, fname, 0)
  77. if err != nil {
  78. panic(err)
  79. }
  80. var buf bytes.Buffer
  81. if err := renderPresentation(&buf, fname, doc); err != nil {
  82. panic(err)
  83. }
  84. return buf.Bytes()
  85. }
  86. func renderPresentation(w io.Writer, fname string, doc *present.Doc) error {
  87. t := presentTemplates[path.Ext(fname)]
  88. if t == nil {
  89. return errors.New("unknown template extension")
  90. }
  91. data := struct {
  92. *present.Doc
  93. Template *template.Template
  94. PlayEnabled bool
  95. NotesEnabled bool
  96. }{doc, t, true, true}
  97. return t.Execute(w, &data)
  98. }
  99. type presFileNotFoundError string
  100. func (s presFileNotFoundError) Error() string { return fmt.Sprintf("File %s not found.", string(s)) }
  101. func writeHTMLHeader(w http.ResponseWriter, status int) {
  102. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  103. w.WriteHeader(status)
  104. }
  105. func writeTextHeader(w http.ResponseWriter, status int) {
  106. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  107. w.WriteHeader(status)
  108. }
  109. func httpClient(r *http.Request) *http.Client {
  110. ctx, _ := context.WithTimeout(appengine.NewContext(r), 10*time.Second)
  111. github := httputil.NewAuthTransportFromEnvironment(nil)
  112. return &http.Client{
  113. Transport: &httputil.AuthTransport{
  114. Token: github.Token,
  115. ClientID: github.ClientID,
  116. ClientSecret: github.ClientSecret,
  117. Base: &urlfetch.Transport{Context: ctx},
  118. UserAgent: fmt.Sprintf("%s (+http://%s/-/bot)", appengine.AppID(ctx), r.Host),
  119. },
  120. }
  121. }
  122. type handlerFunc func(http.ResponseWriter, *http.Request) error
  123. func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  124. ctx := appengine.NewContext(r)
  125. err := f(w, r)
  126. if err == nil {
  127. return
  128. } else if gosrc.IsNotFound(err) {
  129. writeTextHeader(w, 400)
  130. io.WriteString(w, "Not Found.")
  131. } else if e, ok := err.(*gosrc.RemoteError); ok {
  132. writeTextHeader(w, 500)
  133. fmt.Fprintf(w, "Error accessing %s.\n%v", e.Host, e)
  134. log.Infof(ctx, "Remote error %s: %v", e.Host, e)
  135. } else if e, ok := err.(presFileNotFoundError); ok {
  136. writeTextHeader(w, 200)
  137. io.WriteString(w, e.Error())
  138. } else if err != nil {
  139. writeTextHeader(w, 500)
  140. io.WriteString(w, "Internal server error.")
  141. log.Errorf(ctx, "Internal error %v", err)
  142. }
  143. }
  144. func serveRoot(w http.ResponseWriter, r *http.Request) error {
  145. switch {
  146. case r.Method != "GET" && r.Method != "HEAD":
  147. writeTextHeader(w, 405)
  148. _, err := io.WriteString(w, "Method not supported.")
  149. return err
  150. case r.URL.Path == "/":
  151. writeHTMLHeader(w, 200)
  152. _, err := w.Write(homeArticle)
  153. return err
  154. default:
  155. return servePresentation(w, r)
  156. }
  157. }
  158. func servePresentation(w http.ResponseWriter, r *http.Request) error {
  159. ctx := appengine.NewContext(r)
  160. importPath := r.URL.Path[1:]
  161. item, err := memcache.Get(ctx, importPath)
  162. if err == nil {
  163. writeHTMLHeader(w, 200)
  164. w.Write(item.Value)
  165. return nil
  166. } else if err != memcache.ErrCacheMiss {
  167. log.Errorf(ctx, "Could not get item from Memcache: %v", err)
  168. }
  169. log.Infof(ctx, "Fetching presentation %s.", importPath)
  170. pres, err := getPresentation(httpClient(r), importPath)
  171. if err != nil {
  172. return err
  173. }
  174. parser := &present.Context{
  175. ReadFile: func(name string) ([]byte, error) {
  176. if p, ok := pres.Files[name]; ok {
  177. return p, nil
  178. }
  179. return nil, presFileNotFoundError(name)
  180. },
  181. }
  182. doc, err := parser.Parse(bytes.NewReader(pres.Files[pres.Filename]), pres.Filename, 0)
  183. if err != nil {
  184. return err
  185. }
  186. var buf bytes.Buffer
  187. if err := renderPresentation(&buf, importPath, doc); err != nil {
  188. return err
  189. }
  190. if err := memcache.Add(ctx, &memcache.Item{
  191. Key: importPath,
  192. Value: buf.Bytes(),
  193. Expiration: time.Hour,
  194. }); err != nil {
  195. log.Errorf(ctx, "Could not cache presentation %s: %v", importPath, err)
  196. return nil
  197. }
  198. writeHTMLHeader(w, 200)
  199. _, err = w.Write(buf.Bytes())
  200. return err
  201. }
  202. func serveCompile(w http.ResponseWriter, r *http.Request) error {
  203. client := urlfetch.Client(appengine.NewContext(r))
  204. if err := r.ParseForm(); err != nil {
  205. return err
  206. }
  207. resp, err := client.PostForm(playCompileURL, r.Form)
  208. if err != nil {
  209. return err
  210. }
  211. defer resp.Body.Close()
  212. w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
  213. _, err = io.Copy(w, resp.Body)
  214. return err
  215. }
  216. func serveBot(w http.ResponseWriter, r *http.Request) error {
  217. ctx := appengine.NewContext(r)
  218. writeTextHeader(w, 200)
  219. _, err := fmt.Fprintf(w, "Contact %s for help with the %s bot.", contactEmail, appengine.AppID(ctx))
  220. return err
  221. }