update README
[gaetalk.git] / gaetalk.py
blobee4687597f9ce5e263aaf823f9dccd19680c659f
1 #!/usr/bin/env python2
2 # vim:fileencoding=utf-8
4 import re
5 import logging
6 import datetime
8 from google.appengine.ext import db
9 from google.appengine.api import xmpp
11 import utils
12 import config
14 helpre = re.compile(r'^\W{0,2}help$')
16 #用户所有资源离线时,会加上“完全”二字
17 OFFLINE = u'离线'
18 AWAY = u'离开'
19 XAWAY = u'离开'
20 BUSY = u'忙碌'
21 ONLINE = u'在线'
22 CHAT = u'和我说话吧'
24 NEW = u'加入'
25 LEAVE = u'退出'
26 NICK = u'昵称更改 (%s -> %s)'
27 SNOOZE = u'snooze %ds'
28 BLACK = u'禁言 %s %ds'
29 BLACK_AUTO = u'被禁言 %ds'
30 KICK = u'删除 %s (%s)'
31 ADMIN = u'%s 成为管理员 (by %s)'
32 UNADMIN = u'%s 不再是管理员 (by %s)'
33 NOTICE = u'通告:%s'
34 BLOCK = u'封禁 %s,原因:%s'
35 UNBLOCK = u'解封 %s'
37 STATUS_CODE = {
38 '': ONLINE,
39 'away': AWAY,
40 'dnd': BUSY,
41 'xa': XAWAY,
42 'chat': CHAT,
45 #状态的排序顺序
46 STATUS_LIST = [CHAT, ONLINE, AWAY, XAWAY, BUSY, OFFLINE]
48 timezone = datetime.timedelta(hours=config.timezoneoffset)
50 class User(db.Model):
51 jid = db.StringProperty(required=True, indexed=True)
52 nick = db.StringProperty(required=True, indexed=True)
54 add_date = db.DateTimeProperty(auto_now_add=True)
55 last_online_date = db.DateTimeProperty()
56 last_offline_date = db.DateTimeProperty()
57 last_speak_date = db.DateTimeProperty()
59 msg_count = db.IntegerProperty(required=True, default=0)
60 msg_chars = db.IntegerProperty(required=True, default=0)
61 credit = db.IntegerProperty(required=True, default=0)
63 black_before = db.DateTimeProperty(auto_now_add=True)
64 snooze_before = db.DateTimeProperty()
66 avail = db.StringProperty(required=True)
67 is_admin = db.BooleanProperty(required=True, default=False)
68 resources = db.StringListProperty(required=True)
69 #允许他人私信?
70 reject_pm = db.BooleanProperty(default=False)
72 prefix = db.StringProperty(required=True, default=config.default_prefix)
73 nick_pattern = db.StringProperty(required=True, default='[%s]')
74 nick_changed = db.BooleanProperty(required=True, default=False)
75 intro = db.StringProperty()
77 class Log(db.Model):
78 time = db.DateTimeProperty(auto_now_add=True, indexed=True)
79 msg = db.StringProperty(required=True, multiline=True)
80 jid = db.StringProperty()
81 nick = db.StringProperty()
82 type = db.StringProperty(required=True, indexed=True,
83 choices=set(['chat', 'member', 'admin', 'misc']))
85 class Group(db.Model):
86 time = db.DateTimeProperty(auto_now_add=True)
87 topic = db.StringProperty(multiline=True)
88 status = db.StringProperty()
90 class BlockedUser(db.Model):
91 jid = db.StringProperty(required=True, indexed=True)
92 add_date = db.DateTimeProperty(auto_now_add=True)
93 reason = db.StringProperty()
95 def log_msg(sender, msg):
96 l = Log(jid=sender.jid, nick=sender.nick,
97 type='chat', msg=msg)
98 l.put()
100 def log_onoff(sender, action, resource=''):
101 if resource:
102 if action == OFFLINE and not sender.resources:
103 msg = u'完全%s (%s)' % (action, resource)
104 else:
105 msg = '%s (%s)' % (action, resource)
106 else:
107 msg = action
108 l = Log(jid=sender.jid, nick=sender.nick,
109 type='member', msg=msg)
110 l.put()
112 def log_admin(sender, action):
113 l = Log(jid=sender.jid, nick=sender.nick,
114 type='admin', msg=action)
115 l.put()
117 def get_user_by_jid(jid):
118 return User.gql('where jid = :1', jid.lower()).get()
120 def get_user_by_nick(nick):
121 return User.gql('where nick = :1', nick).get()
123 def get_group_info():
124 return Group.all().get()
126 def get_blocked_user(jid):
127 return BlockedUser.gql('where jid = :1', jid.lower()).get()
129 def get_member_list():
130 now = datetime.datetime.now()
131 #一个查询中最多只能有一个不等比较
132 l = User.gql('where avail != :1', OFFLINE)
133 return [unicode(x.jid) for x in l \
134 if x.snooze_before is None or x.snooze_before < now]
136 def send_to_all_except(jid, message):
137 if isinstance(jid, str):
138 jids = [x for x in get_member_list() if x != jid]
139 else:
140 jids = [x for x in get_member_list() if x not in jid]
141 logging.debug(jid)
142 logging.debug(jids)
143 try:
144 xmpp.send_message(jids, message)
145 except xmpp.InvalidJidError:
146 pass
148 def send_to_all(message):
149 jids = get_member_list()
150 xmpp.send_message(jids, message)
152 def send_status(jid):
153 if get_blocked_user(jid):
154 xmpp.send_presence(jid, status=u'您已经被本群封禁')
155 return
157 grp = get_group_info()
158 if grp is None or not grp.status:
159 xmpp.send_presence(jid)
160 else:
161 xmpp.send_presence(jid, status=grp.status)
163 def handle_message(msg):
164 jid = msg.sender.split('/')[0]
166 sender = get_blocked_user(jid)
167 if sender is not None:
168 msg.reply(u'您已经被本群封禁,原因为 %s。' % sender.reason)
169 return
171 sender = get_user_by_jid(jid)
172 if sender is None:
173 msg.reply('很抱歉,出错了,请尝试更改状态或者重新添加好友。')
174 return
176 if msg.body.startswith('?OTR:'):
177 msg.reply('不支持 OTR 加密!')
178 return
180 if msg.body in config.blocked_away_messages:
181 msg.reply('系统认为您的客户端正在自动发送离开消息。如果您认为这并不正确,请向管理员反馈。')
182 return
184 if len(msg.body) > 500 or msg.body.count('\n') > 5:
185 msgbody = config.post_code(msg.body)
186 if msgbody:
187 msg.reply(u'内容过长,已贴至 %s 。' % msgbody)
188 firstline = ''
189 lineiter = iter(msg.body.split('\n'))
190 try:
191 while not firstline:
192 firstline = lineiter.next()
193 except StopIteration:
194 pass
195 if len(firstline) > 40:
196 firstline = firstline[:40]
197 msgbody += '\n' + firstline + '...'
198 else:
199 logging.warn(u'贴代码失败,代码长度 %d' % len(msg.body))
200 msg.reply('由于技术限制,每条消息最长为 500 字。大段文本请贴 paste 网站。\n'
201 '如 http://paste.ubuntu.org.cn/ http://slexy.org/\n'
203 return
204 ch = None
205 else:
206 msgbody = msg.body
207 if sender.is_admin or sender.jid == config.root:
208 ch = AdminCommand(msg, sender)
209 else:
210 ch = BasicCommand(msg, sender)
212 if not ch or not ch.handled:
213 now = datetime.datetime.now()
214 if sender.black_before is not None \
215 and sender.black_before > now:
216 if (datetime.datetime.today()+timezone).date() == \
217 (sender.black_before+timezone).date():
218 format = '%H时%M分%S秒'
219 else:
220 format = '%m月%d日 %H时%M分%S秒'
221 msg.reply('你已被禁言至 ' \
222 + (sender.black_before+timezone).strftime(format))
223 return
225 # handles ping, which does the following:
226 # - tells the user the network and the bot are OK
227 # - undoes snoozing
228 # - tells a previously 'quieted' person if s/he can speak now
229 if msgbody == 'ping':
230 if sender.snooze_before:
231 sender.snooze_before = None
232 sender.put()
233 msg.reply('pong')
234 return
236 if msgbody.lower() in ('test', u'测试'):
237 if sender.snooze_before:
238 sender.snooze_before = None
239 sender.put()
240 msg.reply('test ok')
241 return
243 sender.last_speak_date = now
244 sender.snooze_before = None
245 try:
246 sender.msg_count += 1
247 sender.msg_chars += len(msgbody)
248 except TypeError:
249 sender.msg_count = 1
250 sender.msg_chars = len(msgbody)
251 sender.put()
252 body = utils.removelinks(msgbody)
253 for u in User.gql('where avail != :1', OFFLINE):
254 if u.snooze_before is not None and u.snooze_before >= now:
255 continue
256 if u.jid == sender.jid:
257 continue
258 try:
259 message = '%s %s' % (
260 u.nick_pattern % sender.nick,
261 body
263 xmpp.send_message(u.jid, message)
264 except xmpp.InvalidJidError:
265 pass
266 log_msg(sender, body)
268 def try_add_user(jid, show=OFFLINE, resource=''):
269 '''使用 memcache 作为锁添加用户'''
270 u = get_blocked_user(jid)
271 if u is not None:
272 xmpp.send_message(jid, u'您已经被本群封禁,原因为 %s。' % u.reason)
273 return
275 L = utils.MemLock('add_user')
276 L.require()
277 try:
278 u = get_user_by_jid(jid)
279 if u is not None:
280 return
281 u = add_user(jid, show, resource)
282 finally:
283 L.release()
284 if show != OFFLINE:
285 log_onoff(u, show, resource)
286 logging.info(u'%s added', jid)
288 def add_user(jid, show=OFFLINE, resource=''):
289 '''resource 在 presence type 为 available 里使用'''
290 nick = jid.split('@')[0]
291 # same user name with different domains are possible
292 while get_user_by_nick(nick):
293 nick += '_'
294 u = User(jid=jid.lower(), avail=show, nick=nick)
295 if show != OFFLINE:
296 u.last_online_date = datetime.datetime.now()
297 if resource:
298 u.resources.append(resource)
299 u.put()
300 log_onoff(u, NEW)
301 logging.info(u'%s 已经加入' % jid)
302 send_status(jid)
303 xmpp.send_message(jid, u'欢迎 %s 加入!要获取使用帮助,请输入 %shelp,要获知群主题,请输入 %stopic。' % (
304 u.nick, u.prefix, u.prefix))
305 return u
307 def del_user(jid, by_cmd=False):
308 l = User.gql('where jid = :1', jid.lower())
309 for u in l:
310 if u.jid == config.root:
311 xmpp.send_message(jid, u'root 用户:离开前请确定你已做好善后工作!')
312 log_onoff(u, LEAVE)
313 if by_cmd:
314 logging.info(u'%s (%s) 已经离开 (通过使用命令)' % (u.nick, u.jid))
315 else:
316 logging.info(u'%s (%s) 已经离开' % (u.nick, u.jid))
317 u.delete()
319 class BasicCommand:
320 handled = True
321 def __init__(self, msg, sender):
322 self.sender = sender
323 self.msg = msg
325 if helpre.match(msg.body):
326 self.do_help()
327 elif msg.body.startswith(sender.prefix):
328 cmd = msg.body[len(sender.prefix):].split()
329 try:
330 handle = getattr(self, 'do_' + cmd[0])
331 except AttributeError:
332 msg.reply(u'错误:未知命令 %s' % cmd[0])
333 except IndexError:
334 msg.reply(u'错误:无命令')
335 except UnicodeEncodeError:
336 msg.reply(u'错误:命令名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
337 else:
338 handle(cmd[1:])
339 logging.debug('%s did command %s' % (sender.jid, cmd[0]))
340 else:
341 self.handled = False
343 def get_msg_part(self, index):
344 '''返回消息中第 index 个单词及其后的所有字符'''
345 return self.msg.body[len(self.sender.prefix):].split(None, index)[-1]
347 def do_online(self, args):
348 '''在线成员列表。可带一个参数,指定在名字中出现的一个子串。'''
349 r = []
350 pat = args[0] if args else None
351 now = datetime.datetime.now()
352 l = User.gql('where avail != :1', OFFLINE)
353 for u in l:
354 m = u.nick
355 if pat and m.find(pat) == -1:
356 continue
357 status = u.avail
358 if status != ONLINE:
359 m += u' (%s)' % status
360 if u.snooze_before is not None and u.snooze_before > now:
361 m += u' (snoozing)'
362 if u.black_before is not None and u.black_before > now:
363 m += u' (已禁言)'
364 r.append(unicode('* ' + m))
365 r.sort()
366 n = len(r)
367 if pat:
368 r.insert(0, u'在线成员列表(包含子串 %s):' % pat)
369 r.append(u'共 %d 人。' % n)
370 else:
371 r.insert(0, u'在线成员列表:')
372 r.append(u'共 %d 人在线。' % n)
373 self.msg.reply(u'\n'.join(r).encode('utf-8'))
375 def do_lsadmin(self, args):
376 '''管理员列表'''
377 r = []
378 now = datetime.datetime.now()
379 l = User.gql('where is_admin = :1', True)
380 for u in l:
381 m = u.nick
382 status = u.avail
383 if status != ONLINE:
384 m += u' (%s)' % status
385 if u.snooze_before is not None and u.snooze_before > now:
386 m += u' (snoozing)'
387 if u.black_before is not None and u.black_before > now:
388 m += u' (已禁言)'
389 r.append(unicode('* ' + m))
390 r.sort()
391 n = len(r)
392 r.insert(0, u'管理员列表:')
393 r.append(u'共 %d 位管理员。' % n)
394 self.msg.reply(u'\n'.join(r).encode('utf-8'))
396 def do_lsblocked(self, args):
397 '''列出被封禁用户名单'''
398 r = []
399 l = BlockedUser.all()
400 for u in l:
401 r.append(unicode('* %s (%s, %s)' % (u.jid,
402 utils.strftime(u.add_date, timezone),
403 u.reason)
406 r.sort()
407 n = len(r)
408 r.insert(0, u'封禁列表:')
409 r.append(u'共 %d 个 JID 被封禁。' % n)
410 self.msg.reply(u'\n'.join(r).encode('utf-8'))
412 def do_chatty(self, args):
413 '''消息数排行'''
414 r = []
415 for u in User.gql('ORDER BY msg_count ASC'):
416 m = u.nick
417 m = u'* %s:\t%5d条,共 %s' % (
418 u.nick, u.msg_count,
419 utils.filesize(u.msg_chars))
420 r.append(m)
421 n = len(r)
422 r.insert(0, u'消息数量排行:')
423 r.append(u'共 %d 人。' % n)
424 self.msg.reply(u'\n'.join(r).encode('utf-8'))
426 def do_nick(self, args):
427 '''更改昵称,需要一个参数,不能使用大部分标点符号'''
428 if len(args) != 1:
429 self.msg.reply('错误:请给出你想到的昵称(不能包含空格)')
430 return
432 q = get_user_by_nick(args[0])
433 if q is not None:
434 self.msg.reply('错误:该昵称已被使用,请使用其它昵称')
435 elif not utils.checkNick(args[0]):
436 self.msg.reply('错误:非法的昵称')
437 else:
438 if not config.nick_can_change and self.sender.nick_changed:
439 self.msg.reply('乖哦,你已经没机会再改昵称了')
440 return
441 old_nick = self.sender.nick
442 log_onoff(self.sender, NICK % (old_nick, args[0]))
443 self.sender.nick = args[0]
444 self.sender.nick_changed = True
445 self.sender.put()
446 send_to_all_except(self.sender.jid,
447 (u'%s 的昵称改成了 %s' % (old_nick, args[0])).encode('utf-8'))
448 self.msg.reply('昵称更改成功!')
449 do_nick.__doc__ += ',最长 %d 字节' % config.nick_maxlen
451 def do_whois(self, args):
452 '''查看用户信息,参数为用户昵称'''
453 if len(args) != 1:
454 self.msg.reply('错误:你想知道关于谁的信息?')
455 return
457 u = get_user_by_nick(args[0])
458 if u is None:
459 self.msg.reply(u'Sorry,查无此人。')
460 return
462 now = datetime.datetime.now()
463 status = u.avail
464 addtime = (u.add_date + timezone).strftime('%Y年%m月%d日 %H时%M分').decode('utf-8')
465 allowpm = u'否' if u.reject_pm else u'是'
466 if u.snooze_before is not None and u.snooze_before > now:
467 status += u' (snoozing)'
468 if u.black_before is not None and u.black_before > now:
469 status += u' (已禁言)'
470 r = []
471 r.append(u'昵称:\t%s' % u.nick)
472 if self.sender.is_admin:
473 r.append(u'JID:\t%s' % u.jid)
474 r.append(u'状态:\t%s' % status)
475 r.append(u'消息数:\t%d' % u.msg_count)
476 r.append(u'消息总量:\t%s' % utils.filesize(u.msg_chars))
477 r.append(u'加入时间:\t%s' % addtime)
478 r.append(u'接收私信:\t%s' % allowpm)
479 r.append(u'自我介绍:\t%s' % u.intro)
480 self.msg.reply(u'\n'.join(r).encode('utf-8'))
482 def do_help(self, args=()):
483 '''显示本帮助。参数 long 显示详细帮助,也可指定命令名。'''
484 doc = []
485 prefix = self.sender.prefix
487 if len(args) > 1:
488 self.msg.reply('参数错误。')
489 return
490 arg = args[0] if args else None
492 if arg is None or arg == 'long':
493 for b in self.__class__.__bases__ + (self.__class__,):
494 for c, f in b.__dict__.items():
495 if c.startswith('do_'):
496 if arg is None:
497 doc.append(u'%s%s:\t%s' % (prefix, c[3:], f.__doc__.decode('utf-8').\
498 split(u',', 1)[0].split(u'。', 1)[0]))
499 else:
500 doc.append(u'%s%s:\t%s' % (prefix, c[3:], f.__doc__.decode('utf-8')))
501 doc.sort()
502 if arg is None:
503 doc.insert(0, u'** 命令指南 **\n(当前命令前缀 %s,可设置。使用 %shelp long 显示详细帮助)' % (prefix, prefix))
504 else:
505 doc.insert(0, u'** 命令指南 **\n(当前命令前缀 %s,可设置)' % prefix)
506 doc.append(u'要离开,直接删掉好友即可。')
507 doc.append(u'Gtalk 客户端用户要离开请使用 quit 命令。')
508 self.msg.reply(u'\n'.join(doc).encode('utf-8'))
509 else:
510 try:
511 handle = getattr(self, 'do_' + arg)
512 except AttributeError:
513 self.msg.reply(u'错误:未知命令 %s' % arg)
514 except UnicodeEncodeError:
515 self.msg.reply(u'错误:命令名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
516 else:
517 self.msg.reply(u'%s%s:\t%s' % (prefix, arg, handle.__doc__.decode('utf-8')))
519 def do_topic(self, args=()):
520 '''查看群主题'''
521 grp = get_group_info()
522 if grp is None or not grp.topic:
523 self.msg.reply(u'没有设置群主题。')
524 else:
525 self.msg.reply(grp.topic)
527 def do_iam(self, args):
528 '''查看自己的信息'''
529 u = self.sender
530 addtime = (u.add_date + timezone).strftime('%Y年%m月%d日 %H时%M分').decode('utf-8')
531 allowpm = u'否' if u.reject_pm else u'是'
532 r = []
533 r.append(u'昵称:\t%s' % u.nick)
534 r.append(u'JID:\t%s' % u.jid)
535 r.append(u'资源:\t%s' % u' '.join(u.resources))
536 r.append(u'消息数:\t%d' % u.msg_count)
537 r.append(u'消息总量:\t%s' % utils.filesize(u.msg_chars))
538 r.append(u'加入时间:\t%s' % addtime)
539 r.append(u'命令前缀:\t%s' % u.prefix)
540 r.append(u'接收私信:\t%s' % allowpm)
541 r.append(u'自我介绍:\t%s' % u.intro)
542 self.msg.reply(u'\n'.join(r).encode('utf-8'))
544 def do_m(self, args):
545 '''发私信,需要昵称和内容两个参数。私信不会以任何方式被记录。用户可使用 set 命令设置是否接收私信。'''
546 if len(args) < 2:
547 self.msg.reply('请给出昵称和内容。')
548 return
550 target = get_user_by_nick(args[0])
551 if target is None:
552 self.msg.reply('Sorry,查无此人。')
553 return
555 if target.reject_pm:
556 self.msg.reply('很抱歉,对方不接收私信。')
557 return
559 msg = self.get_msg_part(2)
560 msg = u'_私信_ %s %s' % (target.nick_pattern % self.sender.nick, msg)
561 xmpp.send_message(target.jid, msg)
563 def do_intro(self, arg):
564 '''设置自我介绍信息'''
565 if not arg:
566 self.msg.reply('请给出自我介绍的内容。')
567 return
569 msg = self.get_msg_part(1)
570 u = self.sender
571 try:
572 u.intro = msg
573 except db.BadValueError:
574 # 过长文本已在 handle_message 中被拦截
575 self.msg.reply('错误:自我介绍内容只能为一行。')
576 return
578 u.put()
579 self.msg.reply(u'设置成功!')
581 def do_snooze(self, args):
582 '''暂停接收消息,参数为时间(默认单位为秒)。再次发送消息时自动清除'''
583 if len(args) != 1:
584 self.msg.reply('你想停止接收消息多久?')
585 return
586 else:
587 try:
588 n = utils.parseTime(args[0])
589 except ValueError:
590 self.msg.reply('Sorry,我无法理解你说的时间。')
591 return
593 try:
594 self.sender.snooze_before = datetime.datetime.now() + datetime.timedelta(seconds=n)
595 except OverflowError:
596 self.msg.reply('Sorry,你不能睡太久。')
597 return
598 self.sender.put()
599 if n == 0:
600 self.msg.reply('你已经醒来。')
601 else:
602 self.msg.reply(u'OK,停止接收消息 %s。' % utils.displayTime(n))
603 log_onoff(self.sender, SNOOZE % n)
605 def do_offline(self, args):
606 '''假装离线,让程序认为你的所有资源已离线。如在你离线时程序仍认为你在线,请使用此命令。'''
607 del self.sender.resources[:]
608 self.sender.avail = OFFLINE
609 self.sender.last_offline_date = datetime.datetime.now()
610 self.sender.put()
611 self.msg.reply('OK,在下次你说你在线之前我都认为你已离线。')
613 def do_fakeresource(self, args):
614 '''假装在线,人工加入一个新的资源,使程序认为你总是在线。使用 off 参数来取消。'''
615 if args and args[0] == 'off':
616 try:
617 self.sender.resources.remove('fakeresouce')
618 self.msg.reply('OK,fakeresouce 已取消。')
619 self.sender.put()
620 except ValueError:
621 self.msg.reply('没有设置 fakeresouce。')
622 else:
623 try:
624 self.sender.resources.index('fakeresouce')
625 self.msg.reply('你已经设置了永远在线。')
626 except ValueError:
627 self.sender.resources.append('fakeresouce')
628 self.msg.reply('OK,你将永远在线。')
629 self.sender.put()
631 def do_old(self, args):
632 '''聊天记录查询,可选一个数字参数。默认为最后20条。特殊参数 OFFLINE (不区分大小写)显示离线消息(最多 100 条)'''
633 s = self.sender
634 q = False
635 if not args:
636 q = Log.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT 20")
637 elif len(args) == 1:
638 try:
639 n = int(args[0])
640 if n > 0:
641 q = Log.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT %d" % n)
642 except ValueError:
643 if args[0].upper() == 'OFFLINE':
644 q = Log.gql("WHERE time < :1 AND time > :2 AND type = 'chat' ORDER BY time DESC LIMIT 100", s.last_online_date, s.last_offline_date)
645 else:
646 pass
647 if q is not False:
648 r = []
649 q = list(q)
650 q.reverse()
651 if q:
652 if datetime.datetime.today() - q[0].time > datetime.timedelta(hours=24):
653 show_date = True
654 else:
655 show_date = False
656 for l in q:
657 message = '%s %s %s' % (
658 utils.strftime(l.time, timezone, show_date),
659 s.nick_pattern % l.nick,
660 l.msg
662 r.append(message)
663 if r:
664 self.msg.reply(u'\n'.join(r).encode('utf-8'))
665 else:
666 self.msg.reply('没有符合的聊天记录。')
667 else:
668 self.msg.reply('Oops, 参数不正确哦。')
670 def do_set(self, args):
671 '''设置参数。参数格式 key=value;不带参数以查看说明。'''
672 #注意:选项名/值中不能包含空格
673 if len(args) != 1:
674 doc = []
675 for c, f in self.__class__.__dict__.items():
676 if c.startswith('set_'):
677 doc.append(u'* %s:\t%s' % (c[4:], f.__doc__.decode('utf-8')))
678 for b in self.__class__.__bases__:
679 for c, f in b.__dict__.items():
680 if c.startswith('set_'):
681 doc.append(u'* %s:\t%s' % (c[4:], f.__doc__.decode('utf-8')))
682 doc.sort()
683 doc.insert(0, u'设置选项:')
684 self.msg.reply(u'\n'.join(doc).encode('utf-8'))
685 else:
686 msg = self.msg
687 cmd = args[0].split('=', 1)
688 if len(cmd) == 1 or cmd[1] == '':
689 msg.reply(u'错误:请给出选项值')
690 return
691 try:
692 handle = getattr(self, 'set_' + cmd[0])
693 except AttributeError:
694 msg.reply(u'错误:未知选项 %s' % cmd[0])
695 except IndexError:
696 msg.reply(u'错误:无选项')
697 except UnicodeEncodeError:
698 msg.reply(u'错误:选项名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
699 else:
700 handle(cmd[1])
702 def do_quit(self, args):
703 '''删除用户数据。某些自称“不作恶”的公司的客户端会不按协议要求发送删除好友的消息,请 gtalk 官方客户端用户使用此命令退出。参见 http://xmpp.org/rfcs/rfc3921.html#rfc.section.6.3 。'''
704 del_user(self.sender.jid)
705 self.msg.reply(u'OK.')
707 def set_prefix(self, arg):
708 '''设置命令前缀'''
709 self.sender.prefix = arg
710 self.sender.put()
711 self.msg.reply(u'设置成功!')
713 def set_nickpattern(self, arg):
714 '''设置昵称显示格式,用 %s 表示昵称的位置'''
715 try:
716 arg % 'test'
717 except (TypeError, ValueError):
718 self.msg.reply(u'错误:不正确的格式')
719 return
721 self.sender.nick_pattern = arg
722 self.sender.put()
723 self.msg.reply(u'设置成功!')
725 def set_allowpm(self, arg):
726 '''设置是否接收私信,参数为 y(接收)或者 n(拒绝)'''
727 if arg not in 'yn':
728 self.msg.reply(u'错误的参数。')
729 return
731 if arg == 'y':
732 self.sender.reject_pm = False
733 else:
734 self.sender.reject_pm = True
735 self.sender.put()
736 self.msg.reply(u'设置成功!')
738 class AdminCommand(BasicCommand):
739 def do_kick(self, args):
740 '''删除某人。他仍可以重新加入。'''
741 if len(args) != 1:
742 self.msg.reply('请给出昵称。')
743 return
745 target = get_user_by_nick(args[0])
746 if target is None:
747 self.msg.reply('Sorry,查无此人。')
748 return
750 if target.jid == config.root:
751 self.msg.reply('不能删除 root 用户')
752 return
754 targetjid = target.jid
755 targetnick = target.nick
756 target.delete()
757 self.msg.reply((u'OK,删除 %s。' % target.nick).encode('utf-8'))
758 send_to_all_except(self.sender.jid, (u'%s 已被删除。' % target.nick) \
759 .encode('utf-8'))
760 xmpp.send_message(targetjid, u'你已被管理员从此群中删除,请删除该好友。')
761 log_admin(self.sender, KICK % (targetnick, targetjid))
763 def do_quiet(self, args):
764 '''禁言某人,参数为昵称和时间(默认单位秒)'''
765 if len(args) != 2:
766 self.msg.reply('请给出昵称和时间。')
767 return
768 else:
769 try:
770 n = utils.parseTime(args[1])
771 except ValueError:
772 self.msg.reply('Sorry,我无法理解你说的时间。')
773 return
775 target = get_user_by_nick(args[0])
776 if target is None:
777 self.msg.reply('Sorry,查无此人。')
778 return
780 target.black_before = datetime.datetime.now() + datetime.timedelta(seconds=n)
781 target.put()
782 self.msg.reply(u'OK,禁言 %s %s。' % (target.nick, utils.displayTime(n)))
783 send_to_all_except((self.sender.jid, target.jid),
784 (u'%s 已被禁言 %s。' % (target.nick, utils.displayTime(n))) \
785 .encode('utf-8'))
786 xmpp.send_message(target.jid, u'你已被管理员禁言 %s。' % utils.displayTime(n))
787 log_admin(self.sender, BLACK % (target.nick, n))
789 def do_notice(self, arg):
790 '''发送群通告。只会发给在线的人,包括 snoozing 者。'''
791 if not arg:
792 self.msg.reply('请给出群通告的内容。')
793 return
795 msg = self.msg.body[len(self.sender.prefix):].split(None, 1)[-1]
797 l = User.gql('where avail != :1', OFFLINE)
798 log_admin(self.sender, NOTICE % msg)
799 for u in l:
800 try:
801 xmpp.send_message(u.jid, u'通告:' + msg)
802 except xmpp.InvalidJidError:
803 pass
805 def do_topic(self, args=()):
806 '''查看或设置群主题'''
807 grp = get_group_info()
808 if not args:
809 if grp is None or not grp.topic:
810 self.msg.reply(u'没有设置群主题。')
811 else:
812 self.msg.reply(grp.topic)
813 else:
814 grp = get_group_info()
815 if grp is None:
816 grp = Group()
817 grp.topic = self.msg.body[len(self.sender.prefix):].split(None, 1)[-1]
818 grp.put()
819 self.msg.reply(u'群主题已更新。')
821 def do_admin(self, args):
822 '''将某人添加为管理员'''
823 if len(args) != 1:
824 self.msg.reply(u'请给出昵称。')
825 return
827 target = get_user_by_nick(args[0])
828 if target is None:
829 self.msg.reply(u'Sorry,查无此人。')
830 return
832 if target.is_admin:
833 self.msg.reply(u'%s 已经是管理员了。' % target.nick)
834 return
836 target.is_admin = True
837 target.put()
838 send_to_all_except(target.jid,
839 (u'%s 已成为管理员。' % target.nick) \
840 .encode('utf-8'))
841 xmpp.send_message(target.jid, u'你已是本群管理员。')
842 log_admin(self.sender, ADMIN % (target.nick, self.sender.nick))
844 def do_unadmin(self, args):
845 '''取消某人管理员的权限'''
846 if len(args) != 1:
847 self.msg.reply(u'请给出昵称。')
848 return
850 target = get_user_by_nick(args[0])
851 if target is None:
852 self.msg.reply(u'Sorry,查无此人。')
853 return
855 if not target.is_admin:
856 self.msg.reply(u'%s 不是管理员。' % target.nick)
857 return
859 target.is_admin = False
860 target.put()
861 send_to_all_except(target.jid,
862 (u'%s 已不再是管理员。' % target.nick) \
863 .encode('utf-8'))
864 xmpp.send_message(target.jid, u'你已不再是本群管理员。')
865 log_admin(self.sender, UNADMIN % (target.nick, self.sender.nick))
867 def do_block(self, args):
868 '''封禁某个 ID,参数为用户昵称或者 ID(如果不是已经加入的 ID 的话),以及封禁原因'''
869 if len(args) < 2:
870 self.msg.reply(u'请给出要封禁的用户和原因。')
871 return
873 target = get_user_by_nick(args[0])
874 reason = self.msg.body[len(self.sender.prefix):].split(None, 2)[-1]
875 if target is None:
876 jid = args[0]
877 name = jid
878 fullname = name
879 else:
880 jid = target.jid
881 name = target.nick
882 fullname = '%s (%s)' % (name, jid)
883 u = BlockedUser.gql('where jid = :1', jid).get()
884 if u is not None:
885 self.msg.reply(u'此 JID 已经被封禁。')
886 return
888 if jid == config.root:
889 self.msg.reply('不能封禁 root 用户')
890 return
892 if target:
893 target.delete()
894 u = BlockedUser(jid=jid, reason=reason)
895 u.put()
897 send_to_all_except(self.sender.jid,
898 (u'%s 已被本群封禁,理由为 %s。' % (name, reason)) \
899 .encode('utf-8'))
900 self.msg.reply(u'%s 已被本群封禁,理由为 %s。' % (fullname, reason))
901 xmpp.send_message(jid, u'你已被本群封禁,理由为 %s。' % reason)
902 xmpp.send_presence(jid, status=u'您已经被本群封禁')
903 log_admin(self.sender, BLOCK % (fullname, reason))
905 def do_unblock(self, args):
906 '''解封某个 ID'''
907 if len(args) != 1:
908 self.msg.reply(u'请给出要解封用户的 JID。')
909 return
911 target = get_blocked_user(args[0])
912 if target is None:
913 self.msg.reply(u'封禁列表中没有这个 JID。')
914 return
916 target.delete()
917 send_to_all((u'%s 已被解除封禁。' % args[0]) \
918 .encode('utf-8'))
919 log_admin(self.sender, UNBLOCK % args[0])
921 def do_groupstatus(self, arg):
922 '''设置群状态'''
923 grp = get_group_info()
924 if grp is None:
925 grp = Group()
926 grp.status = self.msg.body[len(self.sender.prefix):].split(None, 1)[-1]
927 grp.put()
928 for u in User.all():
929 xmpp.send_presence(u.jid, status=grp.status)
930 self.msg.reply(u'设置成功!')