こんにちは!corpITチームの、コーポレートエンジニアの長峰です。
ランサーズのcorpITチームでは主に情シス、社内ツール開発の領域を担当しており、今回は社内ツール開発の一例をご紹介できればと思います。
はじめに
Chatworkは優れたチャットツールであり、業務で活用している会社も多いと思います。今回はChatworkにアップロードしたファイルをGoogleDriveにバックアップする必要が生まれたため、作成した「Chatworkのファイル一括バックアップくん」のご紹介です!
Chatworkのファイル一括バックアップくんについて
ファイル一括バックアップくんの処理の流れは
- (手動)各Chatwork使用ユーザーにトークンを取得してもらう
- (バックアップくんでボタン操作)トークンを元に「自分に管理権限がある(そうでないと招待できない)全ての部屋」にバックアップ用管理アカウントを一括で招待する
- (バックアップくんでボタン操作)管理アカウントは各部屋(ソースコードではroomと呼称)にアップロードされているファイルをシート「file」に記載していく
- (トリガーにて1時間ごとに自動実行)シート「file」のファイルを1つずつGoogleDriveにアップロードしていく
となっています。
ソースコード
実際にソースコードにて、どのような処理なのかコメントでお伝えしていきます!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Chatworkのファイルを扱うクラス | |
*/ | |
const FILE = { | |
status: { | |
unsaved: '未保存', | |
done: '保存済', | |
}, | |
}; | |
class File { | |
constructor() { | |
this.rowIndex; | |
this.id; | |
this.name; | |
this.status; | |
this.size; | |
this.room = { | |
id: undefined, | |
name: undefined, | |
}; | |
this.message = { | |
id: undefined, | |
name: undefined, | |
}; | |
this.url; | |
} | |
setDataFromResponse(res, roomId, roomName) { | |
this.id = res.file_id; | |
this.name = res.filename; | |
this.size = res.filesize; | |
this.room.id = roomId; | |
this.room.name = roomName; | |
this.message.id = res.message_id; | |
this.status = FILE.status.unsaved; | |
} | |
setDataFromSheet(row, rowIndex) { | |
this.rowIndex = rowIndex + SHEET.file.row.data; | |
this.id = row[SHEET.file.column.id - 1]; | |
this.name = row[SHEET.file.column.name - 1]; | |
this.status = row[SHEET.file.column.status - 1]; | |
this.size = row[SHEET.file.column.size - 1]; | |
this.room.id = row[SHEET.file.column.room.id - 1]; | |
this.room.name = row[SHEET.file.column.room.name - 1]; | |
this.url = row[SHEET.file.column.url - 1]; | |
} | |
isSameStatus(status) { | |
return this.status === status; | |
} | |
saveGoogleDrive(token) { | |
// https://developer.chatwork.com/reference/get-rooms-room_id-files-file_id | |
const options = { | |
headers: { | |
'accept': 'application/json', | |
'x-chatworktoken': token, | |
}, | |
method: 'get', | |
}; | |
const res = UrlFetchApp.fetch( | |
`https://api.chatwork.com/v2/rooms/${this.room.id}/files/${this.id}?create_download_url=1`, | |
options | |
); | |
const file = UrlFetchApp.fetch(JSON.parse(res).download_url).getBlob().setName(this.name); | |
const url = DriveApp.getFolderById(PropertiesService.getScriptProperties().getProperty('drive_folder_id')).createFile(file).getUrl(); | |
BaseLibrary.setText( | |
SHEET.file, | |
this.rowIndex, | |
SHEET.file.column.url, | |
url | |
); | |
BaseLibrary.setText( | |
SHEET.file, | |
this.rowIndex, | |
SHEET.file.column.status, | |
FILE.status.done | |
); | |
} | |
getOutList() { | |
return [ | |
this.id, | |
this.name, | |
this.status, | |
this.size, | |
this.room.id, | |
this.room.name, | |
this.message.id, | |
this.message.name, | |
this.url | |
]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* トリガーはこのファイルの中の処理しか設定しない。処理の概要を記載するようにする。 | |
*/ | |
// この処理を各ユーザーに実行してもらう。なお、実行の前にユーザーのトークンをシートに記載してもらう。 | |
function refreshRoomMySheet() { | |
// シート「room」からルームIDのリストを取得 | |
const sheetRoomIdList = getRoomListFromSheet().map(room => room.getId()); | |
// メンバーのインスタンスを作成して、トークンをメンバーに設定 | |
const member = new Member(); | |
member.setTokenFromSheet(); | |
// そのメンバーが加入しているroomリストを取得して、グループチャット及び↑で取得しているルームIDにないルームのみに絞る | |
const roomList = member.getJoinRoomList().filter(room => { | |
return room.isSameType(ROOM.type.group) | |
&& !sheetRoomIdList.includes(room.getId()); | |
}); | |
// roomリストをシート「作業」に記載 | |
BaseLibrary.refreshSheet( | |
SHEET.roomMy.name, | |
roomList.map(room => room.getOutListRoomMySheet()) | |
); | |
} | |
// これも各自に実行してもらう。それぞれのroomに管理者をinviteする | |
function inviteAdmin() { | |
// 管理者のメンバーのインスタンス | |
const adminMember = new Member(MEMBER.type.admin); | |
const member = new Member(); | |
member.setTokenFromSheet(); | |
// シート「作業」のroomリストから、各自のユーザーが管理権限を持っているもの(そうでないとinviteできないため)を100件抽出して、管理者メンバーをinvite | |
getRoomListFromRoomMySheet() | |
.filter(room => room.isSameMyRole(ROOM.myRole.admin)) | |
.slice(0, 100) | |
.forEach(room => room.addMember(member.getToken(), adminMember)); | |
// シート「room」「作業」を更新する | |
refreshRoomSheet(); | |
refreshRoomMySheet(); | |
} | |
// シート「room」を更新 | |
function refreshRoomSheet() { | |
const adminMember = new Member(MEMBER.type.admin); | |
// 管理者メンバーが加入しているroomリストを取得して1件ずつ実行 | |
const outList = adminMember.getJoinRoomList().reduce((roomList, room) => { | |
// DMなどは除外 | |
if (!room.isSameType(ROOM.type.group)) return roomList; | |
// シート「room」から取得したroomリストにあるか確認 | |
const r = roomList.find(r => r.isSame(room)); | |
if (r === undefined) { | |
// なければroomリストに追加 | |
roomList.push(room); | |
} else { | |
// あればそのroomでの権限を更新 | |
r.setRole(room.getRole()); | |
} | |
return roomList; | |
}, getRoomListFromSheet()).map(room => room.getOutList()); | |
BaseLibrary.refreshSheet( | |
SHEET.room.name, | |
outList | |
); | |
} | |
// 管理者手順1 シート「file」に追記していく | |
function addFileSheet() { | |
const adminMember = new Member(MEMBER.type.admin); | |
// シート「room」から対象のステータスのものを100件抽出 | |
const roomList = getRoomListFromSheet() | |
.filter(room => room.isSameStatus(ROOM.status.phase1)) | |
.slice(0, 100); | |
roomList.forEach(room => { | |
let status = ROOM.status.phase2; | |
try { | |
// シート「file」にそのroomにアップロードされたファイルを追記 | |
room.addSheetFileList(adminMember.getToken()); | |
} catch (e) { | |
status = e; | |
} | |
// シート「room」の該当roomのステータスを更新 | |
room.setSheetStatus(status); | |
Utilities.sleep(1000); | |
}); | |
} | |
// 管理者手順2 シート「file」のファイルをgoogleDriveに保存 | |
function saveGoogleDrive() { | |
const adminMember = new Member(MEMBER.type.admin); | |
// シート「file」のfileリストから、該当ステータスのものを100件抽出 | |
const fileList = getFileListFromSheet() | |
.filter(file => file.isSameStatus(FILE.status.unsaved)) | |
.slice(0, 100); | |
fileList.forEach(file => { | |
// fileをgoogleDriveに保存していく。 | |
file.saveGoogleDrive(adminMember.getToken()); | |
Utilities.sleep(1000); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Chatworkのユーザーを扱うクラス | |
*/ | |
const MEMBER = { | |
role: { | |
admin: 'admin', | |
member: 'member', | |
readonly: 'readonly', | |
}, | |
type: { | |
admin: 'admin', | |
temporary: 'temporary', | |
}, | |
}; | |
class Member { | |
constructor(type) { | |
switch (type) { | |
case MEMBER.type.admin: | |
this.id = PropertiesService.getScriptProperties().getProperty('id_admin'); | |
this.role = MEMBER.role.admin; | |
this.token = PropertiesService.getScriptProperties().getProperty('token_admin'); | |
this.organizationId; | |
break; | |
default: | |
this.id; | |
this.role; | |
this.token; | |
this.organizationId; | |
break; | |
} | |
} | |
setData(role, id) { | |
this.id = id; | |
this.role = role; | |
} | |
setTokenFromSheet() { | |
this.token = BaseLibrary.getSheet(SHEET.config.name).getRange(SHEET.config.range.token).getValue(); | |
} | |
isSame(member) { | |
return this.getId() === member.getId(); | |
} | |
isSameRole(role) { | |
return this.role === role; | |
} | |
getId() { | |
return this.id; | |
} | |
getToken() { | |
return this.token; | |
} | |
getJoinRoomList() { | |
// https://developer.chatwork.com/reference/get-rooms | |
const options = { | |
headers: { | |
'accept': 'application/json', | |
'x-chatworktoken': this.token, | |
}, | |
method: 'get', | |
}; | |
const res = UrlFetchApp.fetch('https://api.chatwork.com/v2/rooms', options); | |
return JSON.parse(res).map(json => { | |
const room = new Room(); | |
room.setDataFromResponse(json); | |
return room; | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Chatworkの部屋を扱うクラス | |
*/ | |
const ROOM = { | |
status: { | |
phase1: 'フェーズ1', | |
phase2: 'フェーズ2', | |
invalid: { | |
notAdmin: '不可_member権限', | |
}, | |
}, | |
type: { | |
dm: 'direct', | |
my: 'my', | |
group: 'group', | |
}, | |
myRole: { | |
member: 'member', | |
admin: 'admin', | |
}, | |
}; | |
class Room { | |
constructor() { | |
this.rowIndex; | |
this.id; | |
this.name; | |
this.status; | |
this.type; | |
this.myRole; | |
} | |
setDataFromResponse(res) { | |
this.id = res.room_id; | |
this.name = res.name; | |
this.type = res.type; | |
this.myRole = res.role; | |
this.status = this.isSameMyRole(ROOM.myRole.admin) ? ROOM.status.phase1 : ROOM.status.invalid.notAdmin; | |
} | |
setDataFromSheet(row, rowIndex) { | |
this.rowIndex = rowIndex + SHEET.room.row.data; | |
this.id = row[SHEET.room.column.id - 1]; | |
this.name = row[SHEET.room.column.name - 1]; | |
this.status = row[SHEET.room.column.status - 1]; | |
} | |
setDataFromRoomMySheet(row) { | |
this.id = row[SHEET.roomMy.column.id - 1]; | |
this.myRole = row[SHEET.roomMy.column.myRole - 1]; | |
} | |
setRole(myRole) { | |
this.myRole = myRole; | |
} | |
isSame(room) { | |
return this.getId() === room.getId(); | |
} | |
isTarget() { | |
return this.type === ROOM.type.group; | |
} | |
isSameStatus(status) { | |
return this.status === status; | |
} | |
isSameType(type) { | |
return this.type === type; | |
} | |
isSameMyRole(myRole) { | |
return this.myRole === myRole; | |
} | |
getId() { | |
return this.id; | |
} | |
getName() { | |
return this.name; | |
} | |
getRole() { | |
return this.myRole; | |
} | |
getOutList() { | |
return [ | |
this.id, | |
`https://www.chatwork.com/#!rid${this.id}`, | |
this.name, | |
this.myRole, | |
this.status | |
]; | |
} | |
getOutListRoomMySheet() { | |
return [ | |
this.id, | |
this.name, | |
this.myRole, | |
`https://www.chatwork.com/#!rid${this.id}` | |
]; | |
} | |
addSheetFileList(token) { | |
// https://developer.chatwork.com/reference/get-rooms-room_id-files | |
const options = { | |
headers: { | |
'accept': 'application/json', | |
'x-chatworktoken': token, | |
}, | |
method: 'get', | |
}; | |
const res = UrlFetchApp.fetch(`https://api.chatwork.com/v2/rooms/${this.id}/files`, options); | |
const fileList = JSON.parse(res).map(json => { | |
const file = new File(); | |
file.setDataFromResponse(json, this.id, this.name); | |
return file; | |
}); | |
BaseLibrary.addSheetLastRow( | |
SHEET.file, | |
fileList.map(file => file.getOutList()) | |
); | |
} | |
setSheetStatus(status) { | |
BaseLibrary.setText( | |
SHEET.room, | |
this.rowIndex, | |
SHEET.room.column.status, | |
status | |
); | |
} | |
getJoinMember(token) { | |
// https://developer.chatwork.com/reference/get-rooms-room_id-members | |
const options = { | |
headers: { | |
'accept': 'application/json', | |
'x-chatworktoken': token, | |
}, | |
method: 'get', | |
}; | |
const res = UrlFetchApp.fetch(`https://api.chatwork.com/v2/rooms/${this.id}/members`, options); | |
return JSON.parse(res).map(json => { | |
const member = new Member(); | |
member.setDataFromResponse(json); | |
return member; | |
}); | |
} | |
putMemberList(token, memberList) { | |
// https://developer.chatwork.com/reference/put-rooms-room_id-members | |
const getMemberIdList = (memberList, type) => { | |
return memberList.reduce((idList, member) => { | |
if (member.isSameRole(type)) idList.push(member.getId()); | |
return idList; | |
}, []); | |
}; | |
const payload = [ | |
['members_readonly_ids', getMemberIdList(memberList, MEMBER.role.readonly)], | |
['members_admin_ids', getMemberIdList(memberList, MEMBER.role.admin)], | |
['members_member_ids', getMemberIdList(memberList, MEMBER.role.member)] | |
].reduce((payload, row) => { | |
const key = row[0]; | |
const idList = row[1]; | |
if (idList.length) payload[key] = idList.join(','); | |
return payload; | |
}, {}); | |
const options = { | |
headers: { | |
'accept': 'application/json', | |
'content-type': 'application/x-www-form-urlencoded', | |
'x-chatworktoken': token, | |
}, | |
method: 'put', | |
payload: payload, | |
}; | |
UrlFetchApp.fetch(`https://api.chatwork.com/v2/rooms/${this.id}/members`, options); | |
} | |
addMember(token, newMember) { | |
const memberList = this.getJoinMember(token); | |
if (memberList.some(member => member.isSame(newMember))) return; | |
this.putMemberList(token, memberList.concat(newMember)); | |
} | |
getDownloadLink(token, roomId, fileId) { | |
// https://developer.chatwork.com/reference/get-rooms-room_id-files-file_id | |
const options = { | |
headers: { | |
'accept': 'application/json', | |
'x-chatworktoken': token, | |
}, | |
method: 'get', | |
}; | |
const res = UrlFetchApp.fetch( | |
`https://api.chatwork.com/v2/rooms/${roomId}/files/${fileId}?create_download_url=1`, | |
options | |
); | |
return JSON.parse(res).download_url; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* spreadsheetのシート操作を扱うファイル | |
*/ | |
const SHEET = { | |
room: { | |
name: 'room', | |
row: { | |
data: 2, | |
}, | |
column: { | |
id: 1, | |
link: 2, | |
name: 3, | |
myRole: 4, | |
status: 5, | |
}, | |
}, | |
roomMy: { | |
name: '作業', | |
row: { | |
data: 2, | |
}, | |
column: { | |
id: 1, | |
myRole: 3, | |
}, | |
}, | |
file: { | |
name: 'file', | |
row: { | |
data: 2, | |
}, | |
column: { | |
id: 1, | |
name: 2, | |
status: 3, | |
size: 4, | |
room: { | |
id: 5, | |
name: 6, | |
}, | |
message: { | |
id: 7, | |
name: 8, | |
}, | |
url: 9, | |
}, | |
}, | |
config: { | |
name: 'config', | |
range: { | |
token: 'b1', | |
}, | |
}, | |
}; | |
function getRoomListFromSheet() { | |
return BaseLibrary.getSheetData(SHEET.room).map((row, rowIndex) => { | |
const room = new Room(); | |
room.setDataFromSheet(row, rowIndex); | |
return room; | |
}); | |
} | |
function getRoomListFromRoomMySheet() { | |
return BaseLibrary.getSheetData(SHEET.roomMy).map(row => { | |
const room = new Room(); | |
room.setDataFromRoomMySheet(row); | |
return room; | |
}); | |
} | |
function getFileListFromSheet() { | |
return BaseLibrary.getSheetData(SHEET.file).map((row, rowIndex) => { | |
const file = new File(); | |
file.setDataFromSheet(row, rowIndex); | |
return file; | |
}); | |
} |
シート
※IDや名前は例となっています
※API制限により、取得できるのは各roomの直近のファイル100件となります。
終わりに
これで各自のChatworkの部屋にアップロードされたファイルをGoogleDriveにバックアップできました。
GASのプログラミングでも上記ソースコードのように、オブジェクト指向で開発をすることが可能です!GASのプログラミングに興味がある方は参考にしてみてください。本記事が、GASのプログラミングやChatworkの活用に役立てば嬉しいです。