diff --git a/contrib/backends/srndv2/src/srnd/daemon.go b/contrib/backends/srndv2/src/srnd/daemon.go index 5f1259f..69cd8c0 100644 --- a/contrib/backends/srndv2/src/srnd/daemon.go +++ b/contrib/backends/srndv2/src/srnd/daemon.go @@ -535,6 +535,12 @@ func (self *NNTPDaemon) ExpireAll() { self.expire.ExpireOrphans() } +func (self *NNTPDaemon) MarkSpam(msgid string) { + if ValidMessageID(msgid) { + self.modEngine.MarkSpam(msgid) + } +} + // run daemon func (self *NNTPDaemon) Run() { self.spamFilter.Configure(self.conf.spamconf) diff --git a/contrib/backends/srndv2/src/srnd/frontend_http.go b/contrib/backends/srndv2/src/srnd/frontend_http.go index dcd3ca9..dd37673 100644 --- a/contrib/backends/srndv2/src/srnd/frontend_http.go +++ b/contrib/backends/srndv2/src/srnd/frontend_http.go @@ -1405,6 +1405,7 @@ func (self *httpFrontend) Mainloop() { m.Path("/mod/feeds").HandlerFunc(self.modui.ServeModPage).Methods("GET") m.Path("/mod/keygen").HandlerFunc(self.modui.HandleKeyGen).Methods("GET") m.Path("/mod/login").HandlerFunc(self.modui.HandleLogin).Methods("POST") + m.Path("/mod/spam").HandlerFunc(self.modui.HandlePostSpam).Methods("POST") m.Path("/mod/del/{article_hash}").HandlerFunc(self.modui.HandleDeletePost).Methods("GET") m.Path("/mod/ban/{address}").HandlerFunc(self.modui.HandleBanAddress).Methods("GET") m.Path("/mod/unban/{address}").HandlerFunc(self.modui.HandleUnbanAddress).Methods("GET") diff --git a/contrib/backends/srndv2/src/srnd/mod.go b/contrib/backends/srndv2/src/srnd/mod.go index c3ad8c0..19a387d 100644 --- a/contrib/backends/srndv2/src/srnd/mod.go +++ b/contrib/backends/srndv2/src/srnd/mod.go @@ -44,6 +44,10 @@ type ModUI interface { HandleKeyGen(wr http.ResponseWriter, r *http.Request) // handle admin command HandleAdminCommand(wr http.ResponseWriter, r *http.Request) + // handle mark a post as spam + HandlePostSpam(wr http.ResponseWriter, r *http.Request) + + // get outbound message channel MessageChan() chan NNTPMessage } @@ -57,6 +61,8 @@ const ModStick = ModAction("overchan-stick") const ModLock = ModAction("overchan-lock") const ModHide = ModAction("overchan-hide") const ModSage = ModAction("overchan-sage") +const ModSpam = ModAction("spam") +const ModHam = ModAction("ham") const ModDeleteAlt = ModAction("delete") type ModEvent interface { @@ -81,11 +87,15 @@ func (self simpleModEvent) String() string { } func (self simpleModEvent) Action() ModAction { - switch strings.Split(string(self), " ")[0] { + switch strings.ToLower(strings.Split(string(self), " ")[0]) { case "delete": return ModDelete case "overchan-inet-ban": return ModInetBan + case "spam": + return ModSpam + case "ham": + return ModHam } return "" } @@ -122,6 +132,11 @@ func overchanInetBan(encAddr, key string, expire int64) ModEvent { return simpleModEvent(fmt.Sprintf("overchan-inet-ban %s:%s:%d", encAddr, key, expire)) } +// create a mark as spam event +func modMarkSpam(msgid string) ModEvent { + return simpleModEvent(fmt.Sprintf("spam %s", msgid)) +} + // moderation message // wraps multiple mod events // is turned into an NNTPMessage later @@ -171,6 +186,8 @@ type ModEngine interface { HandleMessage(msgid string) // delete post of a poster DeletePost(msgid string) error + // mark message as spam + MarkSpam(msgid string) error // ban a cidr BanAddress(cidr string) error // do we allow this public key to delete this message-id ? @@ -190,9 +207,23 @@ type ModEngine interface { type modEngine struct { database Database store ArticleStore + spam *SpamFilter regen RegenFunc } +func (self *modEngine) MarkSpam(msgid string) (err error) { + if self.spam == nil { + err = self.store.MarkSpam(msgid) + } else { + f, err = self.store.OpenMessage(msgid) + if err == nil { + err = self.spam.MarkSpam(f) + f.Close() + } + } + return +} + func (self *modEngine) LoadMessage(msgid string) NNTPMessage { return self.store.GetMessage(msgid) } @@ -394,6 +425,10 @@ func (mod *modEngine) Do(ev ModEvent) { } else { log.Printf("invalid overchan-inet-ban: target=%s", target) } + } else if action == ModSpam { + if ValidMessageID(target) { + mod.MarkSpam(target) + } } else if action == ModHide { // TODO: implement } else if action == ModLock { @@ -434,6 +469,11 @@ func (mod *modEngine) Execute(ev ModEvent, pubkey string) { mod.Do(ev) } return + case ModSpam: + if mod.AllowJanitor(pubkey) { + mod.Do(ev) + } + return case ModHide: case ModLock: case ModSage: @@ -442,6 +482,7 @@ func (mod *modEngine) Execute(ev ModEvent, pubkey string) { if mod.AllowJanitor(pubkey) { mod.Do(ev) } + return default: // invalid action } diff --git a/contrib/backends/srndv2/src/srnd/mod_http.go b/contrib/backends/srndv2/src/srnd/mod_http.go index 56831df..475cd5b 100644 --- a/contrib/backends/srndv2/src/srnd/mod_http.go +++ b/contrib/backends/srndv2/src/srnd/mod_http.go @@ -523,6 +523,39 @@ func (self httpModUI) asAuthedWithMessage(scope string, handler func(ArticleEntr }, wr, req) } +func (self httpModUI) HandlePostSpam(wr http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + wr.WriteHeader(405) + return + } + resp := make(map[string]interface{}) + self.asAuthed("spam", func(path string) { + var mm ModMessage + keys := string.Split(r.FormValue("spam"), ",") + for _, k := range keys { + k = strings.TrimSpace(k) + go self.daemon.MarkSpam(k) + mm = append(mm, modMarkSpam(k)) + } + privkey_bytes := self.getSessionPrivkeyBytes(r) + if privkey_bytes == nil { + // this should not happen + log.Println("failed to get privkey bytes from session") + resp["error"] = "failed to get private key from session. wtf?" + } else { + // wrap and sign + nntp := wrapModMessage(mm) + nntp, err = signArticle(nntp, privkey_bytes) + if err == nil { + // federate + self.modMessageChan <- nntp + } + resp["error"] = err + } + }) + json.NewEncoder(wr).Encode(resp) +} + func (self httpModUI) HandleAddPubkey(wr http.ResponseWriter, r *http.Request) { } diff --git a/contrib/backends/srndv2/src/srnd/spam.go b/contrib/backends/srndv2/src/srnd/spam.go index 4d92cc2..094a38e 100644 --- a/contrib/backends/srndv2/src/srnd/spam.go +++ b/contrib/backends/srndv2/src/srnd/spam.go @@ -34,20 +34,44 @@ type SpamResult struct { IsSpam bool } +// feed spam subsystem a spam post +func (sp *SpamFilter) MarkSpam(msg io.Reader) (err error) { + var buff [65636]byte + + var u *user.User + u, err = user.Current() + if err != nil { + return + } + var conn *net.TCPConn + conn, err = sp.openConn() + if err != nil { + return + } + defer conn.Close() + fmt.Fprintf(conn, "TELL SPAMC/1.5\r\nUser: %s\r\nMessage-class: spam\r\nSet: local\r\n", u.Username) + io.CopyBuffer(conn, buf[:], msg) + conn.CloseWrite() + r := bufio.NewReader(conn) +} + +func (sp *SpamFilter) openConn() (*net.TCPConn, error) { + addr, err := net.ResolveTCPAddr("tcp", sp.addr) + if err != nil { + return nil, err + } + return net.DialTCP("tcp", nil, addr) +} + func (sp *SpamFilter) Rewrite(msg io.Reader, out io.WriteCloser, group string) (result SpamResult) { var buff [65636]byte if !sp.Enabled(group) { result.Err = ErrSpamFilterNotEnabled return } - var addr *net.TCPAddr - var c *net.TCPConn var u *user.User - addr, result.Err = net.ResolveTCPAddr("tcp", sp.addr) - if result.Err != nil { - return - } - c, result.Err = net.DialTCP("tcp", nil, addr) + var c *net.TCPConn + c, result.Err = sp.openConn() if result.Err != nil { return } diff --git a/contrib/backends/srndv2/src/srnd/store.go b/contrib/backends/srndv2/src/srnd/store.go index bba2361..1adbe7b 100644 --- a/contrib/backends/srndv2/src/srnd/store.go +++ b/contrib/backends/srndv2/src/srnd/store.go @@ -9,6 +9,7 @@ import ( "bytes" "compress/gzip" "errors" + "fmt" "io" "io/ioutil" "log" @@ -94,6 +95,12 @@ type ArticleStore interface { // get filepath for spam file via msgid SpamFile(msgid string) string + MarkHam(msgid string) error + UnmarkHam(msgid string) error + + // get filepath for ham + HamFile(msgid string) string + // iterate over all spam messages IterSpam(func(string) error) error @@ -115,6 +122,7 @@ type articleStore struct { identify_path string placeholder string spamdir string + hamdir string compression bool compWriter *gzip.Writer spamd *SpamFilter @@ -136,6 +144,8 @@ func createArticleStore(config map[string]string, thumbConfig *ThumbnailConfig, compression: config["compression"] == "1", spamd: spamd, spamdir: filepath.Join(config["store_dir"], "spam"), + hamdir: filepath.Join(config["store_dir"], "ham"), + thumbnails: thumbConfig, } store.Init() @@ -781,7 +791,11 @@ func (self *articleStore) AcceptTempArticle(msgid string) (err error) { } else { err = os.Rename(temp, store) } + } else { + err = fmt.Errorf("no such inbound article %s", temp) } + } else { + err = fmt.Errorf("invalid message id %s", msgid) } return } diff --git a/contrib/backends/srndv2/src/srnd/util.go b/contrib/backends/srndv2/src/srnd/util.go index fedd80a..21023f9 100644 --- a/contrib/backends/srndv2/src/srnd/util.go +++ b/contrib/backends/srndv2/src/srnd/util.go @@ -789,14 +789,11 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader) } else { log.Println("error processing message body", err) } - if err != nil { - // clean up - if ValidMessageID(msgid) { - fname := daemon.store.GetFilenameTemp(msgid) - log.Println("clean up", fname) - DelFile(fname) - } - log.Println("error processing message", err) + + // clean up + if ValidMessageID(msgid) { + fname := daemon.store.GetFilenameTemp(msgid) + DelFile(fname) } return } diff --git a/contrib/static/overchan.js b/contrib/static/overchan.js index 487ee05..6395b9b 100644 --- a/contrib/static/overchan.js +++ b/contrib/static/overchan.js @@ -9,6 +9,53 @@ var ready = function() { for(var idx = 0; idx < _onreadyfuncs.length; idx++) _onreadyfuncs[idx](); }; +var nntpchan_mod_mark_spam = function(longhash) { + var elem = document.getElementById(longhash); + if(!elem) return; + elem.dataset.spam = "yes"; + elem.innerText = "spam"; +}; + +var nntpchan_mod_commit_spam = function(elem) { + var formdata = new FormData(); + var posts = document.getElementsByClassName("post"); + var spams = []; + for (var idx = 0; idx < posts.length; idx ++) + { + if(posts[idx].dataset.spam == "yes") + { + spams.push_back(posts[idx].dataset.msgid); + } + } + formdata.set("spam", spams.join(",")); + var ajax = new XMLHttpRequest(); + ajax.onreadystatechange = function() { + if(ajax.readyState == 4) + { + if(ajax.status == 200) + { + // success (?) + var j = JSON.parse(ajax.responseText); + if(j.error) + { + elem.innerText = "could not mark as spam: " + j.error; + } + else + { + elem.innerText = "OK: marked as spam"; + } + } + else + { + elem.innerText = "post not marked as spam on server: "+ ajax.statusText; + } + } + }; + ajax.open("POST", "/mod/spam") + ajax.send(formdata); + +}; + var nntpchan_mod_delete = function(longhash) { var elem = document.getElementById(longhash); var ajax = new XMLHttpRequest(); diff --git a/contrib/templates/placebo/board.mustache b/contrib/templates/placebo/board.mustache index dcea38b..80aa05b 100644 --- a/contrib/templates/placebo/board.mustache +++ b/contrib/templates/placebo/board.mustache @@ -38,6 +38,7 @@ Most of the rest of the wild west.
{{board.Name}}
+

{{{form}}} {{#board.Threads}} diff --git a/contrib/templates/placebo/post.mustache b/contrib/templates/placebo/post.mustache index 25b1293..80b2838 100644 --- a/contrib/templates/placebo/post.mustache +++ b/contrib/templates/placebo/post.mustache @@ -31,6 +31,7 @@ {{/post.Attachments}} [Delete] + [Spam] {{post.Subject}} {{post.Name}} {{post.Frontend}} || {{post.Date}} {{{post.Pubkey}}} [Reply] {{post.ShortHash}} diff --git a/contrib/templates/placebo/thread.mustache b/contrib/templates/placebo/thread.mustache index 34e6ebb..9276cb3 100644 --- a/contrib/templates/placebo/thread.mustache +++ b/contrib/templates/placebo/thread.mustache @@ -48,6 +48,7 @@ Most of the rest of the wild west.
{{thread.Board}}
+

{{{form}}} {{#thread.BumpLock}} diff --git a/contrib/templates/placebo/ukko.mustache b/contrib/templates/placebo/ukko.mustache index 50d5e13..8303cda 100644 --- a/contrib/templates/placebo/ukko.mustache +++ b/contrib/templates/placebo/ukko.mustache @@ -31,6 +31,7 @@

CHANGOLIA

Most of the rest of the wild west. +
{{#prev}}