This is my new blog CMS https://jlelse.blog
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.

193 lines
5.0 KiB

2 months ago
3 weeks ago
3 months ago
2 months ago
2 months ago
2 months ago
3 months ago
3 weeks ago
  1. package main
  2. import (
  3. "bytes"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "log"
  9. "net/http"
  10. "net/url"
  11. "os"
  12. "strings"
  13. "github.com/PuerkitoBio/goquery"
  14. "github.com/joncrlsn/dque"
  15. "github.com/thoas/go-funk"
  16. "willnorris.com/go/microformats"
  17. )
  18. var wmQueue *dque.DQue
  19. func wmMentionBuilder() interface{} {
  20. return &mention{}
  21. }
  22. func initWebmentionQueue() (err error) {
  23. queuePath := "queues"
  24. if _, err := os.Stat(queuePath); os.IsNotExist(err) {
  25. if err := os.Mkdir(queuePath, 0755); err != nil {
  26. return err
  27. }
  28. } else if err != nil {
  29. return err
  30. }
  31. wmQueue, err = dque.NewOrOpen("webmention", queuePath, 5, wmMentionBuilder)
  32. if err != nil {
  33. return err
  34. }
  35. startWebmentionQueue()
  36. return nil
  37. }
  38. func startWebmentionQueue() {
  39. go func() {
  40. for {
  41. if i, err := wmQueue.PeekBlock(); err == nil {
  42. if i == nil {
  43. // Empty request
  44. _, _ = wmQueue.Dequeue()
  45. continue
  46. }
  47. if m, ok := i.(*mention); ok {
  48. err = m.verifyMention()
  49. if err != nil {
  50. log.Println(fmt.Sprintf("Failed to verify webmention from %s to %s: %s", m.Source, m.Target, err.Error()))
  51. }
  52. _, _ = wmQueue.Dequeue()
  53. } else {
  54. // Invalid type
  55. _, _ = wmQueue.Dequeue()
  56. }
  57. }
  58. }
  59. }()
  60. }
  61. func queueMention(m *mention) error {
  62. if wm := appConfig.Webmention; wm != nil && wm.DisableReceiving {
  63. return errors.New("webmention receiving disabled")
  64. }
  65. return wmQueue.Enqueue(m)
  66. }
  67. func (m *mention) verifyMention() error {
  68. req, err := http.NewRequest(http.MethodGet, m.Source, nil)
  69. if err != nil {
  70. return err
  71. }
  72. req.Header.Set(userAgent, appUserAgent)
  73. if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) {
  74. // Set authentication
  75. c, _ := createTokenCookie()
  76. req.AddCookie(c)
  77. }
  78. resp, err := appHttpClient.Do(req)
  79. if err != nil {
  80. return err
  81. }
  82. err = m.verifyReader(resp.Body)
  83. _ = resp.Body.Close()
  84. if err != nil {
  85. _, err := appDbExec("delete from webmentions where source = @source and target = @target", sql.Named("source", m.Source), sql.Named("target", m.Target))
  86. return err
  87. }
  88. if len(m.Content) > 500 {
  89. m.Content = m.Content[0:497] + "…"
  90. }
  91. if len(m.Title) > 60 {
  92. m.Title = m.Title[0:57] + "…"
  93. }
  94. newStatus := webmentionStatusVerified
  95. if webmentionExists(m.Source, m.Target) {
  96. _, err = appDbExec("update webmentions set status = @status, title = @title, content = @content, author = @author where source = @source and target = @target",
  97. sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author), sql.Named("source", m.Source), sql.Named("target", m.Target))
  98. } else {
  99. _, err = appDbExec("insert into webmentions (source, target, created, status, title, content, author) values (@source, @target, @created, @status, @title, @content, @author)",
  100. sql.Named("source", m.Source), sql.Named("target", m.Target), sql.Named("created", m.Created), sql.Named("status", newStatus), sql.Named("title", m.Title), sql.Named("content", m.Content), sql.Named("author", m.Author))
  101. sendNotification(fmt.Sprintf("New webmention from %s to %s", m.Source, m.Target))
  102. }
  103. return err
  104. }
  105. func (m *mention) verifyReader(body io.Reader) error {
  106. var linksBuffer, gqBuffer, mfBuffer bytes.Buffer
  107. if _, err := io.Copy(io.MultiWriter(&linksBuffer, &gqBuffer, &mfBuffer), body); err != nil {
  108. return err
  109. }
  110. // Check if source mentions target
  111. links, err := allLinksFromHTML(&linksBuffer, m.Source)
  112. if err != nil {
  113. return err
  114. }
  115. if _, hasLink := funk.FindString(links, func(s string) bool {
  116. return unescapedPath(s) == unescapedPath(m.Target)
  117. }); !hasLink {
  118. return errors.New("target not found in source")
  119. }
  120. // Set title
  121. doc, err := goquery.NewDocumentFromReader(&gqBuffer)
  122. if err != nil {
  123. return err
  124. }
  125. if title := doc.Find("title"); title != nil {
  126. m.Title = title.Text()
  127. }
  128. // Fill mention attributes
  129. sourceURL, err := url.Parse(m.Source)
  130. if err != nil {
  131. return err
  132. }
  133. m.fillFromData(microformats.Parse(&mfBuffer, sourceURL))
  134. return nil
  135. }
  136. func (m *mention) fillFromData(mf *microformats.Data) {
  137. for _, i := range mf.Items {
  138. m.fill(i)
  139. }
  140. }
  141. func (m *mention) fill(mf *microformats.Microformat) bool {
  142. if mfHasType(mf, "h-entry") {
  143. if name, ok := mf.Properties["name"]; ok && len(name) > 0 {
  144. if title, ok := name[0].(string); ok {
  145. m.Title = title
  146. }
  147. }
  148. if contents, ok := mf.Properties["content"]; ok && len(contents) > 0 {
  149. if content, ok := contents[0].(map[string]string); ok {
  150. if contentValue, ok := content["value"]; ok {
  151. m.Content = contentValue
  152. }
  153. }
  154. }
  155. if authors, ok := mf.Properties["author"]; ok && len(authors) > 0 {
  156. if author, ok := authors[0].(*microformats.Microformat); ok {
  157. if names, ok := author.Properties["name"]; ok && len(names) > 0 {
  158. if name, ok := names[0].(string); ok {
  159. m.Author = name
  160. }
  161. }
  162. }
  163. }
  164. return true
  165. } else if len(mf.Children) > 0 {
  166. for _, mfc := range mf.Children {
  167. if m.fill(mfc) {
  168. return true
  169. }
  170. }
  171. }
  172. return false
  173. }
  174. func mfHasType(mf *microformats.Microformat, typ string) bool {
  175. for _, t := range mf.Type {
  176. if typ == t {
  177. return true
  178. }
  179. }
  180. return false
  181. }