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 prefix
= db
.StringProperty(required
=True, default
=config
.default_prefix
)
70 nick_pattern
= db
.StringProperty(required
=True, default
='[%s]')
71 nick_changed
= db
.BooleanProperty(required
=True, default
=False)
72 intro
= db
.StringProperty()
75 time
= db
.DateTimeProperty(auto_now_add
=True, indexed
=True)
76 msg
= db
.StringProperty(required
=True, multiline
=True)
77 jid
= db
.StringProperty()
78 nick
= db
.StringProperty()
79 type = db
.StringProperty(required
=True, indexed
=True,
80 choices
=set(['chat', 'member', 'misc']))
82 def log_msg(sender
, msg
):
83 l
= Log(jid
=sender
.jid
, nick
=sender
.nick
,
87 def log_onoff(sender
, action
, resource
=''):
89 if action
== OFFLINE
and not sender
.resources
:
90 msg
= u
'完全%s (%s)' % (action
, resource
)
92 msg
= '%s (%s)' % (action
, resource
)
95 l
= Log(jid
=sender
.jid
, nick
=sender
.nick
,
96 type='member', msg
=msg
)
99 def get_user_by_jid(jid
):
100 return User
.gql('where jid = :1', jid
).get()
102 def get_user_by_nick(nick
):
103 return User
.gql('where nick = :1', nick
).get()
105 def get_member_list():
107 now
= datetime
.datetime
.now()
109 l
= User
.gql('where avail != :1', OFFLINE
)
112 return [unicode(x
.jid
) for x
in r \
113 if x
.snooze_before
is None or x
.snooze_before
< now
]
115 def send_to_all_except(jid
, message
):
116 if isinstance(jid
, str):
117 jids
= [x
for x
in get_member_list() if x
!= jid
]
119 jids
= [x
for x
in get_member_list() if x
not in jid
]
123 xmpp
.send_message(jids
, message
)
124 except xmpp
.InvalidJidError
:
127 def send_to_all(message
):
128 jids
= get_member_list()
129 xmpp
.send_message(jids
, message
)
131 def handle_message(msg
):
132 sender
= get_user_by_jid(msg
.sender
.split('/')[0])
134 msg
.reply('很抱歉,出错了,请重新添加好友。')
136 if msg
.body
.startswith('?OTR:'):
137 msg
.reply('不支持 OTR 加密!')
139 if len(msg
.body
) > 500:
140 msg
.reply('由于技术限制,每条消息最长为 500 字。大段文本请贴 paste 网站。')
142 if sender
.is_admin
or sender
.jid
== config
.root
:
143 ch
= AdminCommand(msg
, sender
)
145 ch
= BasicCommand(msg
, sender
)
147 now
= datetime
.datetime
.now()
148 if sender
.black_before
is not None \
149 and sender
.black_before
> now
:
150 if (datetime
.datetime
.today()+timezone
).date() == \
151 (sender
.black_before
+timezone
).date():
154 format
= '%m月%d日 %H时%M分%S秒'
155 msg
.reply('你已被禁言至 ' \
156 + (sender
.black_before
+timezone
).strftime(format
))
159 if sender
.last_speak_date
is not None:
160 d
= now
- sender
.last_speak_date
162 if d
.days
> 0 or t
> 60:
163 sender
.flooding_point
= 0
165 k
= 1000 / (t
* t
+ 1)
167 sender
.flooding_point
+= k
169 sender
.flooding_point
= 0
171 k
= sender
.flooding_point
/ 1500
173 msg
.reply('刷屏啊?禁言 %d 分钟!' % k
)
174 send_to_all_except(sender
.jid
,
175 (u
'%s 已因刷屏而被禁言 %d 分钟。' % (sender
.nick
, k
)) \
177 log_onoff(sender
, BLACK_AUTO
% (60 * k
))
178 sender
.black_before
= now
+ datetime
.timedelta(seconds
=60*k
)
182 sender
.last_speak_date
= now
183 sender
.snooze_before
= None
185 sender
.msg_count
+= 1
186 sender
.msg_chars
+= len(msg
.body
)
189 sender
.msg_chars
= len(msg
.body
)
191 body
= utils
.removelinks(msg
.body
)
192 for u
in User
.gql('where avail != :1', OFFLINE
):
193 if u
.snooze_before
is not None and u
.snooze_before
>= now
:
195 if u
.jid
== sender
.jid
:
198 message
= '%s %s' % (
199 u
.nick_pattern
% sender
.nick
,
202 xmpp
.send_message(u
.jid
, message
)
203 except xmpp
.InvalidJidError
:
205 log_msg(sender
, msg
.body
)
207 def try_add_user(jid
, show
=OFFLINE
, resource
=''):
208 '''使用 memcache 作为锁添加用户'''
209 L
= utils
.MemLock('add_user')
212 u
= get_user_by_jid(jid
)
215 u
= add_user(jid
, show
, resource
)
219 log_onoff(u
, show
, resource
)
220 logging
.info(u
'%s added', jid
)
222 def add_user(jid
, show
=OFFLINE
, resource
=''):
223 '''resource 在 presence type 为 available 里使用'''
224 nick
= jid
.split('@')[0]
225 old
= User
.gql('where nick = :1', nick
).get()
228 old
= User
.gql('where nick = :1', nick
).get()
229 u
= User(jid
=jid
, avail
=show
, nick
=nick
)
231 u
.last_online_date
= datetime
.datetime
.now()
233 u
.resources
.append(resource
)
236 logging
.info(u
'%s 已经加入' % jid
)
237 send_to_all_except(jid
, u
'%s 已经加入' % u
.nick
)
238 xmpp
.send_presence(jid
, status
=notice
)
239 xmpp
.send_message(jid
, u
'欢迎 %s 加入!获取使用帮助,请输入 %shelp' % (
245 def __init__(self
, msg
, sender
):
249 if helpre
.match(msg
.body
):
251 elif msg
.body
.startswith(sender
.prefix
):
252 cmd
= msg
.body
[len(sender
.prefix
):].split()
254 handle
= getattr(self
, 'do_' + cmd
[0])
255 except AttributeError:
256 msg
.reply(u
'错误:未知命令 %s' % cmd
[0])
259 except UnicodeEncodeError:
260 msg
.reply(u
'错误:命令名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
263 logging
.debug('%s did command %s' % (sender
.jid
, msg
.body
))
267 def do_online(self
, args
):
270 now
= datetime
.datetime
.now()
271 l
= User
.gql('where avail != :1', OFFLINE
)
276 m
+= u
' (%s)' % status
277 if u
.snooze_before
is not None and u
.snooze_before
> now
:
279 if u
.black_before
is not None and u
.black_before
> now
:
281 r
.append(unicode('* ' + m
))
284 r
.insert(0, u
'在线成员列表:')
285 r
.append(u
'共 %d 人在线。' % n
)
286 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
288 def do_chatty(self
, args
):
291 for u
in User
.gql('ORDER BY msg_chars ASC'):
293 m
= u
'* %s:\t%5d条,共 %s' % (
295 utils
.filesize(u
.msg_chars
))
298 r
.insert(0, u
'消息数量排行:')
299 r
.append(u
'共 %d 人。' % n
)
300 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
302 def do_nick(self
, args
):
305 self
.msg
.reply('错误:请给出你想到的昵称(不能包含空格)')
308 q
= User
.gql('where nick = :1', args
[0]).get()
310 self
.msg
.reply('错误:该昵称已被使用,请使用其它昵称')
311 elif not utils
.checkNick(args
[0]):
312 self
.msg
.reply('错误:非法的昵称')
314 if not config
.nick_can_change
and self
.sender
.nick_changed
:
315 self
.msg
.reply('乖哦,你已经没机会再改昵称了')
317 old_nick
= self
.sender
.nick
318 log_onoff(self
.sender
, NICK
% (old_nick
, args
[0]))
319 self
.sender
.nick
= args
[0]
320 self
.sender
.nick_changed
= True
322 send_to_all_except(self
.sender
.jid
,
323 (u
'%s 的昵称改成了 %s' % (old_nick
, args
[0])).encode('utf-8'))
324 self
.msg
.reply('昵称更改成功!')
325 do_nick
.__doc
__ += ',最长 %d 字节' % config
.nick_maxlen
327 def do_help(self
, args
=None):
330 prefix
= self
.sender
.prefix
331 for c
, f
in self
.__class
__.__dict
__.items():
332 if c
.startswith('do_'):
333 doc
.append(u
'%s%s:\t%s' % (prefix
, c
[3:], f
.__doc
__.decode('utf-8')))
334 for b
in self
.__class
__.__bases
__:
335 for c
, f
in b
.__dict
__.items():
336 if c
.startswith('do_'):
337 doc
.append(u
'%s%s:\t%s' % (prefix
, c
[3:], f
.__doc
__.decode('utf-8')))
339 doc
.insert(0, u
'命令指南 (当前命令前缀 %s,可设置)' % prefix
)
340 doc
.append(u
'要离开,直接删掉好友即可。')
341 self
.msg
.reply(u
'\n'.join(doc
).encode('utf-8'))
343 def do_iam(self
, args
):
346 r
= u
'昵称:\t\t%s\n消息数:\t\t%d\n消息总量:\t%s\n命令前缀:\t%s\n自我介绍:\t%s' % (
347 s
.nick
, s
.msg_count
, utils
.filesize(s
.msg_chars
), s
.prefix
, s
.intro
)
348 self
.msg
.reply(r
.encode('utf-8'))
350 def do_m(self
, args
):
351 '''给某人发私信,需要昵称和内容两个参数。私信不会以任何方式被记录。'''
353 self
.msg
.reply('请给出昵称和内容。')
356 target
= get_user_by_nick(args
[0])
358 self
.msg
.reply('Sorry,查无此人。')
361 msg
= self
.msg
.body
[len(self
.sender
.prefix
):].split(None, 2)[-1]
362 msg
= u
'_私信_ %s %s' % (target
.nick_pattern
% self
.sender
.nick
, msg
)
363 if xmpp
.send_message(target
.jid
, msg
) == xmpp
.NO_ERROR
:
364 self
.msg
.reply(u
'OK')
366 self
.msg
.reply(u
'消息发送失败')
368 def do_snooze(self
, args
):
369 '''暂停接收消息,参数为时间(默认单位为秒)。再次发送消息时自动清除'''
371 self
.msg
.reply('你想停止接收消息多久?')
375 n
= utils
.parseTime(args
[0])
377 self
.msg
.reply('Sorry,我无法理解你说的时间。')
381 self
.sender
.snooze_before
= datetime
.datetime
.now() + datetime
.timedelta(seconds
=n
)
382 except OverflowError:
383 self
.msg
.reply('Sorry,你不能睡太久。')
387 self
.msg
.reply('你已经醒来。')
389 self
.msg
.reply('OK,停止接收消息 %d 秒。' % n
)
390 log_onoff(self
.sender
, SNOOZE
% n
)
392 def do_offline(self
, args
):
393 '''让程序认为你的所有资源已离线。如在你离线时程序仍认为你在线,请使用此命令。'''
394 del self
.sender
.resources
[:]
395 self
.sender
.avail
= OFFLINE
396 self
.sender
.last_offline_date
= datetime
.datetime
.now()
398 self
.msg
.reply('OK,在下次你说你在线之前我都认为你已离线。')
400 def do_old(self
, args
):
401 '''查询聊天记录,可选一个数字参数。默认为最后20条。特殊参数 OFFLINE 显示离线消息(最多 100 条)'''
405 q
= Log
.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT 20")
410 q
= Log
.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT %d" % n
)
412 if args
[0].upper() == 'OFFLINE':
413 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
)
421 if datetime
.datetime
.today() - q
[0].time
> datetime
.timedelta(hours
=24):
426 message
= '%s %s %s' % (
427 utils
.strftime(l
.time
, timezone
, show_date
),
428 s
.nick_pattern
% l
.nick
,
433 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
435 self
.msg
.reply('没有符合的聊天记录。')
437 self
.msg
.reply('Oops, 参数不正确哦。')
439 def do_set(self
, args
):
440 '''设置一些参数。参数格式 key=value;不带参数以查看说明。'''
444 for c
, f
in self
.__class
__.__dict
__.items():
445 if c
.startswith('set_'):
446 doc
.append(u
'* %s:\t%s' % (c
[4:], f
.__doc
__.decode('utf-8')))
447 for b
in self
.__class
__.__bases
__:
448 for c
, f
in b
.__dict
__.items():
449 if c
.startswith('set_'):
450 doc
.append(u
'* %s:\t%s' % (c
[4:], f
.__doc
__.decode('utf-8')))
452 doc
.insert(0, u
'设置选项:')
453 self
.msg
.reply(u
'\n'.join(doc
).encode('utf-8'))
456 cmd
= msg
.body
.split(None, 1)[1].split('=', 1)
458 msg
.reply(u
'错误:请给出选项值')
461 handle
= getattr(self
, 'set_' + cmd
[0])
462 except AttributeError:
463 msg
.reply(u
'错误:未知选项 %s' % cmd
[0])
466 except UnicodeEncodeError:
467 msg
.reply(u
'错误:选项名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
471 def set_prefix(self
, arg
):
473 self
.sender
.prefix
= arg
475 self
.msg
.reply(u
'设置成功!')
477 def set_nickpattern(self
, arg
):
478 '''设置昵称显示格式,用 %s 表示昵称的位置'''
481 except (TypeError, ValueError):
482 self
.msg
.reply(u
'错误:不正确的格式')
485 self
.sender
.nick_pattern
= arg
487 self
.msg
.reply(u
'设置成功!')
489 class AdminCommand(BasicCommand
):
490 def do_kick(self
, args
):
493 self
.msg
.reply('请给出昵称。')
496 target
= get_user_by_nick(args
[0])
498 self
.msg
.reply('Sorry,查无此人。')
501 if target
.jid
== config
.root
:
502 self
.msg
.reply('不能删除 root 用户')
505 targetjid
= target
.jid
506 targetnick
= target
.nick
508 self
.msg
.reply((u
'OK,删除 %s。' % target
.nick
).encode('utf-8'))
509 send_to_all_except(self
.sender
.jid
, (u
'%s 已被删除。' % self
.sender
.nick
) \
511 xmpp
.send_message(targetjid
, u
'你已被管理员从此群中删除,请删除该好友。')
512 log_onoff(self
.sender
, KICK
% (targetnick
, targetjid
))
514 def do_quiet(self
, args
):
515 '''禁言某人,参数为昵称和时间(默认单位秒)'''
517 self
.msg
.reply('请给出昵称和时间。')
521 n
= utils
.parseTime(args
[1])
523 self
.msg
.reply('Sorry,我无法理解你说的时间。')
526 target
= get_user_by_nick(args
[0])
528 self
.msg
.reply('Sorry,查无此人。')
531 target
.black_before
= datetime
.datetime
.now() + datetime
.timedelta(seconds
=n
)
533 self
.msg
.reply((u
'OK,禁言 %s %d 秒。' % (target
.nick
, n
)).encode('utf-8'))
534 send_to_all_except((self
.sender
.jid
, target
.jid
),
535 (u
'%s 已被禁言 %s 秒。' % (self
.sender
.nick
, n
)) \
537 xmpp
.send_message(target
.jid
, u
'你已被管理员禁言 %d 秒。' % n
)
538 log_onoff(self
.sender
, BLACK
% (target
.nick
, n
))
540 def do_admin(self
, args
):
543 self
.msg
.reply(u
'请给出昵称。')
546 target
= get_user_by_nick(args
[0])
548 self
.msg
.reply(u
'Sorry,查无此人。')
552 self
.msg
.reply(u
'%s 已经是管理员了。' % target
.nick
)
555 target
.is_admin
= True
557 send_to_all_except(target
.jid
,
558 (u
'%s 已成为管理员。' % target
.nick
) \
560 xmpp
.send_message(target
.jid
, u
'你已是本群管理员。')
561 log_onoff(self
.sender
, ADMIN
% (target
.nick
, self
.sender
.nick
))
563 def do_unadmin(self
, args
):
566 self
.msg
.reply(u
'请给出昵称。')
569 target
= get_user_by_nick(args
[0])
571 self
.msg
.reply(u
'Sorry,查无此人。')
574 if not target
.is_admin
:
575 self
.msg
.reply(u
'%s 不是管理员。' % target
.nick
)
578 target
.is_admin
= False
580 send_to_all_except(target
.jid
,
581 (u
'%s 已不再是管理员。' % target
.nick
) \
583 xmpp
.send_message(target
.jid
, u
'你已不再是本群管理员。')
584 log_onoff(self
.sender
, UNADMIN
% (target
.nick
, self
.sender
.nick
))