mail notifier, admin wrappers endpoints, first impl done
This commit is contained in:
parent
2e504eaa65
commit
6cc83620ba
|
@ -1,10 +1,10 @@
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"brainbaking.com/go-jamming/app/mf"
|
|
||||||
"brainbaking.com/go-jamming/common"
|
"brainbaking.com/go-jamming/common"
|
||||||
"brainbaking.com/go-jamming/db"
|
"brainbaking.com/go-jamming/db"
|
||||||
"brainbaking.com/go-jamming/rest"
|
"brainbaking.com/go-jamming/rest"
|
||||||
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -16,32 +16,39 @@ func HandleGet(repo db.MentionRepo) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO validate or not? see webmention.HandlePost
|
|
||||||
// TODO unit tests
|
// TODO unit tests
|
||||||
|
// HandleApprove approves the Mention (by key in URL) and adds to the whitelist.
|
||||||
|
// Returns 200 OK with approved source/target or 404 if key is invalid.
|
||||||
func HandleApprove(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
|
func HandleApprove(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
key := mux.Vars(r)["key"]
|
||||||
wm := mf.Mention{
|
|
||||||
Source: r.FormValue("source"),
|
approved := repo.Approve(key)
|
||||||
Target: r.FormValue("target"),
|
if approved == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.Approve(wm)
|
c.AddToWhitelist(approved.AsMention().SourceDomain())
|
||||||
c.AddToWhitelist(wm.SourceDomain())
|
w.WriteHeader(http.StatusOK)
|
||||||
w.WriteHeader(200)
|
fmt.Fprintf(w, "Approved: %s", approved.AsMention().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleReject rejects the Mention (by key in URL) and adds to the blacklist.
|
||||||
|
// Returns 200 OK with rejected source/target or 404 if key is invalid.
|
||||||
func HandleReject(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
|
func HandleReject(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
key := mux.Vars(r)["key"]
|
||||||
wm := mf.Mention{
|
|
||||||
Source: r.FormValue("source"),
|
rejected := repo.Reject(key)
|
||||||
Target: r.FormValue("target"),
|
if rejected == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.Reject(wm)
|
c.AddToBlacklist(rejected.AsMention().SourceDomain())
|
||||||
c.AddToBlacklist(wm.SourceDomain())
|
w.WriteHeader(http.StatusOK)
|
||||||
w.WriteHeader(200)
|
fmt.Fprintf(w, "Rejected: %s", rejected.AsMention().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,8 @@ func (wm Mention) SourceDomain() string {
|
||||||
// Key returns a unique string representation of the mention for use in storage.
|
// Key returns a unique string representation of the mention for use in storage.
|
||||||
// TODO Profiling indicated that md5() consumes a lot of CPU power, so this could be replaced with db migration.
|
// TODO Profiling indicated that md5() consumes a lot of CPU power, so this could be replaced with db migration.
|
||||||
func (wm Mention) Key() string {
|
func (wm Mention) Key() string {
|
||||||
return fmt.Sprintf("%x", md5.Sum([]byte("source="+wm.Source+",target="+wm.Target)))
|
key := fmt.Sprintf("%x", md5.Sum([]byte("source="+wm.Source+",target="+wm.Target)))
|
||||||
|
return fmt.Sprintf("%s:%s", key, wm.TargetDomain())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm Mention) SourceUrl() *url.URL {
|
func (wm Mention) SourceUrl() *url.URL {
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"brainbaking.com/go-jamming/app/mf"
|
||||||
|
"brainbaking.com/go-jamming/common"
|
||||||
|
"encoding/base64"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"net/mail"
|
||||||
|
"net/smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MailNotifier struct {
|
||||||
|
Conf *common.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendMail is a utility function that sends a mail without authentication to localhost. Tested using postfix.
|
||||||
|
// cheers https://github.com/gadelkareem/go-helpers/blob/master/helpers.go
|
||||||
|
func sendMail(from, subject, body, toName, toAddress string) error {
|
||||||
|
c, err := smtp.Dial("127.0.0.1:25")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
if err = c.Mail(from); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
to := (&mail.Address{toName, toAddress}).String()
|
||||||
|
if err = c.Rcpt(to); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := c.Data()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "To: " + to + "\r\n" +
|
||||||
|
"From: " + from + "\r\n" +
|
||||||
|
"Subject: " + subject + "\r\n" +
|
||||||
|
"Content-Type: text/html; charset=\"UTF-8\"\r\n" +
|
||||||
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
|
"\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
|
||||||
|
|
||||||
|
_, err = w.Write([]byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mn *MailNotifier) NotifyReceived(wm mf.Mention, indieweb *mf.IndiewebData) {
|
||||||
|
err := sendMail(
|
||||||
|
"admin@brainbaking.com",
|
||||||
|
"Webmention in moderation from "+wm.SourceDomain(),
|
||||||
|
BuildNotification(wm, indieweb, mn.Conf),
|
||||||
|
"Go-Jamming User",
|
||||||
|
"wouter@brainbaking.com")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Unable to send notification mail, check localhost postfix settings?")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"brainbaking.com/go-jamming/app/mf"
|
||||||
|
"brainbaking.com/go-jamming/common"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Notifier interface {
|
||||||
|
NotifyReceived(wm mf.Mention, data *mf.IndiewebData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildNotification returns a string representation of the Mention to notify the admin.
|
||||||
|
func BuildNotification(wm mf.Mention, data *mf.IndiewebData, cnf *common.Config) string {
|
||||||
|
enter := "\n"
|
||||||
|
acceptUrl := fmt.Sprintf("%sadmin/approve/%s/%s", cnf.BaseURL, cnf.Token, wm.Key())
|
||||||
|
rejectUrl := fmt.Sprintf("%sadmin/reject/%s/%s", cnf.BaseURL, cnf.Token, wm.Key())
|
||||||
|
|
||||||
|
return fmt.Sprintf("Hi admin, %s%s,A webmention was received: %sSource %s, Target %s%sContent: %s%s%sAccept? %s%sReject? %s%sCheerio, your go-jammin' thing.",
|
||||||
|
enter, enter, enter,
|
||||||
|
wm.Source, wm.Target, enter,
|
||||||
|
data.Content, enter, enter,
|
||||||
|
acceptUrl, enter, rejectUrl, enter)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package notifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"brainbaking.com/go-jamming/app/mf"
|
||||||
|
"brainbaking.com/go-jamming/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildNotification(t *testing.T) {
|
||||||
|
wm := mf.Mention{
|
||||||
|
Source: "https://brainbaking.com/valid-indieweb-source.html",
|
||||||
|
Target: "https://brainbaking.com/valid-indieweb-target.html",
|
||||||
|
}
|
||||||
|
cnf := &common.Config{
|
||||||
|
AllowedWebmentionSources: []string{
|
||||||
|
"brainbaking.com",
|
||||||
|
},
|
||||||
|
BaseURL: "https://jam.brainbaking.com/",
|
||||||
|
Token: "mytoken",
|
||||||
|
Blacklist: []string{},
|
||||||
|
Whitelist: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `Hi admin,
|
||||||
|
|
||||||
|
,A webmention was received:
|
||||||
|
Source https://brainbaking.com/valid-indieweb-source.html, Target https://brainbaking.com/valid-indieweb-target.html
|
||||||
|
Content: somecontent
|
||||||
|
|
||||||
|
Accept? https://jam.brainbaking.com/admin/approve/mytoken/19d462ddff3c3322c662dac3461324bb:brainbaking.com
|
||||||
|
Reject? https://jam.brainbaking.com/admin/reject/mytoken/19d462ddff3c3322c662dac3461324bb:brainbaking.com
|
||||||
|
Cheerio, your go-jammin' thing.`
|
||||||
|
|
||||||
|
result := BuildNotification(wm, &mf.IndiewebData{Content: "somecontent"}, cnf)
|
||||||
|
assert.Equal(t, result, expected)
|
||||||
|
}
|
|
@ -20,11 +20,11 @@ func (s *server) routes() {
|
||||||
s.router.HandleFunc("/pingback", pingback.HandlePost(c, db)).Methods("POST")
|
s.router.HandleFunc("/pingback", pingback.HandlePost(c, db)).Methods("POST")
|
||||||
s.router.HandleFunc("/webmention", webmention.HandlePost(c, db)).Methods("POST")
|
s.router.HandleFunc("/webmention", webmention.HandlePost(c, db)).Methods("POST")
|
||||||
|
|
||||||
s.router.HandleFunc("/webmention/{domain}/{token}", s.authorizedOnly(webmention.HandleGet(db))).Methods("GET")
|
s.router.HandleFunc("/webmention/{domain}/{token}", s.domainAndTokenOnly(webmention.HandleGet(db))).Methods("GET")
|
||||||
s.router.HandleFunc("/webmention/{domain}/{token}", s.authorizedOnly(webmention.HandlePut(c, db))).Methods("PUT")
|
s.router.HandleFunc("/webmention/{domain}/{token}", s.domainAndTokenOnly(webmention.HandlePut(c, db))).Methods("PUT")
|
||||||
s.router.HandleFunc("/webmention/{domain}/{token}", s.authorizedOnly(webmention.HandleDelete(db))).Methods("DELETE")
|
s.router.HandleFunc("/webmention/{domain}/{token}", s.domainAndTokenOnly(webmention.HandleDelete(db))).Methods("DELETE")
|
||||||
|
|
||||||
s.router.HandleFunc("/admin/{domain}/{token}", s.authorizedOnly(admin.HandleGet(db))).Methods("GET")
|
s.router.HandleFunc("/admin/{domain}/{token}", s.domainAndTokenOnly(admin.HandleGet(db))).Methods("GET")
|
||||||
s.router.HandleFunc("/admin/approve/{token}", s.authorizedOnly(admin.HandleApprove(c, db))).Methods("POST")
|
s.router.HandleFunc("/admin/approve/{token}/{key}", s.authorizedOnly(admin.HandleApprove(c, db))).Methods("GET")
|
||||||
s.router.HandleFunc("/admin/reject/{token}", s.authorizedOnly(admin.HandleReject(c, db))).Methods("POST")
|
s.router.HandleFunc("/admin/reject/{token}/{key}", s.authorizedOnly(admin.HandleReject(c, db))).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,25 @@ type server struct {
|
||||||
repo db.MentionRepo
|
repo db.MentionRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *server) domainAndTokenOnly(h http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return s.domainOnly(s.authorizedOnly(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) domainOnly(h http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
if !s.conf.IsAnAllowedDomain(vars["domain"]) {
|
||||||
|
rest.Unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *server) authorizedOnly(h http.HandlerFunc) http.HandlerFunc {
|
func (s *server) authorizedOnly(h http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
if vars["token"] != s.conf.Token || !s.conf.IsAnAllowedDomain(vars["domain"]) {
|
if vars["token"] != s.conf.Token {
|
||||||
rest.Unauthorized(w)
|
rest.Unauthorized(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,13 +34,13 @@ func TestAuthorizedOnlyUnauthorizedWithWrongToken(t *testing.T) {
|
||||||
assert.False(t, passed, "should not have called unauthorized func")
|
assert.False(t, passed, "should not have called unauthorized func")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthorizedOnlyUnauthorizedWithWrongDomain(t *testing.T) {
|
func TestDomainOnlyWithWrongDomain(t *testing.T) {
|
||||||
srv := &server{
|
srv := &server{
|
||||||
conf: conf,
|
conf: conf,
|
||||||
}
|
}
|
||||||
|
|
||||||
passed := false
|
passed := false
|
||||||
handler := srv.authorizedOnly(func(writer http.ResponseWriter, request *http.Request) {
|
handler := srv.domainOnly(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
passed = true
|
passed = true
|
||||||
})
|
})
|
||||||
r, _ := http.NewRequest("PUT", "/whatever", nil)
|
r, _ := http.NewRequest("PUT", "/whatever", nil)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package webmention
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"brainbaking.com/go-jamming/app/mf"
|
"brainbaking.com/go-jamming/app/mf"
|
||||||
|
"brainbaking.com/go-jamming/app/notifier"
|
||||||
"brainbaking.com/go-jamming/app/webmention/recv"
|
"brainbaking.com/go-jamming/app/webmention/recv"
|
||||||
"brainbaking.com/go-jamming/app/webmention/send"
|
"brainbaking.com/go-jamming/app/webmention/send"
|
||||||
"brainbaking.com/go-jamming/db"
|
"brainbaking.com/go-jamming/db"
|
||||||
|
@ -88,6 +89,9 @@ func HandlePost(conf *common.Config, repo db.MentionRepo) http.HandlerFunc {
|
||||||
RestClient: httpClient,
|
RestClient: httpClient,
|
||||||
Conf: conf,
|
Conf: conf,
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
|
Notifier: ¬ifier.MailNotifier{
|
||||||
|
Conf: conf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
go recv.Receive(wm)
|
go recv.Receive(wm)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package recv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"brainbaking.com/go-jamming/app/mf"
|
"brainbaking.com/go-jamming/app/mf"
|
||||||
|
"brainbaking.com/go-jamming/app/notifier"
|
||||||
"brainbaking.com/go-jamming/common"
|
"brainbaking.com/go-jamming/common"
|
||||||
"brainbaking.com/go-jamming/db"
|
"brainbaking.com/go-jamming/db"
|
||||||
"brainbaking.com/go-jamming/rest"
|
"brainbaking.com/go-jamming/rest"
|
||||||
|
@ -16,6 +17,7 @@ import (
|
||||||
|
|
||||||
type Receiver struct {
|
type Receiver struct {
|
||||||
RestClient rest.Client
|
RestClient rest.Client
|
||||||
|
Notifier notifier.Notifier
|
||||||
Conf *common.Config
|
Conf *common.Config
|
||||||
Repo db.MentionRepo
|
Repo db.MentionRepo
|
||||||
}
|
}
|
||||||
|
@ -62,23 +64,30 @@ func (recv *Receiver) processSourceBody(body string, wm mf.Mention) {
|
||||||
indieweb := recv.convertBodyToIndiewebData(body, wm, data)
|
indieweb := recv.convertBodyToIndiewebData(body, wm, data)
|
||||||
recv.processAuthorPicture(indieweb)
|
recv.processAuthorPicture(indieweb)
|
||||||
|
|
||||||
recv.saveMentionToDatabase(wm, indieweb)
|
if recv.Conf.IsWhitelisted(wm.Source) {
|
||||||
|
recv.processWhitelistedMention(wm, indieweb)
|
||||||
|
} else {
|
||||||
|
recv.processMentionInModeration(wm, indieweb)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (recv *Receiver) saveMentionToDatabase(wm mf.Mention, indieweb *mf.IndiewebData) {
|
func (recv *Receiver) processMentionInModeration(wm mf.Mention, indieweb *mf.IndiewebData) {
|
||||||
if recv.Conf.IsWhitelisted(wm.Source) {
|
key, err := recv.Repo.InModeration(wm, indieweb)
|
||||||
key, err := recv.Repo.Save(wm, indieweb)
|
if err != nil {
|
||||||
if err != nil {
|
log.Error().Err(err).Stringer("wm", wm).Msg("Failed to save new mention to in moderation db")
|
||||||
log.Error().Err(err).Stringer("wm", wm).Msg("Failed to save new mention to db")
|
|
||||||
}
|
|
||||||
log.Info().Str("key", key).Msg("OK: Webmention processed, in whitelist.")
|
|
||||||
} else {
|
|
||||||
key, err := recv.Repo.InModeration(wm, indieweb)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Stringer("wm", wm).Msg("Failed to save new mention to in moderation db")
|
|
||||||
}
|
|
||||||
log.Info().Str("key", key).Msg("OK: Webmention processed, in moderation.")
|
|
||||||
}
|
}
|
||||||
|
if recv.Notifier != nil {
|
||||||
|
recv.Notifier.NotifyReceived(wm, indieweb)
|
||||||
|
}
|
||||||
|
log.Info().Str("key", key).Msg("OK: Webmention processed, in moderation.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (recv *Receiver) processWhitelistedMention(wm mf.Mention, indieweb *mf.IndiewebData) {
|
||||||
|
key, err := recv.Repo.Save(wm, indieweb)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Stringer("wm", wm).Msg("Failed to save new mention to db")
|
||||||
|
}
|
||||||
|
log.Info().Str("key", key).Msg("OK: Webmention processed, in whitelist.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (recv *Receiver) processAuthorPicture(indieweb *mf.IndiewebData) {
|
func (recv *Receiver) processAuthorPicture(indieweb *mf.IndiewebData) {
|
||||||
|
|
|
@ -237,7 +237,7 @@ func TestReceiveTargetDoesNotExistAnymoreDeletesPossiblyOlderWebmention(t *testi
|
||||||
assert.Empty(t, indb)
|
assert.Empty(t, indb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReceiveFromNotInWhitelistSavesInModeration(t *testing.T) {
|
func TestReceiveFromNotInWhitelistSavesInModerationAndNotifies(t *testing.T) {
|
||||||
wm := mf.Mention{
|
wm := mf.Mention{
|
||||||
Source: "https://brainbaking.com/valid-indieweb-source.html",
|
Source: "https://brainbaking.com/valid-indieweb-source.html",
|
||||||
Target: "https://brainbaking.com/valid-indieweb-target.html",
|
Target: "https://brainbaking.com/valid-indieweb-target.html",
|
||||||
|
@ -246,22 +246,30 @@ func TestReceiveFromNotInWhitelistSavesInModeration(t *testing.T) {
|
||||||
AllowedWebmentionSources: []string{
|
AllowedWebmentionSources: []string{
|
||||||
"brainbaking.com",
|
"brainbaking.com",
|
||||||
},
|
},
|
||||||
|
BaseURL: "https://jam.brainbaking.com/",
|
||||||
|
Token: "mytoken",
|
||||||
Blacklist: []string{},
|
Blacklist: []string{},
|
||||||
Whitelist: []string{},
|
Whitelist: []string{},
|
||||||
}
|
}
|
||||||
repo := db.NewMentionRepo(cnf)
|
repo := db.NewMentionRepo(cnf)
|
||||||
t.Cleanup(db.Purge)
|
t.Cleanup(db.Purge)
|
||||||
|
notifierMock := &mocks.StringNotifier{
|
||||||
|
Conf: cnf,
|
||||||
|
Output: "",
|
||||||
|
}
|
||||||
receiver := &Receiver{
|
receiver := &Receiver{
|
||||||
Conf: cnf,
|
Conf: cnf,
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
RestClient: &mocks.RestClientMock{
|
RestClient: &mocks.RestClientMock{
|
||||||
GetBodyFunc: mocks.RelPathGetBodyFunc("../../../mocks/"),
|
GetBodyFunc: mocks.RelPathGetBodyFunc("../../../mocks/"),
|
||||||
},
|
},
|
||||||
|
Notifier: notifierMock,
|
||||||
}
|
}
|
||||||
|
|
||||||
receiver.Receive(wm)
|
receiver.Receive(wm)
|
||||||
assert.Empty(t, repo.GetAll("brainbaking.com").Data)
|
assert.Empty(t, repo.GetAll("brainbaking.com").Data)
|
||||||
assert.Equal(t, 1, len(repo.GetAllToModerate("brainbaking.com").Data))
|
assert.Equal(t, 1, len(repo.GetAllToModerate("brainbaking.com").Data))
|
||||||
|
assert.Contains(t, notifierMock.Output, "Accept?")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReceiveFromBlacklistedDomainDoesNothing(t *testing.T) {
|
func TestReceiveFromBlacklistedDomainDoesNothing(t *testing.T) {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// BaseURL should end with a / and is used to build URLs in notifications
|
||||||
|
BaseURL string `json:"baseURL"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
UtcOffset int `json:"utcOffset"`
|
UtcOffset int `json:"utcOffset"`
|
||||||
|
@ -49,6 +51,9 @@ func (c *Config) missingKeys() []string {
|
||||||
if c.Token == "" {
|
if c.Token == "" {
|
||||||
keys = append(keys, "token")
|
keys = append(keys, "token")
|
||||||
}
|
}
|
||||||
|
if c.BaseURL == "" {
|
||||||
|
keys = append(keys, "baseURL")
|
||||||
|
}
|
||||||
if len(c.AllowedWebmentionSources) == 0 {
|
if len(c.AllowedWebmentionSources) == 0 {
|
||||||
keys = append(keys, "allowedWebmentionSources")
|
keys = append(keys, "allowedWebmentionSources")
|
||||||
}
|
}
|
||||||
|
@ -128,7 +133,8 @@ func config() *Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultConfig() *Config {
|
func defaultConfig() *Config {
|
||||||
return &Config{
|
defaultConfig := &Config{
|
||||||
|
BaseURL: "https://jam.brainbaking.com/",
|
||||||
Port: 1337,
|
Port: 1337,
|
||||||
Token: "miauwkes",
|
Token: "miauwkes",
|
||||||
UtcOffset: 60,
|
UtcOffset: 60,
|
||||||
|
@ -136,4 +142,6 @@ func defaultConfig() *Config {
|
||||||
Blacklist: []string{"youtube.com"},
|
Blacklist: []string{"youtube.com"},
|
||||||
Whitelist: []string{"brainbaking.com"},
|
Whitelist: []string{"brainbaking.com"},
|
||||||
}
|
}
|
||||||
|
defaultConfig.Save()
|
||||||
|
return defaultConfig
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ func TestReadFromJsonWithCorrectJsonData(t *testing.T) {
|
||||||
confString := `{
|
confString := `{
|
||||||
"port": 1337,
|
"port": 1337,
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
|
"baseURL": "https://jam.brainbaking.com/",
|
||||||
"token": "miauwkes",
|
"token": "miauwkes",
|
||||||
"utcOffset": 60,
|
"utcOffset": 60,
|
||||||
"allowedWebmentionSources": [
|
"allowedWebmentionSources": [
|
||||||
|
@ -57,6 +58,7 @@ func TestWhitelist(t *testing.T) {
|
||||||
Whitelist: []string{
|
Whitelist: []string{
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
},
|
},
|
||||||
|
BaseURL: "https://jam.brainbaking.com/",
|
||||||
Port: 123,
|
Port: 123,
|
||||||
Token: "token",
|
Token: "token",
|
||||||
AllowedWebmentionSources: []string{"blah.com"},
|
AllowedWebmentionSources: []string{"blah.com"},
|
||||||
|
@ -79,6 +81,7 @@ func TestAddToBlacklistNotYetAddsToListAndSaves(t *testing.T) {
|
||||||
Blacklist: []string{
|
Blacklist: []string{
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
},
|
},
|
||||||
|
BaseURL: "https://jam.brainbaking.com/",
|
||||||
Port: 123,
|
Port: 123,
|
||||||
Token: "token",
|
Token: "token",
|
||||||
AllowedWebmentionSources: []string{"blah.com"},
|
AllowedWebmentionSources: []string{"blah.com"},
|
||||||
|
|
|
@ -58,15 +58,18 @@ func lastSentKey(domain string) string {
|
||||||
|
|
||||||
// Delete removes a possibly present mention by key. Ignores but logs possible errors.
|
// Delete removes a possibly present mention by key. Ignores but logs possible errors.
|
||||||
func (r *mentionRepoBunt) Delete(wm mf.Mention) {
|
func (r *mentionRepoBunt) Delete(wm mf.Mention) {
|
||||||
key := r.mentionToKey(wm)
|
r.deleteByKey(wm.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mentionRepoBunt) deleteByKey(key string) {
|
||||||
err := r.db.Update(func(tx *buntdb.Tx) error {
|
err := r.db.Update(func(tx *buntdb.Tx) error {
|
||||||
_, err := tx.Delete(key)
|
_, err := tx.Delete(key)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("key", key).Stringer("wm", wm).Msg("Unable to delete")
|
log.Warn().Err(err).Str("key", key).Msg("Unable to delete")
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Str("key", key).Stringer("wm", wm).Msg("Deleted.")
|
log.Debug().Str("key", key).Msg("Deleted.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,11 +91,14 @@ func pictureKey(domain string) string {
|
||||||
|
|
||||||
// Save saves the mention by marshalling data. Returns the key or a marshal/persist error.
|
// Save saves the mention by marshalling data. Returns the key or a marshal/persist error.
|
||||||
func (r *mentionRepoBunt) Save(wm mf.Mention, data *mf.IndiewebData) (string, error) {
|
func (r *mentionRepoBunt) Save(wm mf.Mention, data *mf.IndiewebData) (string, error) {
|
||||||
|
return r.saveByKey(wm.Key(), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mentionRepoBunt) saveByKey(key string, data *mf.IndiewebData) (string, error) {
|
||||||
jsonData, err := json.Marshal(data)
|
jsonData, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
key := r.mentionToKey(wm)
|
|
||||||
err = r.db.Update(func(tx *buntdb.Tx) error {
|
err = r.db.Update(func(tx *buntdb.Tx) error {
|
||||||
_, _, err := tx.Set(key, string(jsonData), nil)
|
_, _, err := tx.Set(key, string(jsonData), nil)
|
||||||
return err
|
return err
|
||||||
|
@ -103,14 +109,10 @@ func (r *mentionRepoBunt) Save(wm mf.Mention, data *mf.IndiewebData) (string, er
|
||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mentionRepoBunt) mentionToKey(wm mf.Mention) string {
|
|
||||||
return fmt.Sprintf("%s:%s", wm.Key(), wm.TargetDomain())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a single unmarshalled json value based on the mention key.
|
// Get returns a single unmarshalled json value based on the mention key.
|
||||||
// It returns the unmarshalled result or nil if something went wrong.
|
// It returns the unmarshalled result or nil if something went wrong.
|
||||||
func (r *mentionRepoBunt) Get(wm mf.Mention) *mf.IndiewebData {
|
func (r *mentionRepoBunt) Get(wm mf.Mention) *mf.IndiewebData {
|
||||||
return r.getByKey(r.mentionToKey(wm))
|
return r.getByKey(wm.Key())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mentionRepoBunt) getByKey(key string) *mf.IndiewebData {
|
func (r *mentionRepoBunt) getByKey(key string) *mf.IndiewebData {
|
||||||
|
|
|
@ -7,21 +7,41 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type MentionRepo interface {
|
type MentionRepo interface {
|
||||||
|
// InModeration saves the mention data to the in moderation db to approve or reject later.
|
||||||
|
// Returns the key or a marshal/persist error.
|
||||||
InModeration(key mf.Mention, data *mf.IndiewebData) (string, error)
|
InModeration(key mf.Mention, data *mf.IndiewebData) (string, error)
|
||||||
|
// Save saves the mention to the approved db.
|
||||||
|
// Returns the key or a marshal/persist error.
|
||||||
Save(key mf.Mention, data *mf.IndiewebData) (string, error)
|
Save(key mf.Mention, data *mf.IndiewebData) (string, error)
|
||||||
|
// Delete removes a possibly present mention from the approved db by key.
|
||||||
|
// Ignores but logs possible errors.
|
||||||
Delete(key mf.Mention)
|
Delete(key mf.Mention)
|
||||||
Approve(key mf.Mention)
|
// Approve saves the mention to the approved database and deletes the one in moderation.
|
||||||
Reject(key mf.Mention)
|
// If the key is invalid, it returns nil.
|
||||||
|
Approve(key string) *mf.IndiewebData
|
||||||
|
// Reject removes the in moderation key from the db and returns the deleted entry
|
||||||
|
// If the key is invalid, it returns nil.
|
||||||
|
Reject(key string) *mf.IndiewebData
|
||||||
|
|
||||||
|
// Get returns a single unmarshalled json value based on the approved mention key in the db.
|
||||||
|
// It returns the unmarshalled result or nil if something went wrong.
|
||||||
Get(key mf.Mention) *mf.IndiewebData
|
Get(key mf.Mention) *mf.IndiewebData
|
||||||
|
// GetAll returns a wrapped data result for all approved mentions for a particular domain.
|
||||||
GetAll(domain string) mf.IndiewebDataResult
|
GetAll(domain string) mf.IndiewebDataResult
|
||||||
|
// GetAll returns a wrapped data result for all to approve mentions for a particular domain.
|
||||||
GetAllToModerate(domain string) mf.IndiewebDataResult
|
GetAllToModerate(domain string) mf.IndiewebDataResult
|
||||||
|
|
||||||
|
// CleanupSpam removes potential blacklisted spam from the approved database by checking the url of each entry.
|
||||||
CleanupSpam(domain string, blacklist []string)
|
CleanupSpam(domain string, blacklist []string)
|
||||||
|
|
||||||
|
// SavePicture saves the picture byte data in the approved database and returns a key or error.
|
||||||
SavePicture(bytes string, domain string) (string, error)
|
SavePicture(bytes string, domain string) (string, error)
|
||||||
|
// GetPicture returns a byte slice (or nil if unknown) from the approved database for a particular source domain.
|
||||||
GetPicture(domain string) []byte
|
GetPicture(domain string) []byte
|
||||||
|
// LastSentMention fetches the last known RSS link where mentions were sent from the approved db.
|
||||||
|
// Returns an empty string if an error occured.
|
||||||
LastSentMention(domain string) string
|
LastSentMention(domain string) string
|
||||||
|
// UpdateLastSentMention updates the last sent mention link in the approved db. Logs but ignores errors.
|
||||||
UpdateLastSentMention(domain string, lastSent string)
|
UpdateLastSentMention(domain string, lastSent string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +50,7 @@ type MentionRepoWrapper struct {
|
||||||
approvedRepo *mentionRepoBunt
|
approvedRepo *mentionRepoBunt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save saves the data to the
|
||||||
func (m MentionRepoWrapper) Save(key mf.Mention, data *mf.IndiewebData) (string, error) {
|
func (m MentionRepoWrapper) Save(key mf.Mention, data *mf.IndiewebData) (string, error) {
|
||||||
return m.approvedRepo.Save(key, data)
|
return m.approvedRepo.Save(key, data)
|
||||||
}
|
}
|
||||||
|
@ -46,14 +67,17 @@ func (m MentionRepoWrapper) Delete(key mf.Mention) {
|
||||||
m.approvedRepo.Delete(key)
|
m.approvedRepo.Delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MentionRepoWrapper) Approve(keyInModeration mf.Mention) {
|
func (m MentionRepoWrapper) Approve(keyInModeration string) *mf.IndiewebData {
|
||||||
toApprove := m.toApproveRepo.Get(keyInModeration)
|
toApprove := m.toApproveRepo.getByKey(keyInModeration)
|
||||||
m.Save(keyInModeration, toApprove)
|
m.approvedRepo.saveByKey(keyInModeration, toApprove)
|
||||||
m.toApproveRepo.Delete(keyInModeration)
|
m.toApproveRepo.deleteByKey(keyInModeration)
|
||||||
|
return toApprove
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MentionRepoWrapper) Reject(keyInModeration mf.Mention) {
|
func (m MentionRepoWrapper) Reject(keyInModeration string) *mf.IndiewebData {
|
||||||
m.toApproveRepo.Delete(keyInModeration)
|
toReject := m.toApproveRepo.getByKey(keyInModeration)
|
||||||
|
m.toApproveRepo.deleteByKey(keyInModeration)
|
||||||
|
return toReject
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MentionRepoWrapper) CleanupSpam(domain string, blacklist []string) {
|
func (m MentionRepoWrapper) CleanupSpam(domain string, blacklist []string) {
|
||||||
|
|
|
@ -15,7 +15,23 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestApproveCases(t *testing.T) {
|
func TestRejectUnknownKeyReturnsNil(t *testing.T) {
|
||||||
|
repo := NewMentionRepo(repoCnf)
|
||||||
|
t.Cleanup(Purge)
|
||||||
|
|
||||||
|
result := repo.Reject("fuckballz")
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApproveUnknownKeyReturnsNil(t *testing.T) {
|
||||||
|
repo := NewMentionRepo(repoCnf)
|
||||||
|
t.Cleanup(Purge)
|
||||||
|
|
||||||
|
result := repo.Approve("fuckballz")
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApproveAndRejectCases(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
label string
|
label string
|
||||||
approve bool
|
approve bool
|
||||||
|
@ -42,6 +58,7 @@ func TestApproveCases(t *testing.T) {
|
||||||
defer Purge()
|
defer Purge()
|
||||||
|
|
||||||
wm := mf.Mention{
|
wm := mf.Mention{
|
||||||
|
Source: "https://jefklakscodex.com/dinges",
|
||||||
Target: "https://brainbaking.com/sjiekedinges.html",
|
Target: "https://brainbaking.com/sjiekedinges.html",
|
||||||
}
|
}
|
||||||
data := &mf.IndiewebData{
|
data := &mf.IndiewebData{
|
||||||
|
@ -50,9 +67,9 @@ func TestApproveCases(t *testing.T) {
|
||||||
repo.InModeration(wm, data)
|
repo.InModeration(wm, data)
|
||||||
|
|
||||||
if tc.approve {
|
if tc.approve {
|
||||||
repo.Approve(wm)
|
repo.Approve(wm.Key())
|
||||||
} else {
|
} else {
|
||||||
repo.Reject(wm)
|
repo.Reject(wm.Key())
|
||||||
}
|
}
|
||||||
|
|
||||||
allWms := repo.GetAll("brainbaking.com")
|
allWms := repo.GetAll("brainbaking.com")
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"brainbaking.com/go-jamming/app/mf"
|
||||||
|
"brainbaking.com/go-jamming/app/notifier"
|
||||||
|
"brainbaking.com/go-jamming/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StringNotifier struct {
|
||||||
|
Output string
|
||||||
|
Conf *common.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sn *StringNotifier) NotifyReceived(wm mf.Mention, indieweb *mf.IndiewebData) {
|
||||||
|
sn.Output = notifier.BuildNotification(wm, indieweb, sn.Conf)
|
||||||
|
}
|
Loading…
Reference in New Issue