Slackの過去ログをメール転送する

Slack は無料プランだと10000件までのメッセージしか保存してくれませんので、それ以前のメッセージは~~消えてなくなってしまいます~~。有償プランにすればもっと保存できますが、SlackのUIで古いメッセージを探すのはなかなか大変です。過去ログをメールで転送しておけば古いメッセージもずっと残しておけますし、検索もメールソフトで強力に行えます。ということで、過去ログをメール転送するPerlスクリプトを作ってみました。

(2020/2/7追記)

無料プランでの Slack の過去メッセージは消えてしまうわけではなく不可視になるだけでした。有料プランに切り替えることで取得することができます。

(2020/2/7追記終わり)

# Slackアプリの作成

Slack APIの記事を検索しているとレガシートークンを用いたものがよくみつかります。レガシートークンは全権限が付与されるので楽なんですが、現在は非推奨だそうです。ですのでSlackアプリの作成という方法を採ります。

[Building Slack apps](https://api.slack.com/slack-apps)にアクセスし、「Create a Slack App」というボタンをクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86075/891621b7-3d87-1ac9-6f82-ef8826491981.png)

「Create an App」というダイアログが表示されますので、「App Name」に作成するアプリの名称を入力し、「Development Slack Team」で対象となるSlack Teamを選択し、「Create App」をクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86075/9da634e2-21e0-5f57-4c29-43f71366c18c.png)

「Building Apps for Slack」画面で「Permissions」をクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86075/b31e5b11-1383-77a3-2299-6dd86e61d564.png)

「OAuth & Permissions」画面の少し下にある「Permission Scopes」で次のpermissionsを追加します。

* channels:history
* users.profiles:read

追加したら「Save Changes」をクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86075/4f8bc632-c644-2095-feaa-be37589b9969.png)

「Success!」と表示されたら、「OAuth Tokens & Redirect URLs」に戻って「Install App to Team」をクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86075/10b68651-0048-c7e7-d589-d0b180884c74.png)

しばらく待って準備が完了したら「Install」ボタンをクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86075/d0fce070-2dbe-cba8-f46c-1e24286aaa78.png)

インストールに成功すると、アクセストークンが表示されます。このトークンを使ってSlack APIにアクセスできます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86075/cdb5c4be-ed2d-b831-2ca9-2b69c88361e5.png)

# スクリプト

先ほど取得したトークンを下記のソースの `'token' => '<token>'` のところに貼り付ければOKです。

スクリプトの動作としては、引数無しで起動すると昨日の一日分の全てのチャネルの全てのメッセージを取得してメール送信します。引数で数字8ケタで日付(YYYYMMDD)を指定すると、その日付の一日分のメッセージをメール送信します。あとは詳しくはソースをご覧ください。

```perl
#!/usr/bin/perl

use strict;
use warnings;
use Encode;
use utf8;

use LWP::Simple;
use JSON::XS;
use Time::Local;
use Net::SMTP;

our $config = {
	'mail' => {
		'smtp' => 'smtp.gmail.com',
		'port' => 587,
		'user' => '<account>',
		'pass' => '<password>',
		'from' => '<account>'
	},
	'slack' => {
		'token' => '<token>'
	}
};

my $to = 'saoyagi2@gmail.com';

{
	my $targetdate;
	if( @ARGV > 0 && $ARGV[0] =~ /^\d{8}$/ ) {
		$targetdate = $ARGV[0];
	}
	else {
		my @t = localtime( time() - 86400 );
		$targetdate = sprintf( "%04d%02d%02d", $t[5] + 1900, $t[4] + 1, $t[3] );
	}
	my $oldest = timelocal( 0, 0, 0, substr( $targetdate, 6, 2 ), substr( $targetdate, 4, 2 ) - 1, substr( $targetdate, 0, 4 ) - 1900 );
	my $latest = $oldest + 86400;

	my $users_list = slack_api( 'users.list', $config->{'slack'}->{'token'} ) or die;
	my %members;
	for my $user ( @{$users_list->{'members'}} ) {
		$members{$user->{'id'}} = $user->{'name'};
	}

	my $channels_list = slack_api( 'channels.list', $config->{'slack'}->{'token'} ) or die;
	my %channels;
	for my $channel ( @{$channels_list->{'channels'}} ) {
		$channels{$channel->{'id'}} = $channel->{'name'};
	}

	my $body = '';
	foreach my $channel_id ( keys( %channels ) ) {
		$body .= sprintf( "### %s ###\n\n", $channels{$channel_id} );

		my @messages;
		while(1) {
			my $channel_history = slack_api( 'channels.history', $config->{'slack'}->{'token'}, {
				'channel'=>$channel_id,
				'oldest'=>$oldest,
				'latest'=>$latest,
				'inclusive'=>0
			} ) or die;
			push( @messages, @{$channel_history->{'messages'}} );
			if( !@{$channel_history->{'messages'}} || !$channel_history->{'has_more'} ) {
				last;
			}
			$latest = $channel_history->{'messages'}->[-1]->{'ts'};
		}

		for my $message ( reverse @messages ) {
			next if( $message->{'type'} ne 'message' );
			my @t = localtime( $message->{'ts'} );
			$body .= sprintf( "[%s]%02d:%02d:%02d\n", $members{$message->{'user'}}, $t[2], $t[1], $t[0] );
			$body .= $message->{'text'} . "\n\n";
		}

		$body .= "\n";
	}

	my $subject = sprintf( "[Slack]%d/%d/%d", 
		substr( $targetdate, 0, 4 ),
		substr( $targetdate, 4, 2 ),
		substr( $targetdate, 6, 2 ) );

	my $data = <<"DATA";
To: $to
Subject: $subject
Content-Type: text/plain; charset=\"UTF-8\"

$body
DATA

	my $mail = Net::SMTP->new( $config->{'mail'}->{'smtp'},
		Hello=>$config->{'mail'}->{'smtp'},
		Port=>$config->{'mail'}->{'port'}
	);
	$mail->starttls();
	$mail->auth( $config->{'mail'}->{'user'}, $config->{'mail'}->{'pass'} );
	$mail->mail( $config->{'mail'}->{'from'} );
	for my $_to ( split( ',', $to ) ) {
		$mail->to( $_to );
	}
	$mail->data();
	$mail->datasend( encode( 'UTF-8', $data ) );
	$mail->dataend();
	$mail->quit;
}

sub slack_api {
	my( $path, $token, $params_ref ) = @_;

	my $url = "https://slack.com/api/${path}?token=" . urlencode( $token );
	for my $key ( keys( %$params_ref ) ) {
		$url .= "&" . urlencode( $key ) . "=" . urlencode( $params_ref->{$key} );
	}
	my $retry = 3;
	while( $retry >= 0 ) {
		my $result = get( $url );
		if( defined( $result ) ) {
			return decode_json( $result );
		}
		$retry--;
	}
	return undef;
}

sub urlencode {
	my $str = shift;

	$str =~ s/([^ 0-9a-zA-Z])/"%".uc(unpack("H2",$1))/eg;
	$str =~ s/ /+/g;

	return $str;
}
```

(2017/8/17追記)

書き忘れてましたが、LWP::Simpleでhttps通信を行うには以下のモジュールが必要です。

* Net::SSLeay
* Crypt::SSLeay
* IO-Socket::SSL

全部必須かどうかはちょっと不確かです。適当に見つけた記事の記述に従って入れたら動いたので、とりあえずそのままにしてるというアバウトな状態でして（汗

あと、意外とタイムアウトでAPI呼び出しに失敗しているようなので、リトライを入れてみました。