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 hereScheduledTasks.java (relevant parts of it)@Autowiredprivate ChatBot chatBot;@Autowiredprivate 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 daypublic 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.javapublic 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.javapublic 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.javapublic 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.javapublic 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.QuestionsIs there anyway I can simplify the code?Am I using the Stack Exchange API in a good way?Any other comments?
You are being watched! - Comments of Interest
java;stackexchange;spring