Toggle line numbers
1 #coding=utf-8
2 #!/usr/bin/env python
3 # confbot -- a conference bot for google talk.
4 # Copyright (C) 2005 Perry Lorier (aka Isomer)
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 #
20 # Modified by limodou 2005/09/05
21 # * 进行汉化处理
22 # * 当出现网络中断是不再退出,而是自动重联
23 # * 增加config.py,将配置分离
24 # * 增加是否记录日志标志recordflag
25 # * 增加日志保存路径设置logpath
26 # * 可自定义系统信息提示符 system_prompt
27 # * 将欢迎信息独立出来,可以方便修改welcome
28 # * 将用户名可以使用不同的包括字符处理,帮助显示name_quote_begin和name_quote_end
29 # * 完善表情串emotes
30 # * 增加管理员命令reload,用于重新装入配置信息,这样可以动态进行修改,对于logpath无效
31 #
32 # You will want to change these:
33
34 import config
35
36 # You shouldn't have to change anything below this line --------------------
37 import socket
38 import jabber
39 import xmlstream
40 import sys
41 import time
42 import random
43 import traceback
44 import urllib
45 import os.path
46
47 version='1.2'
48 commandchrs = '/)'
49
50 #打开日志文件 logf用于记录聊天日志 xmllogf用于记录xml消息
51 #每运行一次bot,创建一个新的日志文件
52 if config.recordflag:
53 logf = open(os.path.join(config.logpath, time.strftime("%Y%m%d%H%M%S.log")),"w")
54 xmllogf = open("xmllog","w")
55 last_activity=time.time()
56 #xmllogf = sys.stderr
57
58 #打开管理员列表文件
59 try:
60 adminfile = open("adminlist.txt","r")
61 admins=[i.strip() for i in adminfile.readlines()]
62 adminfile.close()
63 except:
64 print "Could not open admin file, creating a new one"
65
66 con=None
67
68 def getdisplayname(x):
69 "显示一个用户名,去掉/后面的东西和@gmail.com"
70 x=unicode(x)
71 if '/' in x:
72 x=x[:x.find("/")]
73 if '@' in x and x[x.find('@'):]=="@gmail.com":
74 x=x[:x.find("@")]
75 return x
76
77 def getjid(x):
78 "根据显示名得到一个完整的gmail帐号"
79 if '@' not in x:
80 x=x+"@gmail.com"
81 return x
82
83 def saveadminlist():
84 "保存管理员列表到文件中"
85 a=open("adminlist.txt","w")
86 for i in admins:
87 print >>a,i
88 a.close()
89
90 def sendtoone(who,msg):
91 "向一个用户发送消息"
92 m = jabber.Message(who,msg)
93 m.setType('chat')
94 con.send(m)
95
96 def sendtoall(msg,butnot=[],including=[]):
97 "向所有用户发送消息, 列在butnot中的用户不发送消息,including为必发消息名单列表"
98 "如果用户的状态为'available', 'chat', None则可以发送消息,如果为busy则不会收到消息"
99 r = con.getRoster()
100 if config.recordflag:
101 print >>logf,time.strftime("%Y-%m-%d %H:%M:%S"),msg.encode("utf-8")
102 logf.flush()
103 #print time.strftime("%Y-%m-%d %H:%M:%S"),msg.encode("utf-8")
104 for i in r.getJIDs():
105 if getdisplayname(i) in butnot:
106 continue
107 state=r.getShow(unicode(i))
108 if state in ['available','chat',None] or getdisplayname(i) in including:
109 sendtoone(i,msg)
110 time.sleep(.1)
111
112 statuses={}
113 suppressing=1
114 def sendstatus(who,txt,msg):
115 "群发某人的状态"
116 who = getdisplayname(who)
117 if statuses.has_key(who) and statuses[who]==txt:
118 return
119 if txt == 'busy':
120 txt = u'忙碌'
121 statuses[who]=txt
122 if not statuses.has_key(who):
123 # Suppress initial status
124 return
125 if suppressing:
126 return
127 if msg:
128 sendtoall(config.system_prompt + u'%s %s (%s)' % (who,txt,msg),including=[who])
129 else:
130 sendtoall(config.system_prompt + u'%s %s' % (who,txt),including=[who])
131
132 def boot(jid):
133 "将某人从聊天室中去掉"
134 con.send(jabber.Presence(to=jid, type='unsubscribe'))
135 con.send(jabber.Presence(to=jid, type='unsubscribed'))
136 if statuses.has_key(getdisplayname(jid)):
137 del statuses[getdisplayname(jid)]
138
139 def cmd(who,msg):
140 "聊天指令处理"
141 if " " in msg:
142 cmd,msg=msg.split(" ",1)
143 else:
144 cmd,msg=msg.strip(),""
145 if cmd[:1] in commandchrs:
146 cmd=cmd[1:]
147 if cmd in ["me"]:
148 if msg.strip()=="":
149 action=random.choice(config.emotes.keys())
150 sendtoone(who, config.system_prompt + u'用法: /me <表情串>\n表现你的一种表情。\n'
151 u'例如 "/me %(action)s" 将表示为 "* %(nick)s %(emote)s" <消息>' % {
152 "nick" : getdisplayname(who),
153 "action" : action,
154 "emote" : config.emotes[action]
155 })
156 else:
157 if " " in msg:
158 action, msg = msg.split(" ", 1)
159 else:
160 action, msg = "", msg
161 sendtoall('%s%s%s _%s_ %s' % (config.name_quote_begin, getdisplayname(who), config.name_quote_end, config.emotes.get(action, ""), msg),butnot=[getdisplayname(who)])
162 elif cmd in ["help"]:
163 sendtoone(who, config.system_prompt + u"""命令列表:
164 /help 显示本帮助信息
165 /me <emote> <msg> 设置表情串
166 /names 显示聊天室人名
167 /quit <msg> 退出聊天室,一旦退出需要重新加入
168 /msg <nick> <msg> 私聊""")
169 if who.getStripped() in admins:
170 sendtoone(who, config.system_prompt + u'''管理员命令列表:
171 /die 关闭聊天室
172 /addadmin <nick> 增加管理员
173 /deladmin <nick> 删除管理员
174 /kick <nick> 踢除某个人
175 /reload 重新装入配置信息''')
176 sendtoone(who, config.system_prompt + u'请访问 http://coders.meta.net.nz/~perry/jabber/confbot.php 了解更多内容\n'
177 u'还可以访问 http://www.donews.net/limodou 的Blog了解本汉化修正版')
178 elif cmd in ["names"]:
179 r = con.getRoster()
180 names=[]
181 for i in r.getJIDs():
182 state=r.getShow(unicode(i))
183 name=getdisplayname(i)
184 if i.getStripped() in admins:
185 name="@%s" % name
186 if state in ['available','chat',None]:
187 names.insert(0,name)
188 else:
189 names.append('(%s)' % name)
190 sendtoone(who, config.system_prompt + u'名单:\n%s\n有@符的为管理员' % " ".join(names))
191 elif cmd in ["quit","leave","exit"]:
192 if msg:
193 msg = u' (%s)' % msg
194 sendtoall(config.system_prompt + u'%s 已经退出%s' % (getdisplayname(who), msg))
195 boot(who.getStripped())
196 elif cmd in ['msg']:
197 if not ' ' in msg:
198 sendtoone(who, config.system_prompt + u'用法: /msg <对方名称> <消息>')
199 else:
200 target,msg = msg.split(' ',1)
201 sendtoone(getjid(target),'%s%s%s 对你悄悄说: %s' % (config.name_quote_begin, getdisplayname(who),config.name_quote_end, msg))
202 sendtoone(who,'你对 %s%s%s 悄悄说: %s' % (config.name_quote_begin, getdisplayname(target), config.name_quote_end, msg))
203 elif cmd in ['kick','boot'] and who.getStripped() in admins:
204 boot(getjid(msg))
205 sendtoall(config.system_prompt + u'%s 被管理员踢掉了' % msg.strip())
206 elif cmd in ['addadmin'] and who.getStripped() in admins:
207 admins.append(getjid(msg.strip()))
208 sendtoone(who, config.system_prompt + u'把 %s 加为管理员' % getjid(msg.strip()))
209 sendtoone(getjid(msg.strip()), config.system_prompt + u'%s 已经把你加为管理员' % getdisplayname(who))
210 saveadminlist()
211 elif cmd in ['deladmin'] and who.getStripped() in admins:
212 if getjid(msg.strip()) in admins:
213 admins.remove(getjid(msg.strip()))
214 sendtoone(who, config.system_prompt + u'把 %s 从管理员中删除' % getjid(msg.strip()))
215 sendtoone(getjid(msg.strip()), config.system_prompt + u'%s 把你从管理员中删除了' % getdisplayname(who))
216 saveadminlist()
217 else:
218 sendtoone(who, config.system_prompt + u'%s 不是一个管理员' % getjid(msg.strip()))
219 elif cmd in ['die'] and who.getStripped() in admins:
220 sendtoall(config.system_prompt + u'聊天室被 %s 关闭' % who.getStripped())
221 sys.exit(1)
222 elif cmd in ['reload'] and who.getStripped() in admins:
223 reload(config)
224 sendtoone(who, config.system_prompt + u'重载配置成功')
225 print 'reload config'
226 else:
227 sendtoone(who, config.system_prompt + u'不可识别的命令 %s' % cmd)
228
229 def messageCB(con,msg):
230 if msg.getError()!=None:
231 if statuses.has_key(getdisplayname(msg.getFrom())):
232 sendstatus(unicode(msg.getFrom()), u'离开',"Blocked")
233 boot(msg.getFrom().getStripped())
234 elif msg.getBody():
235 if len(msg.getBody())>1024:
236 sendtoall(config.system_prompt + u"%s 正在刷屏" % (getdisplayname(msg.getFrom())))
237 elif msg.getBody()[:1] in commandchrs:
238 cmd(msg.getFrom(),msg.getBody())
239 else:
240 global suppressing,last_activity
241 suppressing=0
242 last_activity=time.time()
243 sendtoall('%s%s%s %s' % (config.name_quote_begin, getdisplayname(msg.getFrom()), config.name_quote_end, msg.getBody()),
244 butnot=[getdisplayname(msg.getFrom())],
245 )
246 print 'status:',con.getRoster().getShow(msg.getFrom().getStripped()),msg.getFrom().getStripped()
247 if con.getRoster().getShow(msg.getFrom()) not in ['available','chat',None]:
248 sendtoone(msg.getFrom(),
249 u'config.system_prompt 警告: 你已经在客户端标记为"忙(busy)",\n'
250 u'你将不会收到其他人的谈话,在客户端将你自已\n'
251 u'设为"在线(available)"才可以看到别人的回复')
252 xmllogf.flush() # just so flushes happen regularly
253
254
255 def presenceCB(con,prs):
256 who = unicode(prs.getFrom())
257 type = prs.getType()
258 # TODO: Try only acking their subscription when they ack ours.
259 if type == 'subscribe':
260 con.send(jabber.Presence(to=who, type='subscribed'))
261 con.send(jabber.Presence(to=who, type='subscribe'))
262 print "Subscribe from",who
263 elif type == 'unsubscribe':
264 boot(prs.getFrom().getStripped())
265 print "Unsubscribe from",who
266 elif type == 'subscribed':
267 sendtoone(who, config.welcome)
268 sendstatus(who,u'在线', u'加入')
269 elif type == 'unsubscribed':
270 sendtoall(config.system_prompt + u'%s 已经退出' % getdisplayname(who))
271 elif type == 'available' or type == None:
272 show = prs.getShow()
273 if show in [None,'chat','available']:
274 sendstatus(who, u'在线', prs.getStatus())
275 elif show in ['xa']:
276 sendstatus(who, u'离开',prs.getStatus())
277 elif show in ['away']:
278 sendstatus(who,u'离开',prs.getStatus())
279 elif show in ['dnd']:
280 sendstatus(who,u'离开',prs.getStatus())
281 else:
282 sendstatus(who,u'离开',show+" [[%s]]" % prs.getStatus())
283
284 elif type == 'unavailable':
285 status = prs.getShow()
286 sendstatus(who, u'离开', status)
287 else:
288 print "Unknown presence:",who,type
289
290 def iqCB(con,iq):
291 # reply to all IQ's with an error
292 reply=None
293 try:
294 # Google are bad bad people
295 # they don't put their query inside a <query> in <iq>
296 reply=jabber.Iq(to=iq.getFrom(),type='error')
297 stuff=iq._node.getChildren()
298 for i in stuff:
299 reply._node.insertNode(i)
300 reply.setError('501','Feature not implemented')
301 con.send(reply)
302 except:
303 traceback.print_exc()
304
305 def disconnectedCB(con):
306 sys.exit(1)
307
308 reload(sys)
309 sys.setdefaultencoding('utf-8')
310
311 def connect():
312 con = jabber.Client(host=config.server,debug=False ,log=xmllogf,
313 port=5223, connection=xmlstream.TCP_SSL)
314 con.connect()
315 con.setMessageHandler(messageCB)
316 con.setPresenceHandler(presenceCB)
317 con.setIqHandler(iqCB)
318 con.setDisconnectHandler(disconnectedCB)
319 con.auth(config.account,config.password,config.resource)
320 con.requestRoster()
321 con.sendInitPresence()
322 _roster = con.getRoster()
323 for jid in _roster.getJIDs():
324 print jid,_roster.getOnline(jid),_roster.getStatus(jid),_roster.getShow(jid)
325 return con
326
327 con = connect()
328
329 JID="%s@%s/%s" % (config.account,config.server,config.resource)
330 last_update=0
331 saveadminlist()
332 while 1:
333 # We announce ourselves to a url, this url then keeps track of all
334 # the conference bots that are running, and provides a directory
335 # for people to browse.
336 if time.time()-last_update>4*60*60: # every 4 hours
337 args={
338 'action':'register',
339 'account':"%s@%s" % (config.account,config.server),
340 'users':len(con.getRoster().getJIDs()),
341 'last_activity':time.time()-last_activity,
342 'version':version,
343 'topic':config.topic,
344 }
345 try:
346 urllib.urlretrieve('http://coders.meta.net.nz/~perry/jabber/confbot.php?'+urllib.urlencode(args))
347 print "Updated directory site"
348 except:
349 print "Can't reach the directory site"
350 traceback.print_exc()
351 last_update = time.time()
352 try:
353 con.process(1)
354 except KeyboardInterrupt:
355 break
356 except SystemExit:
357 break
358 except:
359 traceback.print_exc()
360 try:
361 time.sleep(1)
362 con = connect()
363 except:
364 traceback.print_exc()