Fork me on GitHub

2009/06/18

はてな
WindowsではGrowl For Windowsとそれが使っているプロトコルGNTPにより、Windowsでもアイコンを使ったGrowlアプリケーションの開発が可能になりました。その一つにmiyagawaさんが作ったgithub growlerのGNTP版でもあるyet another github growlerというのも作りました。
これでMac, Windowsでのgithub growlerがある事になるのですが、Linuxにありません。Linuxにはアイコンが表示できてGNTPプロトコルを喋るGrowlシステムがありません。
そこで以前から使っていた、Growlネットワークプロトコル(Growlネットワークプロトコルはアイコンが出せません)をサポートしているmumblesというGrowlシステムを調べて見たところ、内部ではpythonでDBusによるプロセス間通信を行っている事が分かりました。
mumbles-project.org

a plugin driven, modern notification system for Gnome

http://www.mumbles-project.org/
さらにそのDBusインタフェース上ではアイコン表示をサポートしていた為、これは!と思いGitHubのGrowlアプリケーションを作ってみました。
まず、DBusで通信する為のプラグインを作成します。
DBusでメソッドが呼ばれると、MumblesPluginクラスに渡されるMumblesNotifyオブジェクトのalertメソッドを呼び出します。
全体のソースは以下の様になります。
from MumblesPlugin import *
import dbus
import gnomevfs
import os
import urllib

class GithubMumbles(MumblesPlugin):
  plugin_name = 'GithubMumbles'
  dbus_interface = 'com.github.DBus'
  dbus_path = '/com/github/DBus'
  icons = {'github' : 'github.png'}
  __url = None

  def __init__(self, mumbles_notify, session_bus):
    self.signal_config = {
      'Notify': self.Notify,
      'NotifyNum': self.NotifyNum
    }
    MumblesPlugin.__init__(self, mumbles_notify, session_bus)
    self.add_click_handler(self.onClick)

  def NotifyNum(self, num):
    self.__url = 'http://github.com/'
    icon = self.get_icon('github')
    title = 'Github'
    msg = str(num)+' new messages!'
    self.mumbles_notify.alert(self.plugin_name, title, msg, icon)

  def Notify(self, link, author, text):
    self.__url = link
    path = os.path.join(PLUGIN_DIR_USER, 'icons', 'github-%s' % author)
    if os.path.exists(path):
      self.icons[author] = 'github-%s' % author
      icon = self.get_icon(author)
    else:
      icon = self.get_icon('github')
    self.mumbles_notify.alert(self.plugin_name, author, text, icon)

  def onClick(self, widget, event, plugin_name):
    if event.button == 3:
      self.mumbles_notify.close(widget.window)
    else:
      self.open_url(self.__url)

  def open_url(self, url):
    mime_type = gnomevfs.get_mime_type(url)
    application = gnomevfs.mime_get_default_application(mime_type)
    os.system(application[2] + ' "' + url + '" &')
インタフェースはリンク、作者、本文のみとしました。これをegg形式にビルドしてmumblesのpluginフォルダに置くと、上記のインタフェース呼び出しによりGrowlが表示されます。
MumblesPluginにはget_iconメソッドが用意されており、これにはアイコン名称を渡す事になります。実際にはplugin/iconsというフォルダにある名称のファイルが使用されるので、今回の仕組としてはgithubフィードのチェッカースクリプトでアイコンをplugin/iconsフォルダに格納させ、それを使用してプラグイン側が使用するという形になっています。プラグイン側でアイコンを取って来ても良いのですがアイコンをダウンロードしている最中はGrowlが固まってしまう為、今回の様な作りとなっています。
次にチェッカースクリプトですが以下の様なコードになります。
#!/usr/bin/env python

UPDATE_INTERVAL=1000 # 10 minutes
MAX_NOTIFICATIONS = 40
DEBUG = True
##################################################

import os
import sys
import time
import getopt
import rfc822
import calendar
import urllib
import feedparser
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
import gobject
from BeautifulSoup import BeautifulSoup
from pit import Pit

GITHUB_DBUS_INTERFACE = 'com.github.DBus'
GITHUB_DBUS_PATH = '/com/github/DBus'

config = Pit.get('github.com', {'require': {
    'user'  : 'user id on github.com',
    'token' : 'user token on github.com'
    }})

class Usage(Exception):
  def __init__(self, msg=None):
    app = sys.argv[0]
    if msg != 'help':
        self.msg = app+': Invalid options. Try --help for usage details.'
    else:
      self.msg = app+": DBus notifications on new github messages.\n"

class GithubCheck(dbus.service.Object):
  def __init__(self):
    session_bus = dbus.SessionBus()
    bus_name = dbus.service.BusName(GITHUB_DBUS_INTERFACE, bus=session_bus)
    dbus.service.Object.__init__(self, bus_name, GITHUB_DBUS_PATH)

    self.interval = UPDATE_INTERVAL
    self.notifyLimit = MAX_NOTIFICATIONS
    self.debug = DEBUG

    self.lastCheck = None
    self.minInterval = 60000 # 1 minute min refresh interval

    if self.interval < self.minInterval:
      print "Warning: Cannot check github more often than once a minute! Using default of 1 minute."
      self.interval = self.minInterval
    self._check()

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='sss')
  def Notify(self, link, author, text):
    pass

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='i')
  def NotifyNum(self, num):
    pass

  def _check(self):
    if self.debug:
      if self.lastCheck:
        print "checking feed (newer than %s):" %(self.lastCheck)
      else:
        print "checking feed:"
    try:
      items = feedparser.parse("http://github.com/%s.private.atom/?token=%s" % (config['user'], config['token']))['entries']
    except Exception, e:
      items = []

    if self.lastCheck:
      lastCheck = calendar.timegm(time.localtime(calendar.timegm(rfc822.parsedate(self.lastCheck))))
      for item in items:
        if calendar.timegm(item.published_parsed) < lastCheck:
          items.remove(item)

    self.lastCheck = rfc822.formatdate()
    num_notifications = len(items)

    if num_notifications > MAX_NOTIFICATIONS:
      if self.debug:
        print "%s new entries\n" %(num_notifications)
      self.NotifyNum(num_notifications)
    elif num_notifications < 0:
      if self.debug:
        print "no new entries\n"
    else:
      for item in items:
        path = os.path.join(os.path.expanduser('~'), '.mumbles', 'plugins', 'icons', 'github-%s' % item['author'])
        if not os.path.exists(path):
          html = urllib.urlopen('http://github.com/%s' % item['author']).read()
          soup = BeautifulSoup(html)
          img = soup.findAll('div', {'class':'identity'})[0].find('img')['src']
          img = img.replace("?s=50&", "?s=30&");
          urllib.urlretrieve(img, path)
        self.Notify(item['link'], item['author'], item['title'])
        time.sleep(6)
    gobject.timeout_add(self.interval,self._check)

if __name__ == '__main__':
  DBusGMainLoop(set_as_default=True)
  try:
    try:
      opts, args = getopt.getopt(
        sys.argv[1:], "hp", ["help"])
    except getopt.GetoptError:
      raise Usage()

    for o, a in opts:
      if o in ("-h", "--help"):
        raise Usage('help')
      else:
        raise Usage()
  except Usage, err:
    print >> sys.stderr, err.msg
    sys.exit(2)

  t = GithubCheck()
  try:
    loop = gobject.MainLoop()
    loop.run()
  except KeyboardInterrupt:
    print "githubcheck shut down..."
  except Exception, ex:
    print "Exception in githubcheck: %s" %(ex)
だらだらとしたコードですが、大体分かってもらえるかと思います。pitを使っているので初回起動のみユーザとトークンをエディタで入力する必要があります。トークンはGitHubのダッシュボードにあるRSSアイコンのリンク先URLに含まれています。

実行してしばらくすると以下の様な画面が表示されます。
mumbles-github-growler
これで快適になりました。
github上で全てのソースを公開しています。
mattn's mumbles-github-growler at master - GitHub

github growler using mumbles plugin and checker script.

http://github.com/mattn/mumbles-github-growler/tree/master
よろしければどうぞ。

2009/03/31

はてな
Growl For Windowsのコミットビットを貰ったので、国際化の為のresxファイルを書いた。
growl4windows-japanese
次のリリースあたりには降りてくるはず。
その他の活動としてはC言語で書いたGNTP送信プログラムgntp-sendを書いた。
mattn's gntp-send at master - GitHub
あと、perlモジュールGNTP::Growlは暗号化送信出来る様になった。現状AESとDESに対応しています。3DESはまだ未対応です。
mattn's perl-gntp-growl at master - GitHub
ちなみに、GrowlのソースコードにはブランチですがGNTPなコードが入っているっぽいので、もしかしたら上記のツール類でMacにGrowl出来る日が来るのではないか...と期待しています。

2009/03/23

はてな
書いた。
use strict;
use warnings;
use lib qw/lib/;
use GNTP::Growl;

my $growl = GNTP::Growl->new(AppName => "my perl app");
$growl->register([
    { Name => "foo", },
    { Name => "bar", },
]);

$growl->notify(
    Event => "foo",
    Title => "おうっふー おうっふー",
    Message => "大事な事なので\n2回言いました",
    Icon => "http://mattn.kaoriya.net/images/logo.png",
);
こんなソースで
perl-gntp-growl
こんな物が動く。
開発はこの辺で...
mattn's perl-gntp-growl at master - GitHub

はてな
これは嬉しい!Growl for Windowsの2.0 BETAがリリースされています。

これまではGoogle Codeにあるプロジェクトの物を使っていましたが、どうやらこれの正式版がリリース(とは言ってもBETAですが)されていました。
試しにこれまでのコードで試してみてもNetGrowlであれば問題なく動作しました。
そして気になっていたアイコンですが...

出るようになっています!

ただ、MacのGrowlの様にMac専用のインタフェースを使っているのではなく、TCP/IP接続による独自プロトコルGNTPを使っています。

ちょっと調べた感じだと、簡単なソケット通信で実現出来そうだったので適当ですがコマンドラインプログラムを作ってみました。
/**
 * msvc: cl growlc.c
 * mingw32: gcc -o growlc.exe growlc.c -lws2_32
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <locale.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#endif

static void sendline(int sock, const char* str, const char* val) {
    int len = strlen(str);
    char* line;
    if (val) {
        len += strlen(val);
        line = (char*) malloc(len + 3);
        strcpy(line, str);
        strcat(line, val);
        strcat(line, "\r\n");
    } else {
        line = (char*) malloc(len + 3);
        strcpy(line, str);
        strcat(line, "\r\n");
    }
    send(sock, line, len + 2, 0);
    free(line);
}

static char* recvline(int sock) {
    const static int growsize = 80;
    char c = 0;
    char* line = (char*) malloc(growsize);
    int len = growsize, pos = 0;
    while (line) {
        if (recv(sock, &c, 1, 0) <= 0) break;
        if (c == '\r') continue;
        if (c == '\n') break;
        line[pos++] = c;
        if (pos >= len) {
            len += growsize;
            line = (char*) realloc(line, len);
        }
    }
    line[pos] = 0;
    return line;
}

int create_socket() {
    int sock = -1;
    struct sockaddr_in serv_addr;
    struct hostent* host_ent;
    char value = 1;

    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("create socket");
        printf("%d\n", WSAGetLastError());
        return -1;
    }

    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));

    if ((host_ent = gethostbyname("127.0.0.1")) == NULL) {
        perror("gethostbyaddr");
        return -1;
    }
    memset((char *)&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    memcpy(&serv_addr.sin_addr, host_ent->h_addr, host_ent->h_length );
    serv_addr.sin_port = htons((short)23053);

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connect");
        return -1;
    }
    return sock;
}

static void close_socket(int sock) {
#ifdef _WIN32
    if (sock < 0) closesocket(sock);
#else
    if (sock < 0) close(sock);
#endif
}

static char* string_to_utf8_alloc(const char* str) {
#ifdef _WIN32
    unsigned int codepage;
    size_t in_len = strlen(str);
    wchar_t* wcsdata;
    char* mbsdata;
    size_t mbssize, wcssize;

    codepage = GetACP();
    wcssize = MultiByteToWideChar(codepage, 0, str, in_len,  NULL, 0);
    wcsdata = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
    wcssize = MultiByteToWideChar(codepage, 0, str, in_len, wcsdata, wcssize + 1);
    wcsdata[wcssize] = 0;

    mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsdata, -1, NULL, 0, NULL, NULL);
    mbsdata = (char*) malloc((mbssize + 1));
    mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsdata, -1, mbsdata, mbssize, NULL, NULL);
    mbsdata[mbssize] = 0;
    free(wcsdata);
    return mbsdata;
#else
    return strdup(str);
#endif
}

int main(int argc, char* argv[]) {
    int sock = -1;
    char* title = NULL;
    char* message = NULL;
    char* icon = NULL;
#ifdef _WIN32
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0) goto leave;
    setlocale(LC_CTYPE, "");
#endif

    if (argc != 3 && argc != 4) {
        fprintf(stderr, "%s: title message [icon]", argv[0]);
        exit(1);
    }

    title = string_to_utf8_alloc(argv[1]);
    message = string_to_utf8_alloc(argv[2]);
    if (argc == 4) icon = string_to_utf8_alloc(argv[3]);

    sock = create_socket();
    if (sock == -1) goto leave;
    sendline(sock, "GNTP/1.0 REGISTER NONE", NULL);
    sendline(sock, "Application-Name: My Example", NULL);
    sendline(sock, "Notifications-Count: 1", NULL);
    sendline(sock, "", NULL);
    sendline(sock, "Notification-Name: My Notify", NULL);
    sendline(sock, "Notification-Display-Name: My Notify", NULL);
    sendline(sock, "Notification-Enabled: True", NULL);
    sendline(sock, "", NULL);
    while (1) {
        char* line = recvline(sock);
        int len = strlen(line);
        /* fprintf(stderr, "%s\n", line); */
        free(line);
        if (len == 0) break;
    }
    close_socket(sock);

    sock = create_socket();
    if (sock == -1) goto leave;
    sendline(sock, "GNTP/1.0 NOTIFY NONE", NULL);
    sendline(sock, "Application-Name: My Example", NULL);
    sendline(sock, "Notification-Name: My Notify", NULL);
    sendline(sock, "Notification-Title: ", title);
    sendline(sock, "Notification-Text: ", message);
    if (icon) sendline(sock, "Notification-Icon: ", icon);
    sendline(sock, "", NULL);
    while (1) {
        char* line = recvline(sock);
        int len = strlen(line);
        /* fprintf(stderr, "%s\n", line); */
        free(line);
        if (len == 0) break;
    }
    close_socket(sock);
    sock = 0;

leave:
    if (title) free(title);
    if (message) free(message);
    if (icon) free(icon);

#ifdef _WIN32
    WSACleanup();
#endif
    return (sock == 0) ? 0 : -1;
}
mingw32でもMSVCでもビルド出来るかと思います。サンプルとしては第一引数から、タイトル、メッセージ、アイコン(省略可能)となっています。
アイコンは、Growl本体側が取得してくれるようなので例えば
C:¥> growlc パンが焼けました 黒焦げです http://mattn.kaoriya.net/images/logo.png
とすると
growl4windows-example
といったGrwolが表示されます。

すばらしいですね!
黒焦げですね!

あとはMacのGrowlとWindowsのGrowlが抽象化される様なレイヤが揃えば、Growlを使ったマルチプラットフォームなアプリケーションが作れる様になりますね。

2008/08/06

はてな
以前試した時は出来なかったんですが、今日試したら動く様になっていました。
growl-for-windows - Google Code

A port of the Mac app Growl for use on Windows machines

http://code.google.com/p/growl-for-windows/
バージョンが1.1から1.2に上がったからかな?
Growl for Windows 1.2
以下のコードで動きました。
まずperl
use strict;
use warnings;
use Encode;
use Net::Growl;
register(host => 'localhost',
         application=>"My Perl App",
         password=>'Realy Secure', );
notify(
       application=>"My Perl App",
       title=>'warning',
       description=>decode_utf8('あめんぼ赤いなアイウエオ'),
       priority=>2,
       sticky=>0,
       password=>'Realy Secure',
);
そしてpython
#!/usr/bin/python
#-*- coding:utf-8 -*-
import Growl
g = Growl.GrowlNotifier(
    applicationName='My Python App',
    notifications=["PyGrowl"],
    defaultNotifications=[0],
    hostname="localhost",
    password="Realy Secure")
g.register()
g.notify(
    icon=open('unk.gif').read(),
    noteType="PyGrowl",
    title='wanings',
    description=u"あめんぼ赤いなアイウエオ",
    sticky=False)
あれ?1.1の時にもdecode_utf8とかu""とか試したんですけどねぇ...。やっぱり1.2になったから?

それと今日、whineというWindowsで動くGrowlアプリケーションを、某カレー通の方に教えて頂きました。ありがとうございました。
Whine
Growl for Windowsではフォント等、ディスプレイのカスタマイズが出来ないのですがwhineでは出来るので、こちらを使おうかと思います。
猫派の私ですが、これはお勧めです。