├── .gitignore ├── README.md ├── lib ├── formatter │ └── default.js ├── input │ └── slack.js ├── loader.js ├── nippo.js └── output │ ├── esa.js │ └── qiitateam.js ├── package.json └── screenshot ├── esa.png ├── qiita.png └── slack.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /debug.js 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nippo 2 | === 3 | 4 | 分報から日報を作り出すためのnpm。 5 | 1日分の分報を日報として出力します。 6 | 7 | input(slack) → 加工 → output(qiita team or esa.io)に出力。 8 | みたいなイメージです。 9 | 10 | 11 | 分報?って方はこちらの記事をよんでもらえるとわかるかと思います。 12 | [Slackで簡単に「日報」ならぬ「分報」をチームで実現する3ステップ 〜 Problemが10分で解決するチャットを作ろう](http://c16e.com/1511101558/) 13 | 14 | 15 | v0.2.0現在で対応しているサービスは以下のとおり。 16 | 17 | - input 18 | - [slack](https://slack.com/) 19 | - output 20 | - [esa.io](https://esa.io/) 21 | - [qiita team](https://teams.qiita.com/) 22 | 23 | 24 | ## 使い方 25 | 26 | まずはインストールしましょう。 27 | 28 | ``` 29 | $ npm install nippo 30 | ``` 31 | 32 | あとはjsをちょこっと書くだけ 33 | 34 | 35 | ``` 36 | var nippo = require("nippo") 37 | 38 | var config = { 39 | "output": { 40 | "service": "esa", 41 | "team": "your team name", 42 | "token": "your token", 43 | "category": "report/%year%/%month%" 44 | }, 45 | "input": { 46 | "service": "slack", 47 | "token": "your token", 48 | "channel": "report" 49 | }, 50 | "formatter": { 51 | "service": "default" 52 | } 53 | }; 54 | 55 | nippo(config, 1); // 1を指定することで、1日前の日報を作成します。 56 | 57 | // ちなみにこんな事もできます 58 | nippo(config, 1).then(function(outputResponse){ 59 | console.log(outputResponse); 60 | }); 61 | 62 | ``` 63 | 64 | ### QiitaTeamで使ってみる。 65 | 66 | ``` 67 | var nippo = require("nippo") 68 | 69 | var config = { 70 | "output": { 71 | "service": "qiitateam", 72 | "team": "hoge", // https://hoge.qiita.com 73 | "token": "your token", 74 | "category": "report/%year%/%month%" 75 | }, 76 | "input": { 77 | "service": "slack", 78 | "token": "your token", 79 | "channel": "report" 80 | }, 81 | "formatter": { 82 | "service": "default" 83 | } 84 | }; 85 | 86 | nippo(config, 1); // 1を指定することで、1日前の日報を作成します。 87 | 88 | // ちなみにこんな事もできます 89 | nippo(config, 1).then(function(outputResponse){ 90 | console.log(outputResponse); 91 | }); 92 | 93 | ``` 94 | 95 | ## 日報に反映するときのフォーマットを変更する 96 | 97 | esa.ioやqiita teamに投稿するドキュメントを加工することも可能です。 98 | 99 | まずは適当なディレクトリにフォーマッターを用意します。 100 | 101 | ``` 102 | $ mkdir ./formatter 103 | ``` 104 | 105 | 次にフォーマッターファイルを用意します。 106 | 107 | ``` 108 | vi ./formatter/my_formatter.js 109 | 110 | function tsToTimeName(d) { 111 | var hour = ( d.getHours() < 10 ) ? '0' + d.getHours() : d.getHours(); 112 | var min = ( d.getMinutes() < 10 ) ? '0' + d.getMinutes() : d.getMinutes(); 113 | var sec = ( d.getSeconds() < 10 ) ? '0' + d.getSeconds() : d.getSeconds(); 114 | return hour + ":" + min + ":" + sec 115 | } 116 | 117 | module.exports = function(messages) { 118 | return messages.map(function(message){ 119 | var d = message.date; 120 | var text = message.user.name + " 投稿時間 " + tsToTimeName(message.date) + "\n #メッセージ\n\n" + message.messages; 121 | return text; 122 | }).join("\n\n"); 123 | } 124 | ``` 125 | 126 | 最後に、フォーマッターファイルの位置をしていします。 127 | 128 | `service`を先ほど作成した`my_formatter.js`にし、`dir`でディレクトリを指定します。 129 | 130 | ``` 131 | var nippo = require("nippo"); 132 | 133 | 134 | nippo({ 135 | "output": { 136 | "service": "esa", 137 | "team": "polidog", 138 | "token": "access token", 139 | "category": "report/%year%/%month%" 140 | }, 141 | "input": { 142 | "service": "slack", 143 | "token": "access token", 144 | "channel": "report" 145 | }, 146 | "formatter": { 147 | "service": "my_formatter", 148 | "dir": __dirname + "/formatter" 149 | } 150 | },1); 151 | ``` 152 | 153 | これでカスタムのフォーマッターを使うことが出来ます。 154 | 155 | 156 | ## イメージ的なあれ 157 | 158 | こんなSlackで投稿したものが 159 | 160 | ![slack](./screenshot/slack.png) 161 | 162 | こんな風にまとめられます。 163 | 164 | ![esa](./screenshot/esa.png) 165 | 166 | ![qiita](./screenshot/qiita.png) 167 | 168 | 169 | 170 | 171 | ## 最後に 172 | 173 | コードは糞コードですごめんなさい・・・ 174 | テストはこれから頑張って書きます。。 175 | -------------------------------------------------------------------------------- /lib/formatter/default.js: -------------------------------------------------------------------------------- 1 | function tsToTimeName(d) { 2 | var hour = ( d.getHours() < 10 ) ? '0' + d.getHours() : d.getHours(); 3 | var min = ( d.getMinutes() < 10 ) ? '0' + d.getMinutes() : d.getMinutes(); 4 | var sec = ( d.getSeconds() < 10 ) ? '0' + d.getSeconds() : d.getSeconds(); 5 | return hour + ":" + min + ":" + sec 6 | } 7 | 8 | module.exports = function(messages) { 9 | return messages.map(function(message){ 10 | var d = message.date; 11 | var text = message.user.name + " at " + tsToTimeName(message.date) + "\n" + message.messages; 12 | return text; 13 | }).join("\n\n"); 14 | } 15 | -------------------------------------------------------------------------------- /lib/input/slack.js: -------------------------------------------------------------------------------- 1 | var Slack = require("slack-node"); 2 | var moment = require('moment'); 3 | 4 | var SlackInput = function(config) { 5 | if (config === undefined) { 6 | return new Error("'config' must be set. It's need have at least 'token' and 'channel'"); 7 | } 8 | 9 | if (config.token === undefined) { 10 | return new Error("'config.token' must be set"); 11 | } 12 | 13 | if (config.channel === undefined) { 14 | return new Error("'config.channel' must be set"); 15 | } 16 | 17 | this.config = config 18 | this.slack = new Slack(config.token); 19 | this.channel = config.channel; 20 | } 21 | 22 | SlackInput.prototype.read = function(day, options) { 23 | 24 | 25 | 26 | return Promise.resolve() 27 | .then(function(){ 28 | return Promise.all([ 29 | getUsers(this.slack), 30 | getChannelId(this.slack, this.channel) 31 | ]); 32 | }.bind(this)) 33 | .then(function(results) { 34 | var users = results[0]; 35 | var channelId = results[1]; 36 | return getMessages(this.slack, channelId, day, users); 37 | }.bind(this)); 38 | 39 | } 40 | 41 | 42 | 43 | var getMessages = function(slack, channel, day, users) { 44 | return new Promise(function(resolve,reject){ 45 | slack.api('channels.history', { 46 | channel: channel, 47 | latest: endTime(day), 48 | oldest: startTime(day), 49 | count: 1000 50 | }, function(err, response){ 51 | var text = ""; 52 | if (response.ok) { 53 | var messages = response.messages.map(function(message){ 54 | return { 55 | "user": users[message.user], 56 | "messages": message.text, 57 | "date": tsToDate(message.ts) 58 | } 59 | }); 60 | resolve(messages); 61 | } else { 62 | reject(Error("slack response error")); 63 | } 64 | }); 65 | }); 66 | } 67 | 68 | function getChannelId(slack, name) { 69 | return new Promise(function(resolve, reject){ 70 | slack.api("channels.list",function(err, response){ 71 | if (response.ok) { 72 | var flag = false; 73 | 74 | response.channels.forEach(function(data){ 75 | if (data.is_channel && data.name == name) { 76 | flag = true; 77 | resolve(data.id); 78 | } 79 | }) 80 | 81 | if (!flag) { 82 | reject(Error("slack channel not found")); 83 | } 84 | 85 | } else { 86 | reject(Error("slack response error")); 87 | } 88 | }); 89 | }); 90 | } 91 | 92 | 93 | function getUsers(slack) { 94 | return new Promise(function(resolve, reject){ 95 | slack.api('users.list', function(err, response){ 96 | 97 | if (err || !response.ok) { 98 | reject(err); 99 | return; 100 | } 101 | 102 | var users = []; 103 | response.members.forEach(function(member){ 104 | users[member.id] = { 105 | name: member.name, 106 | image: member.profile.image_48 107 | }; 108 | }); 109 | 110 | resolve(users) 111 | }); 112 | 113 | }); 114 | } 115 | 116 | function startTime(n) { 117 | n = n * -1; 118 | var date = moment().day(n).local(); 119 | date.local(); 120 | return date.hours(0).minutes(0).seconds(0).milliseconds(0).unix(); 121 | } 122 | 123 | function endTime(n) { 124 | n = n * -1; 125 | var date = moment().day(n).local(); 126 | return date.hours(23).minutes(59).seconds(59).milliseconds(0).unix(); 127 | } 128 | 129 | function time(d) { 130 | return Math.floor( d.getTime() / 1000 ) ; 131 | } 132 | 133 | function tsToDate(ts) { 134 | ts = Math.floor(ts); 135 | return new Date( ts * 1000 ); 136 | } 137 | 138 | 139 | 140 | module.exports = SlackInput; 141 | -------------------------------------------------------------------------------- /lib/loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function(type, service, extendPath) { 2 | var Input = null; 3 | if (extendPath !== undefined) { 4 | Input = require(extendPath + "/" + service); 5 | } else { 6 | Input = require("./" + type + "/" + service); 7 | } 8 | return Input; 9 | } 10 | -------------------------------------------------------------------------------- /lib/nippo.js: -------------------------------------------------------------------------------- 1 | var loader = require("./loader"); 2 | var formatter = require("./formatter/default"); 3 | 4 | var nippo = function(config, day) { 5 | 6 | day = (day * -1) + 1; 7 | 8 | var Input = loader('input', config.input.service, config.input.dir); 9 | var Output = loader('output', config.output.service, config.output.dir); 10 | var formatter = loader('formatter', config.formatter.service, config.formatter.dir); 11 | 12 | var input = new Input(config.input); 13 | var output = new Output(config.output, formatter); 14 | return input.read(day).then(function(messages){ 15 | return output.write(day,messages) 16 | }); 17 | 18 | } 19 | 20 | module.exports = nippo; 21 | -------------------------------------------------------------------------------- /lib/output/esa.js: -------------------------------------------------------------------------------- 1 | var Esa = require('esa-nodejs'); 2 | var moment = require('moment'); 3 | 4 | var EsaOutput = function(config, formatter) { 5 | 6 | if (config === undefined) { 7 | return new Error("'config' must be set. It's need have at least 'token' and 'team'"); 8 | } 9 | 10 | if (config.token === undefined) { 11 | return new Error("'config.token' must be set"); 12 | } 13 | 14 | if (config.team === undefined) { 15 | return new Error("'config.channel' must be set"); 16 | } 17 | 18 | this.config = config; 19 | this.formatter = formatter; 20 | 21 | this.esa = Esa({ 22 | team: this.config.team, 23 | accessToken: this.config.token 24 | }); 25 | 26 | } 27 | 28 | EsaOutput.prototype.write = function(d, messages) { 29 | 30 | var date = moment().day(d); 31 | date = date.local(); 32 | var name = date.format("YYYY年MM日DD日")+"の日報"; 33 | var category = null; 34 | var wip = this.config.wip; 35 | 36 | if (wip === undefined) { 37 | wip = false; 38 | } 39 | 40 | if (this.config.category !== undefined) { 41 | category = this.config.category.replace("%year%", date.format("YYYY")).replace("%month%", date.format("MM")); 42 | } 43 | if (this.config.name !== undefined) { 44 | name = this.config.name; 45 | } 46 | name = name.replace("%day%", date.format("DD")); 47 | name = category + "/" + name; 48 | 49 | return new Promise(function(resolve, reject){ 50 | this.esa.api.createPost({ 51 | name: name, 52 | category: category, 53 | wip: wip, 54 | body_md: this.formatter(messages), 55 | },function(err, response){ 56 | if (err) { 57 | reject(err); 58 | } else { 59 | resolve(response) 60 | } 61 | }); 62 | }.bind(this)); 63 | 64 | 65 | } 66 | 67 | module.exports = EsaOutput; 68 | -------------------------------------------------------------------------------- /lib/output/qiitateam.js: -------------------------------------------------------------------------------- 1 | var Qiita = require("qiita-js"); 2 | var moment = require('moment'); 3 | require('isomorphic-fetch'); 4 | 5 | var QiitaTeamOutput = function(config, formatter) { 6 | if (config === undefined) { 7 | return new Error("'config' must be set. It's need have at least 'token' and 'team'"); 8 | } 9 | 10 | if (config.token === undefined) { 11 | return new Error("'config.token' must be set"); 12 | } 13 | 14 | if (config.team === undefined) { 15 | return new Error("'config.channel' must be set"); 16 | } 17 | 18 | this.config = config; 19 | this.formatter = formatter; 20 | 21 | Qiita.setToken(config.token); 22 | // Qiita.setEndpoint("https://" + config.team + ".qiita.com/"); 23 | Qiita.setEndpoint("https://" + config.team + ".qiita.com/"); 24 | 25 | } 26 | 27 | 28 | QiitaTeamOutput.prototype.write = function(d, messages) { 29 | 30 | var date = moment().day(d); 31 | date = date.local(); 32 | var title = date.format("YYYY年MM日DD日")+"の日報"; 33 | 34 | if (this.config.title !== undefined) { 35 | title = this.config.title; 36 | } 37 | title = title.replace("%day%", date.format("DD")); 38 | 39 | var tags = []; 40 | if (this.config.tags !== undefined) { 41 | tags = this.config.tags.map(function(tag){ 42 | return {name: tag} 43 | }); 44 | } 45 | 46 | return Qiita.Resources.Item.create_item({ 47 | title: title, 48 | body: this.formatter(messages), 49 | coediting: true, 50 | gist: false, 51 | private: true, 52 | tags: tags, 53 | tweet: false 54 | }); 55 | 56 | } 57 | 58 | module.exports = QiitaTeamOutput; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nippo", 3 | "version": "0.2.0", 4 | "description": "slackの分報からesaに日報を作るあれ", 5 | "main": "lib/nippo.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Ryota Mochizuki", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/polidog/nippo.git" 14 | }, 15 | "dependencies": { 16 | "config": "^1.17.1", 17 | "esa-nodejs": "0.0.6", 18 | "isomorphic-fetch": "^2.2.0", 19 | "moment": "^2.10.6", 20 | "qiita-js": "^0.3.1", 21 | "slack-node": "^0.1.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /screenshot/esa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polidog/nippo/e33faac23600270063a371a2044b047d54763736/screenshot/esa.png -------------------------------------------------------------------------------- /screenshot/qiita.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polidog/nippo/e33faac23600270063a371a2044b047d54763736/screenshot/qiita.png -------------------------------------------------------------------------------- /screenshot/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polidog/nippo/e33faac23600270063a371a2044b047d54763736/screenshot/slack.png --------------------------------------------------------------------------------