先週アメリカ合衆国のトランプ大統領が来日しましたね。そこで今回はトランプ大統領のツイートを
そもそも「biff」とは
「biff」
- comsatデーモンは定期的に
/var/
からログインユーザーのリストを取得しておくrun/ utmp - MTAがメールを受け取る
- MTAがcomsatデーモンに
「受信者」 「メールボックスファイル名」 「受信したメールのファイル上のオフセット」 をUDPで通知する - comsatデーモンは
「受信者」 がログインしているか確認する - comsatデーモンは受信者が使用している端末ファイル
( /dev/
やttyX /dev/
)pts/ X のbiffビットが立っているか確認する - biffビットが立っている端末ファイルに対して、
メールの 「From:」 と 「Subject:」、 さらに本文の一部を書き込む - 先頭7行もしくは560文字の小さいサイズに到達した時点で書き込みを終了する
要するに
しかしながら
ところでこのオリジナルのbiffコマンドとcomsatデーモンそのものはパッケージリポジトリに残っています。そこで今回はあえてこのbiffコマンドを使うことで、
正直に言うと
biffのインストール
biffとcomsatデーモンはともにbiffパッケージに含まれています。よってインストールは簡単です。
$ sudo apt install biff (中略) biff (1:0.17.pre20000412-5) を設定しています ... grep: /etc/inetd.conf: そのようなファイルやディレクトリはありません grep: /etc/inetd.conf: そのようなファイルやディレクトリはありません
最後にエラーが出ていますが、/etc/
の中身を確認した上でupdate-inetd
コマンドを実行しようとするものの、/etc/
が存在しないために上記のようなgrepコマンドのエラーが出ています。本来はbiffパッケージがxinetdなどに依存しておくべきですが、
inetdとsystemd
そう、
biff dgram udp wait root.tty usr/sbin/in.comsat comsat
/etc/
ファイルを確認するとbiffサービスについて記載されていることがわかります。
$ grep biff /etc/services biff 512/udp comsat
inetd経由で起動するサービスの特徴のひとつが、
cc = recv(0, msgbuf, sizeof(msgbuf) - 1, 0);
if (cc <= 0) {
if (errno != EINTR) sleep(1);
continue;
}
このためinetd用のサービスをそのまま実行してもうまく動きません。comsatデーモンを動かすためにはinetdサービスが必要になります。ここで素直にxinetdをインストールしてもいいのですが、
まずsocket unitを作成します。/etc/
」
[Unit]
Description=Biff Comsat
[Socket]
ListenDatagram=[::1]:512
[Install]
WantedBy=sockets.target
内容はごくごくシンプルで、
ListenDatagramのアドレス部分には[::1]:512
」
- 「/」
で始まる場合はUnixドメインソケットのパスとして使われます - 「@」
で始まる場合はLinuxの抽象ソケットアドレスとして扱います - 単一の数字の場合はポート番号として扱い、
システム上のすべてのインターフェースをlistenします - 「A.
B.C. D:PORT」 の場合はIPv4アドレスとポート番号として指定したアドレスでlistenします - IPv6表記の場合はIPv6でlistenしますが、
設定によってはIPv4アドレスも同時にlistenします
上記の例ではIPv6表記のループバックアドレスを指定しているため、
次にこのsocket unitにパケットが届いたときにアクティベートする本体のサービスファイルを/etc/
」
[Unit]
Description=Biff Service
Requires=biff.socket
[Service]
Type=simple
ExecStart=/usr/sbin/in.comsat
StandardInput=socket
StandardError=journal
TimeoutStopSec=5
[Install]
WantedBy=multi-user.target
見た目はごくごく普通のサービスファイルですね。comsatデーモン自身はforkしないのでサービスタイプはType=simple
」Requires
には先程作成したbiff.
を指定しています。
inetdライクなサービスにするためにStandardInput=socket
」send()
やwrite()
を呼びません)。よってStandardOutput
は設定していません。一般的なinetdサービスであれば、StandardOutput
も設定する必要があるでしょう。
biff.
$ sudo systemctl daemon-reload $ sudo systemctl start biff.socket
これで512番ポートにパケットが届くとcomsatが起動するようになりました。
biffのテスト
実際にbiffパケットをcomsatデーモンに送って試してみましょう。biffのパケットは次のようなフォーマットになっています
送信先ユーザー名@ファイルオフセット:ファイル名
「ユーザー名」
「ファイルオフセット」/usr/
の_PATH_
」/var/
」/var/
」
「/var/
」mail
コマンドで表示できます。一般的には
とりあえず適当なメールボックスとメッセージを作成しておきましょう。
$ echo -e "From test\n\nline1\nline2\nline3\nline4\nline5\n" | sudo tee /var/mail/$USER From test line1 line2 line3 line4 line5 $ sudo chown $USER: /var/mail/$USER
端末ファイルのbiff
コマンドでオン・chmod
で代用してもかまいません。とりあえず手元の端末でオンにしてみましょう。デスクトップ環境の端末エミュレーター上のような仮想端末でも動作します。
$ biff y $ ls -l `tty` crwx-w---- 1 shibata tty 136, 33 11月 12 09:39 /dev/pts/33
これでcomsatデーモンがこの端末上にメッセージを表示できるようになりました。では、/dev/
」
$ echo -n "shibata@0:shibata" > /dev/udp/::1/512 New mail for shibata@ubuntu-desktop has arrived: ---- line1 line2 line3 line4 line5 ----
無事にオフセット0、biff y
」biff n
」
特定のユーザーのツイートを取得するスクリプト
仕組みさえわかってしまえば、
Twitterの情報を取得する方法はいろいろありますが、
$ sudo apt install python3-twython
ただしemail.
あらかじめapps.
今回使うPythonスクリプトは次のとおりです。
#!/usr/bin/env python3
from twython import Twython, TwythonStreamer
import time
import sys
from email.message import EmailMessage
import mailbox
import socket
APP_KEY = 'Consumer Key'
APP_SECRET = 'Consumer Secret'
OAUTH_TOKEN = 'Access Token'
OAUTH_TOKEN_SECRET = 'Access Token Secret'
USERS=['realdonaldtrump']
SEND_TO = 'shibata'
HOST = '::1'
PORT = 512
class TwitterStreamer(TwythonStreamer):
def __init__(self, *args, **kwargs):
self.follow = kwargs.pop('follow')
super(TwitterStreamer, self).__init__(*args, **kwargs)
def on_success(self, data):
try:
if data['user']['id_str'] in self.follow:
offset = self.save_mbox(data['user']['screen_name'], data['text'])
self.send_biff(SEND_TO, offset)
except KeyError:
pass
def on_error(self, status_code, data):
sys.stderr.write("failed connect to stream: {}\n".format(status_code))
self.disconnect()
sys.exit(1)
def save_mbox(self, user, text):
msg = EmailMessage()
from_line = 'From Twitter ' + time.asctime(time.gmtime())
msg.set_unixfrom(from_line)
msg.set_content(text)
msg['From'] = user
mb = mailbox.mbox(SEND_TO, create=True)
mb.lock()
key = mb.add(msg)
mb.flush()
mbf = mb.get_file(key)
offset = mbf._start
mb.unlock()
mb.close()
return offset
def send_biff(self, user, offset):
msg = "{user}@{offset}:{file}".format(user=user, offset=offset, file=user)
addr = socket.getaddrinfo(HOST, PORT, proto=socket.IPPROTO_UDP)
s = None
for res in socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC, socket.SOCK_DGRAM):
af, socktype, proto, canonname, sa = res
try:
s = socket.socket(af, socktype, proto)
except OSError as msg:
s = None
continue
try:
s.connect(sa)
except OSError as msg:
s.close()
s = None
continue
break
if s is None:
return
with s:
s.sendall(msg.encode('utf-8'))
twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET)
users = twitter.lookup_user(screen_name=USERS)
follow = [ x['id_str'] for x in users ]
stream = TwitterStreamer(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET, follow=follow)
stream.statuses.filter(follow=follow)
USERS
はツイートを取得したいユーザーのscreen_
のリストです。SEND_
が送信対象のユーザー名になります。HOST
がcomsatデーモンのアドレスPORT
がそのポート番号です。
Twython
とTwythonStreamer
についてはTwythonのドキュメントを見てもらうほうがはやいでしょう。ほぼチュートリアルどおりの構成です。引数にfollow
を追加しているのは、on_
で送信者がfollow
にリストアップしたユーザーのときのみcomsatに通知するようにしています。
save_
とsend_
がbiff固有の処理です。save_
ではemail.
で電子メールのメッセージを作成しています。最初の行にFrom:
」screen_
を残しています。
def save_mbox(self, user, text):
msg = EmailMessage()
from_line = 'From Twitter ' + time.asctime(time.gmtime())
msg.set_unixfrom(from_line)
msg.set_content(text)
msg['From'] = user
mb = mailbox.mbox(SEND_TO, create=True)
mb.lock()
key = mb.add(msg)
mb.flush()
mbf = mb.get_file(key)
offset = mbf._start
mb.unlock()
mb.close()
return offset
mbox形式のファイルはmailboxライブラリーのmailbox.
で操作できます。add()
メソッドでメッセージを簡単に追加できるのですが、get_
メソッドが存在しますので、_start
プロパティで代用しています。アンドキュメンテッドなプロパティを使いたくない場合は、from_
で検索して算出してもいいかもしれません。
保存先SEND_
)/var/
までスクリプトの中で含めてしまうという方法もあるでしょう。
send_
の方はごく普通のネットワークプログラミングです。以下のフォーマットのメッセージを送っています。
msg = "{user}@{offset}:{file}".format(user=user, offset=offset, file=user)
サービスファイルの作成
先ほど作成したスクリプトもsystemdから起動するように/etc/
」
[Unit]
Description=Trump Logger
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=shibata
Group=mail
WorkingDirectory=/var/mail
ExecStart=/usr/local/bin/trump.py
[Install]
WantedBy=multi-user.target
network-online.
を使うことで、
$ sudo systemctl start trump.service
あとはbiff y
した端末の前で靴紐がほどけているか確認しながら待っていると、
ちなみにcomsatは渡された文字列に対してtoascii()
で最上位ビットを落とそうとします。つまり日本語や絵文字はまったく表示できません。ただemail.
の時点でBase64エンコードするため、base64
コマンドでデコードすれば日本語や絵文字に戻すことは可能です。