2 # vim:fileencoding=utf-8
8 from google
.appengine
.ext
import db
9 from google
.appengine
.api
import xmpp
15 helpre
= re
.compile(r
'^\W{0,2}help$')
27 NICK
= u
'昵称更改 (%s -> %s)'
28 SNOOZE
= u
'snooze %ds'
30 BLACK_AUTO
= u
'被禁言 %ds'
32 ADMIN
= u
'%s 成为管理员 (by %s)'
33 UNADMIN
= u
'%s 不再是管理员 (by %s)'
44 STATUS_LIST
= [CHAT
, ONLINE
, AWAY
, XAWAY
, BUSY
, OFFLINE
]
46 timezone
= datetime
.timedelta(hours
=config
.timezoneoffset
)
49 jid
= db
.StringProperty(required
=True, indexed
=True)
50 nick
= db
.StringProperty(required
=True, indexed
=True)
52 add_date
= db
.DateTimeProperty(auto_now_add
=True)
53 last_online_date
= db
.DateTimeProperty()
54 last_offline_date
= db
.DateTimeProperty()
55 last_speak_date
= db
.DateTimeProperty()
57 msg_count
= db
.IntegerProperty(required
=True, default
=0)
58 msg_chars
= db
.IntegerProperty(required
=True, default
=0)
59 credit
= db
.IntegerProperty(required
=True, default
=0)
61 black_before
= db
.DateTimeProperty(auto_now_add
=True)
62 snooze_before
= db
.DateTimeProperty()
63 flooding_point
= db
.IntegerProperty(default
=0)
65 avail
= db
.StringProperty(required
=True)
66 is_admin
= db
.BooleanProperty(required
=True, default
=False)
67 resources
= db
.StringListProperty(required
=True)
69 reject_pm
= db
.BooleanProperty(default
=False)
71 prefix
= db
.StringProperty(required
=True, default
=config
.default_prefix
)
72 nick_pattern
= db
.StringProperty(required
=True, default
='[%s]')
73 nick_changed
= db
.BooleanProperty(required
=True, default
=False)
74 intro
= db
.StringProperty()
77 time
= db
.DateTimeProperty(auto_now_add
=True, indexed
=True)
78 msg
= db
.StringProperty(required
=True, multiline
=True)
79 jid
= db
.StringProperty()
80 nick
= db
.StringProperty()
81 type = db
.StringProperty(required
=True, indexed
=True,
82 choices
=set(['chat', 'member', 'misc']))
84 def log_msg(sender
, msg
):
85 l
= Log(jid
=sender
.jid
, nick
=sender
.nick
,
89 def log_onoff(sender
, action
, resource
=''):
91 if action
== OFFLINE
and not sender
.resources
:
92 msg
= u
'完全%s (%s)' % (action
, resource
)
94 msg
= '%s (%s)' % (action
, resource
)
97 l
= Log(jid
=sender
.jid
, nick
=sender
.nick
,
98 type='member', msg
=msg
)
101 def get_user_by_jid(jid
):
102 return User
.gql('where jid = :1', jid
.lower()).get()
104 def get_user_by_nick(nick
):
105 return User
.gql('where nick = :1', nick
).get()
107 def get_member_list():
109 now
= datetime
.datetime
.now()
111 l
= User
.gql('where avail != :1', OFFLINE
)
114 return [unicode(x
.jid
) for x
in r \
115 if x
.snooze_before
is None or x
.snooze_before
< now
]
117 def send_to_all_except(jid
, message
):
118 if isinstance(jid
, str):
119 jids
= [x
for x
in get_member_list() if x
!= jid
]
121 jids
= [x
for x
in get_member_list() if x
not in jid
]
125 xmpp
.send_message(jids
, message
)
126 except xmpp
.InvalidJidError
:
129 def send_to_all(message
):
130 jids
= get_member_list()
131 xmpp
.send_message(jids
, message
)
133 def handle_message(msg
):
134 sender
= get_user_by_jid(msg
.sender
.split('/')[0])
136 msg
.reply('很抱歉,出错了,请重新添加好友。')
138 if msg
.body
.startswith('?OTR:'):
139 msg
.reply('不支持 OTR 加密!')
141 if len(msg
.body
) > 500:
142 msg
.reply('由于技术限制,每条消息最长为 500 字。大段文本请贴 paste 网站。')
144 if sender
.is_admin
or sender
.jid
== config
.root
:
145 ch
= AdminCommand(msg
, sender
)
147 ch
= BasicCommand(msg
, sender
)
149 now
= datetime
.datetime
.now()
150 if sender
.black_before
is not None \
151 and sender
.black_before
> now
:
152 if (datetime
.datetime
.today()+timezone
).date() == \
153 (sender
.black_before
+timezone
).date():
156 format
= '%m月%d日 %H时%M分%S秒'
157 msg
.reply('你已被禁言至 ' \
158 + (sender
.black_before
+timezone
).strftime(format
))
161 if sender
.last_speak_date
is not None:
162 d
= now
- sender
.last_speak_date
164 if d
.days
> 0 or t
> 60:
165 sender
.flooding_point
= 0
167 k
= 1000 / (t
* t
+ 1)
169 sender
.flooding_point
+= k
171 sender
.flooding_point
= 0
173 k
= sender
.flooding_point
/ 1500
175 msg
.reply('刷屏啊?禁言 %d 分钟!' % k
)
176 send_to_all_except(sender
.jid
,
177 (u
'%s 已因刷屏而被禁言 %d 分钟。' % (sender
.nick
, k
)) \
179 log_onoff(sender
, BLACK_AUTO
% (60 * k
))
180 sender
.black_before
= now
+ datetime
.timedelta(seconds
=60*k
)
184 sender
.last_speak_date
= now
185 sender
.snooze_before
= None
187 sender
.msg_count
+= 1
188 sender
.msg_chars
+= len(msg
.body
)
191 sender
.msg_chars
= len(msg
.body
)
193 body
= utils
.removelinks(msg
.body
)
194 for u
in User
.gql('where avail != :1', OFFLINE
):
195 if u
.snooze_before
is not None and u
.snooze_before
>= now
:
197 if u
.jid
== sender
.jid
:
200 message
= '%s %s' % (
201 u
.nick_pattern
% sender
.nick
,
204 xmpp
.send_message(u
.jid
, message
)
205 except xmpp
.InvalidJidError
:
207 log_msg(sender
, msg
.body
)
209 def try_add_user(jid
, show
=OFFLINE
, resource
=''):
210 '''使用 memcache 作为锁添加用户'''
211 L
= utils
.MemLock('add_user')
214 u
= get_user_by_jid(jid
)
217 u
= add_user(jid
, show
, resource
)
221 log_onoff(u
, show
, resource
)
222 logging
.info(u
'%s added', jid
)
224 def add_user(jid
, show
=OFFLINE
, resource
=''):
225 '''resource 在 presence type 为 available 里使用'''
226 nick
= jid
.split('@')[0]
227 old
= get_user_by_nick(nick
)
230 old
= get_user_by_nick(nick
)
231 u
= User(jid
=jid
.lower(), avail
=show
, nick
=nick
)
233 u
.last_online_date
= datetime
.datetime
.now()
235 u
.resources
.append(resource
)
238 logging
.info(u
'%s 已经加入' % jid
)
239 send_to_all_except(jid
, u
'%s 已经加入' % u
.nick
)
240 xmpp
.send_presence(jid
, status
=notice
)
241 xmpp
.send_message(jid
, u
'欢迎 %s 加入!获取使用帮助,请输入 %shelp' % (
247 def __init__(self
, msg
, sender
):
251 if helpre
.match(msg
.body
):
253 elif msg
.body
.startswith(sender
.prefix
):
254 cmd
= msg
.body
[len(sender
.prefix
):].split()
256 handle
= getattr(self
, 'do_' + cmd
[0])
257 except AttributeError:
258 msg
.reply(u
'错误:未知命令 %s' % cmd
[0])
261 except UnicodeEncodeError:
262 msg
.reply(u
'错误:命令名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
265 logging
.debug('%s did command %s' % (sender
.jid
, cmd
[0]))
269 def do_online(self
, args
):
270 '''在线成员列表。可带一个参数,指定在名字中出现的一个子串。'''
272 pat
= args
[0] if args
else None
273 now
= datetime
.datetime
.now()
274 l
= User
.gql('where avail != :1', OFFLINE
)
277 if pat
and m
.find(pat
) == -1:
281 m
+= u
' (%s)' % status
282 if u
.snooze_before
is not None and u
.snooze_before
> now
:
284 if u
.black_before
is not None and u
.black_before
> now
:
286 r
.append(unicode('* ' + m
))
290 r
.insert(0, u
'在线成员列表(包含子串 %s):' % pat
)
291 r
.append(u
'共 %d 人。' % n
)
293 r
.insert(0, u
'在线成员列表:')
294 r
.append(u
'共 %d 人在线。' % n
)
295 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
297 def do_lsadmin(self
, args
):
300 now
= datetime
.datetime
.now()
301 l
= User
.gql('where is_admin = :1', True)
306 m
+= u
' (%s)' % status
307 if u
.snooze_before
is not None and u
.snooze_before
> now
:
309 if u
.black_before
is not None and u
.black_before
> now
:
311 r
.append(unicode('* ' + m
))
314 r
.insert(0, u
'管理员列表:')
315 r
.append(u
'共 %d 位管理员。' % n
)
316 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
318 def do_chatty(self
, args
):
321 for u
in User
.gql('ORDER BY msg_chars ASC'):
323 m
= u
'* %s:\t%5d条,共 %s' % (
325 utils
.filesize(u
.msg_chars
))
328 r
.insert(0, u
'消息数量排行:')
329 r
.append(u
'共 %d 人。' % n
)
330 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
332 def do_nick(self
, args
):
333 '''更改昵称,需要一个参数,不能使用大部分标点符号'''
335 self
.msg
.reply('错误:请给出你想到的昵称(不能包含空格)')
338 q
= get_user_by_nick(args
[0])
340 self
.msg
.reply('错误:该昵称已被使用,请使用其它昵称')
341 elif not utils
.checkNick(args
[0]):
342 self
.msg
.reply('错误:非法的昵称')
344 if not config
.nick_can_change
and self
.sender
.nick_changed
:
345 self
.msg
.reply('乖哦,你已经没机会再改昵称了')
347 old_nick
= self
.sender
.nick
348 log_onoff(self
.sender
, NICK
% (old_nick
, args
[0]))
349 self
.sender
.nick
= args
[0]
350 self
.sender
.nick_changed
= True
352 send_to_all_except(self
.sender
.jid
,
353 (u
'%s 的昵称改成了 %s' % (old_nick
, args
[0])).encode('utf-8'))
354 self
.msg
.reply('昵称更改成功!')
355 do_nick
.__doc
__ += ',最长 %d 字节' % config
.nick_maxlen
357 def do_whois(self
, args
):
360 self
.msg
.reply('错误:你想知道关于谁的信息?')
363 u
= get_user_by_nick(args
[0])
365 self
.msg
.reply(u
'Sorry,查无此人。')
368 now
= datetime
.datetime
.now()
370 addtime
= (u
.add_date
+ timezone
).strftime('%Y年%m月%d日 %H时%M分').decode('utf-8')
371 allowpm
= u
'否' if u
.reject_pm
else u
'是'
372 if u
.snooze_before
is not None and u
.snooze_before
> now
:
373 status
+= u
' (snoozing)'
374 if u
.black_before
is not None and u
.black_before
> now
:
377 r
.append(u
'昵称:\t%s' % u
.nick
)
378 if self
.sender
.is_admin
:
379 r
.append(u
'JID:\t%s' % u
.jid
)
380 r
.append(u
'状态:\t%s' % status
)
381 r
.append(u
'消息数:\t%d' % u
.msg_count
)
382 r
.append(u
'消息总量:\t%s' % utils
.filesize(u
.msg_chars
))
383 r
.append(u
'加入时间:\t%s' % addtime
)
384 r
.append(u
'接收私信:\t%s' % allowpm
)
385 r
.append(u
'自我介绍:\t%s' % u
.intro
)
386 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
388 def do_help(self
, args
=()):
389 '''显示本帮助。参数 long 显示详细帮助,也可指定命令名。'''
391 prefix
= self
.sender
.prefix
394 self
.msg
.reply('参数错误。')
396 arg
= args
[0] if args
else None
398 if arg
is None or arg
== 'long':
399 for b
in self
.__class
__.__bases
__ + (self
.__class
__,):
400 for c
, f
in b
.__dict
__.items():
401 if c
.startswith('do_'):
403 doc
.append(u
'%s%s:\t%s' % (prefix
, c
[3:], f
.__doc
__.decode('utf-8').split(u
',', 1)[0].split(u
'。', 1)[0]))
405 doc
.append(u
'%s%s:\t%s' % (prefix
, c
[3:], f
.__doc
__.decode('utf-8')))
408 doc
.insert(0, u
'** 命令指南 **\n(当前命令前缀 %s,可设置。使用 %shelp long 显示详细帮助)' % (prefix
, prefix
))
410 doc
.insert(0, u
'** 命令指南 **\n(当前命令前缀 %s,可设置)' % prefix
)
411 doc
.append(u
'要离开,直接删掉好友即可。')
412 self
.msg
.reply(u
'\n'.join(doc
).encode('utf-8'))
415 handle
= getattr(self
, 'do_' + arg
)
416 except AttributeError:
417 self
.msg
.reply(u
'错误:未知命令 %s' % arg
)
418 except UnicodeEncodeError:
419 self
.msg
.reply(u
'错误:命令名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
421 self
.msg
.reply(u
'%s%s:\t%s' % (prefix
, arg
, handle
.__doc
__.decode('utf-8')))
423 def do_iam(self
, args
):
426 addtime
= (u
.add_date
+ timezone
).strftime('%Y年%m月%d日 %H时%M分').decode('utf-8')
427 allowpm
= u
'否' if u
.reject_pm
else u
'是'
429 r
.append(u
'昵称:\t%s' % u
.nick
)
430 r
.append(u
'JID:\t%s' % u
.jid
)
431 r
.append(u
'消息数:\t%d' % u
.msg_count
)
432 r
.append(u
'消息总量:\t%s' % utils
.filesize(u
.msg_chars
))
433 r
.append(u
'加入时间:\t%s' % addtime
)
434 r
.append(u
'命令前缀:\t%s' % u
.prefix
)
435 r
.append(u
'接收私信:\t%s' % allowpm
)
436 r
.append(u
'自我介绍:\t%s' % u
.intro
)
437 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
439 def do_m(self
, args
):
440 '''发私信,需要昵称和内容两个参数。私信不会以任何方式被记录。用户可使用 set 命令设置是否接收私信。'''
442 self
.msg
.reply('请给出昵称和内容。')
445 target
= get_user_by_nick(args
[0])
447 self
.msg
.reply('Sorry,查无此人。')
451 self
.msg
.reply('很抱歉,对方不接收私信。')
454 msg
= self
.msg
.body
[len(self
.sender
.prefix
):].split(None, 2)[-1]
455 msg
= u
'_私信_ %s %s' % (target
.nick_pattern
% self
.sender
.nick
, msg
)
456 if xmpp
.send_message(target
.jid
, msg
) == xmpp
.NO_ERROR
:
457 self
.msg
.reply(u
'OK')
459 self
.msg
.reply(u
'消息发送失败')
461 def do_intro(self
, arg
):
464 self
.msg
.reply('请给出自我介绍的内容。')
467 msg
= self
.msg
.body
[len(self
.sender
.prefix
):].split(None, 1)[-1]
471 except db
.BadValueError
:
472 # 过长文本已在 handle_message 中被拦截
473 self
.msg
.reply('错误:自我介绍内容只能为一行。')
477 send_to_all_except(u
.jid
,
478 (u
'%s 的新自我介绍:%s' % (u
.nick
, msg
)).encode('utf-8'))
479 self
.msg
.reply(u
'设置成功!')
481 def do_snooze(self
, args
):
482 '''暂停接收消息,参数为时间(默认单位为秒)。再次发送消息时自动清除'''
484 self
.msg
.reply('你想停止接收消息多久?')
488 n
= utils
.parseTime(args
[0])
490 self
.msg
.reply('Sorry,我无法理解你说的时间。')
494 self
.sender
.snooze_before
= datetime
.datetime
.now() + datetime
.timedelta(seconds
=n
)
495 except OverflowError:
496 self
.msg
.reply('Sorry,你不能睡太久。')
500 self
.msg
.reply('你已经醒来。')
502 self
.msg
.reply('OK,停止接收消息 %d 秒。' % n
)
503 log_onoff(self
.sender
, SNOOZE
% n
)
505 def do_offline(self
, args
):
506 '''假装离线,让程序认为你的所有资源已离线。如在你离线时程序仍认为你在线,请使用此命令。'''
507 del self
.sender
.resources
[:]
508 self
.sender
.avail
= OFFLINE
509 self
.sender
.last_offline_date
= datetime
.datetime
.now()
511 self
.msg
.reply('OK,在下次你说你在线之前我都认为你已离线。')
513 def do_fakeresource(self
, args
):
514 '''假装在线,人工加入一个新的资源,使程序认为你总是在线。使用 offline 命令可删除所有资源的记录。'''
515 self
.sender
.resources
.append('fakeresouce')
517 self
.msg
.reply('OK,你将永远在线。')
519 def do_old(self
, args
):
520 '''聊天记录查询,可选一个数字参数。默认为最后20条。特殊参数 OFFLINE (不区分大小写)显示离线消息(最多 100 条)'''
524 q
= Log
.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT 20")
529 q
= Log
.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT %d" % n
)
531 if args
[0].upper() == 'OFFLINE':
532 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
)
540 if datetime
.datetime
.today() - q
[0].time
> datetime
.timedelta(hours
=24):
545 message
= '%s %s %s' % (
546 utils
.strftime(l
.time
, timezone
, show_date
),
547 s
.nick_pattern
% l
.nick
,
552 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
554 self
.msg
.reply('没有符合的聊天记录。')
556 self
.msg
.reply('Oops, 参数不正确哦。')
558 def do_set(self
, args
):
559 '''设置参数。参数格式 key=value;不带参数以查看说明。'''
563 for c
, f
in self
.__class
__.__dict
__.items():
564 if c
.startswith('set_'):
565 doc
.append(u
'* %s:\t%s' % (c
[4:], f
.__doc
__.decode('utf-8')))
566 for b
in self
.__class
__.__bases
__:
567 for c
, f
in b
.__dict
__.items():
568 if c
.startswith('set_'):
569 doc
.append(u
'* %s:\t%s' % (c
[4:], f
.__doc
__.decode('utf-8')))
571 doc
.insert(0, u
'设置选项:')
572 self
.msg
.reply(u
'\n'.join(doc
).encode('utf-8'))
575 cmd
= msg
.body
.split(None, 1)[1].split('=', 1)
577 msg
.reply(u
'错误:请给出选项值')
580 handle
= getattr(self
, 'set_' + cmd
[0])
581 except AttributeError:
582 msg
.reply(u
'错误:未知选项 %s' % cmd
[0])
585 except UnicodeEncodeError:
586 msg
.reply(u
'错误:选项名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
590 def set_prefix(self
, arg
):
592 self
.sender
.prefix
= arg
594 self
.msg
.reply(u
'设置成功!')
596 def set_nickpattern(self
, arg
):
597 '''设置昵称显示格式,用 %s 表示昵称的位置'''
600 except (TypeError, ValueError):
601 self
.msg
.reply(u
'错误:不正确的格式')
604 self
.sender
.nick_pattern
= arg
606 self
.msg
.reply(u
'设置成功!')
608 def set_allowpm(self
, arg
):
609 '''设置是否接收私信,参数为 y(接收)或者 n(拒绝)'''
611 self
.msg
.reply(u
'错误的参数。')
615 self
.sender
.reject_pm
= False
617 self
.sender
.reject_pm
= True
619 self
.msg
.reply(u
'设置成功!')
621 class AdminCommand(BasicCommand
):
622 def do_kick(self
, args
):
625 self
.msg
.reply('请给出昵称。')
628 target
= get_user_by_nick(args
[0])
630 self
.msg
.reply('Sorry,查无此人。')
633 if target
.jid
== config
.root
:
634 self
.msg
.reply('不能删除 root 用户')
637 targetjid
= target
.jid
638 targetnick
= target
.nick
640 self
.msg
.reply((u
'OK,删除 %s。' % target
.nick
).encode('utf-8'))
641 send_to_all_except(self
.sender
.jid
, (u
'%s 已被删除。' % self
.sender
.nick
) \
643 xmpp
.send_message(targetjid
, u
'你已被管理员从此群中删除,请删除该好友。')
644 log_onoff(self
.sender
, KICK
% (targetnick
, targetjid
))
646 def do_quiet(self
, args
):
647 '''禁言某人,参数为昵称和时间(默认单位秒)'''
649 self
.msg
.reply('请给出昵称和时间。')
653 n
= utils
.parseTime(args
[1])
655 self
.msg
.reply('Sorry,我无法理解你说的时间。')
658 target
= get_user_by_nick(args
[0])
660 self
.msg
.reply('Sorry,查无此人。')
663 target
.black_before
= datetime
.datetime
.now() + datetime
.timedelta(seconds
=n
)
665 self
.msg
.reply((u
'OK,禁言 %s %d 秒。' % (target
.nick
, n
)).encode('utf-8'))
666 send_to_all_except((self
.sender
.jid
, target
.jid
),
667 (u
'%s 已被禁言 %s 秒。' % (target
.nick
, n
)) \
669 xmpp
.send_message(target
.jid
, u
'你已被管理员禁言 %d 秒。' % n
)
670 log_onoff(self
.sender
, BLACK
% (target
.nick
, n
))
672 def do_admin(self
, args
):
675 self
.msg
.reply(u
'请给出昵称。')
678 target
= get_user_by_nick(args
[0])
680 self
.msg
.reply(u
'Sorry,查无此人。')
684 self
.msg
.reply(u
'%s 已经是管理员了。' % target
.nick
)
687 target
.is_admin
= True
689 send_to_all_except(target
.jid
,
690 (u
'%s 已成为管理员。' % target
.nick
) \
692 xmpp
.send_message(target
.jid
, u
'你已是本群管理员。')
693 log_onoff(self
.sender
, ADMIN
% (target
.nick
, self
.sender
.nick
))
695 def do_unadmin(self
, args
):
698 self
.msg
.reply(u
'请给出昵称。')
701 target
= get_user_by_nick(args
[0])
703 self
.msg
.reply(u
'Sorry,查无此人。')
706 if not target
.is_admin
:
707 self
.msg
.reply(u
'%s 不是管理员。' % target
.nick
)
710 target
.is_admin
= False
712 send_to_all_except(target
.jid
,
713 (u
'%s 已不再是管理员。' % target
.nick
) \
715 xmpp
.send_message(target
.jid
, u
'你已不再是本群管理员。')
716 log_onoff(self
.sender
, UNADMIN
% (target
.nick
, self
.sender
.nick
))