XMPPボット-リマインダーボットの作成

Last-modified: 2014-08-16 (土) 12:41:50 (2013d)

[ 前のページ : XMPPボット-メモボットの作成 | ]

登録した時刻が来た時に、登録したメッセージを伝えてくれる、いわゆる「リマインダー」をXMPPのボットで作成します。

電子メールでのリマインダーはよく見かけますが、PCにいつも向かっている人にとっては、IMは電子メールよりもリマインダーの通知に向いていると思います。リアルタイム性や、通知のわかりやすさ等の点において。

ただ、作ってみてわかったのですが、文字入力では時刻の指定が大変です。今回のものは、cron式の登録方法を採用し、たとえば 1/15 13:30 にメッセージがほしい場合は、

set 30 13 15 1 メッセージ

と入力するようにしましたが、cronを普段いじらない人にはわかりにくいでしょう。

なお、後方のパラメータを省略できるため、

set 30 メッセージ

とすると、次の30分にメッセージが出るようになります。

入力に関しては、webの登録画面を作った方が便利かもしれません。

pythonのスレッドを使ってタイマーチェックをしています。毎分ごとにDBをオープンクローズするため、オーバーヘッドが若干心配です。常駐が数プロセスであれば問題ないでしょう。

xmpp-reminderbot.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# XMPPリマインダーボット
#


import xmpp
import sqlite3
import time
import thread
import config as CONFIG


# DB設計
# 
# [reminder] テーブル
# 
# seq_id   (integer) ID,オートインクリメント
# user     (text)    登録ユーザー
# textdata (text)    文字情報
# smonth   (integer) 予定月(-1で無効(ワイルドカード))
# sdate    (integer) 予定日(-1で無効(ワイルドカード))
# shour    (integer) 予定時(-1で無効(ワイルドカード))
# sminute  (integer) 予定分(-1で無効(ワイルドカード))
# charge   (integer) 残り告知回数(-1で無限大、0で告知しない)
# ctime    (integer) 登録日時タイムスタンプ

db=sqlite3.connect(CONFIG.dbFileName)

# Table check
c = db.cursor()
c.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='reminder'")
if c.fetchone()[0] == 0 :
	# テーブルが存在しない(=初回起動)。作成する。
	
	sql="""CREATE TABLE reminder (
	 seq_id   INTEGER NOT NULL PRIMARY KEY ,
	 user     TEXT,
	 textdata TEXT,
	 smonth   INTEGER,
	 sdate    INTEGER,
	 shour    INTEGER,
	 sminute  INTEGER,
	 charge   INTEGER,
	 ctime    INTEGER
	 )"""
	c = db.cursor()
	c.execute(sql)
	db.commit()
	print "##### DB create."
	
c.close()

c1 = db.cursor()

def timerWork(conn,t):
	db4Timer=sqlite3.connect(CONFIG.dbFileName)
	sql = """SELECT seq_id,user,textdata,smonth,sdate,shour,sminute,charge
	 FROM reminder
	 WHERE (smonth  = ? or smonth  < 0)
	   and (sdate   = ? or sdate   < 0)
	   and (shour   = ? or shour   < 0)
	   and (sminute = ? or sminute < 0)
	   and charge  != 0
	"""
	c2 = db4Timer.cursor()
	c2.execute(sql,(t.tm_mon,t.tm_mday,t.tm_hour,t.tm_min))
	for record in c2:
		#print record
		seq_id  = record[0]
		user    = record[1]
		message = record[2]
		charge  = record[7]
		conn.send(xmpp.Message(user,message))
		
		#chageを減算
		if charge > 0 :
			sql = "UPDATE reminder SET charge = charge - 1 WHERE seq_id = ?"
			c2.execute(sql,(seq_id,))
			db4Timer.commit()
		
	c2.close()
	db4Timer.close()



def parseMessage(conn,mess):
	global db
	
	text=mess.getBody()
	
	userFull = unicode(mess.getFrom())
	if userFull.find('/')+1: user,userResource=userFull.split('/',1)
	else: user,userResource=userFull,''
	
	if text is  None: return
	
	if text.find(' ')+1: command,args=text.split(' ',1)
	else: command,args=text,''
	command=command.lower()
	
	if command == "stat":
		reply = u"[stat]\n"
		c = db.cursor()
		recordCount = c.execute("SELECT count(*) FROM reminder").fetchone()[0]
		c.close()
		reply = u"総レコード数 : "+str(recordCount)
	
	elif command == "all" or command == "list" or command == "ls":
		reply = u"[all]\n"
		sql="SELECT seq_id,textdata,ctime,smonth,sdate,shour,sminute,charge FROM reminder WHERE user = ? ORDER BY ctime"
		c = db.cursor()
		c.execute(sql,(user,))
		for record in c:
			#print "####",record
			#strTime = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(record[2]))
			
			reply += "[ "+str(record[0])+" ] "
			#reply += t2s(record[3])+"/"+t2s(record[4])+" "+t2s(record[5])+":"+t2s(record[6])+" ("+t2s(record[7])+") "
			reply += makeDateString(record[3],record[4],record[5],record[6],record[7])+" "
			reply += record[1]+"\n"
		c.close()
	
	elif command == "del":
		reply = u"[del]\n"
		if args.isdigit():
			num = int(args)
			sql="SELECT seq_id,textdata,ctime,smonth,sdate,shour,sminute,charge FROM reminder WHERE seq_id = ? and user = ?"
			c = db.cursor()
			c.execute(sql,(num,user,))
			record = c.fetchone()
			c.close()
			
			#print "#### record = ",record
			if record == None:
				reply += u"削除対象が存在しません。"
			else:
				#strTime = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(record[2]))
				reply += "[ "+str(record[0])+" ] "
				reply += makeDateString(record[3],record[4],record[5],record[6],record[7])+" "
				reply += record[1]+"\n"
				
				sql="DELETE FROM reminder WHERE seq_id = ? and user = ?"
				c = db.cursor()
				c.execute(sql,(num,user))
				c.close()
				db.commit()
				reply += u"削除しました。"
			
		else:
			reply += u"文法: del 削除するID"
		
	elif command == "charge":
		reply = u"[charge]\n"
		
		success = False
		
		if args :
			tmpArg = args.split(' ')
			
			if len(tmpArg) :
				if len(tmpArg) == 1:
					tmpArg += ["1"]
				
				if tmpArg[0].isdigit() and tmpArg[1].isdigit():
					seq_id      = int(tmpArg[0])
					chargeCount = int(tmpArg[1])
					sql="SELECT seq_id,textdata,ctime,smonth,sdate,shour,sminute,charge FROM reminder WHERE seq_id = ? and user = ?"
					c = db.cursor()
					c.execute(sql,(seq_id,user))
					record = c.fetchone()
					c.close()
					
					#print "#### record = ",record
					
					if record == None:
						reply += u"対象が存在しません。"
						success = True
					else:
						reply += "[ "+str(record[0])+" ] "
						reply += makeDateString(record[3],record[4],record[5],record[6],record[7])+" "
						reply += record[1]+"\n"
						
						sql="UPDATE reminder SET charge = charge + ? WHERE seq_id = ? and user = ?"
						c = db.cursor()
						c.execute(sql,(chargeCount,seq_id,user))
						c.close()
						db.commit()
						reply += u"チャージしました。"
						success = True
		if success == False : 
			reply += u"文法: charge <チャージするID> <チャージ数>"
		
	elif command == "set":
		reply = u"[set]\n"
		#タイマーに登録する
		# 分 時 日 月 回数 テキストメッセージ
		sheduleField = args.split(' ',5)
		#print sheduleField
		message = ""
		
		parsedField = [-1,-1,-1,-1,-1]
		#replyField  = [u"*",u"*",u"*",u"*",u"*"] #リプライメッセージ用
		
		#print "Length=",len(sheduleField)
		#print "replyField=",replyField
		#フィールドを解析
		for i in range(5):
			#print "i=",i
			if len(sheduleField) <= i : continue
			if sheduleField[i].isdigit() :
				parsedField[i] = int(sheduleField[i]) #数字なら分として登録
				#replyField[i]  = sheduleField[i]
			else :
				parsedField[i] = -1 #数字でないならワイルドカード
				#replyField[i]  = u"*"
				if "*-+".find(sheduleField[i])+1 :
					pass #ワイルドカード風文字なら無視して
				else :
					message += sheduleField[i]+" " #そうでないならメッセージとして登録
					sheduleField[i] = "" #連続登録を防ぐため削除
		
		message += sheduleField[-1]
		#print "replyField=",replyField
		
		if len(message) :
			timeStamp = time.mktime(time.localtime())
			sql="INSERT INTO reminder (user,textdata,ctime,smonth,sdate,shour,sminute,charge) VALUES (?,?,?,?,?,?,?,?)"
			c = db.cursor()
			c.execute(sql,(user,message,timeStamp,parsedField[3],parsedField[2],parsedField[1],parsedField[0],parsedField[4]))
			c.close()
			db.commit()
			#reply += u"[ "+replyField[3]+u"/"+replyField[2]+u" "+replyField[1]+u":"+replyField[0]+u" x"+replyField[4]+"] "+message
			#reply += u"[ "+t2s(parsedField[3])+u"/"+t2s(parsedField[2])+u" "+t2s(parsedField[1])+u":"+t2s(parsedField[0])+u" ("+t2s(parsedField[4])+")] "+message
			reply += makeDateString(parsedField[3],parsedField[2],parsedField[1],parsedField[0],parsedField[4])+" "+message
			reply += u" 登録しました\n"
		else :
			reply += u"文法: set <分> [時] [日] [月] [回数] <メッセージ>\n"
			reply += u"予定を登録する。各項目は * とするとワイルドカード指定となる。\n"
			reply += u"回数は、告知毎に1づつ減っていき、0となるとそれ以上告知を行わない。\n"
	
	elif command == "min":
		reply = u"[min]\n"
		#タイマーに登録する
		# 分 テキストメッセージ
		
		success = False
		if args :
			sheduleField = args.split(' ',1)
			#print sheduleField
			
			if len(sheduleField) >= 2 :
				if sheduleField[0].isdigit() and sheduleField[1] :
					
					min = int(sheduleField[0])
					message = sheduleField[1]
					
					timeStamp = time.mktime(time.localtime())
					t = time.localtime(timeStamp +  (60 * min))
					sql="INSERT INTO reminder (user,textdata,ctime,smonth,sdate,shour,sminute,charge) VALUES (?,?,?,?,?,?,?,?)"
					c = db.cursor()
					c.execute(sql,(user,message,timeStamp,t.tm_mon,t.tm_mday,t.tm_hour,t.tm_min,1))
					c.close()
					db.commit()
					reply += makeDateString(timeStamp,t.tm_mon,t.tm_mday,t.tm_hour,1)+" "+message
					reply += u" 登録しました\n"
					success = True
		if success == False :
			reply += u"文法: min <分> <メッセージ>\n"
			reply += u"<分>分後の予定を登録する。\n"
			reply += u"ただし、時刻の「分」の変更で判断するため、指定時刻より最大60秒短くなる可能性がある。\n"
	
	else:
		reply = u"""[help]
stat : ステータスを表示
all  : 全登録レコードを表示
del <ID> : レコード<ID>を削除
charge <ID> [回数] : レコード<ID>の回数をチャージ

set <分> [時] [日] [月] [回数] <メッセージ> :
予定を登録する。各項目は * とするとワイルドカード指定となる。

回数は、告知毎に1づつ減っていき、0となるとそれ以上告知を行わない。

min <分> <メッセージ> :
<分>分後の予定を登録する。
ただし、時刻の「分」の変更で判断するため、指定時刻より最大60秒短くなる可能性がある。
"""
	
	#print "##### reply = ",reply.encode('utf-8')
	
	conn.send(xmpp.Message(mess.getFrom(),reply))
	
# スケジュール数値を文字列にする。
# -1を"*"として返す。
def t2s(timerNum):
	if timerNum < 0:
		return u"*"
	else:
		return unicode(timerNum)

#2桁バージョン
def t2s2d(timerNum):
	if timerNum < 0:
		return u"*"
	else:
		return unicode('%02d'%timerNum)

#日付とカウントを整形して返却
def makeDateString(month,day,hour,minute,count):
	buffer = u" "+t2s(month)+u"/"+t2s(day)+u" "
	buffer += t2s(hour)+u":"+t2s2d(minute)
	buffer += u" ("+t2s(count)+") "
	return buffer

class ConnectionError: pass
class AuthorizationError: pass

class Bot:
	def __init__(self, JID, Password,Server,Port):
		jid = xmpp.JID(JID)
		self.connection = xmpp.Client(jid.getDomain(), debug=[])
		
		result = self.connection.connect(server=(Server,Port))
		if result is None: raise ConnectionError
		result = self.connection.auth(jid.getNode(), Password)
		if result is None: raise AuthorizationError
		
		self.connection.RegisterHandler('message',parseMessage)
		self.connection.sendInitPresence()
		
	def loop(self):
		try:
			while self.connection.Process(1):
				pass
		except KeyboardInterrupt:
			pass
			
	def timerThread(self):
		#print "Timer start"
		oldMin = -1
		while True :
			t = time.localtime()
			if oldMin != t.tm_min :
				#Timer Program
				#print t.tm_min
				timerWork(self.connection,t)
				oldMin = t.tm_min
			time.sleep(1)
		print "Timer Broken!"

bot = Bot(**CONFIG.account)
thread.start_new_thread(bot.timerThread,())
bot.loop()

config.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

account = {
	'JID'     : 'reminder-bot@example.com',
	'Password': 'PASSWORD',
	'Server'  : '127.0.0.1',
	'Port'    : 5222,
}

dbFileName = "reminder.db"

実行結果

reminderbot.png
17:47:26のボットの発言の[charge]はバグです。正しくは(上記ソースコードでは)[min]となります。