You are being watched
Code Review has an open system
A machine that spies on you on every hour of every day
I know because I built it.
I designed the machine to detect suggestions to post on Code Review but it sees everything
Horrible comments involving ordinary users
Users like you
Comments that Stack Exchange considers irrelevant
They wouldn't act so I decided I would
But I needed some partners
Regulars with the skills to intervene
Loved by the moderators, we work in chat
You can easily find us
But on-topic or off-topic, if your comment's up
We'll find you
(Person of Interest intro, adapted to Comments of Interest)
For quite a while, users on Stack Overflow have posted comments directing people to post on Code Review. It has been noted several times on Stack Overflow Meta that users should be careful when doing that. In an effort to educate Stack Overflow users on which posts that belong here, and which does not, I decided to include the feature in my SE-chatbot named Duga.
The bot is running on a Spring MVC environment. It uses the Stack Exchange API to check Stack Overflow comments once every two minutes. It scans through the retrieved comments and posts them in The 2nd Monitor where regulars can check the Stack Overflow question to determine if it belongs on Code Review or not. It also sends some information in a special chatroom when there is a message that I am interested in - when there has been an excessive amount of comments recently or when the mysterious rate quota is reset. (I am not expecting it to be 100 comments very often. So far it has never happened within two minutes)
If you want to see an example of the API result, you can use this link.
Github repository for the whole bot can be found here
ScheduledTasks.java (relevant parts of it)
@Autowired
private ChatBot chatBot;
@Autowired
private StackExchangeAPIBean stackAPI;
private Instant nextFetch = Instant.now();
private long lastComment;
private long fromDate;
private int remainingQuota;
private final WebhookParameters params = WebhookParameters.toRoom("8595");
private final WebhookParameters debug = WebhookParameters.toRoom("20298");
@Scheduled(cron = "0 */2 * * * *") // second minute hour day day day
public void scanComments() {
if (!Instant.now().isAfter(nextFetch)) {
return;
}
try {
StackComments comments = stackAPI.fetchComments("stackoverflow", fromDate);
int currentQuota = comments.getQuotaRemaining();
if (currentQuota > remainingQuota && fromDate != 0) {
chatBot.postMessage(debug, Instant.now() + " Quota has been reset. Was " + remainingQuota + " is now " + currentQuota);
}
remainingQuota = currentQuota;
List<StackExchangeComment> items = comments.getItems();
if (items != null) {
if (items.size() >= 100) {
chatBot.postMessage(debug, Instant.now() + " Warning: Retrieved 100 comments. Might have missed some. This is unlikely to happen");
}
long previousLastComment = lastComment;
for (StackExchangeComment comment : items) {
if (comment.getCommentId() <= previousLastComment) {
continue;
}
lastComment = Math.max(comment.getCommentId(), lastComment);
fromDate = Math.max(comment.getCreationDate(), fromDate);
if (isInterestingComment(comment)) {
chatBot.postMessage(params, comment.getLink());
}
}
}
if (comments.getBackoff() != 0) {
nextFetch = Instant.now().plusSeconds(comments.getBackoff() + 10);
chatBot.postMessage(debug, Instant.now() + " Next fetch: " + nextFetch + " because of backoff " + comments.getBackoff());
}
} catch (Exception e) {
logger.error("Error retrieving comments", e);
chatBot.postMessage(debug, Instant.now() + " Exception in comment task " + e);
return;
}
}
private boolean isInterestingComment(StackExchangeComment comment) {
String commentText = comment.getBodyMarkdown().toLowerCase();
return commentText.contains("code review") || commentText.contains("codereview");
}
WebhookParameters.java
public class WebhookParameters {
private String roomId;
private Boolean post;
public String getRoomId() {
return roomId;
}
public void setRoomId(String roomId) {
this.roomId = roomId;
}
public void useDefaultRoom(String defaultRoomId) {
if (roomId == null) {
roomId = defaultRoomId;
}
}
public boolean getPost() {
return post == null ? true : post;
}
public void setPost(Boolean post) {
this.post = post;
}
public static WebhookParameters toRoom(String roomId) {
WebhookParameters params = new WebhookParameters();
params.setPost(true);
params.setRoomId(roomId);
return params;
}
}
StackExchangeAPIBean.java
public class StackExchangeAPIBean {
private final ObjectMapper mapper = new ObjectMapper();
@Autowired
private BotConfiguration config;
public StackExchangeAPIBean() {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public StackComments fetchComments(String site, long fromDate) throws JsonParseException, JsonMappingException, IOException {
final String filter = "!1zSk*x-OuqVk2k.(bS0NB";
final String apiKey = config.getStackAPIKey();
URL url = new URL("https://api.stackexchange.com/2.2/comments?page=1&pagesize=100&fromdate=" + fromDate +
"&order=desc&sort=creation&site=" + site + "&filter=" + filter + "&key=" + apiKey);
URLConnection connection = url.openConnection();
connection.setRequestProperty("Accept-Encoding", "identity");
return mapper.readValue(new GZIPInputStream(connection.getInputStream()), StackComments.class);
}
}
StackComments.java
public class StackComments {
@JsonProperty
private List<StackExchangeComment> items;
@JsonProperty("has_more")
private boolean hasMore;
@JsonProperty("quota_max")
private int quotaMax;
@JsonProperty("quota_remaining")
private int quotaRemaining;
@JsonProperty
private int backoff;
@JsonProperty("error_id")
private int errorId;
@JsonProperty("error_message")
private String errorMessage;
@JsonProperty("error_name")
private String errorName;
public int getBackoff() {
return backoff;
}
public int getErrorId() {
return errorId;
}
public String getErrorMessage() {
return errorMessage;
}
public String getErrorName() {
return errorName;
}
public List<StackExchangeComment> getItems() {
return items;
}
public int getQuotaMax() {
return quotaMax;
}
public int getQuotaRemaining() {
return quotaRemaining;
}
}
StackExchangeComment.java
public class StackExchangeComment {
@JsonProperty("post_id")
private long postId;
@JsonProperty("comment_id")
private long commentId;
@JsonProperty("creation_date")
private long creationDate;
@JsonProperty
private String body;
@JsonProperty
private String link;
@JsonProperty("body_markdown")
private String bodyMarkdown;
public String getBody() {
return body;
}
public String getBodyMarkdown() {
return bodyMarkdown;
}
public long getCommentId() {
return commentId;
}
public long getPostId() {
return postId;
}
public String getLink() {
return link;
}
public long getCreationDate() {
return creationDate;
}
}
I am aware that WebhookParameters
is not immutable, this is primarily because it can be used as URL request params in Spring MVC.
Questions
- Is there anyway I can simplify the code?
- Am I using the Stack Exchange API in a good way?
- Any other comments?
code review || codereview
that is related to code review? Off the top of my head I can think of refactor and decouple. I'm not entirely sure how robust you want/expect the bot to be. Maybe adding too many 'search terms' would cause a lot of false/positives. Just food for though, good idea nonetheless! \$\endgroup\$