Skip to content

Commit

Permalink
added Akismet API integration and automatic spam protection
Browse files Browse the repository at this point in the history
  • Loading branch information
albogdano committed Jun 5, 2024
1 parent 23cfe7e commit 5b46703
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 22 deletions.
9 changes: 5 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<dependency>
<groupId>net.thauvin.erik</groupId>
<artifactId>akismet-kotlin</artifactId>
<version>1.0.0</version>
</dependency>

<!-- TESTING -->
<dependency>
Expand Down Expand Up @@ -251,10 +256,6 @@
<encoding>${project.build.sourceEncoding}</encoding>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
<compilerArgument>-Xlint:-options</compilerArgument>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
</configuration>
</plugin>
<plugin>
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/erudika/scoold/ScooldConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3403,6 +3403,25 @@ public boolean notificationsAsReportsEnabled() {
return getConfigBoolean("notifications_as_reports_enabled", false);
}

@Documented(position = 3100,
identifier = "akismet_api_key",
category = "Miscellaneous",
description = "API Key for Akismet for activating anti-spam protection of all posts.")
public String akismetApiKey() {
return getConfigParam("akismet_api_key", "");
}

@Documented(position = 3110,
identifier = "automatic_spam_protection_enabled",
value = "true",
type = Boolean.class,
category = "Miscellaneous",
description = "Enable/disable autonomous action taken against spam posts - if detected a spam post will be "
+ "blocked without notice. By default, spam posts will require action and approval by admins.")
public boolean automaticSpamProtectionEnabled() {
return getConfigBoolean("automatic_spam_protection_enabled", true);
}

/* **********************************************************************************************************/

public boolean inDevelopment() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@
import com.erudika.para.core.utils.Config;
import com.erudika.para.core.utils.ParaObjectUtils;
import com.erudika.para.core.utils.Utils;
import com.erudika.scoold.ScooldConfig;
import static com.erudika.scoold.ScooldServer.HOMEPAGE;
import com.erudika.scoold.core.Comment;
import com.erudika.scoold.core.Post;
import com.erudika.scoold.core.Profile;
import static com.erudika.scoold.core.Profile.Badge.COMMENTATOR;
import static com.erudika.scoold.core.Profile.Badge.DISCIPLINED;
import com.erudika.scoold.core.Report;
import com.erudika.scoold.utils.ScooldUtils;
import java.util.List;
import java.util.Map;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
Expand All @@ -50,6 +52,7 @@
@RequestMapping("/comment")
public class CommentController {

private static final ScooldConfig CONF = ScooldUtils.getConfig();
private final ScooldUtils utils;
private final ParaClient pc;

Expand Down Expand Up @@ -110,14 +113,15 @@ public String createAjax(@RequestParam String comment, @RequestParam String pare
Comment showComment = utils.populate(req, new Comment(), "comment");
showComment.setCreatorid(authUser.getId());
Map<String, String> error = utils.validate(showComment);
handleSpam(showComment, authUser, error, req);
if (error.isEmpty()) {
showComment.setComment(comment);
showComment.setParentid(parentid);
showComment.setAuthorName(authUser.getName());

if (showComment.create() != null) {
long commentCount = authUser.getComments();
utils.addBadgeOnce(authUser, COMMENTATOR, commentCount >= ScooldUtils.getConfig().commentatorIfHasRep());
utils.addBadgeOnce(authUser, COMMENTATOR, commentCount >= CONF.commentatorIfHasRep());
authUser.setComments(commentCount + 1);
authUser.update();
model.addAttribute("showComment", showComment);
Expand All @@ -132,4 +136,21 @@ public String createAjax(@RequestParam String comment, @RequestParam String pare
}
return "comment";
}

private void handleSpam(Comment c, Profile authUser, Map<String, String> error, HttpServletRequest req) {
boolean isSpam = utils.isSpam(c, authUser, req);
if (isSpam && CONF.automaticSpamProtectionEnabled()) {
error.put("comment", "spam");
} else if (isSpam && !CONF.automaticSpamProtectionEnabled()) {
Report rep = new Report();
rep.setContent(Utils.abbreviate(Utils.markdownToHtml(c.getComment()), 2000));
rep.setParentid(c.getId());
rep.setCreatorid(authUser.getId());
rep.setDescription("SPAM detected");
rep.setSubType(Report.ReportType.SPAM);
rep.setLink(CONF.serverUrl() + "/comment/" + c.getId());
rep.setAuthorName(authUser.getName());
rep.create();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,24 @@
import com.erudika.para.core.utils.Config;
import com.erudika.para.core.utils.Pager;
import com.erudika.para.core.utils.Utils;
import com.erudika.scoold.ScooldConfig;
import static com.erudika.scoold.ScooldServer.FEEDBACKLINK;
import static com.erudika.scoold.ScooldServer.HOMEPAGE;
import static com.erudika.scoold.ScooldServer.SIGNINLINK;
import com.erudika.scoold.core.Feedback;
import com.erudika.scoold.core.Post;
import com.erudika.scoold.core.Profile;
import com.erudika.scoold.core.Reply;
import com.erudika.scoold.core.Report;
import com.erudika.scoold.utils.ScooldUtils;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -53,6 +55,7 @@
@RequestMapping("/feedback")
public class FeedbackController {

private static final ScooldConfig CONF = ScooldUtils.getConfig();
private final ScooldUtils utils;
private final ParaClient pc;

Expand Down Expand Up @@ -158,6 +161,7 @@ public String createAjax(HttpServletRequest req, Model model) {
Profile authUser = utils.getAuthUser(req);
Post post = utils.populate(req, new Feedback(), "title", "body", "tags|,");
Map<String, String> error = utils.validate(post);
handleSpam(post, authUser, error, req);
if (authUser != null && error.isEmpty()) {
post.setCreatorid(authUser.getId());
post.create();
Expand All @@ -184,14 +188,15 @@ public String replyAjax(@PathVariable String id, @PathVariable(required = false)
//create new answer
Reply answer = utils.populate(req, new Reply(), "body");
Map<String, String> error = utils.validate(answer);
handleSpam(answer, authUser, error, req);
if (!error.containsKey("body")) {
answer.setTitle(showPost.getTitle());
answer.setCreatorid(authUser.getId());
answer.setParentid(showPost.getId());
answer.create();

showPost.setAnswercount(showPost.getAnswercount() + 1);
if (showPost.getAnswercount() >= ScooldUtils.getConfig().maxRepliesPerPost()) {
if (showPost.getAnswercount() >= CONF.maxRepliesPerPost()) {
showPost.setCloserid("0");
}
// update without adding revisions
Expand Down Expand Up @@ -228,4 +233,21 @@ public String deleteAjax(@PathVariable String id, HttpServletRequest req) {
}
return "redirect:" + FEEDBACKLINK;
}

private void handleSpam(Post q, Profile authUser, Map<String, String> error, HttpServletRequest req) {
boolean isSpam = utils.isSpam(q, authUser, req);
if (isSpam && CONF.automaticSpamProtectionEnabled()) {
error.put("body", "spam");
Report rep = new Report();
rep.setName(q.getTitle());
rep.setContent(Utils.abbreviate(Utils.markdownToHtml(q.getBody()), 2000));
rep.setCreatorid(authUser.getId());
rep.setParentid(q.getId());
rep.setDescription("SPAM detected");
rep.setSubType(Report.ReportType.SPAM);
rep.setLink(q.getPostLinkForRedirect());
rep.setAuthorName(authUser.getName());
rep.create();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.erudika.scoold.core.Profile.Badge;
import com.erudika.scoold.core.Question;
import com.erudika.scoold.core.Reply;
import com.erudika.scoold.core.Report;
import com.erudika.scoold.core.Revision;
import com.erudika.scoold.core.UnapprovedQuestion;
import com.erudika.scoold.core.UnapprovedReply;
Expand Down Expand Up @@ -186,7 +187,7 @@ public String edit(@PathVariable String id, @RequestParam(required = false) Stri
if (showPost.hasUpdatedContent(beforeUpdate)) {
Revision.createRevisionFromPost(showPost, false);
}
updatePost(showPost, authUser);
updatePost(showPost, authUser, req);
updateLocation(showPost, authUser, location, latlng);
utils.addBadgeOnceAndUpdate(authUser, Badge.EDITOR, true);
if (req.getParameter("notificationsDisabled") == null) {
Expand Down Expand Up @@ -227,6 +228,7 @@ public String reply(@PathVariable String id, @PathVariable(required = false) Str
boolean needsApproval = CONF.answersNeedApproval() && utils.postsNeedApproval(req) && utils.userNeedsApproval(authUser);
Reply answer = utils.populate(req, needsApproval ? new UnapprovedReply() : new Reply(), "body");
Map<String, String> error = utils.validate(answer);
answer = handleSpam(answer, authUser, error, req);
if (!error.containsKey("body") && !StringUtils.isBlank(answer.getBody())) {
answer.setTitle(showPost.getTitle());
answer.setCreatorid(authUser.getId());
Expand Down Expand Up @@ -528,7 +530,25 @@ public List<Reply> getAllAnswers(Profile authUser, Post showPost, Pager itemcoun
return answers;
}

private void updatePost(Post showPost, Profile authUser) {
private void updatePost(Post showPost, Profile authUser, HttpServletRequest req) {
boolean isSpam = utils.isSpam(showPost, authUser, req);
if (isSpam) {
if (CONF.automaticSpamProtectionEnabled()) {
return;
} else {
Report rep = new Report();
rep.setName(showPost.getTitle());
rep.setContent(Utils.abbreviate(Utils.markdownToHtml(showPost.getBody()), 2000));
rep.setParentid(showPost.getId());
rep.setCreatorid(authUser.getId());
rep.setDescription("SPAM detected");
rep.setSubType(Report.ReportType.SPAM);
rep.setLink(showPost.getPostLink(false, false));
rep.setAuthorName(authUser.getName());
rep.addProperty(utils.getLang(req).get("spaces.title"), utils.getSpaceName(showPost.getSpace()));
rep.create();
}
}
showPost.setLasteditby(authUser.getId());
showPost.setLastedited(System.currentTimeMillis());
if (showPost.isQuestion()) {
Expand Down Expand Up @@ -650,4 +670,23 @@ private void addRepOnReplyOnce(Post parentPost, Profile author, boolean isModAct
pc.update(author);
}
}

private Reply handleSpam(Reply a, Profile authUser, Map<String, String> error, HttpServletRequest req) {
boolean isSpam = utils.isSpam(a, authUser, req);
if (isSpam && CONF.automaticSpamProtectionEnabled()) {
error.put("body", "spam");
} else if (isSpam && !CONF.automaticSpamProtectionEnabled()) {
UnapprovedReply spama = new UnapprovedReply();
spama.setTitle(a.getTitle());
spama.setBody(a.getBody());
spama.setTags(a.getTags());
spama.setCreatorid(a.getCreatorid());
spama.setParentid(a.getParentid());
spama.setAuthor(authUser);
spama.setSpace(a.getSpace());
spama.setSpam(true);
return spama;
}
return a;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ public String post(@RequestParam(required = false) String location, @RequestPara
q.setTags(Arrays.asList(CONF.defaultQuestionTag().isBlank() ? "" : CONF.defaultQuestionTag()));
}
Map<String, String> error = utils.validateQuestionTags(q, utils.validate(q), req);
q = handleSpam(q, authUser, error, req);
if (error.isEmpty()) {
String qid = StringUtils.isBlank(postId) ? Utils.getNewId() : postId;
q.setId(qid);
Expand Down Expand Up @@ -493,4 +494,23 @@ private Map<String, Object> getNewQuestionPayload(Question q) {
utils.triggerHookEvent("question.create", payload);
return payload;
}

private Question handleSpam(Question q, Profile authUser, Map<String, String> error, HttpServletRequest req) {
boolean isSpam = utils.isSpam(q, authUser, req);
if (isSpam && CONF.automaticSpamProtectionEnabled()) {
error.put("body", "spam");
} else if (isSpam && !CONF.automaticSpamProtectionEnabled()) {
UnapprovedQuestion spamq = new UnapprovedQuestion();
spamq.setTitle(q.getTitle());
spamq.setBody(q.getBody());
spamq.setTags(q.getTags());
spamq.setLocation(q.getLocation());
spamq.setCreatorid(q.getCreatorid());
spamq.setAuthor(authUser);
spamq.setSpace(q.getSpace());
spamq.setSpam(true);
return spamq;
}
return q;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,30 @@ public String delete(@PathVariable String id, HttpServletRequest req, HttpServle
return "base";
}

@PostMapping("/{id}/confirm-spam")
public String confirmSpam(@PathVariable String id, HttpServletRequest req, HttpServletResponse res) {
if (utils.isAuthenticated(req) && !StringUtils.isBlank(CONF.akismetApiKey())) {
Profile authUser = utils.getAuthUser(req);
Report rep = pc.read(id);
if (rep != null && utils.isAdmin(authUser)) {
utils.confirmSpam(utils.buildAkismetCommentFromReport(rep, req),
"true".equals(req.getParameter("spam")), true, req);

if ("true".equals(req.getParameter("deleteUser"))) {
Profile p = pc.read(rep.getCreatorid());
if (p != null && !utils.isMod(p)) {
p.delete();
}
}
rep.delete();
}
}
if (!utils.isAjaxRequest(req)) {
return "redirect:" + REPORTSLINK;
}
return "base";
}

@PostMapping("/delete-all")
public String deleteAll(HttpServletRequest req, HttpServletResponse res) {
if (utils.isAuthenticated(req)) {
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/com/erudika/scoold/core/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
import com.erudika.scoold.ScooldServer;
import com.erudika.scoold.utils.ScooldUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
Expand All @@ -42,9 +45,6 @@
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import org.apache.commons.lang3.StringUtils;

/**
Expand Down Expand Up @@ -89,6 +89,7 @@ public abstract class Post extends Sysprop {
private transient List<Comment> comments;
private transient Pager itemcount;
private transient Vote vote;
private transient boolean spam;

public Post() {
this.answercount = 0L;
Expand Down Expand Up @@ -279,6 +280,14 @@ public void setBody(String body) {
this.body = body;
}

public boolean isSpam() {
return spam;
}

public void setSpam(boolean spam) {
this.spam = spam;
}

public boolean isClosed() {
return !StringUtils.isBlank(this.closerid);
}
Expand Down
Loading

0 comments on commit 5b46703

Please sign in to comment.