今天继续完善我们的在线聊天室
TODO
- 定时清理过期消息
- 禁言功能
- 踢人功能
- 对接聊天机器人
清理过期消息
由于我们需要定时清理 redis 中保存的聊天记录,那么就需要一个定时任务。flask 有一个完善的插件 flask-apscheduler,但是简单试用了下,限制还是挺多的。所以,我这里选择自己实现一个简单的定时器功能。
创建一个 tasks.py 文件
首先定义定时器类
1from threading import Timer 2 3 4class Scheduler(object): 5 def __init__(self, sleep_time, func, mytime=None): 6 self.sleep_time = sleep_time 7 self.func = func 8 self._t = None 9 self.mytime = mytime 10 11 def start(self): 12 if self._t is None: 13 self._t = Timer(self.sleep_time, self._run) 14 self._t.start() 15 else: 16 raise Exception("this timer is already running") 17 18 def _run(self): 19 if self.mytime is not None: 20 self.func(self.mytime) 21 else: 22 self.func() 23 self._t = Timer(self.sleep_time, self._run) 24 self._t.start() 25 26 def stop(self): 27 if self._t is not None: 28 self._t.cancel() 29 self._t = None 30 31 @staticmethod 32 def init_app(app): 33 pass
使用线程中的 Timer 来调用真正的函数,通过 sleep time 的方式达到定时调用的效果。
然后编写需要定时调用的函数,即清理数据的函数。
1def keep_msg(mytime=None): 2 if mytime is not None: 3 expare_time = mytime 4 else: 5 expare_time = 604800 6 msg_list = r.keys("msg-*") 7 for msg in msg_list: 8 _ = r.zrange(msg, 0, 0) 9 for i in _: 10 score = r.zscore(msg, i) 11 if time.time() - score > expare_time: 12 r.zrem(msg, i)
比较简单,判断 redis 中的 score 是否处于过期时间,是,则删除。
接下来注册函数到我们的 flask 应用当中。
在 __init__.py 中填入如下代码:
1from .tasks import Scheduler, keep_msg 2 3 4sch = Scheduler(86400, keep_msg) # 每间隔一天执行 5 6 7def create_app(config_name): 8 ... 9 sch.init_app(app) 10 ... 11 return app
最后还要注意的是,由于我们前面是使用 socketio 来启动应用的,因为 socketio 是异步 io,而我们的 scheduler 是阻塞运行的,所以需要在 socketid 中创建子线程来启动。
修改 manage.py 如下:
1import os 2from app import create_app, socketio, sch 3 4 5app = create_app(os.getenv('FLASK_CONFIG') or 'default') 6 7 8if __name__ == '__main__': 9 my = sch.start 10 socketio.start_background_task(target=my) # 启动一个子线程 11 socketio.run(app, debug=True)
这样,一个简单的定时任务就做好了。
禁言功能
正所谓“林子大了,什么鸟都有”,当聊天室人数很多的时候,经常会出现一些不和谐的话语,那么禁言功能就很有必要了。
首先在 views 中创建一个新的函数
1@main.route('/chat/block/roomuser/', methods=['GET', 'POST']) 2@login_required 3def block_roomuser(): 4 rname = request.args.get('rname', "") 5 new_b_user = request.args.get('b_user', "") 6 b_time = request.args.get('b_time', "") 7 if b_time is "": 8 r.set('b_user-' + new_b_user, new_b_user, ex=None) 9 else: 10 r.set('b_user-' + new_b_user, new_b_user, ex=b_time) 11 return redirect(url_for('main.room_user_list', rname=rname))
从前端获取到对应的聊天室名字、需要禁言的用户和禁言时间,然后根据禁言时间,把用户添加到 redis 中。
再来看看禁言功能的入口函数
1@main.route('/chat/roomuser/list', methods=['GET', 'POST']) 2@login_required 3def room_user_list(): 4 rname = request.args.get('rname', "") 5 ulist = r.zrange("chat-" + rname, 0, -1) 6 b_user = r.keys('b_user-*') 7 b_user_list = [] 8 for b in b_user: 9 b_user_list.append(r.get(b)) 10 return render_template('roomuser_list.html', ulist=ulist, rname=rname, b_user=b_user_list)
从 redis 对应的有序集合中取出正处于禁言状态的用户,把这些用户传递到模板供渲染使用。
对应的 roomuser_list.html 代码为:
1<div class="container"> 2 <div class="page-header"> 3 <h1>Hello, 这里是聊天室 {{ rname }} 所有的用户哦!</h1> 4 </div> 5 {% for user in ulist %} 6 <p> 7 <button class="btn btn-primary">{{ user }}</button> 8 {% if user in b_user %} 9 <span class="label label-default">禁言中。。。</span> 10 {% endif %} 11 </p> 12 <p> 13 <div class="btn-group"> 14 <button type="button" class="btn btn-warning">禁言</button> 15 <button type="button" class="btn btn-warning dropdown-toggle dropdown-toggle-split" data-toggle="dropdown"> 16 <span class="caret"></span> 17 </button> 18 <div class="dropdown-menu"> 19 <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=300)}}">5 Mins</a></li> 20 <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=600)}}">10 Mins</a></li> 21 <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=18000)}}">5 Hours</a></li> 22 <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user)}}">永久禁言</a></li> 23 </div> 24 </div> 25 <a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=1)}}" class="btn btn-info" role="button">解禁</a> 26 <a href="{{ url_for('main.kick_roomuser', rname=rname, del_user=user) }}" class="btn btn-danger" role="button">踢出</a> 27 </p> 28 {% endfor %} 29</div>
方便起见,直接使用 bootstrap 框架渲染页面。同时这里取了个巧,在“解禁”的时候,只是传入 b_time 为1,这样1秒之后,用户就自动从 redis 中过期了,也就成功解禁了。
最后,再来处理聊天室的消息,禁言的用户,当然不能再发消息啦。
在 chat 函数中,添加代码:
1@main.route('/chat/', methods=['GET', 'POST']) 2def chat(): 3 ... 4 b_user = r.keys('b_user-*') 5 b_user_list = [] 6 for b in b_user: 7 b_user_list.append(r.get(b)) 8 ... 9 if current_user.is_authenticated: 10 return render_template('chat.html', rname=rname, user_list=ulist, msg_list=msg_list, 11 b_user_list=b_user_list)
把处于禁言的用户取出,传递给模板。
在 send_chat 函数中添加代码:
1@main.route('/api/sendchat/<info>', methods=['GET', 'POST']) 2def send_chat(info): 3 ... 4 b_user = r.exists('b_user-%s' % current_user.username) 5 if b_user: 6 data = json.dumps({'code': 201, 'msg': 'Your are under block now!'}) 7 return data 8 ...
如果用户处于禁言状态,直接返回 json 消息。
修改 chat.html 中的 javascript 函数 sendToServer,增加代码如下:
1var jsondata = JSON.parse(myObj); 2 if ( jsondata.code == 201 || jsondata.code == 403) { 3 var htmlData3 = '<div class="msg_item fn-clear">' 4 + ' <div class="uface"><img src="{{ url_for('static', filename='chat/images/duck.jpg')}}" width="40" height="40" alt=""/></div>' 5 + ' <div class="item_right">' 6 + ' <div class="msg">' + "自动回复: " + jsondata.msg + '</div>' 7 + ' <div class="name_time">' + '小黄鸭' + ' · ' + myTime +'</div>' 8 + ' </div>' 9 + '</div>'; 10 $("#message_box").append(htmlData3); 11 $('#message_box').scrollTop($("#message_box")[0].scrollHeight + 20); 12 }
判断返回的 json 中 code 值如果是 201 或 403,则由小黄鸭自动回复消息。
最后的效果如下:
踢人
如果在聊天室中,这个人真的让人忍无可忍,那么踢人就是最好的办法了。
其实实现思想和逻辑都和禁言相类似,这里直接给出部分代码
新增函数 kick_roomuser
1@main.route('/chat/kick/roomuser/', methods=['GET', 'POST']) 2@login_required 3def kick_roomuser(): 4 rname = request.args.get("rname", "") 5 del_user = request.args.get("del_user", "") 6 r.zrem("chat-" + rname, del_user) 7 return redirect(url_for('main.room_user_list', rname=rname))
修改 send_chat 函数
1@main.route('/api/sendchat/<info>', methods=['GET', 'POST']) 2def send_chat(info): 3 ... 4 if current_user.is_authenticated: 5 rname = request.form.get("rname", "") 6 ulist = r.zrange("chat-" + rname, 0, -1) 7 if current_user.username in ulist: 8 body = {"username": current_user.username, "msg": info} 9 r.zadd("msg-" + rname, json.dumps(body), time.time()) 10 socket_send(info, current_user.username) 11 data = json.dumps({'code': 200, 'msg': info}) 12 return data 13 else: 14 data = json.dumps({'code': 403, 'msg': 'You are not in this room'}) 15 return data 16 else: 17 data = json.dumps({'code': 202, 'msg': info}) 18 return data
最后效果如下
对接聊天机器人
当前,如果用户没有登陆,是无法和其他人聊天的。那么一个友好的聊天机器人就非常有必要了。我们可以使用免费的图灵聊天机器人,当然也可以自己训练一个。以前我也写过一篇关于如何训练聊天机器人,感兴趣的小伙伴儿可以戳这里(链接)。
在这里直接复用以前部署的 API 了,只需要增加几行代码即可
修改 send_chat 函数
1@main.route('/api/sendchat/<info>', methods=['GET', 'POST']) 2def send_chat(info): 3 ... 4 else: 5 base_url = 'http://luobodazahui.top:8889/api/chat/' 6 chat_text = requests.get(base_url + info).text 7 return chat_text
在函数中调用聊天机器人的 API 地址,将返回的内容传递给前端即可。
最终的效果如下:
华丽丽的分割线
到今天为止,从头搭建在线聊天室系列就告一段落了,如果大家认为项目还可以,欢迎到 GitHub 上给个 star,同时也欢迎 fork,后面再有任何的优化或者功能增强,都会直接提交到 GitHub 上,谢谢大家的阅读!