Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

macOSの通知リストをpython経由で取得しShellに流す

- はじめに -

macOSを使っていると、通知(Notifications)に色んな情報が集約される。
メールが来ただのSlackの更新だの、アプリのアップデート等がそれにあたる。
f:id:vaaaaaanquish:20181124204404p:plain
「OS通知を定期的に取得してシェルのbottom barとかに表示してやれば、作業中でも社内チャットやらメールの通知とかを厳選して気付けたりするしサイコーなのでは?」という所を目的にした作業ログを示すものである。

ちなみにこの記事は、Xonsh Advent Calendar 2018 - Qiitaの7日目の記事。

- macOSの通知の取得 -

2014年くらいにHigh Serriaになった時、ユーザ向けにもAPIが公開されたNotifications。それらを叩けるterminal-notifierや、そのPython wrapperとなるpyncなどが出てきて、ユーザが通知欄に任意のメッセージを送る事は難しくなくなった。
GitHub - julienXX/terminal-notifier: Send User Notifications on macOS from the command-line.
GitHub - SeTeM/pync: Python wrapper for Mac OS 10.8 Notification Center


macOSが提供するAPIでは「アプリXから通知を送る」「アプリXから送った通知を消す」「アプリXが送った通知のリストを取得する」が利用できるが、これらを利用しても「全てのアプリから送られた全ての通知を取得する」事はできない(記事執筆時、2018/11/24)。

完全な情報を取得する事はできないが、以下のpatreon記事によると、システムデータベースには通知の断片ログが残っているようである。patreon主はSignalや各所登壇で良く見るあのPatrick Wardle氏である。
www.patreon.com
Macのsystemのpythonに含まれるFoundationクラスを利用する事でこのログを読むことができる。*1

 
上記記事内のdumpNotificationDB.pyをベースに、通知毎のタイトルと内容と日付、アプリ名を取得し表示するスクリプトを以下に示す。

# -*- coding: utf-8 -*-
# /usr/bin/python (macOS system python)
import os
import sys
import sqlite3
import datetime
import tempfile
import Foundation


def parse_req(req):
    # reqをparseしtitleとbodyのみ取得する
    res = {}
    for x in str(req).split(';'):
        if 'body' in x or 'titl' in x:
            d = x.replace('{','').strip().split(' = ')
            res[d[0]] = d[1].replace('"','')
    return res


def get_notif_json():
    # DBファイルにアクセスしparseして返す
    notificationDB = os.path.realpath(
            tempfile.gettempdir() + '/../0/com.apple.notificationcenter/db2/db')
    conn = sqlite3.connect(notificationDB)
    conn.row_factory = sqlite3.Row
    cursor = conn.execute("SELECT data from record");

    res_j = []
    for row in cursor:
        plist, fmt, err = \
                Foundation.NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(
                buffer(row[0]),
                Foundation.NSPropertyListMutableContainers,
                None, None)
        if err is not None:
            continue

        notif_dic = {}
        for key, value in plist.iteritems() :
            if key == 'date':
                notif_dic['date'] = Foundation.NSDate.alloc().initWithTimeIntervalSinceReferenceDate_(value)
            if key == 'req':
                req = parse_req(value)
                if 'titl' in req.keys():
                    notif_dic['title'] = req['titl']
                notif_dic['body'] = req['body']
            elif key == 'app':
                notif_dic['app'] = value
        res_j.append(notif_dic)
    return res_j

# データ取得
d = get_notif_json()
# 日付順ソート
d = sorted(d, key=lambda x: x['date'])
# 表示
import codecs
for x in d:
    print x['date']
    print x['app']
    print codecs.decode(x.get('title','no title').lower(), 'unicode-escape')
    print codecs.decode(x['body'].lower(), 'unicode-escape')
    print ''

macOSのpython2系を利用するため、codecsを利用してunicodeをdecodeしたり、printがアレだったりするが仕方ない。

 
先述の通り、system-pyhtonのFoundationが必要なため、/usr/bin/pythonを利用して実行する。

$ /usr/bin/python get_notification.py

2018-11-14 14:58:05 +0000
_SYSTEM_CENTER_:com.apple.battery-monitor
バッテリー残量低下
電源コンセントに接続しない場合、お使いのmacは間もなくスリープ状態に入ります。

2018-11-16 23:27:59 +0000
com.adobe.acc.AdobeCreativeCloud
creative cloud
4 個のアプリのアップデートがあります

2018-11-21 10:08:50 +0000
com.tinyspeck.slackmacgap
teamy
miku : 完全に未来人扱いされてますね笑

2018-11-21 10:10:36 +0000
com.tinyspeck.slackmacgap
teamy
miku: オッケです!

あくまで通知時のログなので、既に通知欄から削除した、していないという情報は得られないが、概ね十分そうである。


 

- xonshを利用してshell上に表示する -

xonshのアドベントカレンダーなのでxonshを利用してshellに流し込む

上記スクリプトの出力部分を一行空白区切りで情報を出力するように修正します。

import codecs
for x in d:
    date = str(x['date']).split(' ')
    print date[0].encode('utf-8'), date[1].encode('utf-8'), x['app'].encode('utf-8'),\
            codecs.decode(x.get('title','no title').lower(), 'unicode-escape').replace(' ','').encode('utf-8'),\
            codecs.decode(x['body'].lower(), 'unicode-escape').replace('\n','').replace(' ', '').encode('utf-8')

そもそもコードも酷いというのもあるが、encodeしないと「UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-8: ordinal not in range(128)」みたいになるためさらに酷くなっているが仕方ない。
 
このpython2のスクリプトにおけるprintを標準出力から受け取り、xonshで表示できるような関数にする。
 
python3からsubprocess経由で/usr/bin/pythonを使い、上記ファイルを実行するスクリプトを以下に示す。

import subprocess

res = subprocess.run(['/usr/bin/python get_notification.py'], stdout=subprocess.PIPE, shell=True)
print(res.stdout)

これをxonshの関数にそのまま採用して情報を厳選して、xonsh toolbarに表示してみる。

 
python2と3を経由するかなり気持ち悪い感じだが出来たコマンドは以下のような感じ。

import subprocess

def get_last_notif():
    # 最後の通知を返す
    res = subprocess.run(['/usr/bin/python /path/to/get_notification.py'], stdout=subprocess.PIPE, shell=True)
    return res.stdout.decode('utf-8', 'replace').strip().split('\n')[-1]

# xonsh bottom toolbarに表示する
$BOTTOM_TOOLBAR = get_last_notif

f:id:vaaaaaanquish:20181124215447p:plain
(クソ…!よこせストレージを…!!)

 
このままだとUPDATE_COMPLETIONS_ON_KEYPRESSをTrueにしていれば、キー入力毎に評価され、DBを読みに行ってparseしてという処理がキー入力毎に発生してしまいますので、一度どこかにキャッシュして一定時間ごとに更新するなどの処理が必要そうである。

まあとりあえず目的は達成したので、キャッシュの話はまた別途という事にする。


 

- おわりに -

通知をキャッシュして、直近のものを順繰りで表示しておいたりすると便利そう。
ちゃんと書けばxontribみたいにできるかも…?

ただMacの通知取得の方法が若干アレなので、アップデートで使えなくなる可能性もあるし、素直にAppleが相応のAPIを公開してくれたほうが良さそうだなと思う。

 
xonshアドベントカレンダーの方がまだまだ後半ガラ空きという状態なので書いてほしいです。宜しくお願いします。
qiita.com

 
 

*1:記事内にもある通りsystem pythonが実行できる権限があれば過去の通知の一部を読めてしまうため脆弱性になり得る要素である