Archived
1
0

initial spam ui

This commit is contained in:
Jeff Becker 2018-11-06 15:05:59 -05:00
parent 0ae8107138
commit 196acdb134
No known key found for this signature in database
GPG Key ID: F357B3B42F6F9B05
12 changed files with 183 additions and 16 deletions

View File

@ -535,6 +535,12 @@ func (self *NNTPDaemon) ExpireAll() {
self.expire.ExpireOrphans() self.expire.ExpireOrphans()
} }
func (self *NNTPDaemon) MarkSpam(msgid string) {
if ValidMessageID(msgid) {
self.modEngine.MarkSpam(msgid)
}
}
// run daemon // run daemon
func (self *NNTPDaemon) Run() { func (self *NNTPDaemon) Run() {
self.spamFilter.Configure(self.conf.spamconf) self.spamFilter.Configure(self.conf.spamconf)

View File

@ -1405,6 +1405,7 @@ func (self *httpFrontend) Mainloop() {
m.Path("/mod/feeds").HandlerFunc(self.modui.ServeModPage).Methods("GET") m.Path("/mod/feeds").HandlerFunc(self.modui.ServeModPage).Methods("GET")
m.Path("/mod/keygen").HandlerFunc(self.modui.HandleKeyGen).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/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/del/{article_hash}").HandlerFunc(self.modui.HandleDeletePost).Methods("GET")
m.Path("/mod/ban/{address}").HandlerFunc(self.modui.HandleBanAddress).Methods("GET") m.Path("/mod/ban/{address}").HandlerFunc(self.modui.HandleBanAddress).Methods("GET")
m.Path("/mod/unban/{address}").HandlerFunc(self.modui.HandleUnbanAddress).Methods("GET") m.Path("/mod/unban/{address}").HandlerFunc(self.modui.HandleUnbanAddress).Methods("GET")

View File

@ -44,6 +44,10 @@ type ModUI interface {
HandleKeyGen(wr http.ResponseWriter, r *http.Request) HandleKeyGen(wr http.ResponseWriter, r *http.Request)
// handle admin command // handle admin command
HandleAdminCommand(wr http.ResponseWriter, r *http.Request) HandleAdminCommand(wr http.ResponseWriter, r *http.Request)
// handle mark a post as spam
HandlePostSpam(wr http.ResponseWriter, r *http.Request)
// get outbound message channel // get outbound message channel
MessageChan() chan NNTPMessage MessageChan() chan NNTPMessage
} }
@ -57,6 +61,8 @@ const ModStick = ModAction("overchan-stick")
const ModLock = ModAction("overchan-lock") const ModLock = ModAction("overchan-lock")
const ModHide = ModAction("overchan-hide") const ModHide = ModAction("overchan-hide")
const ModSage = ModAction("overchan-sage") const ModSage = ModAction("overchan-sage")
const ModSpam = ModAction("spam")
const ModHam = ModAction("ham")
const ModDeleteAlt = ModAction("delete") const ModDeleteAlt = ModAction("delete")
type ModEvent interface { type ModEvent interface {
@ -81,11 +87,15 @@ func (self simpleModEvent) String() string {
} }
func (self simpleModEvent) Action() ModAction { func (self simpleModEvent) Action() ModAction {
switch strings.Split(string(self), " ")[0] { switch strings.ToLower(strings.Split(string(self), " ")[0]) {
case "delete": case "delete":
return ModDelete return ModDelete
case "overchan-inet-ban": case "overchan-inet-ban":
return ModInetBan return ModInetBan
case "spam":
return ModSpam
case "ham":
return ModHam
} }
return "" 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)) 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 // moderation message
// wraps multiple mod events // wraps multiple mod events
// is turned into an NNTPMessage later // is turned into an NNTPMessage later
@ -171,6 +186,8 @@ type ModEngine interface {
HandleMessage(msgid string) HandleMessage(msgid string)
// delete post of a poster // delete post of a poster
DeletePost(msgid string) error DeletePost(msgid string) error
// mark message as spam
MarkSpam(msgid string) error
// ban a cidr // ban a cidr
BanAddress(cidr string) error BanAddress(cidr string) error
// do we allow this public key to delete this message-id ? // do we allow this public key to delete this message-id ?
@ -190,9 +207,23 @@ type ModEngine interface {
type modEngine struct { type modEngine struct {
database Database database Database
store ArticleStore store ArticleStore
spam *SpamFilter
regen RegenFunc 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 { func (self *modEngine) LoadMessage(msgid string) NNTPMessage {
return self.store.GetMessage(msgid) return self.store.GetMessage(msgid)
} }
@ -394,6 +425,10 @@ func (mod *modEngine) Do(ev ModEvent) {
} else { } else {
log.Printf("invalid overchan-inet-ban: target=%s", target) log.Printf("invalid overchan-inet-ban: target=%s", target)
} }
} else if action == ModSpam {
if ValidMessageID(target) {
mod.MarkSpam(target)
}
} else if action == ModHide { } else if action == ModHide {
// TODO: implement // TODO: implement
} else if action == ModLock { } else if action == ModLock {
@ -434,6 +469,11 @@ func (mod *modEngine) Execute(ev ModEvent, pubkey string) {
mod.Do(ev) mod.Do(ev)
} }
return return
case ModSpam:
if mod.AllowJanitor(pubkey) {
mod.Do(ev)
}
return
case ModHide: case ModHide:
case ModLock: case ModLock:
case ModSage: case ModSage:
@ -442,6 +482,7 @@ func (mod *modEngine) Execute(ev ModEvent, pubkey string) {
if mod.AllowJanitor(pubkey) { if mod.AllowJanitor(pubkey) {
mod.Do(ev) mod.Do(ev)
} }
return
default: default:
// invalid action // invalid action
} }

View File

@ -523,6 +523,39 @@ func (self httpModUI) asAuthedWithMessage(scope string, handler func(ArticleEntr
}, wr, req) }, 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) { func (self httpModUI) HandleAddPubkey(wr http.ResponseWriter, r *http.Request) {
} }

View File

@ -34,20 +34,44 @@ type SpamResult struct {
IsSpam bool 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) { func (sp *SpamFilter) Rewrite(msg io.Reader, out io.WriteCloser, group string) (result SpamResult) {
var buff [65636]byte var buff [65636]byte
if !sp.Enabled(group) { if !sp.Enabled(group) {
result.Err = ErrSpamFilterNotEnabled result.Err = ErrSpamFilterNotEnabled
return return
} }
var addr *net.TCPAddr
var c *net.TCPConn
var u *user.User var u *user.User
addr, result.Err = net.ResolveTCPAddr("tcp", sp.addr) var c *net.TCPConn
if result.Err != nil { c, result.Err = sp.openConn()
return
}
c, result.Err = net.DialTCP("tcp", nil, addr)
if result.Err != nil { if result.Err != nil {
return return
} }

View File

@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"errors" "errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@ -94,6 +95,12 @@ type ArticleStore interface {
// get filepath for spam file via msgid // get filepath for spam file via msgid
SpamFile(msgid string) string 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 // iterate over all spam messages
IterSpam(func(string) error) error IterSpam(func(string) error) error
@ -115,6 +122,7 @@ type articleStore struct {
identify_path string identify_path string
placeholder string placeholder string
spamdir string spamdir string
hamdir string
compression bool compression bool
compWriter *gzip.Writer compWriter *gzip.Writer
spamd *SpamFilter spamd *SpamFilter
@ -136,6 +144,8 @@ func createArticleStore(config map[string]string, thumbConfig *ThumbnailConfig,
compression: config["compression"] == "1", compression: config["compression"] == "1",
spamd: spamd, spamd: spamd,
spamdir: filepath.Join(config["store_dir"], "spam"), spamdir: filepath.Join(config["store_dir"], "spam"),
hamdir: filepath.Join(config["store_dir"], "ham"),
thumbnails: thumbConfig, thumbnails: thumbConfig,
} }
store.Init() store.Init()
@ -781,7 +791,11 @@ func (self *articleStore) AcceptTempArticle(msgid string) (err error) {
} else { } else {
err = os.Rename(temp, store) 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 return
} }

View File

@ -789,15 +789,12 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader)
} else { } else {
log.Println("error processing message body", err) log.Println("error processing message body", err)
} }
if err != nil {
// clean up // clean up
if ValidMessageID(msgid) { if ValidMessageID(msgid) {
fname := daemon.store.GetFilenameTemp(msgid) fname := daemon.store.GetFilenameTemp(msgid)
log.Println("clean up", fname)
DelFile(fname) DelFile(fname)
} }
log.Println("error processing message", err)
}
return return
} }

View File

@ -9,6 +9,53 @@ var ready = function() {
for(var idx = 0; idx < _onreadyfuncs.length; idx++) _onreadyfuncs[idx](); 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 nntpchan_mod_delete = function(longhash) {
var elem = document.getElementById(longhash); var elem = document.getElementById(longhash);
var ajax = new XMLHttpRequest(); var ajax = new XMLHttpRequest();

View File

@ -38,6 +38,7 @@
<b>Most of the rest of the wild west.</b> <b>Most of the rest of the wild west.</b>
</div> </div>
<center><b>{{board.Name}}</b></center> <center><b>{{board.Name}}</b></center>
<center><button onclick="nntpchan_mod_commit_spam(this)">Moderate</button></center>
<br /> <br />
{{{form}}} {{{form}}}
{{#board.Threads}} {{#board.Threads}}

View File

@ -31,6 +31,7 @@
<a href="{{Source}}" class="image_link" target="_blank" title="{{Filename}}"><img src="{{Thumbnail}}" class="image" /></a> <a href="{{Source}}" class="image_link" target="_blank" title="{{Filename}}"><img src="{{Thumbnail}}" class="image" /></a>
{{/post.Attachments}} {{/post.Attachments}}
<a href="#" onclick="nntpchan_mod_delete('{{post.PostHash}}');">[Delete]</a> <a href="#" onclick="nntpchan_mod_delete('{{post.PostHash}}');">[Delete]</a>
<a href="#" onclick="nntpchan_mod_mark_spam('{{post.PostHash}}');">[Spam]</a>
<a name="{{post.PostHash}}"></a><span class="topicline"><b data-subject="{{post.Subject}}" class="subject">{{post.Subject}}</b> {{post.Name}} <span class="published">{{post.Frontend}} || {{post.Date}}</span> {{{post.Pubkey}}} <a href="{{post.PostURL}}">[Reply]</a> <a name="{{post.PostHash}}"></a><span class="topicline"><b data-subject="{{post.Subject}}" class="subject">{{post.Subject}}</b> {{post.Name}} <span class="published">{{post.Frontend}} || {{post.Date}}</span> {{{post.Pubkey}}} <a href="{{post.PostURL}}">[Reply]</a>
<a href="#" onclick="return quickreply('{{post.ShortHash}}', '{{post.PostHash}}', '{{post.PostURL}}');"> {{post.ShortHash}}</a> <a href="#" onclick="return quickreply('{{post.ShortHash}}', '{{post.PostHash}}', '{{post.PostURL}}');"> {{post.ShortHash}}</a>
</span> </span>

View File

@ -48,6 +48,7 @@
<b>Most of the rest of the wild west.</b> <b>Most of the rest of the wild west.</b>
</div> </div>
<center><b><a href="{{thread.BoardURL}}">{{thread.Board}}</a></b></center> <center><b><a href="{{thread.BoardURL}}">{{thread.Board}}</a></b></center>
<center><button onclick="nntpchan_mod_commit_spam(this)">Moderate</button></center>
<br /> <br />
{{{form}}} {{{form}}}
{{#thread.BumpLock}} {{#thread.BumpLock}}

View File

@ -31,6 +31,7 @@
<div class="sitetitle"> <div class="sitetitle">
<h2><a href="#">CHANGOLIA</a></h2> <h2><a href="#">CHANGOLIA</a></h2>
<b>Most of the rest of the wild west.</b> <b>Most of the rest of the wild west.</b>
<center><button onclick="nntpchan_mod_commit_spam(this)">Moderate</button></center>
</div> </div>
<div id="paginator"> <div id="paginator">
{{#prev}} {{#prev}}