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
):
272 now
= datetime
.datetime
.now()
273 l
= User
.gql('where avail != :1', OFFLINE
)
278 m
+= u
' (%s)' % status
279 if u
.snooze_before
is not None and u
.snooze_before
> now
:
281 if u
.black_before
is not None and u
.black_before
> now
:
283 r
.append(unicode('* ' + m
))
286 r
.insert(0, u
'在线成员列表:')
287 r
.append(u
'共 %d 人在线。' % n
)
288 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
290 def do_lsadmin(self
, args
):
293 now
= datetime
.datetime
.now()
294 l
= User
.gql('where is_admin = :1', True)
299 m
+= u
' (%s)' % status
300 if u
.snooze_before
is not None and u
.snooze_before
> now
:
302 if u
.black_before
is not None and u
.black_before
> now
:
304 r
.append(unicode('* ' + m
))
307 r
.insert(0, u
'管理员列表:')
308 r
.append(u
'共 %d 位管理员。' % n
)
309 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
311 def do_chatty(self
, args
):
314 for u
in User
.gql('ORDER BY msg_chars ASC'):
316 m
= u
'* %s:\t%5d条,共 %s' % (
318 utils
.filesize(u
.msg_chars
))
321 r
.insert(0, u
'消息数量排行:')
322 r
.append(u
'共 %d 人。' % n
)
323 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
325 def do_nick(self
, args
):
328 self
.msg
.reply('错误:请给出你想到的昵称(不能包含空格)')
331 q
= get_user_by_nick(args
[0])
333 self
.msg
.reply('错误:该昵称已被使用,请使用其它昵称')
334 elif not utils
.checkNick(args
[0]):
335 self
.msg
.reply('错误:非法的昵称')
337 if not config
.nick_can_change
and self
.sender
.nick_changed
:
338 self
.msg
.reply('乖哦,你已经没机会再改昵称了')
340 old_nick
= self
.sender
.nick
341 log_onoff(self
.sender
, NICK
% (old_nick
, args
[0]))
342 self
.sender
.nick
= args
[0]
343 self
.sender
.nick_changed
= True
345 send_to_all_except(self
.sender
.jid
,
346 (u
'%s 的昵称改成了 %s' % (old_nick
, args
[0])).encode('utf-8'))
347 self
.msg
.reply('昵称更改成功!')
348 do_nick
.__doc
__ += ',最长 %d 字节' % config
.nick_maxlen
350 def do_whois(self
, args
):
353 self
.msg
.reply('错误:你想知道关于谁的信息?')
356 u
= get_user_by_nick(args
[0])
358 self
.msg
.reply(u
'Sorry,查无此人。')
361 now
= datetime
.datetime
.now()
363 addtime
= (u
.add_date
+ timezone
).strftime('%Y年%m月%d日 %H时%M分').decode('utf-8')
364 allowpm
= u
'否' if u
.reject_pm
else u
'是'
365 if u
.snooze_before
is not None and u
.snooze_before
> now
:
366 status
+= u
' (snoozing)'
367 if u
.black_before
is not None and u
.black_before
> now
:
369 r
= u
'昵称:\t\t%s\n状态:\t\t%s\n消息数:\t\t%d\n消息总量:\t%s\n加入时间:\t%s\n接受私信:\t%s\n自我介绍:\t%s' % (
370 u
.nick
, status
, u
.msg_count
, utils
.filesize(u
.msg_chars
), addtime
,
372 self
.msg
.reply(r
.encode('utf-8'))
374 def do_help(self
, args
=None):
377 prefix
= self
.sender
.prefix
379 for b
in self
.__class
__.__bases
__ + (self
.__class
__,):
380 for c
, f
in b
.__dict
__.items():
381 if c
.startswith('do_'):
382 doc
.append(u
'%s%s:\t%s' % (prefix
, c
[3:], f
.__doc
__.decode('utf-8')))
385 doc
.insert(0, u
'命令指南 (当前命令前缀 %s,可设置)' % prefix
)
386 doc
.append(u
'要离开,直接删掉好友即可。')
387 self
.msg
.reply(u
'\n'.join(doc
).encode('utf-8'))
389 def do_iam(self
, args
):
392 addtime
= (u
.add_date
+ timezone
).strftime('%Y年%m月%d日 %H时%M分').decode('utf-8')
393 allowpm
= u
'否' if u
.reject_pm
else u
'是'
394 r
= u
'昵称:\t\t%s\nJID:\t\t%s\n消息数:\t\t%d\n消息总量:\t%s\n加入时间:\t%s\n接受私信:\t%s\n命令前缀:\t%s\n自我介绍:\t%s' % (
395 u
.nick
, u
.jid
, u
.msg_count
, utils
.filesize(u
.msg_chars
), addtime
,
396 allowpm
, u
.prefix
, u
.intro
)
397 self
.msg
.reply(r
.encode('utf-8'))
399 def do_m(self
, args
):
400 '''给某人发私信,需要昵称和内容两个参数。私信不会以任何方式被记录。'''
402 self
.msg
.reply('请给出昵称和内容。')
405 target
= get_user_by_nick(args
[0])
407 self
.msg
.reply('Sorry,查无此人。')
411 self
.msg
.reply('很抱歉,对方不接受私信。')
414 msg
= self
.msg
.body
[len(self
.sender
.prefix
):].split(None, 2)[-1]
415 msg
= u
'_私信_ %s %s' % (target
.nick_pattern
% self
.sender
.nick
, msg
)
416 if xmpp
.send_message(target
.jid
, msg
) == xmpp
.NO_ERROR
:
417 self
.msg
.reply(u
'OK')
419 self
.msg
.reply(u
'消息发送失败')
421 def do_intro(self
, arg
):
424 self
.msg
.reply('请给出自我介绍的内容。')
427 msg
= self
.msg
.body
[len(self
.sender
.prefix
):].split(None, 1)[-1]
431 except db
.BadValueError
:
432 # 过长文本已在 handle_message 中被拦截
433 self
.msg
.reply('错误:自我介绍内容只能为一行。')
437 send_to_all_except(u
.jid
,
438 (u
'%s 的新自我介绍:%s' % (u
.nick
, msg
)).encode('utf-8'))
439 self
.msg
.reply(u
'设置成功!')
441 def do_snooze(self
, args
):
442 '''暂停接收消息,参数为时间(默认单位为秒)。再次发送消息时自动清除'''
444 self
.msg
.reply('你想停止接收消息多久?')
448 n
= utils
.parseTime(args
[0])
450 self
.msg
.reply('Sorry,我无法理解你说的时间。')
454 self
.sender
.snooze_before
= datetime
.datetime
.now() + datetime
.timedelta(seconds
=n
)
455 except OverflowError:
456 self
.msg
.reply('Sorry,你不能睡太久。')
460 self
.msg
.reply('你已经醒来。')
462 self
.msg
.reply('OK,停止接收消息 %d 秒。' % n
)
463 log_onoff(self
.sender
, SNOOZE
% n
)
465 def do_offline(self
, args
):
466 '''让程序认为你的所有资源已离线。如在你离线时程序仍认为你在线,请使用此命令。'''
467 del self
.sender
.resources
[:]
468 self
.sender
.avail
= OFFLINE
469 self
.sender
.last_offline_date
= datetime
.datetime
.now()
471 self
.msg
.reply('OK,在下次你说你在线之前我都认为你已离线。')
473 def do_old(self
, args
):
474 '''查询聊天记录,可选一个数字参数。默认为最后20条。特殊参数 OFFLINE 显示离线消息(最多 100 条)'''
478 q
= Log
.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT 20")
483 q
= Log
.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT %d" % n
)
485 if args
[0].upper() == 'OFFLINE':
486 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
)
494 if datetime
.datetime
.today() - q
[0].time
> datetime
.timedelta(hours
=24):
499 message
= '%s %s %s' % (
500 utils
.strftime(l
.time
, timezone
, show_date
),
501 s
.nick_pattern
% l
.nick
,
506 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
508 self
.msg
.reply('没有符合的聊天记录。')
510 self
.msg
.reply('Oops, 参数不正确哦。')
512 def do_set(self
, args
):
513 '''设置一些参数。参数格式 key=value;不带参数以查看说明。'''
517 for c
, f
in self
.__class
__.__dict
__.items():
518 if c
.startswith('set_'):
519 doc
.append(u
'* %s:\t%s' % (c
[4:], f
.__doc
__.decode('utf-8')))
520 for b
in self
.__class
__.__bases
__:
521 for c
, f
in b
.__dict
__.items():
522 if c
.startswith('set_'):
523 doc
.append(u
'* %s:\t%s' % (c
[4:], f
.__doc
__.decode('utf-8')))
525 doc
.insert(0, u
'设置选项:')
526 self
.msg
.reply(u
'\n'.join(doc
).encode('utf-8'))
529 cmd
= msg
.body
.split(None, 1)[1].split('=', 1)
531 msg
.reply(u
'错误:请给出选项值')
534 handle
= getattr(self
, 'set_' + cmd
[0])
535 except AttributeError:
536 msg
.reply(u
'错误:未知选项 %s' % cmd
[0])
539 except UnicodeEncodeError:
540 msg
.reply(u
'错误:选项名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
544 def set_prefix(self
, arg
):
546 self
.sender
.prefix
= arg
548 self
.msg
.reply(u
'设置成功!')
550 def set_nickpattern(self
, arg
):
551 '''设置昵称显示格式,用 %s 表示昵称的位置'''
554 except (TypeError, ValueError):
555 self
.msg
.reply(u
'错误:不正确的格式')
558 self
.sender
.nick_pattern
= arg
560 self
.msg
.reply(u
'设置成功!')
562 def set_allowpm(self
, arg
):
563 '''设置是否接受私信,参数为 y(接受)或者 n(拒绝)'''
565 self
.msg
.reply(u
'错误的参数。')
569 self
.sender
.reject_pm
= False
571 self
.sender
.reject_pm
= True
573 self
.msg
.reply(u
'设置成功!')
575 class AdminCommand(BasicCommand
):
576 def do_kick(self
, args
):
579 self
.msg
.reply('请给出昵称。')
582 target
= get_user_by_nick(args
[0])
584 self
.msg
.reply('Sorry,查无此人。')
587 if target
.jid
== config
.root
:
588 self
.msg
.reply('不能删除 root 用户')
591 targetjid
= target
.jid
592 targetnick
= target
.nick
594 self
.msg
.reply((u
'OK,删除 %s。' % target
.nick
).encode('utf-8'))
595 send_to_all_except(self
.sender
.jid
, (u
'%s 已被删除。' % self
.sender
.nick
) \
597 xmpp
.send_message(targetjid
, u
'你已被管理员从此群中删除,请删除该好友。')
598 log_onoff(self
.sender
, KICK
% (targetnick
, targetjid
))
600 def do_quiet(self
, args
):
601 '''禁言某人,参数为昵称和时间(默认单位秒)'''
603 self
.msg
.reply('请给出昵称和时间。')
607 n
= utils
.parseTime(args
[1])
609 self
.msg
.reply('Sorry,我无法理解你说的时间。')
612 target
= get_user_by_nick(args
[0])
614 self
.msg
.reply('Sorry,查无此人。')
617 target
.black_before
= datetime
.datetime
.now() + datetime
.timedelta(seconds
=n
)
619 self
.msg
.reply((u
'OK,禁言 %s %d 秒。' % (target
.nick
, n
)).encode('utf-8'))
620 send_to_all_except((self
.sender
.jid
, target
.jid
),
621 (u
'%s 已被禁言 %s 秒。' % (self
.sender
.nick
, n
)) \
623 xmpp
.send_message(target
.jid
, u
'你已被管理员禁言 %d 秒。' % n
)
624 log_onoff(self
.sender
, BLACK
% (target
.nick
, n
))
626 def do_admin(self
, args
):
629 self
.msg
.reply(u
'请给出昵称。')
632 target
= get_user_by_nick(args
[0])
634 self
.msg
.reply(u
'Sorry,查无此人。')
638 self
.msg
.reply(u
'%s 已经是管理员了。' % target
.nick
)
641 target
.is_admin
= True
643 send_to_all_except(target
.jid
,
644 (u
'%s 已成为管理员。' % target
.nick
) \
646 xmpp
.send_message(target
.jid
, u
'你已是本群管理员。')
647 log_onoff(self
.sender
, ADMIN
% (target
.nick
, self
.sender
.nick
))
649 def do_unadmin(self
, args
):
652 self
.msg
.reply(u
'请给出昵称。')
655 target
= get_user_by_nick(args
[0])
657 self
.msg
.reply(u
'Sorry,查无此人。')
660 if not target
.is_admin
:
661 self
.msg
.reply(u
'%s 不是管理员。' % target
.nick
)
664 target
.is_admin
= False
666 send_to_all_except(target
.jid
,
667 (u
'%s 已不再是管理员。' % target
.nick
) \
669 xmpp
.send_message(target
.jid
, u
'你已不再是本群管理员。')
670 log_onoff(self
.sender
, UNADMIN
% (target
.nick
, self
.sender
.nick
))