前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从头搭建一个在线聊天室(三)

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

作者头像
周萝卜
发布2019-07-17 17:19:19
2.1K0
发布2019-07-17 17:19:19
举报
文章被收录于专栏:萝卜大杂烩

今天是从头开始做一个在线聊天网站系类的第三部分,调整项目结构,增强功能。

第一部分可以看这里(链接) 第二部分可以看这里(链接

调整项目结构

随着我们项目功能越来越多,把所有的逻辑代码都写在一个文件里已经不太合适了,下面就通过 flask 的工厂模式,把项目代码拆分开。

首先来看下拆分后的项目结构:

main 中主要存放后台逻辑代码。 static 中存放 js,css 以及用到的图片等。 templates 中存放 HTML 模板。 models.py 中是数据库模型。 config.py 中是一些公共的配置信息。 manage.py 中是项目的启动信息。

下面我们分别来看看各个模块对应的代码

具体代码拆分

1. 配置信息

在 config.py 中,填入代码:

代码语言:javascript
复制
import os
import redis


basedir = os.path.abspath(os.path.dirname(__file__))


class Config:
    SECRET_KEY = 'hardtohard'
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'chat.sqlite3')

    @staticmethod
    def init_app(app):
        pass


class DevelopmentConfig(Config):
    pass


class TestingConfig(Config):
    pass


class ProductionConfig(Config):
    pass


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
    }

2. 使用工厂函数

在 app/__init__.py 中填入代码:

代码语言:javascript
复制
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap
from flask_socketio import SocketIO
from config import config


login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'main.login'
db = SQLAlchemy()
bootstrap = Bootstrap()
socketio = SocketIO()


def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)
    socketio.init_app(app)
    login_manager.init_app(app)
    db.init_app(app)
    bootstrap.init_app(app)

    # 注册蓝本
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app

create_app 函数就是程序的工厂函数,它接受一个配置名的参数。

3. 使用蓝本

蓝本和程序类似,也可以定义路由。不同的是,在蓝本中定义的路由处于休眠状态,直到蓝本注册到程序上后,路由才真正成为程序的一部分。

在 main/__init__.py 中创建蓝本

代码语言:javascript
复制
from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, forms

通过实例化一个 Blueprint 类对象可以创建蓝本。这个构造函数有两个必须指定的参数: 蓝本的名字和蓝本所在的包或模块。和程序一样,大多数情况下第二个参数使用 Python 的 __name__ 变量即可。

4. 修改 view 视图

对于视图函数,需要导入相关的包,同时由于使用了蓝本,原来用来装饰路由的 app.route 都要修改为 main.route,url_for 函数也需要增加 main 作用域,修改后的部分代码如下:

代码语言:javascript
复制
from flask import render_template, redirect, url_for, request
from flask_login import login_required, login_user, logout_user, current_user
from . import main
from .. import db
from .forms import LoginForm
from ..models import User
from config import config
import time
import json
from ..socket_conn import socket_send

pool = redis.ConnectionPool(host='redis-12143.c8.us-east-1-3.ec2.cloud.redislabs.com', port=12143,
                            decode_responses=True, password='pkAWNdYWfbLLfNOfxTJinm9SO16eSJFx')
r = redis.Redis(connection_pool=pool)



@main.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user:
            login_user(user)
            return redirect(url_for('main.index'))
    return render_template('login.html', form=form)


@main.route('/createroom', methods=["GET", 'POST'])
@login_required
def create_room():
    rname = request.form.get('chatroomname', '')
    if r.exists("chat-" + rname) is False:
        r.zadd("chat-" + rname, current_user.username, 1)
        return redirect(url_for('main.chat', rname=rname))
    else:
        return redirect(url_for('main.chat_room_list'))

5. 编写 socket 连接函数

在 models.py 的同级目录下创建 socket_conn.py 文件,添加代码如下:

代码语言:javascript
复制
from . import socketio
from flask_socketio import emit


@socketio.on('request_for_response', namespace='/testnamespace')
def socket_send(data, user):
    emit("response", {"code": '200', "msg": data, "username": user}, broadcast=True, namespace='/testnamespace')

该函数供视图函数调用,广播 socket 消息。

6. 完成 forms 和 models

将原来的表单代码和数据库模型代码分别拷贝到这两个文件中 forms.py

代码语言:javascript
复制
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
from flask_wtf import FlaskForm


class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), ])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Keep me logged in')
    submit = SubmitField('Log in')

models.py

代码语言:javascript
复制
from . import db
from flask_login import UserMixin
from flask import request
import hashlib


class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    password = db.Column(db.String(64))
    avatar_hash = db.Column(db.String(32))

    def gravatar(self, name=None, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://secure.gravatar.com/avatar'
        else:
            url = 'http://www.gravatar.com/avatar'
        if name is not None:
            email = name + "@hihichat.com"
        else:
            email = self.username + "@hihichat.com"
        myhash = self.avatar_hash or hashlib.md5(email.encode('utf-8')).hexdigest()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=myhash, size=size,
                                                                     default=default, rating=rating)

这里把生成头像的函数整合优化了。

7. 修改模板

把 HTML 模板里的 url_for() 函数都增加 main.,再放置到 templates 下面即可。

8. 启动脚本

顶级文件夹中的 manage.py 文件用于启动程序。

代码语言:javascript
复制
import os
from app import create_app, socketio


app = create_app(os.getenv('FLASK_CONFIG') or 'default')


if __name__ == '__main__':
    socketio.run(app, debug=True)

还是使用 socketio.run 的方式启动应用。

至此,代码拆分完毕。

功能增强

1. 新增用户

以前我们都是使用浏览器 URL 直接新增用户的,即函数 adduser,现在我们做一个简单的页面,来规范这个操作。

定义表单

代码语言:javascript
复制
class CreateUserForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired(), EqualTo('password2',
                                                                            message='Password must match.')])
    password2 = PasswordField('Confirm password', validators=[DataRequired()])
    submit = SubmitField('Create User')

    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('Username already in use.')

定义了一个函数,来校验用户名是否重复。

修改原来的视图函数 adduser

代码语言:javascript
复制
@main.route('/adduser', methods=['GET', 'POST'])
@login_required
def adduser():
    form = CreateUserForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, password=form.password.data)
        db.session.add(user)
        db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('adduser.html', form=form)

还要再修改下 User 模型,因为当前保存的是明文密码,修改成使用 hash 存储。

代码语言:javascript
复制
    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

分别设置密码的只读权限,以及 hash 计算和验证功能。

接下来编写 HTML 模板

代码语言:javascript
复制
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Flasky</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
                {% if current_user.is_authenticated %}
                <li><a href="{{ url_for('main.logout') }}">Logout</a></li>
                {% else %}
                <li><a href="{{ url_for('main.login') }}">Login</a></li>
                {% endif %}
            </ul>
        </div>
    </div>
</div> {% endblock %}

{% block content %}
<div class="container">
    <div class="page-header">
        <h1>Hello, New 一个 User 吧!</h1>
    </div>
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

至此,一个简单的新增用户功能就好了。当然,我们还可以增加删除用户,重置密码等功能,这些的具体实现,都可以在文末的连接中找到哦,就不再赘述了。

2. 权限控制

我们其实并不希望所有人都能够创建聊天室,那么就要做一个简单的控制功能。 首先定义一个 permission 表,用来存储创建聊天室等权限,再定义一个用户和权限的关联关系表

代码语言:javascript
复制
class Permission(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    permission_name = db.Column(db.String(64), unique=True, index=True)


class RelUserPermission(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer)
    permission_id = db.Column(db.Integer)

然后我们还需要一个增加权限的表,以及一个用户列表页面 在 forms.py 中添加

代码语言:javascript
复制
class EditUserForm(FlaskForm):
    permission = SelectMultipleField('Permission', coerce=int)
    submit = SubmitField('Submit')

    def __init__(self, user, *args, **kwargs):
        super(EditUserForm, self).__init__(*args, **kwargs)
        self.permission.choices = [(per.id, per.permission_name)
                                   for per in Permission.query.order_by(Permission.permission_name).all()]
        self.user = user

定义了一个初始化函数,会获取到 Permission 表中的 name,id 等信息

接下来编写视图函数

代码语言:javascript
复制
@main.route('/listuser/', methods=['GET', 'POST'])
@login_required
def listuser():
    user_list = User.query.all()
    return render_template('listuser.html', user_list=user_list)


@main.route('/addper/', methods=['GET', 'POST'])
@login_required
def addper():
    form = CreatePerForm()
    if form.validate_on_submit():
        per = Permission(permission_name=form.permissionname.data)
        db.session.add(per)
        db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('addper.html', form=form)


@main.route('/edituser/<int:id>/', methods=['GET', 'POST'])
@login_required
def edituser(id):
    user = User.query.filter_by(id=id).first()
    form = EditUserForm(user=user)
    if form.validate_on_submit():
        for p in form.permission.data:
            rup = RelUserPermission(user_id=id, permission_id=p)
            db.session.add(rup)
            db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('edituser.html', form=form)

三个函数,分别是展示用户列表,增加权限,以及为用户添加权限。

然后再修改下 chat_room_list 函数,使得没有权限的用户不能展示创建聊天室的表单。

代码语言:javascript
复制
@main.route('/roomlist/', methods=["GET", 'POST'])
@login_required
def chat_room_list():
    roomlist_tmp = r.keys(pattern='chat-*')
    roomlist = []
    can_create = False
    create_room_id = Permission.query.filter_by(permission_name='createroom').first().id
    rel_user_id = RelUserPermission.query.filter_by(user_id=current_user.id).first()
    rel_permission = RelUserPermission.query.filter_by(user_id=current_user.id).first()
    if rel_permission and rel_user_id and create_room_id:
        rel_permission_id = rel_permission.permission_id
        if rel_permission_id == create_room_id:
            can_create = True
    for i in roomlist_tmp:
        i_str = str(i)
        istr_list = i_str.split('-', 1)
        roomlist.append(istr_list[1])
    return render_template('chatroomlist.html', roomlist=roomlist, can_create=can_create)

这里主要是判断用户是否拥有 createroom 权限,其实还有一种更加简便,但是稍微有些绕的鉴权方式,可以在文末的链接中找到,大家也可以尝试下。

最后处理 HTML 表单

对于聊天室列表页面:

代码语言:javascript
复制
  {% if can_create %}
  <form action="{{ url_for('main.create_room') }}" method="POST" class="comment-form">
         <div class="form-group comment-form-author">
        <label for="chatroomname">Chat Room Name <span class="required">*</span></label>
        <input class="form-control" id="chatroomname" name="chatroomname" type="text" value="" size="30" aria-required='true' />
        </div>
        <div class="form-group comment-form-comment">
        <label for="description">Chat Room Description <span class="required">*</span></label>
        <textarea class="form-control" id="description" name="description" cols="45" rows="6"></textarea>
        </div>
        <button  name="submit" type="submit" id="submit" class="btn btn-primary" value="Submit Comment">Create Room</button>
  </form>
  {% endif %}

对于用户列表页面:

代码语言:javascript
复制
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Flasky</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
                {% if current_user.is_authenticated %}
                <li><a href="{{ url_for('main.logout') }}">Logout</a></li>
                {% else %}
                <li><a href="{{ url_for('main.login') }}">Login</a></li>
                {% endif %}
            </ul>
        </div>
    </div>
</div> {% endblock %}

{% block content %}
<div class="container">
    <div class="page-header">
        <h1>Hello, 这里是所有的用户哦!</h1>
    </div>
    {% for user in user_list %}
    <a href="{{ url_for('main.edityouser', id=user.id) }}" class="btn btn-default" role="button">{{ user.username }}</a>
    {% endfor %}
</div>
{% endblock %}

这里为了方便起见,当点击用户时,就会跳转至编辑用户权限的页面。

现在,没有权限的用户,就不能看到创建聊天室的表单喽!

当前只增加了创建聊天室的权限,我们同样还可以创建是否有权限加入某个聊天室的权限,大家自己可以先实现下哦。

3.登陆优化

当前的登陆,只要用户名是正确的,不会验证密码,直接登陆成功,现在来处理下密码校验功能。其实也简单,我们在 User 模型中新增了一个函数 verify_password,只要登陆的时候,调用该函数来验证密码即可。

代码语言:javascript
复制
@main.route('/login/', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is not None and user.verify_password(form.password.data):
            login_user(user)
            return redirect(url_for('main.index'))
    return render_template('login.html', form=form)

ok,密码错误的你,是没法再登陆了。

4. 放开非登陆也可进入聊天室

  1. 去掉 chat_room_list,join_chat_room,send_chat 和 chat 视图函数的登陆装饰器 @login_required
  2. 修改 chat_room_list,判断当前用户是否已经登陆
代码语言:javascript
复制
@main.route('/roomlist/', methods=["GET", 'POST'])
def chat_room_list():
    roomlist_tmp = r.keys(pattern='chat-*')
    roomlist = []
    can_create = False
    create_room = Permission.query.filter_by(permission_name='createroom').first()
    if current_user.is_authenticated:  # 判断用户是否登陆
        rel_user_id = RelUserPermission.query.filter_by(user_id=current_user.id).first()
        rel_permission = RelUserPermission.query.filter_by(user_id=current_user.id).first()
        if rel_permission and rel_user_id and create_room:
            rel_permission_id = rel_permission.permission_id
            create_room_id = create_room.id
            if rel_permission_id == create_room_id:
                can_create = True
    for i in roomlist_tmp:
        i_str = str(i)
        istr_list = i_str.split('-', 1)
        roomlist.append(istr_list[1])
    return render_template('chatroomlist.html', roomlist=roomlist, can_create=can_create)
  1. 导航栏增加 room list 入口
代码语言:javascript
复制
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
                <li><a href="{{ url_for('main.chat_room_list') }}">Room List</a></li>
            </ul>
  1. chat 视图函数增加判断逻辑
代码语言:javascript
复制
@main.route('/chat/', methods=['GET', 'POST'])
def chat():
    rname = request.args.get('rname', "")
    ulist = r.zrange("chat-" + rname, 0, -1)
    messages = r.zrange("msg-" + rname, 0, -1, withscores=True)
    msg_list = []
    for i in messages:
        msg_list.append([json.loads(i[0]), time.strftime("%Y/%m/%d %p%H:%M:%S", time.localtime(i[1]))])
    if current_user.is_authenticated:
        return render_template('chat.html', rname=rname, user_list=ulist, msg_list=msg_list)
    else:
        email = "youke" + "@hihichat.com"
        hash = hashlib.md5(email.encode('utf-8')).hexdigest()
        gravatar_url = 'http://www.gravatar.com/avatar/' + hash + '?s=40&d=identicon&r=g'
        return render_template('chat.html', rname=rname, user_list=ulist,
                               msg_list=msg_list, g=gravatar_url)
  1. 修改 send_chat 视图
代码语言:javascript
复制
@main.route('/api/sendchat/<info>', methods=['GET', 'POST'])
def send_chat(info):
    if current_user.is_authenticated:
        rname = request.form.get("rname", "")
        body = {"username": current_user.username, "msg": info}
        r.zadd("msg-" + rname, json.dumps(body), time.time())
        socket_send(info, current_user.username)
        return info
    else:
        return info

当前对于未登陆的用户(游客),直接回复游客发送的消息。

今天的分享就到这里了,在下次的分享中,我们会尝试增加自己训练的聊天机器人到系统中,这样就能让没有登陆的用户,也能愉快的耍起来了。

所有的代码,都已经上传到 GitHub 上了,喜欢的小伙伴还请给个 star 啊,感谢!

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 萝卜大杂烩 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 调整项目结构
  • 具体代码拆分
  • 1. 配置信息
  • 2. 使用工厂函数
  • 3. 使用蓝本
  • 4. 修改 view 视图
  • 5. 编写 socket 连接函数
  • 6. 完成 forms 和 models
  • 7. 修改模板
  • 8. 启动脚本
    • 功能增强
    • 1. 新增用户
    • 2. 权限控制
    • 3.登陆优化
    • 4. 放开非登陆也可进入聊天室
    相关产品与服务
    云数据库 Redis®
    腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档