diff --git a/INSTALL.md b/INSTALL.md
index 035a857..86d9705 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -32,11 +32,12 @@ Place a `config.json` file in the same directory that looks like this: (below ar
}
```
-- baseURL, with trailing slash: base access point, used in approval/admin panel
-- adminEmail, the e-mail address to send notificaions to. If absent, will not send out mails. **uses 127.0.0.1:25 postfix** at the moment.
-- port, host: http server params
-- token, allowedWebmentionSources: see below, used for authentication
-- blacklist/whitelist: domains from which we do (NOT) send to or accept mentions from.
+- `baseURL`, with trailing slash: base access point, used in approval/admin panel
+- `adminEmail`, the e-mail address to send notificaions to. If absent, will not send out mails. **uses 127.0.0.1:25 postfix** at the moment.
+- `port`, host: http server params
+- `token`: see below, used for authentication
+- `blacklist`/`whitelist`: domains from which we do (NOT) send to or accept mentions from. This is usually the domain of the `source` in the receiving mention. **Note**: the blacklist is also used to block outgoing mentions.
+- `allowedWebmentionSources`: your own domains which go-jamming is able to receive mentions from. This is usually domain of the `target` in the receiving mention.
If a config file is missing, or required keys are missing, a warning will be generated and default values will be used instead. See `common/config.go`.
diff --git a/README.md b/README.md
index 3fb67c9..86562e6 100644
--- a/README.md
+++ b/README.md
@@ -168,6 +168,18 @@ Will result in a `200 OK` - that returns XML according to [The W3 pingback XML-R
Happens automatically through `PUT /webmention/:domain/:token`! Links that are discovered as `rel="pingback"` that **do not** already have a webmention link will be processed as XML-RPC requests to be send.
+### 3. Keep track of mentions
+
+#### 3.1 `GET /feed/[domain]/[token]`
+
+To keep track of all incoming mentions, simply subscribe to the above URL for each of your domains using your favorite RSS reader.
+
+Mentions to approve also appear in the feed, together with links to accept or reject them with a single click! (See below, section "mentions in moderation").
+
+#### 3.2 SMTP `localhost`
+
+Messages of incoming mentions can also be sent via e-mail if you have a localhost SMTP server setup. See the `INSTALL.md` for more instructions and config parameters on how to do so.
+
---
## Troubleshooting
diff --git a/app/mf/microformats.go b/app/mf/microformats.go
index 96405c1..b9c3e1e 100644
--- a/app/mf/microformats.go
+++ b/app/mf/microformats.go
@@ -9,7 +9,7 @@ import (
)
const (
- dateFormatWithTimeZone = "2006-01-02T15:04:05-07:00"
+ DateFormatWithTimeZone = "2006-01-02T15:04:05-07:00"
dateFormatWithAbsoluteTimeZone = "2006-01-02T15:04:05-0700"
dateFormatWithTimeZoneSuffixed = "2006-01-02T15:04:05.000Z"
dateFormatWithoutTimeZone = "2006-01-02T15:04:05"
@@ -22,7 +22,7 @@ var (
// This is similar to Hugo's string-to-date casting system
// See https://github.com/spf13/cast/blob/master/caste.go
supportedFormats = []string{
- dateFormatWithTimeZone,
+ DateFormatWithTimeZone,
dateFormatWithAbsoluteTimeZone,
dateFormatWithTimeZoneSuffixed,
dateFormatWithSecondsWithoutTimeZone,
@@ -81,6 +81,10 @@ type IndiewebData struct {
Target string `json:"target"`
}
+func (id *IndiewebData) PublishedDate() time.Time {
+ return common.ToTime(id.Published, DateFormatWithTimeZone)
+}
+
func (id *IndiewebData) AsMention() Mention {
return Mention{
Source: id.Source,
@@ -93,7 +97,7 @@ func (id *IndiewebData) IsEmpty() bool {
}
func PublishedNow() string {
- return common.Now().UTC().Format(dateFormatWithTimeZone)
+ return common.Now().UTC().Format(DateFormatWithTimeZone)
}
func shorten(txt string) string {
@@ -185,7 +189,7 @@ func Published(hEntry *microformats.Microformat) string {
if err != nil {
continue
}
- return formatted.Format(dateFormatWithTimeZone)
+ return formatted.Format(DateFormatWithTimeZone)
}
return PublishedNow()
diff --git a/app/routes.go b/app/routes.go
index 2d15ae6..6753928 100644
--- a/app/routes.go
+++ b/app/routes.go
@@ -5,6 +5,7 @@ import (
"brainbaking.com/go-jamming/app/index"
"brainbaking.com/go-jamming/app/pictures"
"brainbaking.com/go-jamming/app/pingback"
+ "brainbaking.com/go-jamming/app/rss"
"brainbaking.com/go-jamming/app/webmention"
)
@@ -20,12 +21,14 @@ func (s *server) routes() {
s.router.HandleFunc("/pingback", pingback.HandlePost(c, db)).Methods("POST")
s.router.HandleFunc("/webmention", webmention.HandlePost(c, db)).Methods("POST")
+ s.router.HandleFunc("/feed/{domain}/{token}", s.domainAndTokenOnly(rss.HandleGet(c, db))).Methods("GET")
+
s.router.HandleFunc("/webmention/{domain}/{token}", s.domainAndTokenOnly(webmention.HandleGet(db))).Methods("GET")
s.router.HandleFunc("/webmention/{domain}/{token}", s.domainAndTokenOnly(webmention.HandlePut(c, db))).Methods("PUT")
s.router.HandleFunc("/webmention/{domain}/{token}", s.domainAndTokenOnly(webmention.HandleDelete(db))).Methods("DELETE")
- s.router.HandleFunc("/admin/{token}", s.authorizedOnly(admin.HandleGet(c, db))).Methods("GET")
+ s.router.HandleFunc("/admin/{token}", s.tokenOnly(admin.HandleGet(c, db))).Methods("GET")
s.router.HandleFunc("/admin/{domain}/{token}", s.domainAndTokenOnly(admin.HandleGetToApprove(db))).Methods("GET")
- s.router.HandleFunc("/admin/approve/{token}/{key}", s.authorizedOnly(admin.HandleApprove(c, db))).Methods("GET")
- s.router.HandleFunc("/admin/reject/{token}/{key}", s.authorizedOnly(admin.HandleReject(c, db))).Methods("GET")
+ s.router.HandleFunc("/admin/approve/{token}/{key}", s.tokenOnly(admin.HandleApprove(c, db))).Methods("GET")
+ s.router.HandleFunc("/admin/reject/{token}/{key}", s.tokenOnly(admin.HandleReject(c, db))).Methods("GET")
}
diff --git a/app/rss/handler.go b/app/rss/handler.go
new file mode 100644
index 0000000..fef7bc5
--- /dev/null
+++ b/app/rss/handler.go
@@ -0,0 +1,92 @@
+package rss
+
+import (
+ "brainbaking.com/go-jamming/app/mf"
+ "brainbaking.com/go-jamming/common"
+ "brainbaking.com/go-jamming/db"
+ "fmt"
+ "github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
+ "net/http"
+ "sort"
+ "text/template"
+ "time"
+)
+
+import _ "embed"
+
+const (
+ MaxRssItems = 50
+)
+
+//go:embed mentionsrss.xml
+var mentionsrssTemplate []byte
+
+type RssMentions struct {
+ Domain string
+ Date time.Time
+ Items []*RssMentionItem
+}
+
+type RssMentionItem struct {
+ ApproveURL string
+ RejectURL string
+ Data *mf.IndiewebData
+}
+
+func asTemplate(name string, data []byte) *template.Template {
+ tmpl, err := template.New(name).Parse(string(data))
+ if err != nil {
+ log.Fatal().Err(err).Str("name", name).Msg("Template invalid")
+ }
+ return tmpl
+}
+
+func HandleGet(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
+ tmpl := asTemplate("mentionsRss", mentionsrssTemplate)
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ domain := mux.Vars(r)["domain"]
+
+ mentions := getLatestMentions(domain, repo, c)
+ err := tmpl.Execute(w, RssMentions{
+ Items: mentions,
+ Date: time.Now(),
+ Domain: domain,
+ })
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Error().Err(err).Msg("Unable to fill in dashboard template")
+ }
+ }
+}
+
+func getLatestMentions(domain string, repo db.MentionRepo, c *common.Config) []*RssMentionItem {
+ toMod := repo.GetAllToModerate(domain).Data
+ all := repo.GetAll(domain).Data
+
+ var data []*RssMentionItem
+ for _, v := range toMod {
+ wm := v.AsMention()
+ data = append(data, &RssMentionItem{
+ Data: v,
+ ApproveURL: fmt.Sprintf("%sadmin/approve/%s/%s", c.BaseURL, c.Token, wm.Key()),
+ RejectURL: fmt.Sprintf("%sadmin/reject/%s/%s", c.BaseURL, c.Token, wm.Key()),
+ })
+ }
+ for _, v := range all {
+ data = append(data, &RssMentionItem{
+ Data: v,
+ })
+ }
+
+ // TODO this date is the published date, not the webmention received date!
+ // This means it "might" disappear after the cutoff point in the RSS feed, and we don't store a received timestamp
+ sort.Slice(data, func(i, j int) bool {
+ return data[i].Data.PublishedDate().After(data[j].Data.PublishedDate())
+ })
+ if len(data) > MaxRssItems {
+ return data[0:MaxRssItems]
+ }
+ return data
+}
diff --git a/app/rss/handler_test.go b/app/rss/handler_test.go
new file mode 100644
index 0000000..1377177
--- /dev/null
+++ b/app/rss/handler_test.go
@@ -0,0 +1,75 @@
+package rss
+
+import (
+ "brainbaking.com/go-jamming/app/mf"
+ "brainbaking.com/go-jamming/common"
+ "brainbaking.com/go-jamming/db"
+ "fmt"
+ "github.com/gorilla/mux"
+ "github.com/stretchr/testify/assert"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+)
+
+var (
+ cnf = &common.Config{
+ BaseURL: "http://localhost:1337/",
+ Port: 1337,
+ Token: "miauwkes",
+ AllowedWebmentionSources: []string{"brainbaking.com"},
+ Blacklist: []string{},
+ Whitelist: []string{"brainbaking.com"},
+ }
+ repo db.MentionRepo
+)
+
+func init() {
+ repo = db.NewMentionRepo(cnf)
+}
+func TestHandleGet(t *testing.T) {
+ wmInMod := mf.Mention{
+ Source: "https://infos.by/markdown-v-nauke/",
+ Target: "https://brainbaking.com/post/2021/02/writing-academic-papers-in-markdown/",
+ }
+ wmApproved := mf.Mention{
+ Source: "https://brainbaking.com/post/2022/04/equality-in-game-credits/",
+ Target: "https://brainbaking.com/",
+ }
+
+ repo.InModeration(wmInMod, &mf.IndiewebData{
+ Source: wmInMod.Source,
+ Target: wmInMod.Target,
+ Name: "inmod1",
+ })
+ repo.Save(wmApproved, &mf.IndiewebData{
+ Source: wmApproved.Source,
+ Target: wmApproved.Target,
+ Name: "approved1",
+ })
+ r := mux.NewRouter()
+ r.HandleFunc("/feed/{domain}/{token}", HandleGet(cnf, repo)).Methods("GET")
+ ts := httptest.NewServer(r)
+
+ t.Cleanup(func() {
+ os.Remove("config.json")
+ ts.Close()
+ db.Purge()
+ })
+
+ client := &http.Client{}
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/feed/%s/%s", ts.URL, cnf.AllowedWebmentionSources[0], cnf.Token), nil)
+
+ resp, err := client.Do(req)
+ assert.NoError(t, err)
+
+ contentBytes, _ := ioutil.ReadAll(resp.Body)
+ content := string(contentBytes)
+ defer resp.Body.Close()
+
+ assert.Contains(t, content, "