Slack フリープランでのメッセージ、ファイルの保存期間が 90日間 になって困った友人から相談を受けたのがきっかけ
- React.js + MUI でサクッと
- Slack からエクスポートした zip ファイルをアップロードするだけ
デモサイト : Slack Archive Viewer
何ができるの?
- Slackライクな UI 上でログが読めます
- 画像、ファイル等も表示および保存ができます
- ローカル(ブラウザ)にデータを保存できます1
作ってみて面倒だったこと
zip 解凍→マルチバイト文字列が文字化け
解凍には定番の zlib.js を使用
解凍後、チャンネル名が日本語の場合に文字化けが発生するので encoding.js を使用する事で回避
import Encoding from 'encoding-japanese';
import {Zlib} from '../node_modules/zlibjs/bin/unzip.min.js';
const unzipSlackExportFiles = (event, callback) => {
const zipfile = event.target.files?.[0] || null;
if(!zipfile){
alert('Please select a zip file');
return;
}
const slackName = zipfile.name.replace(/\sSlack\sexport.*zip$/, '');
const fileReader = new FileReader();
fileReader.onload = function(evt) {
const buffer = evt.target.result;
const uint8Array = new Uint8Array(buffer);
const unzip = new Zlib.Unzip(uint8Array);
const lists = unzip.getFilenames();
const slackData = {
setting : {},
channels : {},
workSpace: slackName,
};
lists.forEach(list => {
const name = Encoding.convert(list,"UNICODE","AUTO");
if(list.slice(-1) !== '/'){
console.log({name});
const dataAsU8Array = unzip.decompress(list);
const jsonString = Buffer.from(dataAsU8Array).toString('utf8');
const json = JSON.parse(jsonString);
const path = name.split('/');
if(path.length > 1){
const [
pathName,
fileName,
] = path;
const yyyymmdd = fileName.replace('.json', '');
if(slackData.channels[pathName] === undefined){
slackData.channels[pathName] = {};
}
slackData.channels[pathName][yyyymmdd] = json;
}else{
slackData.setting[name.replace('.json', '')] = json;
}
}
});
console.log({slackData});
callback({
[slackName] : slackData,
}, slackName);
};
fileReader.readAsArrayBuffer(zipfile);
}
ワークスペース名は json ファイルに含まれていないようなので、 zip のファイル名から取得
親スレッド / 子スレッドの判定
zipを解凍すると中身はこんな感じ
├── channels.json
├── integration_logs.json
├── users.json
└── {channel name}
├── YYYY-MM-DD.json
...
└── YYYY-MM-DD.json
最初に各チャンネルのログ (/{channel_name}/YYYY-MM-DD.json
) をざっと見た感じ下記の仕様でいけるように見えた
-
client_msg_id
+replies
パラメータを持つログが親スレッド -
client_msg_id
を持たないログが子スレッド
[{
"client_msg_id": "c5f2db7d-ae1a-46dc-8b7b-a5b90d3668ca",
"type": "message",
"text": "xxxxxx",
"user": "U017T8Z3U86",
"ts": "1618660451.001300",
"team": "T017YKHG8R3",
"user_team": "T017YKHG8R3",
"source_team": "T017YKHG8R3",
"user_profile": {...},
"replies": [
{
"user": "U01LC16DU72",
"ts": "1618708626.000100"
}
],
...
},{...}]
が、上記は間違いで下記の仕様に変更
-
replies
を持つログを親スレッド -
replies
配列からuser
+ts
でログを検索して子スレッドを判定
当初、親スレッドのログには必ず client_msg_id
が存在すると思っていたが無いログが散見された為、
client_msg_id
が存在しないログに関しては user
+ ts
を ID の代わりにした
ログのフォーマット
Slack からエクスポートしたデータの読み方 に最低限のフォーマットの解説はあリマス
添付ファイルの保存
エクスポートした zip には画像をはじめとした添付ファイル等は含まれない
データは Slackサーバー( https://files.slack.com/ )に保存されていて、 json ファイル内にパスが書いてあります
[{
"files": [
{
"id": "F040R2PCH40",
"created": 1661653151,
"timestamp": 1661653151,
"name": "image.png",
"title": "image.png",
"mimetype": "image\/png",
"filetype": "png",
"pretty_type": "PNG",
"user": "U040E0FV46M",
"editable": false,
"size": 209178,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https:\/\/files.slack.com\/files-pri\/T0403QUD0EQ-F040R2PCH40\/image.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"url_private_download": "https:\/\/files.slack.com\/files-pri\/T0403QUD0EQ-F040R2PCH40\/download\/image.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"media_display_type": "unknown",
"thumb_64": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_64.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_80": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_80.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_360": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_360.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_360_w": 360,
"thumb_360_h": 244,
"thumb_480": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_480.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_480_w": 480,
"thumb_480_h": 325,
"thumb_160": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_160.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_720": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_720.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_720_w": 720,
"thumb_720_h": 487,
"thumb_800": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_800.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_800_w": 800,
"thumb_800_h": 542,
"thumb_960": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_960.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_960_w": 960,
"thumb_960_h": 650,
"thumb_1024": "https:\/\/files.slack.com\/files-tmb\/T0403QUD0EQ-F040R2PCH40-e125949425\/image_1024.png?t=xoxe-4003844442500-4056197772560-4034953908116-4bc6374563bac811389d699126e26a02",
"thumb_1024_w": 1024,
"thumb_1024_h": 693,
"original_w": 1616,
"original_h": 1094,
"thumb_tiny": "AwAgADDTP0zTc\/7JpxpPxoAPwpcCkyOmaMigBcCijIooADnBwMmoGuoFYqzgEcHg1MxwpPAwO9V3tYXcswOSc\/eoAUXcBPDAnrwD\/hU4OQCOhqstpEpyuQSMZ3mrC\/KABjAFADqKTn2paAP\/2Q==",
"permalink": "https:\/\/export-eog3912.slack.com\/files\/U040E0FV46M\/F040R2PCH40\/image.png",
"permalink_public": "https:\/\/slack-files.com\/T0403QUD0EQ-F040R2PCH40-4466d3eace",
"is_starred": false,
"has_rich_preview": false,
"file_access": "visible"
}
]
}]
これらのデータもいずれは削除されて閲覧不可になるので保存したい...が、
- DB準備したりするの面倒
- fetchしてzipで圧縮してローカルに保存するにしてもデータサイズが予測できないので不安(最悪
JSON.parse
できるサイズを超える可能性大)
どこに保存するべきか悩んだ結果
ブラウザに内蔵されている IndexedDB に保存すれば良いじゃないか!
- 各種モダンブラウザで利用可能
- ストレージサイズは PC のストレージの 1/2 まで利用可能 (内蔵ストレージが 100GB なら 50GB まで利用可能すごいぜ)
素の IndexedDB API は扱いにくいので Dexie.js を使用
-
fetch
で slack サーバからファイルを取得 -
blob
データで取得してcontent-type
を取得 -
blob
データをbase64
に変換 - IndexedDB に保存
import Dexie from 'dexie';
const db = new Dexie('slackViewerDB');
db.version(1).stores({
store: '&id',
});
const fetchFile = async (params) => {
const {
id,
url,
} = params;
let successfulToSave = false;
const file = await fetch(url)
.then(res => {
if(res.status === 200){
return res.blob().then(blob => ({
contentType: res.headers.get("Content-Type"),
blob: blob
}));
}
throw new Error('File does not exist');
})
.catch(err => {
console.error(err)
return null;
});
if(file){
const {
blob,
contentType,
} = file;
const base64 = await convertBlobToBase64(blob);
db.store.put({
id : id,
contentType : contentType,
data : base64,
});
successfulToSave = true;
}
return {
id,
successfulToSave,
};
}
const convertBlobToBase64 = (blob) => new Promise((resolve, reject) => {
const reader = new FileReader;
reader.onerror = reject;
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(blob);
});
当然 CORS(Cross-Origin Resource Sharing error) エラーが発生する
ドメインが異なるslackサーバに対して fetch
するので CORS エラーが発生します
お行儀が悪いですが下記のいづれかの方法で回避
ブラウザの起動オプションを指定して回避
[PATH_TO_CHROME]chrome.exe" --disable-web-security --disable-gpu --user-data-dir=~/chromeTemp
open -n -a /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
CORS エラー回避アドオンをブラウザにインストール
- CORS Unblock (Chrome add-on)
90日間を超えてもログが見えるように
土日を使ってツール作った翌日、きれいにログが見えなくなってた
なお、ログが見えなくなった状態 = 即データ削除ではない模様
本ツールで https://files.slack.com/ にリクエストを実行したところ問題なくデータ取得して保存することができた
あらかじめデータのエクスポートさえ終えておけば、Slack上でログが見えなくなっても慌てる必要はなさそう
-
データの保存には CORS エラーを回避する必要があります ↩