package org.kalipo.agent;
import org.apache.commons.lang.BooleanUtils;
import org.joda.time.DateTime;
import org.kalipo.aop.KalipoExceptionHandler;
import org.kalipo.domain.Comment;
import org.kalipo.domain.Site;
import org.kalipo.domain.Thread;
import org.kalipo.domain.User;
import org.kalipo.repository.CommentRepository;
import org.kalipo.repository.SiteRepository;
import org.kalipo.repository.ThreadRepository;
import org.kalipo.service.NotificationService;
import org.kalipo.service.UserService;
import org.kalipo.service.WebSocketService;
import org.kalipo.service.util.NumUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.DoubleFunction;
/**
* Scheduled jobs for Comment entity
*/
@SuppressWarnings("unused")
@Service
@KalipoExceptionHandler
public class CommentAgent {
private final Logger log = LoggerFactory.getLogger(CommentAgent.class);
@Inject
private CommentRepository commentRepository;
@Inject
private SiteRepository siteRepository;
@Inject
private ThreadRepository threadRepository;
@Inject
private UserService userService;
@Inject
private NotificationService notificationService;
@Inject
private WebSocketService webSocketService;
@Scheduled(fixedDelay = 2000)
public void setStatusAndQuality() {
try {
final PageRequest pageable = new PageRequest(0, 50);
// todo create SVM with all approved comments
Page<Comment> listWithNoneStatus = commentRepository.findByStatus(Comment.Status.NONE, pageable);
if (listWithNoneStatus.hasContent()) {
/*
todo difference between quality and influence?
quality: quality of comment refrlecting author and
*/
// todo use SVM of R to classify spam
// List<Comment> approved = commentRepository.findByStatusAndThreadId(Comment.Status.APPROVED, thread.getId());
for (Comment comment : listWithNoneStatus) {
Thread thread = threadRepository.findOne(comment.getThreadId());
Site site = siteRepository.findOne(thread.getSiteId());
final String authorId = comment.getAuthorId();
final boolean isSuperMod = userService.isSuperMod(authorId);
final boolean isMod = site.getModeratorIds().contains(authorId);
User author = userService.findOne(authorId);
double innovative = 1d; // todo compare to other approved comments in this thread using SVM
// double spam = 0d;
double quality = author.getTrustworthiness();
// todo map quality in [0..1]
comment.setQuality(quality);
// if (spam > 0.8d) {
//// comment.setStatus(Comment.Status.SPAM);
// log.info(String.format("%s creates spam comment %s ", authorId, comment.toString()));
//
// } else {
if (isMod || isSuperMod) {
comment.setStatus(Comment.Status.APPROVED);
webSocketService.broadcast(comment.getThreadId(), WebSocketService.Type.COMMENT, comment);
onApproval(comment);
log.info(String.format("Auto-approved comment %s cause author is mod", comment.getId()));
} else if (quality > 0.5) {
Comment.Status status;
if (excessiveUpperCase(comment)) {
status = Comment.Status.REJECTED;
comment.setReviewMsg("Excessive upper-case usage");
log.info(String.format("Auto-rejected comment %s, due to excessive uppercase usage", comment.getId()));
notificationService.announceCommentRejected(comment);
} else if (excessiveSpecialChars(comment)) {
status = Comment.Status.REJECTED;
comment.setReviewMsg("Excessive special-char usage");
log.info(String.format("Auto-rejected comment %s, due to excessive special chars usage", comment.getId()));
notificationService.announceCommentRejected(comment);
} else {
status = Comment.Status.APPROVED;
onApproval(comment);
log.info(String.format("Auto-approved comment %s, due to good quality %s", comment.getId(), quality));
webSocketService.broadcast(comment.getThreadId(), WebSocketService.Type.COMMENT, comment);
}
comment.setStatus(status);
} else {
comment.setStatus(Comment.Status.PENDING);
log.info(String.format("Pending comment %s (q:%s)", comment.getId(), quality));
notificationService.announcePendingComment(thread, comment);
webSocketService.broadcast(comment.getThreadId(), WebSocketService.Type.COMMENT, comment);
}
}
commentRepository.save(listWithNoneStatus);
}
} catch (Exception e) {
log.error("Influence estimation failed.", e);
}
}
private void onApproval(Comment comment) {
final String authorId = getAuthorIdRespectingAnonymity(comment);
notificationService.notifyMentionedUsers(comment, authorId);
if (comment.getParentId() != null) {
notificationService.notifyAuthorOfParent(comment, authorId);
}
}
private String getAuthorIdRespectingAnonymity(Comment comment) {
return BooleanUtils.isTrue(comment.getAnonymous()) ? "Anon" : comment.getAuthorId();
}
private boolean excessiveSpecialChars(Comment comment) {
// todo implement: reject comments only written in UPPER CASE or so
return false;
}
private boolean excessiveUpperCase(Comment comment) {
String text = comment.getBody();
long ucCount = text.chars().filter(Character::isUpperCase).count();
int len = text.length();
return len > 20 && ucCount > 15;
}
@Scheduled(fixedDelay = 5000, initialDelay = 5000)
public void estimateCommentsInfluence() {
try {
/*
Notes from "Identifying the Influential Bloggers in a Community"
*/
// weight incoming
final DoubleFunction<Double> w_in = influence -> log(influence) * 1.7;
// weight outgoing
final DoubleFunction<Double> w_out = influence -> log(influence) * 1.7;
// weight dislikes
final DoubleFunction<Double> w_dislikes = dislikes -> log(dislikes) * 1.7;
// weight likes
final DoubleFunction<Double> w_likes = likes -> log(likes) * 1.7;
Sort sortByDate = new Sort(Sort.Direction.ASC, "lastModifiedDate");
PageRequest pageable = new PageRequest(0, 10, sortByDate);
List<Thread> threads = threadRepository.findByStatus(Thread.Status.OPEN, pageable);
for (Thread thread : threads) {
thread.setLastModifiedDate(DateTime.now());
threadRepository.save(thread);
Sort sortByLevel = new Sort(Sort.Direction.DESC, "level");
List<Comment> comments = commentRepository.findByThreadId(thread.getId(), sortByLevel);
log.debug(String.format("Updating comment-influence of thread %s with %s comments", thread.getId(), comments.size()));
// Map<String,Comment> thisComments = new HashMap<>(comments.size() * 2);
// comments.forEach(c -> thisComments.put(c.getId(), c));
// id to influence map
final Map<String, Double> influenceMap = new HashMap<>();
// todo influence should be calculated in one loop, not several
int changed = 0;
for (Comment comment : comments) {
// = replies
Set<Comment> i = commentRepository.findByParentId(comment.getId()); //iota - incoming influence
// double transitiveInfluence = w_in.apply(influence_incoming(i, influenceMap));// - w_out.apply(NumUtils.nullToZero(θ == null ? null : θ.getInfluence()));
// = parent, linked
// Comment θ = comment.getParentId()==null ? null : thisComments.get(comment.getParentId()); //theta - outgoing influence
Double i_out = 0d; //w_out.apply(NumUtils.nullToZero(θ == null ? null : θ.getInfluence()));
int i_inCount = i.isEmpty() ? 0 : 1 + i.size();
Double i_in = w_in.apply(i_inCount + influence_incoming(i, influenceMap));
double transitiveInfluence = i_in - i_out;
// todo include comment.getQuality()
double selfInfluence = w_likes.apply(NumUtils.nullToZero(comment.getLikes())) - w_dislikes.apply(NumUtils.nullToZero(comment.getDislikes()));
double influence = selfInfluence + transitiveInfluence;
influenceMap.put(comment.getId(), influence);
if (comment.getInfluence() == null) {
changed++;
log.debug(String.format("comment %s first influence %s", comment.getId(), influence));
} else if (comment.getInfluence() != influence) {
changed++;
log.debug(String.format("comment %s changed influence (%s, %s) %s -> %s", comment.getId(), i_out, i_in, comment.getInfluence(), influence));
}
comment.setInfluence(influence);
}
if (changed > 0) {
log.debug(String.format("influence changed in %s comments in thread %s", changed, thread.getId()));
commentRepository.save(comments);
}
}
} catch (Exception e) {
log.error("Influence estimation failed.", e);
}
}
private Double influence_incoming(Set<Comment> incoming, Map<String, Double> influenceMap) {
if (incoming.isEmpty()) {
return 0d;
}
double total = 0d;
for (Comment comment : incoming) {
if (influenceMap.containsKey(comment.getId())) {
total += influenceMap.get(comment.getId());
}
}
return total;
}
private double log(Double num) {
return Math.log(Math.max(1, NumUtils.nullToZero(num) + 1));
}
}