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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 从头搭建一个在线聊天室(四)

今天继续完善我们的在线聊天室

TODO

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

清理过期消息

由于我们需要定时清理 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 上,谢谢大家的阅读!

GitHub:https://github.com/zhouwei713/online-chat

相关文章
|
9月前
|
人工智能 数据可视化 API
Deepseek 本地部署“网页版”与“软件版”超级详细教学(deepseek+Ollama+OpenWebUI+Chatbox AI+Cherry Studio)
近期,人工智能领域迎来了一股新的热潮,DeepSeek作为一款备受瞩目的开源语言模型,凭借其卓越的性能和广泛的应用场景,迅速在全球范围内引起了广泛关注。从技术社区到商业领域,DeepSeek的热度不断攀升,甚至有“挤爆”的趋势。这不仅反映了其强大的技术实力,也体现了市场和用户对其的高度期待。 在这样的背景下,本地部署DeepSeek模型的需求也日益增加。本地部署不仅可以避免网络延迟和数据隐私问题,还能根据用户需求进行定制化优化。结合deepseek+Ollama+OpenWebUI+Chatbox AI+Cherry Studio AI等工具,用户可以轻松实现模型的本地化部署,并通过可视化面板
1422 8
Deepseek 本地部署“网页版”与“软件版”超级详细教学(deepseek+Ollama+OpenWebUI+Chatbox AI+Cherry Studio)
|
机器学习/深度学习 人工智能 算法
体验升级:扫描全能王智能高清滤镜2.0全面测评
**扫描全能王智能高清滤镜2.0测评概览** - **技术亮点:** 结合深度学习与多尺度感知融合,提升图像清晰度。 - **智能处理:** 利用深度学习识别透字、颜色和文字,自适应调整处理策略。 - **测评场景:** - **透字文件**:有效抑制透字噪声,增强文字可读性。 - **有阴影的发票**:去除阴影,清晰呈现内容。 - **曲度较大书籍**:准确扫描曲面,保持文字形状。 - **电脑屏幕文本**:优化屏幕显示文本的扫描质量。 - **图画扫描**:颜色还原准确,保持图像细节。 - **总结展望:** 强大的处理能力,满足多样化文档需求,期待未来功能拓展。
375 6
|
缓存 JavaScript
Vue加载网络组件(远程组件)
【10月更文挑战第23天】在 Vue 中实现加载网络组件(远程组件)可以通过多种方式来完成。
|
传感器 机器学习/深度学习 编解码
智能驾驶--语义分割 公开数据集 汇总
本文整理了10个质量较好,数据集较大,比较新的,图像语义分割的公开数据集;主要服务于智能驾驶方向(辅助驾驶、自动驾驶等)。
2169 0
技术指标和振荡器大全(二)(1)
技术指标和振荡器大全(二)(1)
318 0
|
监控 前端开发 JavaScript
记录浏览器节能机制导致Websocket断连问题
近期,在使用WebSocket(WS)连接时遇到了频繁断连的问题,这种情况在单个用户上每天发生数百次。尽管利用了socket.io的自动重连机制能够在断连后迅速恢复连接,但这并不保证每一次重连都能成功接收WS消息。因此,我们进行了一些的排查和测试工作。
768 1
记录浏览器节能机制导致Websocket断连问题
|
消息中间件 关系型数据库 Kafka
实时计算 Flink版操作报错合集之在进行数据处理时,遇到文件末尾添加了回车换行符但仍然报错,该怎么解决
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
数据采集 机器学习/深度学习 算法
聚类算法
【6月更文挑战第6天】聚类算法是无监督学习方法,用于将数据集划分成相似样本的类别。常见的聚类算法有K均值、层次聚类和DBSCAN等。在分析时,涉及数据预处理、选择算法、确定聚类数目、执行聚类及评估结果。层次聚类分为自底向上和自顶向下两种,而K-Means是基于质心的聚类算法。评估指标如轮廓系数可衡量聚类效果。聚类过程包括初始化中心、计算样本与中心距离、分配类别和更新中心,直到收敛。
610 2
|
芯片
模电练习题-多路信号发生器(仿真解答)
模电练习题-多路信号发生器(仿真解答)
2340 3
模电练习题-多路信号发生器(仿真解答)
|
弹性计算 对象存储 CDN
阿里云服务器带宽计费模式怎么选?按固定带宽还是按流量合适?
阿里云服务器公网带宽计费模式按固定带宽和按使用流量哪个划算?阿里云百科以北京地域为例,按固定带宽计费1M带宽一个月23元,按使用流量计费1GB流量0.8元,如果云服务器带宽使用率低于10%,那么首选按使用流量计费,如果带宽实际利用率较高的话,按固定带宽计费更划算一些。阿里云百科来详细说下阿里云服务器带宽不同计费模式下收费价格表、费用计算方法及如何选择带宽计费模式更合适:
610 0
阿里云服务器带宽计费模式怎么选?按固定带宽还是按流量合适?