您正在受到监视
代码审查有一个开放的系统
一台机器每天每天都在监视您。
我知道是因为我构建了它。
我设计了该机器以检测要在Code Review上发布的建议,但它可以看到所有内容
涉及普通用户的可怕评论
像您这样的用户
Stack Exchange认为不相关的评论
他们不会采取行动所以我决定要
,但我需要一些合作伙伴
有常规技能的常客
主持人的挚爱,我们在聊天中工作
您可以轻松找到我们
但是在主题上或主题外,如果您的评论上升了
,我们会找到您
(兴趣介绍人,适应于兴趣评论)


相当长一段时间以来,Stack Overflow上的用户都发表了评论,指导人们在Code Review上发表评论。已经在Stack Overflow Meta上多次指出,用户在执行此操作时应格外小心。为了教育Stack Overflow用户,其中哪些帖子属于此处,哪些不属于,我决定将该功能包含在名为Duga的SE聊天机器人中。

该机器人在Spring MVC上运行环境。它使用Stack Exchange API每两分钟检查一次Stack Overflow注释。它扫描检索到的注释,并将其发布到第二监视器中,常规人员可以在其中检查“堆栈溢出”问题以确定其是否属于“代码审查”。当有我感兴趣的消息时,它还会在特殊的聊天室中发送一些信息-当最近的评论过多或重置神秘的费率配额时。 (我不希望它经常出现100条注释。到目前为止,两分钟之内从未发生过)。

如果要查看API结果的示例,可以使用此链接。 br />
整个机器人的Github存储库可以在这里找到

ScheduledTasks.java(它的相关部分)

@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;
    }

}


我知道WebhookParameters并不是不可变的,这主要是因为它可以用作URL请求Spring MVC中的参数。 ?
还有其他评论吗?


评论

我不会像在“有趣的注释”中寻找的字符串那样对代码进行注释。仅是代码审查||与代码审查相关的codereview?我可以想到重构和分离。我不确定您希望/期望该机器人具有多强的功能。也许添加太多“搜索词”会导致很多错误/肯定。虽然只是食物,但是好主意!

@ChrisCirefice谢谢。确实可以关联,是的。但是,我们主要要抓住的是发布在CR上的建议。机器人已经发布了一些错误的肯定信息(特别是从今天起,它也为“程序员”提供帮助的同时也在做同样的事情)。

您不认为投票最多的答案应该能赢得支票吗?一年过去了。

#1 楼

我在“其他任何评论”类别中有一个:)

本地化?
我知道,这不是问题。还是?本地化似乎总是落伍!具有讽刺意味的是,正是由于这个原因,对应用程序进行本地化常常是一件令人头疼的事。
if (items.size() >= 100) {
    chatBot.postMessage(debug, Instant.now() + " Warning: Retrieved 100 comments. Might have missed some. This is unlikely to happen");
}


像这样:
if (items.size() >= 100) {
    chatBot.postMessage(debug, Instant.now() + Resources.Warn100CommentsReceived);
}

我假设Java在这里有类似c#的资源,如果我只是将一只脚放在嘴里,我深表歉意。它看起来确实更干净:)

'100'是一个魔术数字
这就是说,items.size() >= 100不是“收到100条评论”-该消息不能反映代码的作用,这意味着蛋糕消息很可能是谎言。而且,如果您突破了该限制,您可能想知道多少。
如何从items.size()中提取变量,然后将其串联到消息中,而不是硬编码的100中?在Stack Overflow活动的最后26周中,数字为:


4,320,919条评论。
116,900和188,280条评论之间-每周平均166,190条。
52周的平均数量为每周177,757条评论:未来几个月可能比我们后面的评论忙。
每分钟16.5条评论,两分钟内为32.97条。

我同意100是可以使用的合理数字。但是...“ 100”是一个神奇的数字!虽然您在两分钟内收到30条评论,但至少有4-5个新用户加入了Stack Overflow(每周约有23K新用户)-以这种速率,最终可能需要用更高的数量代替100。该值是完全任意的(为什么不是255?),并且显然属于private static final字段-如果没有在应用程序设置/配置文件中,这是我希望在其中找到的值。

#2 楼

就像Mat的杯子已经指出的那样,您的代码中有一些魔术数字。


private final WebhookParameters params = WebhookParameters.toRoom("8595");
private final WebhookParameters debug = WebhookParameters.toRoom("20298");  



摆脱它们。


您不应该对stackexchange api网址进行硬编码。而是将其放在配置文件中。如果api更改,则网址也会更改。如果更改没有破坏您的应用程序,则UR​​L可能会中断。

也许使用Url composeUrl()方法是一个好主意。


恕我直言,您在scanComments()方法中做了很多工作。您正在从StackExchangeAPIBean提取注释,处理剩余配额,处理注释并通过退避值计算下一次提取。应该/应该将其提取为单独的方法。


如果chatBot.postMessage抛出异常,则也将在catch部分内部发生。您最好将postMessage()方法包含在try..catch中,并引发自己的异常,然后不应通过再次调用postMessage()来处理该异常。


useDefaultRoom()方法有点奇怪,恕我直言。我将其重命名为setRoomIdIfNull(),但我认为它没有任何意义(也许缺少某些上下文?)。