liaoziyang's blog

  • ホーム

  • アーカイブ

  • サイトマップ

人事労務freeeとSlackを連携させるためにやったこと

投稿日 2018-12-08 | に編集されました 2018-12-24 |

この記事は裏freee developers Advent Calendar 2018の8日目&freee 19新卒 Advent Calendar 2018の24日目です。今年の10月から新卒で入社した@liaoziyangと申します。現在freee株式会社の大阪開発拠点で人事労務freeeの勤怠周り改善の開発を担当しています。

freeeではいろんなサービスとの連携を積極的に推進していて、その一環として私が人事労務freeeとSlackの連携機能の開発を担当することになりました。今回は人事労務freeeとSlackを連携させるためにやったことをお話します。

今回作ったもの

freee hr slack app

ざっくり機能を説明するとこんな感じです。

  • 勤怠打刻
  • 指定月の勤怠サマリを確認
  • 通知の受け取り(給与明細、年末調整)

開発環境の構築

ざっくり流れとしては、Slackアプリケーションを作る→外部公開用の設定をする→Oauthの認証を行うためのコードを書くになっています。
まずSlackからメッセージを受信するためには、ローカル環境に構築したアプリケーションに対して外部(Slack)からhttpsリクエストを受け付けられるようにする必要がありますが、ngrokを使えば簡単にできます。詳細な手順はこちらです。
https://api.slack.com/tutorials/tunneling-with-ngrok

外部に公開するためのSlack Appを作る手順

社内や自分のSlack WorkSpaceにHubotなどを使ってBotを追加することはよくあると思いますが、この場合はBot OAuth Access Tokenを取得し、設定に入れれば動かせることができます。

一般公開するためのSlack APPはこちらの公式ドキュメントに従って作る必要があります。

Slack App設定画面の設定

Create New Appをクリックして、名前と開発用のSlack WorkSpaceを設定した後に、Slack Appの設定画面に入ります。いろんな項目がありますが、最低限必要なものは

  • 使う機能の設定
  • OAuthのリダイレクトURL
  • 必要とする権限の設定

になります。

OAuth認証

本家のドキュメントに書いてあるように、まずはユーザに対して、Slackと自分が作ったアプリケーションが連携することを承認(許可)してもらう必要があります。

https://slack.com/oauth/authorizeに対して、GETで

  • client_id - アプリの作成時に発行されます(必須)
  • scope - 要求する権限(下記参照)(必須)
  • redirect_uri - リダイレクト先のURL(下記参照)(オプション)
  • state - 完了時に戻される一意の文字列(オプション)
  • team - 連携できるWorkspaceを指定する(オプション)

を渡します。

Railsで動的にURLを生成するならこのようになります。

1
2
3
4
5
6
7
8
9
10
11
def activate
oauth_state = Slack::GenerateOauthStateService.new(company: current_company).execute
uri = URI('https://slack.com/oauth/authorize')
uri.query = {
client_id: SLACK_CLIENT_ID,
scope: 'bot,users:read.email,commands,users:read',
state: oauth_state,
}.to_query

redirect_to uri.to_s
end

生成したリンクにアクセスするとSlackの認証画面が表示され、scopeに応じた権限の承認を求められます。ユーザーが承認すると、設定したredirect_uriに対してアクセストークンの認証コードと共にリダイレクトされます。

slack auth window

アクセストークンの認証コードを手に入れたら、認証コードを使ってアクセストークンと交換することができます。
slack-ruby-clientを使えば簡単にSlack APIを叩くことができます。

1
2
3
4
5
6
client = Slack::Web::Client.new
response = client.oauth_access({
client_id: SLACK_CLIENT_ID,
client_secret: SLACK_CLIENT_SECRET,
code: oauth_params[:code]
})

帰ってきたresponseにはこのようにaccess_tokenが含まれているので、access_tokenを使ってSlack APIを叩いていろんな情報を取ることができるようになりました。

勤怠打刻コマンドを作る

機能はいくつかありますが、ここでは打刻コマンドの実装のみを説明します。

初期設定

Slackメッセージ入力欄に/を打つことでSlash Commandを使うことができます。この機能を使って打刻コマンドを作ります。
https://api.slack.com/slash-commands

slack slash command

Slash Commandは以下のようにSlack設定画面で設定することができます。設定したCommandが使われたら、自分で設定したRequest URLに情報が送信されます。

slack edit command

打刻メッセージを返す

Slackユーザーが打刻コマンドを打ったら、そのユーザーの現時点の打刻状況に応じて、打刻メッセージを返す必要があります。例えばまだ出勤していなかったら「出勤ボタン」を表示し、既に出勤していたら「休憩開始ボタン」と「退勤ボタン」を表示します。

Slackのメッセージの組み立て方がここに書いてありますので、これに従って打刻メッセージを作ってユーザーに返せば、打刻ボタンが含まれたメッセージが表示されます。
https://api.slack.com/docs/message-attachments

実際の打刻の流れは、このように/freee_dakokuを送信することで、出勤ボタンが表示されて、そのボタンを押せば出勤できましたっという感じになっています。
slack dakoku command

打刻ボタンを押されたら打刻する

出勤などの処理は上記のメッセージにあるボタンを通してやっていて、Slackではinteractive messageと呼んでいます。ボタンを押すと事前に設定したURLにリクエストがPOSTされ、リクエスト内容は以下のjsonのようになっており、パラメータにあるresponse_urlに対して、ボタンを押した後に表示したい内容を返すことによって、打刻処理の結果をユーザーに通知することができます。
https://api.slack.com/interactive-messages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{
"type": "interactive_message",
"actions": [
{
"name": "channel_list",
"type": "select",
"selected_options":[
{
"value": "C24BTKDQW"
}
]
}
],
"callback_id": "pick_channel_for_fun",
"team": {
"id": "T1ABCD2E12",
"domain": "hooli-hq"
},
"channel": {
"id": "C9C2VHR7D",
"name": "triage-random"
},
"user": {
"id": "U900MV5U7",
"name": "gbelson"
},
"action_ts": "1520966872.245369",
"message_ts": "1520965348.000538",
"attachment_id": "1",
"token": "lbAZE0ckwoSNJcsGWE7sqX5j",
"is_app_unfurl": false,
"original_message": {
"text": "",
"username": "Belson Bot",
"bot_id": "B9DKHFZ1E",
"attachments":[
{
"callback_id": "pick_channel_for_fun",
"text": "Choose a channel",
"id": 1,
"color": "2b72cb",
"actions": [
{
"id": "1",
"name": "channel_list",
"text": "Public channels",
"type": "select",
"data_source": "channels"
}
],
"fallback":"Choose a channel"
}
],
"type": "message",
"subtype": "bot_message",
"ts": "1520965348.000538"
},
"response_url": "https://hooks.slack.com/actions/T1ABCD2E12/330361579271/0dAEyLY19ofpLwxqozy3firz",
"trigger_id": "328654886736.72393107734.9a0f78bccc3c64093f4b12fe82ccd51e"
}

Slack Appを公開する

公開する準備ができたら、Slackに審査を出します。
審査のチェックリストに従って準備を行う必要があります。https://api.slack.com/docs/slack-apps-checklist
とくに、以下の二点は注意が必要です。

  • プライバシーポリシーの英語版
  • Permissionsのscopeの必要な理由をきちんと書く

Slack側が審査が終え、作ったアプリがApprovedされました!この状態ではまだ審査通っただけなので、また一般公開されません。

slack app approved

Publishボタンを押したら、Slack App Directoryに一般公開されます。

slack app live

終わりに

エンジニアで自分でも使えるようなものを作るのは結構楽しいですね。機能面も今後たくさん追加する予定です。お楽しみに。

freee大阪開発拠点も絶賛メンバー募集中ですので、興味ある方はぜひ!

明日は

  • プロダクト開発基盤チームに所属するcindyさん(裏freee developers Advent Calendar 2018)
  • 19新卒の仲間宇都宮まゆさん(freee 19新卒 Advent Calendar 2018)

です!ご期待ください〜

Average of Levels in Binary Tree

投稿日 2017-07-19 |

Description

Given a non-empty binary tree, return the average value of the nodes on each level in the form of an array.

Example 1:

1
2
3
4
5
6
7
Input:
3
/ \
9 20
/ \
15 7
Output: [3, 14.5, 11]

Explanation:
The average value of nodes on level 0 is 3, on level 1 is 14.5, and on level 2 is 11. Hence return [3, 14.5, 11].

Note:

The range of node’s value is in the range of 32-bit signed integer.

Method

先遍历整个数,将每个节点按深度放置,然后求平均值。

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> res = new ArrayList<Double>();
List<List<TreeNode>> temp = new ArrayList<List<TreeNode>>();
LevelTravel(temp, 0, root);
for(int i = 0; i < temp.size(); i++){
Double sum = 0.0;
for(int j = 0; j < temp.get(i).size(); j++){
sum += temp.get(i).get(j).val;
}
res.add(sum/temp.get(i).size());
}
return res;

}

public void LevelTravel(List<List<TreeNode>> temp, int level, TreeNode node){
if(node == null) return;
if(temp.size() < level + 1){
temp.add(new ArrayList<TreeNode>());
}
temp.get(level).add(node);
LevelTravel(temp, level+1, node.left);
LevelTravel(temp, level+1, node.right);
}
}

Triangle

投稿日 2017-07-16 |

Description

Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.

1
2
3
4
5
6
7
8
For example, given the following triangle
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).

Note:
Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.

Method

这是一个动态规划问题,用一个数组存入每一个数字的最优值,然后求最后一行的最小值即可。
具体流程如下:

  1. 初始dp[0][0] = triangle.get(0).get(0)。
  2. dp[i][j]会等于相邻父亲数字里最小值加上他本身的值。如果是端点则直接等于父亲数字的最优值加上它本身。
  3. 求最后一行的最小值。

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int size = triangle.size();
int[][] dp = new int[size][triangle.get(size-1).size()];
dp[0][0] = triangle.get(0).get(0);
for(int i = 1; i < size; i++){
int min;
for(int j = 0; j < triangle.get(i).size(); j++){
if(j == 0)
min = dp[i-1][j];
else if(j == (triangle.get(i).size() - 1))
min = dp[i-1][j-1];
else
min = Math.min(dp[i-1][j], dp[i-1][j-1]);
dp[i][j] = min + triangle.get(i).get(j);
}
}
int min = Integer.MAX_VALUE;
for(int i = 0; i < size; i++){
min = Math.min(min, dp[size-1][i]);
}
return min;
}
}

Third Maximum Number

投稿日 2017-06-14 |

Description

Given a non-empty array of integers, return the third maximum number in this array. If it does not exist, return the maximum number. The time complexity must be in O(n).

1
2
3
4
5
6
Example 1:
Input: [3, 2, 1]

Output: 1

Explanation: The third maximum is 1.
1
2
3
4
5
6
Example 2:
Input: [1, 2]

Output: 2

Explanation: The third maximum does not exist, so the maximum (2) is returned instead.
1
2
3
4
Example 3:
Input: [2, 2, 3, 1]

Output: 1

Explanation: Note that the third maximum here means the third maximum distinct number.
Both numbers with value 2 are both considered as second maximum.

Method

先将数组进行排序,排序的复杂度是O(n)。然后从大数开始往Set里面加,当Set的长度为3时,返回这个数。如果没有长度为3的情况,则返回最大的数。

Solution

1
2
3
4
5
6
7
8
9
10
11
public class Solution {
public int thirdMax(int[] nums) {
Arrays.sort(nums);
Set<Integer> set = new HashSet<Integer>();
for(int i = nums.length-1; i >= 0; i--){
if(!set.contains(nums[i])) set.add(nums[i]);
if(set.size() == 3) return nums[i];
}
return nums[nums.length-1];
}
}

Find All Duplicates in an Array

投稿日 2017-06-13 |

Description

Given an array of integers, 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and others appear once.

Find all the elements that appear twice in this array.

Could you do it without extra space and in O(n) runtime?

1
2
3
4
5
Input:
[4,3,2,7,8,2,3,1]

Output:
[2,3]

Method

要在O(n)时间复杂度之内完成的话,就只能遍历一次数组。扫一遍数组然后将扫描过的数放进HashSet里,然后判断数有没有在HashSet中即可。

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Solution {
public List<Integer> findDuplicates(int[] nums) {
List<Integer> res = new ArrayList<Integer>();
Set<Integer> set = new HashSet<Integer>();
for(int i = 0; i < nums.length; i++){
if(set.contains(nums[i])){
res.add(nums[i]);
}else{
set.add(nums[i]);
}
}
return res;
}
}

K-diff Pairs in an Array

投稿日 2017-06-07 |

Description

Given an array of integers and an integer k, you need to find the number of unique k-diff pairs in the array. Here a k-diff pair is defined as an integer pair (i, j), where i and j are both numbers in the array and their absolute difference is k.

1
2
3
4
5
Example 1:
Input: [3, 1, 4, 1, 5], k = 2
Output: 2
Explanation: There are two 2-diff pairs in the array, (1, 3) and (3, 5).
Although we have two 1s in the input, we should only return the number of unique pairs.
1
2
3
4
Example 2:
Input:[1, 2, 3, 4, 5], k = 1
Output: 4
Explanation: There are four 1-diff pairs in the array, (1, 2), (2, 3), (3, 4) and (4, 5).
1
2
3
4
Example 3:
Input: [1, 3, 1, 5, 4], k = 0
Output: 1
Explanation: There is one 0-diff pair in the array, (1, 1).

Note:

  • The pairs (i, j) and (j, i) count as the same pair.
  • The length of the array won’t exceed 10,000.
  • All the integers in the given input belong to the range: [-1e7, 1e7].

Method

题目中给的是无序的数组然后需要求差值。我们可以先把无序数组进行排序将其变成有序,然后从第一个数开始遍历,通过两个pointer来比较两个数之间的差值。这里要注意的是重复的数字只计算一次,排序之后重复的数字合并到了一起,我们只需要判定第一个pointer前面的数字是否和pointer指向的数字的值相等,若相等则跳过。

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {
public int findPairs(int[] nums, int k) {
Arrays.sort(nums);
int count = 0;
for(int i = 0; i < nums.length; i++){
if(i > 0 && nums[i-1] == nums[i]) continue;
int diff = Integer.MIN_VALUE;
int j = i + 1;
while(k > diff && j < nums.length){
diff = Math.abs(nums[j]-nums[i]);
j++;
if(diff == k) count++;
}
}
return count;
}
}

Reshape the Matrix

投稿日 2017-06-06 |

Description

In MATLAB, there is a very useful function called ‘reshape’, which can reshape a matrix into a new one with different size but keep its original data.

You’re given a matrix represented by a two-dimensional array, and two positive integers r and c representing the row number and column number of the wanted reshaped matrix, respectively.

The reshaped matrix need to be filled with all the elements of the original matrix in the same row-traversing order as they were.

If the ‘reshape’ operation with given parameters is possible and legal, output the new reshaped matrix; Otherwise, output the original matrix.

Example 1:

1
2
3
4
5
6
7
8
9
Input: 
nums =
[[1,2],
[3,4]]
r = 1, c = 4
Output:
[[1,2,3,4]]
Explanation:
The row-traversing of nums is [1,2,3,4]. The new reshaped matrix is a 1 * 4 matrix, fill it row by row by using the previous list.

Example 2:

1
2
3
4
5
6
7
8
9
10
Input: 
nums =
[[1,2],
[3,4]]
r = 2, c = 4
Output:
[[1,2],
[3,4]]
Explanation:
There is no way to reshape a 2 * 2 matrix to a 2 * 4 matrix. So output the original matrix.

Note:

  • The height and width of the given matrix is in range [1, 100].
  • The given r and c are all positive.

Method

先求出数组的长和宽,然后遍历这个数组的所有元素,将元素存到新的数组里即可。

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Solution {
public int[][] matrixReshape(int[][] nums, int r, int c) {
int l = nums.length;
int s = nums[0].length;
int count = 0;
if(l*s != r*c) return nums;
int[][] res = new int[r][c];
for(int i = 0; i < l; i++){
for(int j = 0; j < s; j++){
res[count/c][count%c] = nums[i][j];
count++;
}
}
return res;
}
}

Can Place Flowers

投稿日 2017-06-06 |

Description

Suppose you have a long flowerbed in which some of the plots are planted and some are not. However, flowers cannot be planted in adjacent plots - they would compete for water and both would die.

Given a flowerbed (represented as an array containing 0 and 1, where 0 means empty and 1 means not empty), and a number n, return if n new flowers can be planted in it without violating the no-adjacent-flowers rule.

1
2
3
Example 1:
Input: flowerbed = [1,0,0,0,1], n = 1
Output: True
1
2
3
Example 2:
Input: flowerbed = [1,0,0,0,1], n = 2
Output: False

Note:

  • The input array won’t violate no-adjacent-flowers rule.
  • The input array size is in the range of [1, 20000].
  • n is a non-negative integer which won’t exceed the input array size.

Method

从头到尾遍历一遍,如果前面的节点和后面的节点的值都为0,则这个点可以种花。然后将这个节点的值设为1。

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int count = 0;
for(int i = 0; i < flowerbed.length; i++){
int prev = (i - 1) > -1 ? flowerbed[i-1] : 0;
int next = (i + 1) < flowerbed.length ? flowerbed[i+1] : 0;
if(prev == 0 && next == 0 && flowerbed[i] != 1){
flowerbed[i] = 1;
count++;
}
}
return count >= n;
}
}

Convert a Number to Hexadecimal

投稿日 2017-06-02 |

Description

Given an integer, write an algorithm to convert it to hexadecimal. For negative integer, two’s complement method is used.

Note:

  • All letters in hexadecimal (a-f) must be in lowercase.
  • The hexadecimal string must not contain extra leading 0s. If the number is zero, it is represented by a single zero character ‘0’; otherwise, the first character in the hexadecimal string will not be the zero character.
  • The given number is guaranteed to fit within the range of a 32-bit signed integer.
  • You must not use any method provided by the library which converts/formats the number to hex directly.

Method

正数转换的时候直接除以16求余数就行,负数的时候比较麻烦,直接运算的话需要和正数分开处理。一种方法是将正数和负数转换成同一种形式。下面的方法可以将有符号正数转换成无符号long。

1
long n = num & 0x00000000ffffffffL;

Solution

1
2
3
4
5
6
7
8
9
10
11
12
public class Solution {
public String toHex(int num) {
long n = num & 0x00000000ffffffffL;
StringBuilder sb = new StringBuilder();
char[] map = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
while(n > 0){
sb.insert(0, map[(int)(n%16)]);
n = n/16;
}
return num == 0 ? "0" : sb.toString();
}
}

Convert Sorted Array to Binary Search Tree

投稿日 2017-05-30 | に編集されました 2017-06-01 |

Description

Given an array where elements are sorted in ascending order, convert it to a height balanced BST.

Method

1.将字符串的中间值设为头结点并返回这个头结点。

2.对中间值左边的数组重复1的操作,并作为头结点的左节点返回。

3.对中间值右边的数组重复1的操作,并作为头结点的右节点返回。

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
if(nums.length == 0) return null;
return makeTree(nums, 0, nums.length - 1);
}

public TreeNode makeTree(int[] nums, int start, int end){
if(end < start) return null;
int mid = start + (end - start)/2;
TreeNode node = new TreeNode(nums[mid]);
node.right = makeTree(nums, mid + 1, end);
node.left = makeTree(nums, start, mid - 1);
return node;
}
}
12
Ziyang Liao

Ziyang Liao

11 ポスト
6 タグ
GitHub E-Mail Weibo Twitter
Creative Commons
© 2018 Ziyang Liao