# -*- coding: utf-8 -*- from twisted.internet import reactor from twisted.internet import interfaces, defer from twisted.web.resource import Resource, NoResource import tornado.options import tornado.httpserver import tornado.web import tornado.websocket import tornado.escape import logging import traceback import json #import txmongo import os from widgets import widgets import uimodules import rss import base64 import math from tornado.options import define, options import bnw.core.bnw_objects as objs import bnw.core.post as post import bnw.core.base from bnw.core.bnw_mongo import get_db from bnw.core.bnw_gridfs import get_fs from bnw.handlers.command_show import cmd_feed, cmd_today from bnw.handlers.command_clubs import cmd_clubs, cmd_tags from bnw.handlers.command_userinfo import cmd_userinfo from bnw.web.base import BnwWebHandler, BnwWebRequest from bnw.web.auth import LoginHandler, LogoutHandler, requires_auth, AuthMixin from bnw.web.api import ApiHandler define("port", default=8888, help="run on the given port", type=int) wscount = 0 class WsHandler(tornado.websocket.WebSocketHandler): """Helper class for websocket handlers. Register listeners and send new events to clients. Unregister listeners on close. """ def get_handlers(self, *args): return () def open(self, *args): self.version = self.request.arguments.get('v', ['0'])[0] self.handlers = self.get_handlers(*args) for etype, handler in self.handlers: post.register_listener(etype, id(self), handler) global wscount wscount += 1 print 'Opened connection %d (v%s). %d connections. %s' % (id(self), self.version, wscount, self.request.path) def on_close(self): for etype, _ in self.handlers: post.unregister_listener(etype, id(self)) global wscount wscount -= 1 print 'Closed connection %d. %d connections active.' % (id(self), wscount) class MainWsHandler(WsHandler, AuthMixin): """Deliver new events on main page via websockets.""" def get_handlers(self): if self.version == '2': return ( ('new_message', self.new_message), ('del_message', self.del_message), ('upd_comments_count', self.upd_comments_count), ('upd_recommendations_count', self.upd_recommendations_count), ) else: return ( ('new_message', self.new_message_compat), ) @defer.inlineCallbacks def new_message(self, msg): user = yield self.get_auth_user() if user and ['user', msg['user']] in user.get('blacklist', []): return html = uimodules.Message(self).render(msg) msg = msg.copy() if user: msg['subscribed'] = user['name'] == msg['user'] msg.update(dict(type='new_message', html=html)) self.write_message(json.dumps(msg)) @defer.inlineCallbacks def new_message_compat(self, msg): user = yield self.get_auth_user() if user and ['user', msg['user']] in user.get('blacklist', []): return self.write_message(json.dumps(msg)) def del_message(self, msg_id): self.write_message(json.dumps({'type': 'del_message', 'id': msg_id})) def upd_comments_count(self, msg_id, num): self.write_message(json.dumps({ 'type': 'upd_comments_count', 'id': msg_id, 'num': num})) def upd_recommendations_count(self, msg_id, num, recommendations): self.write_message(json.dumps({ 'type': 'upd_recommendations_count', 'id': msg_id, 'num': num, 'recommendations': recommendations})) class UserWsHandler(MainWsHandler): """Deliver new events on user page via websockets.""" def get_handlers(self, user): return ( ('new_message_on_user_' + user, self.new_message), ('del_message_on_user_' + user, self.del_message), # TODO: Should we update only user's messages? ('upd_comments_count', self.upd_comments_count), ('upd_recommendations_count', self.upd_recommendations_count), ) class CommentsWsHandler(WsHandler): """Deliver new comments via websockets.""" def get_handlers(self): return ( ('new_comment', self.new_comment), ) def new_comment(self, comment): self.write_message(json.dumps(comment)) class MessageWsHandler(MainWsHandler): """Deliver new events on message page via websockets.""" def get_handlers(self, msgid): if self.version == '2': return ( ('new_comment_in_' + msgid, self.new_comment), ('del_comment_in_' + msgid, self.del_comment), ('upd_comments_count_in_' + msgid, self.upd_comments_count), ('upd_recommendations_count_in_' + msgid, self.upd_recommendations_count), ) else: return ( ('new_comment_in_' + msgid, self.new_comment_compat), ) @defer.inlineCallbacks def new_comment(self, comment): user = yield self.get_auth_user() if user and ['user', comment['user']] in user.get('blacklist', []): return html = uimodules.Comment(self).render(comment) comment = comment.copy() comment.update(dict(type='new_comment', html=html)) self.write_message(json.dumps(comment)) def new_comment_compat(self, comment): self.write_message(json.dumps(comment)) def del_comment(self, comment_id): self.write_message(json.dumps( {'type': 'del_comment', 'id': comment_id})) def get_page(self): ra = self.request.arguments rv = ra.get('page', ['0'])[0] if rv.isdigit(): rv = int(rv) return rv if isinstance(rv, int) else 0 # no long here return 0 @defer.inlineCallbacks def is_hasmes(qdict, page): """Return True if summary page count bigger than given (given page numbering starting from 0). """ count = yield objs.Message.count(qdict) defer.returnValue(int(math.ceil(count / 20.0)) > page + 1) class UserHandler(BnwWebHandler, AuthMixin): templatename = 'user.html' @defer.inlineCallbacks def respond(self, username, reco=None, tag=None): _ = yield self.get_auth_user() f = [("date", -1)] user = (yield objs.User.find_one({'name': username})) page = get_page(self) if reco == "recommendations": qdict = {'recommendations': username} elif reco == "all": qdict = {'$or': [{'user': username}, { 'recommendations': username}]} else: qdict = {'user': username} if tag: tag = tornado.escape.url_unescape(tag) qdict['tags'] = tag messages = list((yield objs.Message.find_sort(qdict, sort=f, limit=20, skip=20 * page))) hasmes = yield is_hasmes(qdict, page) format = self.get_argument("format", "") if format == 'rss': self.set_header( "Content-Type", 'application/rss+xml; charset=UTF-8') defer.returnValue(rss.message_feed(messages, link=widgets.user_url(username), title='Поток сознания @%s' % username)) elif format == 'json': json_messages = [message.filter_fields() for message in messages] defer.returnValue(json.dumps(json_messages, ensure_ascii=False)) else: self.set_header("Cache-Control", "max-age=1") defer.returnValue({ 'username': username, 'user': user, 'messages': messages, 'page': page, 'tag': tag, 'hasmes': hasmes, }) class UserRecoHandler(BnwWebHandler, AuthMixin): templatename = 'user.html' @defer.inlineCallbacks def respond(self, username, tag=None): _ = yield self.get_auth_user() f = [("date", -1)] user = (yield objs.User.find_one({'name': username})) page = get_page(self) qdict = {'recommendations': username} if tag: tag = tornado.escape.url_unescape(tag) qdict['tags'] = tag messages = list((yield objs.Message.find_sort(qdict, sort=f, limit=20, skip=20 * page))) hasmes = yield is_hasmes(qdict, page) self.set_header("Cache-Control", "max-age=1") defer.returnValue({ 'username': username, 'user': user, 'messages': messages, 'page': page, 'tag': tag, 'hasmes': hasmes, }) class UserInfoHandler(BnwWebHandler, AuthMixin): templatename = 'userinfo.html' @defer.inlineCallbacks def respond(self, user): req = BnwWebRequest((yield self.get_auth_user())) self.set_header('Cache-Control', 'max-age=10') defer.returnValue((yield cmd_userinfo(req, user))) class MessageHandler(BnwWebHandler, AuthMixin): templatename = 'message.html' @defer.inlineCallbacks def respond(self, msgid): user = yield self.get_auth_user() f = [("date", 1)] msg = (yield objs.Message.find_one({'id': msgid})) qdict = {'message': msgid} if user: bl = [] for e in user.get('blacklist', []): if e[0] == 'user': bl.append(e[1]) if bl: qdict['user'] = {'$nin': bl} is_subscribed = yield objs.Subscription.count({ 'user': user['name'], 'type': 'sub_message', 'target': msgid, }) else: is_subscribed = False # TODO: Converting generator to list may be inefficient. comments = list((yield objs.Comment.find(qdict, sort=f, limit=10000))) self.set_header("Cache-Control", "max-age=5") if not msg: self.set_status(404) defer.returnValue({ 'msgid': msgid, 'msg': msg, 'auth_user': user, 'comments': comments, 'is_subscribed': is_subscribed, }) class MainHandler(BnwWebHandler, AuthMixin): templatename = 'main.html' @defer.inlineCallbacks def respond(self, club=None, tag=None): f = [("date", -1)] user = yield self.get_auth_user() page = get_page(self) qdict = {} if tag: tag = tornado.escape.url_unescape(tag) qdict['tags'] = tag if club: club = tornado.escape.url_unescape(club) qdict['clubs'] = club if user: bl = [] for e in user.get('blacklist', []): if e[0] == 'user': bl.append(e[1]) if bl: qdict['user'] = {'$nin': bl} messages = list((yield objs.Message.find_sort(qdict, sort=f, limit=20, skip=20 * page))) hasmes = yield is_hasmes(qdict, page) uc = (yield objs.User.count()) format = self.get_argument("format", "") self.set_header("Cache-Control", "max-age=1") if format == 'rss': self.set_header( "Content-Type", 'application/rss+xml; charset=UTF-8') if self.request.protocol == "https": base = bnw.core.base.get_https_webui_base() else: base = bnw.core.base.get_http_webui_base() defer.returnValue( rss.message_feed( messages, link=base, title='Коллективное бессознательное BnW')) elif format == 'json': json_messages = [message.filter_fields() for message in messages] defer.returnValue(json.dumps(json_messages, ensure_ascii=False)) else: req = BnwWebRequest((yield self.get_auth_user())) tagres = yield cmd_tags(req) toptags = tagres['tags'] if tagres['ok'] else [] defer.returnValue({ 'messages': messages, 'toptags': toptags, 'users_count': int(uc), 'page': page, 'tag': tag, 'club': club, 'hasmes': hasmes, }) class FeedHandler(BnwWebHandler, AuthMixin): templatename = 'feed.html' @requires_auth @defer.inlineCallbacks def respond(self): page = get_page(self) req = BnwWebRequest((yield self.get_auth_user())) result = yield cmd_feed(req, page=page) self.set_header("Cache-Control", "max-age=1") defer.returnValue({ 'result': result, 'page': page, 'hasmes': result['ok'] and len(result['messages']) == 20, }) class TodayHandler(BnwWebHandler, AuthMixin): templatename = 'today.html' @defer.inlineCallbacks def respond(self): req = BnwWebRequest((yield self.get_auth_user())) result = yield cmd_today(req) self.set_header("Cache-Control", "max-age=300") defer.returnValue({ 'result': result, }) class ClubsHandler(BnwWebHandler, AuthMixin): templatename = 'clubs.html' @defer.inlineCallbacks def respond(self): user = yield self.get_auth_user() req = BnwWebRequest((yield self.get_auth_user())) result = yield cmd_clubs(req) self.set_header("Cache-Control", "max-age=3600") defer.returnValue({ 'result': result, }) class BlogHandler(BnwWebHandler, AuthMixin): @requires_auth @defer.inlineCallbacks def respond(self): user = yield self.get_auth_user() self.redirect(str('/u/' + user['name'])) defer.returnValue('') class PostHandler(BnwWebHandler, AuthMixin): templatename = 'post.html' @requires_auth @defer.inlineCallbacks def respond_post(self): tags = [i[:128] for i in self.get_argument("tags", "") .split(",", 5)[:5] if i] clubs = [i[:128] for i in self.get_argument("clubs", "") .split(",", 5)[:5] if i] text = self.get_argument("text", "") user = yield self.get_auth_user() ok, result = yield post.postMessage(user, tags, clubs, text) if ok: (msg_id, qn, recs) = result self.redirect('/p/' + msg_id) defer.returnValue('') else: defer.returnValue({'error': result}) @requires_auth @defer.inlineCallbacks def respond(self): user = yield self.get_auth_user() default_text = self.get_argument("url", "") if not default_text: default_text = self.get_argument("text", "") default_clubs = self.get_argument("clubs", "") default_tags = self.get_argument("tags", "") self.set_header("Cache-Control", "max-age=1") defer.returnValue({'auth_user': user, 'default_text': default_text, 'default_tags': default_tags, 'default_clubs': default_clubs, 'error': None}) class CommentHandler(BnwWebHandler, AuthMixin): templatename = 'comment.html' @requires_auth @defer.inlineCallbacks def respond_post(self): msg = self.get_argument("msg", "") comment = self.get_argument("comment", "") if comment: comment = msg + "/" + comment text = self.get_argument("text", "") noredir = self.get_argument("noredir", "") user = yield self.get_auth_user() ok, result = yield post.postComment(msg, comment, text, user) if ok: (msg_id, num, qn, recs) = result if noredir: defer.returnValue('Posted with ' + msg_id) else: redirtarget = '/p/' + msg_id.replace('/', '#') # странная хуйня с твистедом или еще чем-то # если в редиректе unicode-объект - реквест не финиширует self.redirect(str(redirtarget)) defer.returnValue('') else: defer.returnValue({'error': result}) emptypng = base64.b64decode( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAAAnRSTlMAAHaTzTgAAAAKSURB' 'VAjXY2AAAAACAAHiIbwzAAAAAElFTkSuQmCC') class AvatarHandler(BnwWebHandler): @defer.inlineCallbacks def respond(self, username, thumb=''): self.set_header('Cache-Control', 'max-age=3600, public') self.set_header('Vary', 'Accept-Encoding') user = yield objs.User.find_one({'name': username}) if not (user and user.get('avatar') and False): self.set_header('Content-Type', 'image/png') defer.returnValue(emptypng) if thumb: av_id = user['avatar'][2] mimetype = 'image/png' else: av_id = user['avatar'][0] mimetype = user['avatar'][1] fs = yield get_fs('avatars') # воркэраунд недопила в txmongo. TODO: зарепортить или починить doc = yield fs._GridFS__files.find_one({'_id': av_id}) avatar = yield fs.get(doc) avatar_data = yield avatar.read() self.set_header('Content-Type', mimetype) defer.returnValue(avatar_data) class SitemapHandler(BnwWebHandler): @defer.inlineCallbacks def respond(self, type_): self.set_header('Cache-Control', 'public') self.set_header('Vary', 'Accept-Encoding') f = [("date", -1)] skip = 0 self.write('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n\n') while True: messages = list((yield objs.Message.find_sort({}, sort=f, fields={'id':1}, limit=20, skip=skip))) for message in messages: self.write('<url><loc>http://%s/p/%s</loc><priority>1.0</priority></url>\n' % (bnw.core.base.config.webui_base, message['id'])) if len(messages)==0 or skip>1000: break skip += 20 self.write('</urlset>\n') defer.returnValue('') def get_site(): settings = { "template_path": os.path.join(os.path.dirname(__file__), "templates"), "xsrf_cookies": True, "static_path": os.path.join(os.path.dirname(__file__), "static"), "ui_modules": uimodules, "autoescape": None, "gzip": False, } application = tornado.web.Application([ (r"/p/([A-Z0-9]+)/?", MessageHandler), (r"/p/([A-Z0-9]+)/ws/?", MessageWsHandler), (r"/u/([0-9a-z_-]+)(?:/(recommendations|all))?/?", UserHandler), (r"/u/([0-9a-z_-]+)/ws/?", UserWsHandler), (r"/u/([0-9a-z_-]+)/avatar(/thumb)?/?", AvatarHandler), (r"/u/([0-9a-z_-]+)/info/?", UserInfoHandler), (r"/u/([0-9a-z_-]+)(?:/(recommendations|all))?/t/(.*)/?", UserHandler), (r"/", MainHandler), (r"/ws/?", MainWsHandler), (r"/comments/ws/?", CommentsWsHandler), (r"/t/()(.*)/?", MainHandler), (r"/c/(.*)()/?", MainHandler), (r"/login", LoginHandler), (r"/logout", LogoutHandler), (r"/post", PostHandler), (r"/feed", FeedHandler), (r"/today", TodayHandler), (r"/clubs", ClubsHandler), (r"/blog", BlogHandler), (r"/comment", CommentHandler), (r"/sitemap-(messages).xml", SitemapHandler), (r"/api/([0-9a-z/]*)", ApiHandler), ], **settings) return tornado.httpserver.HTTPServer(application, xheaders=True) def main(): tornado.options.parse_command_line() site = get_site() reactor.listenTCP(options.port, site) reactor.run() if __name__ == '__main__': main()