ランサーズ(Lancers)エンジニアブログ > ツール/ライブラリ > [Chatwork + GAS]ChatworkのファイルをGoogleDriveに格納する

[Chatwork + GAS]ChatworkのファイルをGoogleDriveに格納する

nagamine.ken|2023年10月24日
ツール/ライブラリ

こんにちは!corpITチームの、コーポレートエンジニアの長峰です。

ランサーズのcorpITチームでは主に情シス、社内ツール開発の領域を担当しており、今回は社内ツール開発の一例をご紹介できればと思います。

はじめに

Chatworkは優れたチャットツールであり、業務で活用している会社も多いと思います。今回はChatworkにアップロードしたファイルをGoogleDriveにバックアップする必要が生まれたため、作成した「Chatworkのファイル一括バックアップくん」のご紹介です!

Chatworkのファイル一括バックアップくんについて

ファイル一括バックアップくんの処理の流れは

  1. (手動)各Chatwork使用ユーザーにトークンを取得してもらう
  2. (バックアップくんでボタン操作)トークンを元に「自分に管理権限がある(そうでないと招待できない)全ての部屋」にバックアップ用管理アカウントを一括で招待する
  3. (バックアップくんでボタン操作)管理アカウントは各部屋(ソースコードではroomと呼称)にアップロードされているファイルをシート「file」に記載していく
  4. (トリガーにて1時間ごとに自動実行)シート「file」のファイルを1つずつGoogleDriveにアップロードしていく

となっています。

ソースコード

実際にソースコードにて、どのような処理なのかコメントでお伝えしていきます!

/**
* 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
];
}
}
view raw File.gs hosted with ❤ by GitHub
/**
* トリガーはこのファイルの中の処理しか設定しない。処理の概要を記載するようにする。
*/
// この処理を各ユーザーに実行してもらう。なお、実行の前にユーザーのトークンをシートに記載してもらう。
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);
});
}
view raw main.gs hosted with ❤ by GitHub
/**
* 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;
});
}
}
view raw Member.gs hosted with ❤ by GitHub
/**
* 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;
}
}
view raw Room.gs hosted with ❤ by GitHub
/**
* 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;
});
}
view raw sheet.gs hosted with ❤ by GitHub

シート

※IDや名前は例となっています

※API制限により、取得できるのは各roomの直近のファイル100件となります。

終わりに

これで各自のChatworkの部屋にアップロードされたファイルをGoogleDriveにバックアップできました。

GASのプログラミングでも上記ソースコードのように、オブジェクト指向で開発をすることが可能です!GASのプログラミングに興味がある方は参考にしてみてください。本記事が、GASのプログラミングやChatworkの活用に役立てば嬉しいです。