从头搭建一个在线聊天室(四)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 今天继续完善我们的在线聊天室

TODO


  1. 定时清理过期消息
  2. 禁言功能
  3. 踢人功能
  4. 对接聊天机器人


清理过期消息


由于我们需要定时清理 redis 中保存的聊天记录,那么就需要一个定时任务。flask 有一个完善的插件 flask-apscheduler,但是简单试用了下,限制还是挺多的。所以,我这里选择自己实现一个简单的定时器功能。

创建一个 tasks.py 文件

首先定义定时器类

from threading import Timer
 class Scheduler(object):
     def __init__(self, sleep_time, func, mytime=None):
         self.sleep_time = sleep_time
         self.func = func
         self._t = None
         self.mytime = mytime
    def start(self):
        if self._t is None:
            self._t = Timer(self.sleep_time, self._run)
            self._t.start()
        else:
            raise Exception("this timer is already running")
    def _run(self):
        if self.mytime is not None:
            self.func(self.mytime)
        else:
            self.func()
        self._t = Timer(self.sleep_time, self._run)
        self._t.start()
    def stop(self):
        if self._t is not None:
            self._t.cancel()
            self._t = None
    @staticmethod
    def init_app(app):
        pass

使用线程中的 Timer 来调用真正的函数,通过 sleep time 的方式达到定时调用的效果。

然后编写需要定时调用的函数,即清理数据的函数。

def keep_msg(mytime=None):
     if mytime is not None:
         expare_time = mytime
     else:
         expare_time = 604800
     msg_list = r.keys("msg-*")
     for msg in msg_list:
         _ = r.zrange(msg, 0, 0)
        for i in _:
            score = r.zscore(msg, i)
            if time.time() - score > expare_time:
                r.zrem(msg, i)

比较简单,判断 redis 中的 score 是否处于过期时间,是,则删除。


接下来注册函数到我们的 flask 应用当中。

在 __init__.py 中填入如下代码:

from .tasks import Scheduler, keep_msg
 sch = Scheduler(86400, keep_msg)  # 每间隔一天执行
 def create_app(config_name):
     ...
     sch.init_app(app)
    ...
    return app

最后还要注意的是,由于我们前面是使用 socketio 来启动应用的,因为 socketio 是异步 io,而我们的 scheduler 是阻塞运行的,所以需要在 socketid 中创建子线程来启动。

修改 manage.py 如下:

import os
 from app import create_app, socketio, sch
 app = create_app(os.getenv('FLASK_CONFIG') or 'default')
 if __name__ == '__main__':
     my = sch.start
    socketio.start_background_task(target=my)  # 启动一个子线程
    socketio.run(app, debug=True)

这样,一个简单的定时任务就做好了。


禁言功能


正所谓“林子大了,什么鸟都有”,当聊天室人数很多的时候,经常会出现一些不和谐的话语,那么禁言功能就很有必要了。

首先在 views 中创建一个新的函数

@main.route('/chat/block/roomuser/', methods=['GET', 'POST'])
 @login_required
 def block_roomuser():
     rname = request.args.get('rname', "")
     new_b_user = request.args.get('b_user', "")
     b_time = request.args.get('b_time', "")
     if b_time is "":
         r.set('b_user-' + new_b_user, new_b_user, ex=None)
     else:
        r.set('b_user-' + new_b_user, new_b_user, ex=b_time)
    return redirect(url_for('main.room_user_list', rname=rname))

从前端获取到对应的聊天室名字、需要禁言的用户和禁言时间,然后根据禁言时间,把用户添加到 redis 中。


再来看看禁言功能的入口函数

@main.route('/chat/roomuser/list', methods=['GET', 'POST'])
 @login_required
 def room_user_list():
     rname = request.args.get('rname', "")
     ulist = r.zrange("chat-" + rname, 0, -1)
     b_user = r.keys('b_user-*')
     b_user_list = []
     for b in b_user:
         b_user_list.append(r.get(b))
    return render_template('roomuser_list.html', ulist=ulist, rname=rname, b_user=b_user_list)

从 redis 对应的有序集合中取出正处于禁言状态的用户,把这些用户传递到模板供渲染使用。


对应的 roomuser_list.html 代码为:

<div class="container">
     <div class="page-header">
         <h1>Hello, 这里是聊天室 {{ rname }} 所有的用户哦!</h1>
     </div>
     {% for user in ulist %}
     <p>
         <button class="btn btn-primary">{{ user }}</button>
         {% if user in b_user %}
         <span class="label label-default">禁言中。。。</span>
        {% endif %}
    </p>
    <p>
        <div class="btn-group">
            <button type="button" class="btn btn-warning">禁言</button>
            <button type="button" class="btn btn-warning dropdown-toggle dropdown-toggle-split" data-toggle="dropdown">
                <span class="caret"></span>
            </button>
            <div class="dropdown-menu">
                <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=300)}}">5 Mins</a></li>
                <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=600)}}">10 Mins</a></li>
                <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=18000)}}">5 Hours</a></li>
                <li><a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user)}}">永久禁言</a></li>
            </div>
        </div>
        <a href="{{ url_for('main.block_roomuser', rname=rname, b_user=user, b_time=1)}}" class="btn btn-info" role="button">解禁</a>
        <a href="{{ url_for('main.kick_roomuser', rname=rname, del_user=user) }}" class="btn btn-danger" role="button">踢出</a>
    </p>
    {% endfor %}
</div>

方便起见,直接使用 bootstrap 框架渲染页面。同时这里取了个巧,在“解禁”的时候,只是传入 b_time 为1,这样1秒之后,用户就自动从 redis 中过期了,也就成功解禁了。


最后,再来处理聊天室的消息,禁言的用户,当然不能再发消息啦。

在 chat 函数中,添加代码:

@main.route('/chat/', methods=['GET', 'POST'])
 def chat():
     ...
     b_user = r.keys('b_user-*')
     b_user_list = []
     for b in b_user:
         b_user_list.append(r.get(b))
     ...
     if current_user.is_authenticated:
        return render_template('chat.html', rname=rname, user_list=ulist, msg_list=msg_list,
                               b_user_list=b_user_list)

把处于禁言的用户取出,传递给模板。

在 send_chat 函数中添加代码:

@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
def send_chat(info):
    ...
    b_user = r.exists('b_user-%s' % current_user.username)
    if b_user:
        data = json.dumps({'code': 201, 'msg': 'Your are under block now!'})
        return data
    ...

如果用户处于禁言状态,直接返回 json 消息。

修改 chat.html 中的 javascript 函数 sendToServer,增加代码如下:

var jsondata = JSON.parse(myObj);
               if ( jsondata.code == 201 || jsondata.code == 403) {
                    var htmlData3 =   '<div class="msg_item fn-clear">'
                    + '   <div class="uface"><img src="{{ url_for('static', filename='chat/images/duck.jpg')}}" width="40" height="40"  alt=""/></div>'
                    + '   <div class="item_right">'
                    + '     <div class="msg">' + "自动回复: " + jsondata.msg + '</div>'
                    + '     <div class="name_time">' + '小黄鸭' + ' · ' + myTime +'</div>'
                    + '   </div>'
                    + '</div>';
               $("#message_box").append(htmlData3);
               $('#message_box').scrollTop($("#message_box")[0].scrollHeight + 20);
               }

判断返回的 json 中 code 值如果是 201 或 403,则由小黄鸭自动回复消息。

最后的效果如下:

image.png


踢人


如果在聊天室中,这个人真的让人忍无可忍,那么踢人就是最好的办法了。

其实实现思想和逻辑都和禁言相类似,这里直接给出部分代码

新增函数 kick_roomuser

@main.route('/chat/kick/roomuser/', methods=['GET', 'POST'])
@login_required
def kick_roomuser():
    rname = request.args.get("rname", "")
    del_user = request.args.get("del_user", "")
    r.zrem("chat-" + rname, del_user)
    return redirect(url_for('main.room_user_list', rname=rname))

修改 send_chat 函数

@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
 def send_chat(info):
     ...
     if current_user.is_authenticated:
         rname = request.form.get("rname", "")
         ulist = r.zrange("chat-" + rname, 0, -1)
         if current_user.username in ulist:
             body = {"username": current_user.username, "msg": info}
             r.zadd("msg-" + rname, json.dumps(body), time.time())
            socket_send(info, current_user.username)
            data = json.dumps({'code': 200, 'msg': info})
            return data
        else:
            data = json.dumps({'code': 403, 'msg': 'You are not in this room'})
            return data
    else:
        data = json.dumps({'code': 202, 'msg': info})
        return data

最后效果如下

image.png


对接聊天机器人


当前,如果用户没有登陆,是无法和其他人聊天的。那么一个友好的聊天机器人就非常有必要了。我们可以使用免费的图灵聊天机器人,当然也可以自己训练一个。以前我也写过一篇关于如何训练聊天机器人,感兴趣的小伙伴儿可以戳这里(链接)。

在这里直接复用以前部署的 API 了,只需要增加几行代码即可

修改 send_chat 函数

@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
def send_chat(info):
    ...
    else:
        base_url = 'http://luobodazahui.top:8889/api/chat/'
       chat_text = requests.get(base_url + info).text
        return chat_text

在函数中调用聊天机器人的 API 地址,将返回的内容传递给前端即可。

最终的效果如下:

image.png


华丽丽的分割线


到今天为止,从头搭建在线聊天室系列就告一段落了,如果大家认为项目还可以,欢迎到 GitHub 上给个 star,同时也欢迎 fork,后面再有任何的优化或者功能增强,都会直接提交到 GitHub 上,谢谢大家的阅读!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
4月前
|
前端开发 JavaScript 机器人
从头搭建一个在线聊天室(一)
从头搭建一个在线聊天室(一)
|
网络协议 计算机视觉
项目实战:Qt多人聊天室程序(在线、离线、离线信息再次登录后发送等)
项目实战:Qt多人聊天室程序(在线、离线、离线信息再次登录后发送等)
项目实战:Qt多人聊天室程序(在线、离线、离线信息再次登录后发送等)
|
4月前
|
存储 NoSQL 前端开发
从头搭建一个在线聊天室(二)
从头搭建一个在线聊天室(二)
|
4月前
|
前端开发 JavaScript NoSQL
在线聊天室优化之私聊
在线聊天室优化之私聊
在线聊天室优化之私聊
|
4月前
|
JSON NoSQL 前端开发
从头搭建一个在线聊天室(四)
从头搭建一个在线聊天室(四)
|
4月前
|
存储 前端开发 数据库
从头搭建一个在线聊天室(三)
从头搭建一个在线聊天室(三)
|
4月前
|
存储 关系型数据库 MySQL
基于SSM实现在线聊天系统
基于SSM实现在线聊天系统
|
Web App开发 前端开发
|
SQL JSON 前端开发
easyswoole实现在线聊天室功能
easyswoole实现在线聊天室功能
115 0
easyswoole实现在线聊天室功能
|
JSON 负载均衡 NoSQL
WebSocket集群分布式改造——实现多人在线聊天室
本文内容摘要: 为何要改造为分布式集群 如何改造为分布式集群 用户在聊天室集群如何发消息 用户在聊天室集群如何接收消息 补充知识点:STOMP 简介 功能一:向聊天室集群中的全体用户发消息——Redis的订阅/发布 功能二:集群集群用户上下线通知——Redis订阅发布 功能三:集群用户信息维护——Redis集合 WebSocket集群还有哪些可能性
1170 0