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
= User
.gql('where nick = :1', nick
).get()
230 old
= User
.gql('where nick = :1', nick
).get()
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_chatty(self
, args
):
293 for u
in User
.gql('ORDER BY msg_chars ASC'):
295 m
= u
'* %s:\t%5d条,共 %s' % (
297 utils
.filesize(u
.msg_chars
))
300 r
.insert(0, u
'消息数量排行:')
301 r
.append(u
'共 %d 人。' % n
)
302 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
304 def do_nick(self
, args
):
307 self
.msg
.reply('错误:请给出你想到的昵称(不能包含空格)')
310 q
= User
.gql('where nick = :1', args
[0]).get()
312 self
.msg
.reply('错误:该昵称已被使用,请使用其它昵称')
313 elif not utils
.checkNick(args
[0]):
314 self
.msg
.reply('错误:非法的昵称')
316 if not config
.nick_can_change
and self
.sender
.nick_changed
:
317 self
.msg
.reply('乖哦,你已经没机会再改昵称了')
319 old_nick
= self
.sender
.nick
320 log_onoff(self
.sender
, NICK
% (old_nick
, args
[0]))
321 self
.sender
.nick
= args
[0]
322 self
.sender
.nick_changed
= True
324 send_to_all_except(self
.sender
.jid
,
325 (u
'%s 的昵称改成了 %s' % (old_nick
, args
[0])).encode('utf-8'))
326 self
.msg
.reply('昵称更改成功!')
327 do_nick
.__doc
__ += ',最长 %d 字节' % config
.nick_maxlen
329 def do_help(self
, args
=None):
332 prefix
= self
.sender
.prefix
333 for c
, f
in self
.__class
__.__dict
__.items():
334 if c
.startswith('do_'):
335 doc
.append(u
'%s%s:\t%s' % (prefix
, c
[3:], f
.__doc
__.decode('utf-8')))
336 for b
in self
.__class
__.__bases
__:
337 for c
, f
in b
.__dict
__.items():
338 if c
.startswith('do_'):
339 doc
.append(u
'%s%s:\t%s' % (prefix
, c
[3:], f
.__doc
__.decode('utf-8')))
341 doc
.insert(0, u
'命令指南 (当前命令前缀 %s,可设置)' % prefix
)
342 doc
.append(u
'要离开,直接删掉好友即可。')
343 self
.msg
.reply(u
'\n'.join(doc
).encode('utf-8'))
345 def do_iam(self
, args
):
348 r
= u
'昵称:\t\t%s\n消息数:\t\t%d\n消息总量:\t%s\n命令前缀:\t%s\n自我介绍:\t%s' % (
349 s
.nick
, s
.msg_count
, utils
.filesize(s
.msg_chars
), s
.prefix
, s
.intro
)
350 self
.msg
.reply(r
.encode('utf-8'))
352 def do_m(self
, args
):
353 '''给某人发私信,需要昵称和内容两个参数。私信不会以任何方式被记录。'''
355 self
.msg
.reply('请给出昵称和内容。')
358 target
= get_user_by_nick(args
[0])
360 self
.msg
.reply('Sorry,查无此人。')
364 self
.msg
.reply('很抱歉,对方不授受私信。')
367 msg
= self
.msg
.body
[len(self
.sender
.prefix
):].split(None, 2)[-1]
368 msg
= u
'_私信_ %s %s' % (target
.nick_pattern
% self
.sender
.nick
, msg
)
369 if xmpp
.send_message(target
.jid
, msg
) == xmpp
.NO_ERROR
:
370 self
.msg
.reply(u
'OK')
372 self
.msg
.reply(u
'消息发送失败')
374 def do_snooze(self
, args
):
375 '''暂停接收消息,参数为时间(默认单位为秒)。再次发送消息时自动清除'''
377 self
.msg
.reply('你想停止接收消息多久?')
381 n
= utils
.parseTime(args
[0])
383 self
.msg
.reply('Sorry,我无法理解你说的时间。')
387 self
.sender
.snooze_before
= datetime
.datetime
.now() + datetime
.timedelta(seconds
=n
)
388 except OverflowError:
389 self
.msg
.reply('Sorry,你不能睡太久。')
393 self
.msg
.reply('你已经醒来。')
395 self
.msg
.reply('OK,停止接收消息 %d 秒。' % n
)
396 log_onoff(self
.sender
, SNOOZE
% n
)
398 def do_offline(self
, args
):
399 '''让程序认为你的所有资源已离线。如在你离线时程序仍认为你在线,请使用此命令。'''
400 del self
.sender
.resources
[:]
401 self
.sender
.avail
= OFFLINE
402 self
.sender
.last_offline_date
= datetime
.datetime
.now()
404 self
.msg
.reply('OK,在下次你说你在线之前我都认为你已离线。')
406 def do_old(self
, args
):
407 '''查询聊天记录,可选一个数字参数。默认为最后20条。特殊参数 OFFLINE 显示离线消息(最多 100 条)'''
411 q
= Log
.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT 20")
416 q
= Log
.gql("WHERE type = 'chat' ORDER BY time DESC LIMIT %d" % n
)
418 if args
[0].upper() == 'OFFLINE':
419 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
)
427 if datetime
.datetime
.today() - q
[0].time
> datetime
.timedelta(hours
=24):
432 message
= '%s %s %s' % (
433 utils
.strftime(l
.time
, timezone
, show_date
),
434 s
.nick_pattern
% l
.nick
,
439 self
.msg
.reply(u
'\n'.join(r
).encode('utf-8'))
441 self
.msg
.reply('没有符合的聊天记录。')
443 self
.msg
.reply('Oops, 参数不正确哦。')
445 def do_set(self
, args
):
446 '''设置一些参数。参数格式 key=value;不带参数以查看说明。'''
450 for c
, f
in self
.__class
__.__dict
__.items():
451 if c
.startswith('set_'):
452 doc
.append(u
'* %s:\t%s' % (c
[4:], f
.__doc
__.decode('utf-8')))
453 for b
in self
.__class
__.__bases
__:
454 for c
, f
in b
.__dict
__.items():
455 if c
.startswith('set_'):
456 doc
.append(u
'* %s:\t%s' % (c
[4:], f
.__doc
__.decode('utf-8')))
458 doc
.insert(0, u
'设置选项:')
459 self
.msg
.reply(u
'\n'.join(doc
).encode('utf-8'))
462 cmd
= msg
.body
.split(None, 1)[1].split('=', 1)
464 msg
.reply(u
'错误:请给出选项值')
467 handle
= getattr(self
, 'set_' + cmd
[0])
468 except AttributeError:
469 msg
.reply(u
'错误:未知选项 %s' % cmd
[0])
472 except UnicodeEncodeError:
473 msg
.reply(u
'错误:选项名解码失败。此问题在 GAE 升级其 Python 到 3.x 后方能解决。')
477 def set_prefix(self
, arg
):
479 self
.sender
.prefix
= arg
481 self
.msg
.reply(u
'设置成功!')
483 def set_nickpattern(self
, arg
):
484 '''设置昵称显示格式,用 %s 表示昵称的位置'''
487 except (TypeError, ValueError):
488 self
.msg
.reply(u
'错误:不正确的格式')
491 self
.sender
.nick_pattern
= arg
493 self
.msg
.reply(u
'设置成功!')
495 def set_allowpm(self
, arg
):
496 '''设置是否接受私信,参数为 y (接受)或者 n (拒绝)'''
498 self
.msg
.reply(u
'错误的参数。')
502 self
.sender
.reject_pm
= False
504 self
.sender
.reject_pm
= True
506 self
.msg
.reply(u
'设置成功!')
508 class AdminCommand(BasicCommand
):
509 def do_kick(self
, args
):
512 self
.msg
.reply('请给出昵称。')
515 target
= get_user_by_nick(args
[0])
517 self
.msg
.reply('Sorry,查无此人。')
520 if target
.jid
== config
.root
:
521 self
.msg
.reply('不能删除 root 用户')
524 targetjid
= target
.jid
525 targetnick
= target
.nick
527 self
.msg
.reply((u
'OK,删除 %s。' % target
.nick
).encode('utf-8'))
528 send_to_all_except(self
.sender
.jid
, (u
'%s 已被删除。' % self
.sender
.nick
) \
530 xmpp
.send_message(targetjid
, u
'你已被管理员从此群中删除,请删除该好友。')
531 log_onoff(self
.sender
, KICK
% (targetnick
, targetjid
))
533 def do_quiet(self
, args
):
534 '''禁言某人,参数为昵称和时间(默认单位秒)'''
536 self
.msg
.reply('请给出昵称和时间。')
540 n
= utils
.parseTime(args
[1])
542 self
.msg
.reply('Sorry,我无法理解你说的时间。')
545 target
= get_user_by_nick(args
[0])
547 self
.msg
.reply('Sorry,查无此人。')
550 target
.black_before
= datetime
.datetime
.now() + datetime
.timedelta(seconds
=n
)
552 self
.msg
.reply((u
'OK,禁言 %s %d 秒。' % (target
.nick
, n
)).encode('utf-8'))
553 send_to_all_except((self
.sender
.jid
, target
.jid
),
554 (u
'%s 已被禁言 %s 秒。' % (self
.sender
.nick
, n
)) \
556 xmpp
.send_message(target
.jid
, u
'你已被管理员禁言 %d 秒。' % n
)
557 log_onoff(self
.sender
, BLACK
% (target
.nick
, n
))
559 def do_admin(self
, args
):
562 self
.msg
.reply(u
'请给出昵称。')
565 target
= get_user_by_nick(args
[0])
567 self
.msg
.reply(u
'Sorry,查无此人。')
571 self
.msg
.reply(u
'%s 已经是管理员了。' % target
.nick
)
574 target
.is_admin
= True
576 send_to_all_except(target
.jid
,
577 (u
'%s 已成为管理员。' % target
.nick
) \
579 xmpp
.send_message(target
.jid
, u
'你已是本群管理员。')
580 log_onoff(self
.sender
, ADMIN
% (target
.nick
, self
.sender
.nick
))
582 def do_unadmin(self
, args
):
585 self
.msg
.reply(u
'请给出昵称。')
588 target
= get_user_by_nick(args
[0])
590 self
.msg
.reply(u
'Sorry,查无此人。')
593 if not target
.is_admin
:
594 self
.msg
.reply(u
'%s 不是管理员。' % target
.nick
)
597 target
.is_admin
= False
599 send_to_all_except(target
.jid
,
600 (u
'%s 已不再是管理员。' % target
.nick
) \
602 xmpp
.send_message(target
.jid
, u
'你已不再是本群管理员。')
603 log_onoff(self
.sender
, UNADMIN
% (target
.nick
, self
.sender
.nick
))