Life

Sunday, September 27, 2020

关于WakaTime的闲折腾

我干了什么

        突发奇想想要看看每天花在纯码代码(CTRL+CV)上的时间到底有多少,第一反应就是去找找Intellij IDEA的Marketplace,随便装一个,计时的插件应该都大同小异吧。
        用time做关键字搜索出来的结果排在第一位的就是WakaTime,嚯,下载量不低,就你了。

安装之后restart IDE,要求输入api key,这个api key 是需要去它的官网注册后每个帐号对应一个api key,注册过程按下不表。在settings-account拿到api key填上进入IDE,它就开始计时了。过一段时间,就可以在网站的Dashboard中看到自己的编程时间统计图。

类似这样: 

        它将按照项目、语言、工具(如果在多款工具上安装了此款插件的话)等多个维度对每天的编程时间进行分类统计并绘图。看起来还是比较清晰的。程序员装个这个插件就可以明确自己每天花在不同项目上的具体时间,知道自己到底做了些啥事。

没事找事

        最初的目的已经实现了,但,这样就完了吗?NO!

        我发现官网提供了API文档,详细说明了API接口的请求格式;认证方式;以及多种不同资源的数据接口的请求路径格式,参数,响应实例,例如可查询对单个提交所花费的时间,对所有提交的编程时间列表,所有项目的编程时间列表,给定时间范围编程活动列表等。

       出于闲,那就拿这个API玩一玩吧。

        要请求接口需要对用户身份进行验证,API文档给出两种认证方式:
  • 一种是使用OAuth 2.0
  • 一种是使用API密钥
在使用API密钥的方式进行认证也有两种方式,
  • 或是使用HTTP基本认证,在请求头的Authorization字段带上base64编码的API key
  • 或是直接将API key做为查询参数传递

拿到数据

我使用HTTP基本认证的方式对我的请求进行认证。以请求我的一段时间的编码活动为例,部分代码如下:
private static final String KEY = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";//API key

    public static String getHeader() {
		//base64编码
        byte[] encodedAuth = Base64.encode(KEY.getBytes(Charset.forName("US-ASCII")));
        return "Basic " + new String(encodedAuth);
    }

    public static Object getSummaries(String urlStr) throws Exception {
        //创建HttpClient对象
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();
        HttpGet get = new HttpGet(urlStr);
		//设置HTTP基本认证
        get.addHeader("Authorization", getHeader());

        HttpResponse response = httpClient.execute(get);
        if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
            //返回json格式
            String res = EntityUtils.toString(response.getEntity());
            JSONObject object = JSONObject.parseObject(res);
            JSONObject data = (JSONObject) object.getJSONArray("data")
            Gson gson = new Gson();
            Summary summaries;
	    //反序列化
            summaries = gson.fromJson(data.toString(), Summary.class);            
            return summaries;       
        }
        return response.getStatusLine().getReasonPhrase();
    }
	
    public static void main(String[] args) {
        Object summaries = null;
        String url = "https://wakatime.com/api/v1/users/current/summaries?start=2020-09-23&end=2020-09-27";
        summaries = getSummaries(url);         
    }

        该接口的必要参数是start范围开始日期和end范围结束日期,可选参数如project可具体到某一项目,branches具体到某些分支等。

        返回的json字符串又臭又长,看起来很伤脑筋。我利用Gson将获得的json字符串反序列化得到了我根据json数据结构定义的实体Summary,它只有一个属性:
private List<JsonOuter> days;
其中包含start-end之间的每一天(JsonOuter)的统计数据。有了这个实体,才可更愉快的玩下去。

发送邮件

        拿到了数据实体,对数据的处理结果需要通知给我,我就想到可以试试邮件通知。

汇整的数据应以表格的形式呈现出来,邮件内容支持html语言,所以可以将html源码做为邮件正文发送。

再者,表格仍然不够直观,WakaTime的Dashboard是用图表现的,我也可以试试。

表格

以某一天的categories和languages为例,创建表格所需的html源码的方法函数如下:
  private static String createMailContext(Object Outer){
        Map<String,List<JsonInner>> items = new HashMap<String,List<JsonInner>>();
        StringBuffer sb = new StringBuffer();

        JsonOuter jsonOuter = null;
        if (Outer instanceof JsonOuter) {
            jsonOuter = (JsonOuter)Outer;
            List<JsonInner> categories = jsonOuter.getCategories();
            List<JsonInner> languages = jsonOuter.getLanguages();
            items.put("categories",categories);
            items.put("languages",languages);
        }else{
            sb.append(Outer.toString());
            return sb.toString();
        }

        for(String key : items.keySet()){
            List<JsonInner> part = items.get(key);
            StringBuffer oneItemStr = new StringBuffer("<TABLE width=\"100%\" border=\"1\" cellspacing=\"0\" >\n" +
                    "<tr style='background-color:#fdf6e3;'> \n" +
                    "  <th colspan=\"3\">");
            oneItemStr.append(key);
            oneItemStr.append("</th>\n" +
                    " </tr>\n" +
                    "<tr style='text-align:center;background-color:#77773c'> \n" +
                    "<td>name</td> \n" +
                    "<td>digital</td>\n" +
                    "<td>percent</td>\n" +
                    "</tr> \n");

            for(JsonInner inner : part){
                oneItemStr.append("<tr style=\"text-align:center;background-color:#5cd65c;\"> \n" );

                oneItemStr.append("<td>\n") ;
                oneItemStr.append(inner.getName());
                oneItemStr.append("</td> \n");

                oneItemStr.append("<td>") ;
                oneItemStr.append(inner.getDigital());
                oneItemStr.append("</td> \n");

                oneItemStr.append("<td>") ;
                oneItemStr.append(inner.getPercent());
                oneItemStr.append("</td> \n");

                oneItemStr.append(" </tr> \n");
            }
            oneItemStr.append("</TABLE>\n<br /><br/>");

            sb.append(oneItemStr);
        }

        return sb.toString();
    };
JsonOuter是一天的统计数据,它包括了几个维度:
        private List<JsonInner> categories;

    private List<JsonInner> editors;

    private JsonInner grand_total;

    private List<JsonInner> languages;

    private List<JsonInner> projects;
而JsonInner是每个维度的内部属性,上述的五个维度都具有这些属性,它们是:
public class JsonInner {
    private String digital;

    private int hours;

    private int minutes;

    private int seconds;

    private String name;

    private String text;

    private Double percent;

    private Double total_seconds;
    }
为直观理解Summary/JsonOuter/JsonInner三者的关系,上图:

        接回上面所说的表格生成方法,它将生成categories、languages两个表格,有name、digital、percent三列。

饼图

以languages为例,生成各种编程语言所占比例饼图并将生成的图片保存。创建方法如下:
    private static void createPieChart(JsonOuter outer,String filePath) {
        List<JsonInner> languages = outer.getLanguages();

        DefaultPieDataset pds = new DefaultPieDataset();
		
        for(JsonInner language : languages){
            pds.setValue(language.getName(),language.getTotal_seconds());
        }

        JFreeChart chart = ChartFactory.createPieChart("", pds, false, false, true);
        try {
            // 分别是:显示图表的标题、需要提供对应图表的DateSet对象、是否显示图例、是否生成贴士以及是否生成URL链接
            // 如果不使用Font,中文将显示不出来
            Font font = new Font("宋体", Font.BOLD, 16);
            // 设置图片标题的字体
            chart.getTitle().setFont(font);
            // 得到图块,准备设置标签的字体
            PiePlot plot = (PiePlot) chart.getPlot();
            // 设置标签字体
            plot.setLabelFont(font);
            plot.setStartAngle(new Float(3.14f / 2f));
            // 设置plot的前景色透明度
            plot.setForegroundAlpha(0.7f);
            // 设置plot的背景色透明度
            plot.setBackgroundAlpha(0.0f);
            // 设置标签生成器(默认{0})
            // {0}:key {1}:value {2}:百分比 {3}:sum
            plot.setLabelGenerator(new StandardPieSectionLabelGenerator("{0}({1}占{2})"));
            File directory = new File("./");
            System.out.println(directory.getAbsolutePath());
            // 将内存中的图片写到本地硬盘
            ChartUtilities.saveChartAsJPEG(new File(filePath), chart, 600, 300);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
当然了,看到注释如此完整就知道肯定是借来的轮子咯。

组装

显然,这封邮件包含了文本和图片,需要把它们组装在一起作为邮件的正文。看代码:
     /**
     * 创建一封邮件
     *
     * @param session     和服务器交互的会话
     * @param sendMail    发件人邮箱
     * @param receiveMail 收件人邮箱
     * @return
     * @throws Exception
     */
    public static MimeMessage createMimeMessage(Session session,Object jsonOuter, String sendMail, String receiveMail) throws Exception {
        // 1. 创建一封邮件
        MimeMessage message = new MimeMessage(session);

        // 2. From: 发件人
        message.setFrom(new InternetAddress(sendMail, "工具人", "UTF-8"));

        // 3. To: 收件人(可以增加多个收件人、抄送、密送)
        message.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(receiveMail, "自由人", "UTF-8"));

        // 4. Subject: 邮件主题
        message.setSubject(Util.getLastDate(new Date()) +"编程汇报", "UTF-8");

        MimeBodyPart image = new MimeBodyPart();
        if(jsonOuter instanceof JsonOuter){
            // 5. 创建图片"节点"
            createPieChart((JsonOuter)jsonOuter,LanguageImagePath);
            // 读取本地文件
            DataHandler dh = new DataHandler(new FileDataSource(LanguageImagePath));
            // 将图片数据添加到"节点"
            image.setDataHandler(dh);
            // 为"节点"设置一个唯一编号(在文本"节点"将引用该ID)
            //image.setContentID("mailTestPic");
            image.setHeader("Content-ID", "<image>");

        }
        String context = createMailContext(jsonOuter);  //生成正文

        // 5. Content: 邮件正文(可以使用html标签)
        MimeBodyPart text = new MimeBodyPart();
        text.setContent(context+"<img src='cid:image'/>", "text/html;charset=UTF-8");

        // 7. (文本+图片)设置 文本 和 图片"节点"的关系(将 文本 和 图片"节点"合成一个混合"节点")
        MimeMultipart mm_text_image = new MimeMultipart();
        mm_text_image.addBodyPart(text);
        mm_text_image.addBodyPart(image);
        mm_text_image.setSubType("related");    // 关联关系

        message.setContent(mm_text_image);
        // 6. 设置发件时间
        message.setSentDate(new Date());

        // 7. 保存设置
        message.saveChanges();

        return message;
    }
    
现在,一封邮件就正式生成了。让我们把它发出去吧!

发送

我将利用我的qq邮箱把这份邮件发送至我的gmail邮箱。注意,发送邮件需要开通SMTP服务并获得授权码。发送邮件如下:

    public static String myEmailAccount = "xxxxxxxxxx@qq.com";
    public static String myEmailPassword = "xxxxxxxxxxxxxxxx";    //授权码

    public static String myEmailSMTPHost = "smtp.qq.com";
    public static String receiveMailAccount = "xxxxxxxxxxxxx@gmail.com";
	    public static void sendEmail(Object jsonOuter){

        // 1. 创建参数配置, 用于连接邮件服务器的参数配置
        Properties props = new Properties();                    // 参数配置
        props.setProperty("mail.transport.protocol", "smtp");   // 使用的协议(JavaMail规范要求)
        props.setProperty("mail.smtp.host", myEmailSMTPHost);   // 发件人的邮箱的 SMTP 服务器地址
        props.setProperty("mail.smtp.auth", "true");            // 需要请求认证

        // 获取默认session对象
        Session session = Session.getDefaultInstance(props,new Authenticator(){
            public PasswordAuthentication getPasswordAuthentication()
            {
				//发件人邮件用户名、授权码
                return new PasswordAuthentication(myEmailAccount, myEmailPassword); 
            }
        });
        // 设置为debug模式, 可以查看详细的发送 log
        session.setDebug(true);

        // 3. 创建一封邮件
        MimeMessage message = null;

        try {
            message = createMimeMessage(session,jsonOuter, myEmailAccount, receiveMailAccount);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 4. 根据 Session 获取邮件传输对象
        Transport transport = null;
        try {
            transport = session.getTransport();

        // 5. 使用 邮箱账号 和 密码 连接邮件服务器, 这里认证的邮箱必须与 message 中的发件人邮箱一致, 否则报错
        transport.connect(myEmailAccount, myEmailPassword);

        // 6. 发送邮件, 发到所有的收件地址, message.getAllRecipients() 获取到的是在创建邮件对象时添加的所有收件人, 抄送人, 密送人
        transport.sendMessage(message, message.getAllRecipients());

        // 7. 关闭连接
        transport.close();

        File languageFile = new File(LanguageImagePath);
		
        //删除图片
        languageFile.delete();

        } catch (NoSuchProviderException e) {
            e.printStackTrace();
        } catch (javax.mail.MessagingException e) {
            e.printStackTrace();
        }
    }
至此,一封汇报通知邮件就发送出去了。

邮件效果

至于效果嘛,大家自己看:
反正是设想达到了不是吗~

永无止境

事实上,我也已经将程序打包为jar并在cmd中运行它。这样做的目的是我希望能够把它做成一个自动任务在后它进行,比如每天的早上九点就调用一次接口查询昨天的编程数据,生成邮件并发送至我的邮箱。这样我就可以看到昨天做了什么。
可以对日期进行判断,在每周一都对上周总体情况进行汇总并报告。
免费用户只能查询过去两周,因此无法做月报。但我可以在每天对昨天数据查询后把json字符串保存在数据库里,这样我就可以做月报了。
另外还可以写一个脚本,使该自动任务随系统启动而启动,无需我再去使用java -jar命令启动它。
毕竟折腾没有尽头~~
版权声明
本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者Leslie Tien和本文原始地址:
https://leslietien.blogspot.com/2020/09/wakatime.html

No comments:

Post a Comment