Лабораторная работа №13. MicroSlack
Эта лабораторная работа посвящена созданию простого клона мессенджера Slack.
from aiohttp import web
app = web.Application()
web.run_app(app, host='127.0.0.1', port=8080)
from aiohttp import web
async def index(request):
return web.Response(text='Index page')
app = web.Application()
app.router.add_route('GET', '/', index)
web.run_app(app, host='127.0.0.1', port=8080)
from aiohttp import web
import jinja2
import aiohttp_jinja2
@aiohttp_jinja2.template('index.html')
async def index(request):
return {'title': 'Index page'}
app = web.Application()
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))
app.router.add_get('/', index)
web.run_app(app, host='127.0.0.1', port=8080)
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>Welcome to {{ title }}</h1>
</body>
</html>
from aiohttp import web
import jinja2
import aiohttp_jinja2
from chat.views import Index
async def create_app():
app = web.Application()
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))
app.router.add_static('/static', 'static', name='static') # аргумент name используется для того, чтобы мы могли обратиться к app.router.static
app.router.add_get('/', Index)
return app
loop = asyncio.get_event_loop()
app = loop.run_until_complete(create_app())
web.run_app(app, host='127.0.0.1', port=8080)
from aiohttp import web
import aiohttp_jinja2
class Index(web.View):
@aiohttp_jinja2.template('index.html')
async def get(self):
return {'title': 'Index page'}
...
<head>
<title>{{ title }}</title>
<script src="{{ app.router.static.url(filename='chat.js') }}"></script>
</head>
...
from aiohttp import web
import aiohttp_jinja2
class LoginView(web.View):
# http://aiohttp.readthedocs.io/en/stable/web.html#class-based-views
@aiohttp_jinja2.template('login.html')
async def get(self):
if self.request.cookies.get('user'):
return web.HTTPFound('/')
return {'title': 'Login'}
async def post(self):
response = web.HTTPFound('/')
# http://aiohttp.readthedocs.io/en/stable/web.html#http-forms
data = await self.request.post()
response.set_cookie('user', data['name'])
return response
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<link rel="stylesheet" href="//oss.maxcdn.com/semantic-ui/2.1.8/semantic.min.css" type="text/css">
</head>
<body>
<div class="ui three column stackable centered grid">
<div class="column">
<form action="/login" method="post">
<div class="ui fluid action input">
<input type="text" name="name" placeholder="Username" required>
<button type="submit" class="ui primary button">Log in</button>
</div>
</form>
</div>
</div>
</body>
</html>
...
from accounts.views import Login
...
async def create_app():
...
app.router.add_route('*', '/login', Login)
...
from aiohttp import web
async def auth_cookie_factory(app, handler):
# http://aiohttp.readthedocs.io/en/stable/web.html#middlewares
async def middleware(request):
# http://aiohttp.readthedocs.io/en/stable/web_reference.html#aiohttp.web.BaseRequest.path
# http://aiohttp.readthedocs.io/en/stable/web_reference.html#aiohttp.web.BaseRequest.cookies
if request.path != '/login' and request.cookies.get('user') is None:
# http://aiohttp.readthedocs.io/en/stable/web.html#exceptions
return web.HTTPFound('/login')
return await handler(request)
return middleware
...
from middlewares import auth_cookie_factory
...
async def create_app():
app = web.Application(middlewares=[auth_cookie_factory,])
...
Замена кук на сессии:
from aiohttp import web
import jinja2
import aiohttp_jinja2
import aioredis
from aiohttp_session import session_middleware
from aiohttp_session.redis_storage import RedisStorage
from chat.views import Index
from accounts.views import Login
from middlewares import auth_session_factory, request_session_factory
async def create_app():
async def close_redis(app):
app.redis_pool.close()
await app.redis_pool.wait_closed()
redis_pool = await aioredis.create_pool(('127.0.0.1', 6379), loop=loop)
app = web.Application(middlewares=[
session_middleware(RedisStorage(redis_pool)),
request_session_factory,
auth_session_factory
])
app.redis_pool = redis_pool
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))
app.router.add_static('/static', 'static', name='static')
app.router.add_get('/', Index)
app.router.add_route('*', '/login', Login)
app.on_shutdown.append(close_redis)
return app
loop = asyncio.get_event_loop()
app = loop.run_until_complete(create_app())
web.run_app(app, host='127.0.0.1', port=8080)
from aiohttp import web
from aiohttp_session import get_session
async def request_session_factory(app, handler):
async def middleware(request):
request.session = await get_session(request)
return await handler(request)
return middleware
async def auth_session_factory(app, handler):
async def middleware(request):
if request.path != '/login' and request.session.get('user') is None:
return web.HTTPFound('/login')
return await handler(request)
return middleware
...
from aiohttp_session import get_session
class Login(web.View):
@aiohttp_jinja2.template('login.html')
async def get(self):
if self.request.session.get('user'):
...
async def post(self):
...
self.request.session['user'] = data['name']
...
Шаблон интерфейса
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,700,900&subset=cyrillic,cyrillic-ext" rel="stylesheet">
<link href="{{ app.router.static.url(filename='css/style.css') }}" rel='stylesheet' type='text/css'>
<script src="{{ app.router.static.url(filename='js/vue.js') }}"></script>
<body>
<div id="app">
<app-header></app-header>
<div class="main">
<channels></channels>
<messages></messages>
</div>
<app-footer></app-footer>
</div>
<script src="{{ app.router.static.url(filename='js/chat.js') }}"></script>
</body>
</html>
Vue.component('app-header', {
template: `
<div class="header">
<div class="team-menu">MicroSlack</div>
<div class="channel-menu">
<span class="channel-menu_name">
<span class="channel-menu_prefix">#</span>
general
</span>
</div>
</div>
`
})
Vue.component('app-footer', {
template: `
<div class="footer">
<div class="user-menu">
<span class="user-menu_profile-pic"></span>
<span class="user-menu_username">scotch</span>
<img class="connection_icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAABmFBMVEUAAAD////////////////////////////////////2+/LR5bKw1Hmfy1KUxz2VyD2izVKz1nnS5rP////A3JuOw0qKwkCNxD+QxT6Sxj6Txz6SxUnC3Jv1+fGXx2GDvkCGwECIwUCLwj+PxD6PxT+JwUCFwECZyGD2+vGSxWF9vEGAvkGDv0CMwz+Wx2GPw2F4ukJ7u0J+vUGBvkGHwUB8u0KSxGG31pp0uEN3uUJ5u0KFv0CCv0B6u0K415p5uU1yt0N/vUF1uEN8u0zG3bFttURwtkR5ukLH3rGWxnlqtERutUR2uUOZx3l6uVZos0VvtkRxt0Nzt0N8ulVisUVlskVns0VzuENmskVfsEVps0VztlZer0VhsEVjsUVstER1t1aOwXhcrkZdr0VgsEaQwnm/2a9YrUZbrka/2rDz+PFhr09XrEZksE6pzplUq0ZVrEZarUaqzpl0tWJRq0dWrEZ1tmJztWJOqUdSq0dxtGJMqEdNqUdQqkdytWKmzJhXrFBKqEdZrU+716+GvXhjr1dIp0hkr1dYtVOVAAAAFHRSTlMAV8/v/wCH+x/n////////////9kvBHZAAAAG7SURBVHgBvdOxjtNAEIDhGe/MZO3sxVaiIJkiSNdQUPJOeQlqXoCCIg/EU9BQHRKg5CT7ErzrHTa+aBOqaxC/tdLK+2kbj+H/hoWhlCmQr0HeyYxyM8mvkWHKoAfBS6cBWEeYugAzf4QGp1SV8DvU/ZjBdN7iud6hdnOTdl+TuALyrUPEwfdu3nc1ipr9AwdIFZPysJylRDfa6cZL2rfgMd9QjO8R0Y+/u7sa4LHZz4wN/MXEyw1hbK1VZdV7PZ1OyufzktsxXADCW5EkXq06Paan02Uoo3kHmAEzJ8HBN6v5qlkqaxTmCdAzQK8Noi6rXwCrJyutepUMAARnXS++3cvm2xvftR0PzAyQAXtwdNChifvFHppBdR003IDCIg6JDOse4DX8WIdo1TwfpaUgqWC9c4eqqg5HF20QZdAMmDlasdHWkrKR03J0A4iIXRTrpba29laiY8YMyOyMKYkXroyROZZuwVTyztAFJPmZKBGq+FxFVBr5BHr7ubd3GICfAM+88qDHHYe/BmbbIAaGKU/Fz10emDxyHxBhgJTg+DGP3O3QbltMBkd92F2H9sWxB772wo9z2z8FfwDHWbdKLDfq1AAAAABJRU5ErkJggg==">
<span class="connection_status">online</span>
</div>
<div class="input-box">
<input type="text" class="input-box_text">
</div>
</div>
`
})
Vue.component('channel', {
template: `
<li class="channel active">
<a class="channel_name" href="#">
<span class="unread">0</span>
<span><span class="prefix">#</span>general</span>
</a>
</li>
`
})
Vue.component('channels', {
template: `
<div class="listings">
<div class="listings_channels">
<h2 class="listings_header">Channels</h2>
<ul class="channel_list">
<channel></channel>
</ul>
</div>
<div class="listings_direct-messages"></div>
</div>
`
})
Vue.component('message', {
template: `
<div class="message">
<a href="#" class="message_profile-pic"></a>
<a href="#" class="message_username">scotch</a>
<span class="message_timestamp"></span>
<span class="message_star"></span>
<span class="message_content">Message</span>
</div>
`
})
Vue.component('messages', {
template: `
<div class="message-history">
<message></message>
</div>
`
})
var app = new Vue({
el: '#app'
})
Подгрузка временных сообщений
...
class Channel(web.View):
async def get(self):
return web.json_response({
'messages': [
{'username': 'CIA Agent', 'text': "At least you can talk. Who are you?"},
{'username': 'Bane', 'text': "It doesn't matter who we are... what matters is our plan."},
{'username': 'Bane', 'text': "No one cared who I was until I put on the mask."},
]
})
var app = new Vue({
el: '#app',
data: {
messages: [],
},
methods: {
getChannelHistory: function() {
axios.get('/general/history').then(function(response) {
app.messages.unshift.apply(app.messages, response.data.messages);
});
}
},
created: function() {
this.getChannelHistory();
}
})
Отображение сообщений
<div class="main">
<channels></channels>
<messages :messages="messages"></messages>
</div>
Vue.component('message', {
props: ['message'],
template: `
<div class="message">
<a href="#" class="message_profile-pic"></a>
<a href="#" class="message_username">{{ message.username }}</a>
<span class="message_timestamp"></span>
<span class="message_star"></span>
<span class="message_content">{{ message.text }}</span>
</div>
`
})
Vue.component('messages', {
props: ['messages'],
template: `
<div class="message-history">
<message
v-for="message in messages"
:message="message">
</message>
</div>
`
})
Добавление сообщениям идентификаторов
class Channel(web.View):
async def get(self):
return web.json_response({
'messages': [
{'id': 0, 'username': 'CIA Agent', 'text': "At least you can talk. Who are you?"},
{'id': 1, 'username': 'Bane', 'text': "It doesn't matter who we are... what matters is our plan."},
{'id': 2, 'username': 'Bane', 'text': "No one cared who I was until I put on the mask."},
]
})
Vue.component('messages', {
props: ['messages'],
template: `
<div class="message-history">
<message
v-for="message in messages"
:message="message"
:key="message.id">
</message>
</div>
`
})
Шаблон для отправки сообщений
Vue.component('app-footer', {
template: `
<div class="footer">
<div class="user-menu">
<span class="user-menu_profile-pic"></span>
<span class="user-menu_username">scotch</span>
<img class="connection_icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAABmFBMVEUAAAD////////////////////////////////////2+/LR5bKw1Hmfy1KUxz2VyD2izVKz1nnS5rP////A3JuOw0qKwkCNxD+QxT6Sxj6Txz6SxUnC3Jv1+fGXx2GDvkCGwECIwUCLwj+PxD6PxT+JwUCFwECZyGD2+vGSxWF9vEGAvkGDv0CMwz+Wx2GPw2F4ukJ7u0J+vUGBvkGHwUB8u0KSxGG31pp0uEN3uUJ5u0KFv0CCv0B6u0K415p5uU1yt0N/vUF1uEN8u0zG3bFttURwtkR5ukLH3rGWxnlqtERutUR2uUOZx3l6uVZos0VvtkRxt0Nzt0N8ulVisUVlskVns0VzuENmskVfsEVps0VztlZer0VhsEVjsUVstER1t1aOwXhcrkZdr0VgsEaQwnm/2a9YrUZbrka/2rDz+PFhr09XrEZksE6pzplUq0ZVrEZarUaqzpl0tWJRq0dWrEZ1tmJztWJOqUdSq0dxtGJMqEdNqUdQqkdytWKmzJhXrFBKqEdZrU+716+GvXhjr1dIp0hkr1dYtVOVAAAAFHRSTlMAV8/v/wCH+x/n////////////9kvBHZAAAAG7SURBVHgBvdOxjtNAEIDhGe/MZO3sxVaiIJkiSNdQUPJOeQlqXoCCIg/EU9BQHRKg5CT7ErzrHTa+aBOqaxC/tdLK+2kbj+H/hoWhlCmQr0HeyYxyM8mvkWHKoAfBS6cBWEeYugAzf4QGp1SV8DvU/ZjBdN7iud6hdnOTdl+TuALyrUPEwfdu3nc1ipr9AwdIFZPysJylRDfa6cZL2rfgMd9QjO8R0Y+/u7sa4LHZz4wN/MXEyw1hbK1VZdV7PZ1OyufzktsxXADCW5EkXq06Paan02Uoo3kHmAEzJ8HBN6v5qlkqaxTmCdAzQK8Noi6rXwCrJyutepUMAARnXS++3cvm2xvftR0PzAyQAXtwdNChifvFHppBdR003IDCIg6JDOse4DX8WIdo1TwfpaUgqWC9c4eqqg5HF20QZdAMmDlasdHWkrKR03J0A4iIXRTrpba29laiY8YMyOyMKYkXroyROZZuwVTyztAFJPmZKBGq+FxFVBr5BHr7ubd3GICfAM+88qDHHYe/BmbbIAaGKU/Fz10emDxyHxBhgJTg+DGP3O3QbltMBkd92F2H9sWxB772wo9z2z8FfwDHWbdKLDfq1AAAAABJRU5ErkJggg==">
<span class="connection_status">online</span>
</div>
<div class="input-box">
<input type="text" class="input-box_text" v-model="newMsg" @keypress="sendMessage">
</div>
</div>
`,
data: function() {
return {
newMsg: '',
}
},
methods: {
sendMessage(kbEvent) {
if (kbEvent.keyCode == 13 && this.newMsg != '') {
app.messages.push({
'username': 'anonymous',
'text': this.newMsg,
'id': 3
})
this.newMsg = '';
}
}
}
})
Броадкаст сообщений
import json
...
class WebSocketHandler(web.View):
async def get(self):
ws = web.WebSocketResponse()
await ws.prepare(self.request)
channel_waiters = self.request.app['waiters']['general']
channel_waiters.append(ws)
try:
async for msg in ws:
if msg.tp == web.MsgType.text:
data = json.loads(msg.data)
data['username'] = self.request.session['user']
channel_waiters.broadcast(json.dumps(data))
elif msg.tp == web.MsgType.error:
print('connection closed with exception')
finally:
if ws in channel_waiters:
channel_waiters.remove(ws)
await ws.close()
return ws
...
from collections import defaultdict
...
from chat.views import Index, Channel, WebSocketHandler
...
class BroadcastList(list):
def broadcast(self, message):
for waiter in self:
try:
waiter.send_str(message)
except Exception as exc:
print('error was happining during broadcast', exc)
async def create_app():
...
async def close_websockets(app):
for channel in app['waiters'].values():
while channel:
ws = channel.pop()
await ws.close(code=1000, message='Server shutdown')
...
app['waiters'] = defaultdict(BroadcastList)
...
app.router.add_get('/general/messages', WebSocketHandler)
...
app.on_shutdown.append(close_websockets)
return app
sendMessage(kbEvent) {
if (kbEvent.keyCode == 13 && this.newMsg != '') {
app.ws.send(JSON.stringify({
'text': this.newMsg,
}));
this.newMsg = '';
}
}
...
data: {
messages: [],
ws: null,
},
...
created: function() {
var ws = new WebSocket('ws://' + window.location.host + '/general/messages');
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
if (data.hasOwnProperty('text')) {
app.messages.push(data);
}
}
this.ws = ws;
this.getChannelHistory();
}
Добавляем каналы
Хранение сообщений в mongodb и подгрузка сообщений
import asyncio
import motor.motor_asyncio
import faker
import random
from datetime import datetime as dt
client = motor.motor_asyncio.AsyncIOMotorClient('localhost', 27017)
db = client.test_database
collection = db.test_collection
def generate_collection_data(N):
f = faker.Faker()
channels = ('general', 'random')
messages = ({
'text': f.text(),
'username': f.user_name(),
'ts': dt.timestamp(f.date_time()),
'channel': channels[random.choice([0,1])]
} for _ in range(N))
return messages
async def do_insert():
messages = generate_collection_data(30)
await collection.insert_many(messages)
count = await collection.count()
print(f"count: {count}")
loop = asyncio.get_event_loop()
loop.run_until_complete(do_insert())