├── tmp └── .empty ├── log └── supervisor │ └── .empty ├── src ├── utils │ ├── __init__.py │ └── text.py ├── config.py ├── templates │ ├── layout.html │ ├── index.html │ ├── room.html │ └── chat.html ├── gc.py ├── static │ ├── script │ │ ├── main.coffee │ │ └── main.js │ └── css │ │ └── main.css └── app.py ├── scripts └── clear_key.py ├── .gitignore ├── setup.py ├── etc └── supervisord.conf.in ├── README.md └── bootstrap.py /tmp/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /log/supervisor/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/clear_key.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | import redis 3 | 4 | rc = redis.Redis() 5 | for item in ['online_*', 'room_*']: 6 | for key in rc.keys(item): 7 | print 'deleting %s'%key 8 | rc.delete(key) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | *.swp 5 | *.pid 6 | *.bak 7 | dump.rdb 8 | eggs/* 9 | *.o 10 | *.so 11 | *.egg-info 12 | build 13 | *.log 14 | .installed.cfg 15 | bin/ 16 | develop-eggs/ 17 | parts/ 18 | supervisord.conf 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name = 'comet', 5 | version = '1.0', 6 | description = 'comet test', 7 | author = 'lzyy', 8 | author_email = 'healdream@gmail.com', 9 | url = 'http://blog.leezhong.com', 10 | packages = find_packages('src'), 11 | package_dir = {'': 'src'}, 12 | install_requires = [ 13 | 'flask', 14 | 'redis', 15 | 'gevent', 16 | 'apscheduler', 17 | ], 18 | ) 19 | 20 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | DEBUG = True 4 | PORT = 9527 5 | SECRET_KEY = 'i have a dream' 6 | SESSION_COOKIE_HTTPONLY = False 7 | 8 | CHAT_NAME = u'谈天说地老天荒' 9 | 10 | ONLINE_USER_CHANNEL = 'online_user_channel' 11 | ROOM_ONLINE_USER_CHANNEL = 'room_online_user_channel_{room}' 12 | ROOM_CHANNEL = 'room_channel_{room}' 13 | 14 | ROOM_INCR_KEY = 'room_incr_key' 15 | ROOM_CONTENT_INCR_KEY = 'room_content_incr_key' 16 | ROOM_INFO_KEY = 'room_info_key_{room}' 17 | 18 | ONLINE_USER_SIGNAL = ONLINE_USER_CHANNEL 19 | ROOM_ONLINE_USER_SIGNAL = ROOM_ONLINE_USER_CHANNEL 20 | ROOM_SIGNAL = ROOM_CHANNEL 21 | 22 | CONN_CHANNEL_SET = 'conn_channel_set_{channel}' 23 | 24 | COMET_TIMEOUT = 30 25 | COMET_POLL_TIME = 2 26 | 27 | CHANNEL_PLACEHOLDER = 'jwdlh' 28 | -------------------------------------------------------------------------------- /src/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock %} 5 | 6 | 7 | 8 | 9 | 15 | 16 |
17 | {% block main %} 18 | {% endblock %} 19 |
20 | 21 | 22 | 23 | {% block script %} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /src/gc.py: -------------------------------------------------------------------------------- 1 | from apscheduler.scheduler import Scheduler 2 | import signal 3 | import redis 4 | import config 5 | import time 6 | import json 7 | 8 | rc = redis.Redis() 9 | 10 | sched = Scheduler() 11 | sched.start() 12 | 13 | def clear(): 14 | current_time = time.time() 15 | affcted_num = rc.zremrangebyscore(config.ONLINE_USER_CHANNEL, '-inf', current_time - 60) 16 | if affcted_num: 17 | rc.zadd(config.ONLINE_USER_CHANNEL, config.CHANNEL_PLACEHOLDER, time.time()) 18 | 19 | for key in rc.keys(config.ROOM_ONLINE_USER_CHANNEL.format(room='*')): 20 | affcted_num = rc.zremrangebyscore(key, '-inf', current_time - 60) 21 | if affcted_num: 22 | rc.zadd(key, config.CHANNEL_PLACEHOLDER, time.time()) 23 | 24 | sched.add_cron_job(clear, minute='*') 25 | 26 | signal.pause() 27 | -------------------------------------------------------------------------------- /src/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{config['CHAT_NAME']}} 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | {% for cate, message in get_flashed_messages(with_categories=true) %} 14 |

{{message}}

15 | {% endfor %} 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 | 24 | -------------------------------------------------------------------------------- /etc/supervisord.conf.in: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/tmp/supervisor_comet.sock ; path to your socket file 3 | 4 | ;[inet_http_server] 5 | ;port = 127.0.0.1:9001 6 | 7 | [supervisorctl] 8 | serverurl=unix:///tmp/supervisor_comet.sock ; use a unix:// URL for a unix socket 9 | ;[supervisorctl] 10 | ;serverurl = http://localhost:9001 11 | 12 | 13 | [supervisord] 14 | logfile = ${buildout:directory}/log/supervisor/supervisord.log 15 | logfile_maxbytes = 50MB 16 | logfile_backups = 10 17 | loglevel = info 18 | pidfile = ${buildout:directory}/tmp/supervisord.pid 19 | nodaemon = false 20 | 21 | 22 | [rpcinterface:supervisor] 23 | supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface 24 | 25 | #[include] 26 | #files = *.supervisor 27 | 28 | [program:web] 29 | command=${buildout:directory}/bin/python ${buildout:directory}/src/app.py 30 | stderr_logfile = ${buildout:directory}/log/supervisor/web-stderr.log 31 | stdout_logfile = ${buildout:directory}/log/supervisor/web-stdout.log 32 | 33 | [program:gc] 34 | command=${buildout:directory}/bin/python ${buildout:directory}/src/gc.py 35 | stderr_logfile = ${buildout:directory}/log/supervisor/gc-stderr.log 36 | stdout_logfile = ${buildout:directory}/log/supervisor/gc-stdout.log 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | a live chat room built with python(flask / gevent / apscheduler) + redis 2 | 3 | Basic Architecture 4 | ================== 5 | 6 | ![architecture](http://blog.leezhong.com/image/comet_arch.png) 7 | 8 | Screenshot 9 | ========== 10 | 11 | ![chat](http://blog.leezhong.com/image/comet_chat.png) 12 | 13 | Install 14 | ======= 15 | 16 | - cd /path/to/source 17 | - python bootstrap.py 18 | - bin/buildout 19 | - make sure redis-server is started 20 | - bin/supervisord 21 | - [optional] bin/supervisorctl 22 | - goto localhost:9527 23 | 24 | Tips 25 | ==== 26 | 27 | - open multi browser to test live communication 28 | - execute `bin/python scripts/clear_key.py` to clear all data 29 | 30 | Changes 31 | ======= 32 | 33 | 0.2 34 | --- 35 | 36 | * adjust comet strategy 37 | * add admin role 38 | * fix duplicate name 39 | 40 | 0.1.1 41 | ----- 42 | 43 | * adjust create room UI / UE 44 | * add rm room func 45 | * improve add chat message's response speed 46 | * bugfixes 47 | 48 | 0.1 49 | --- 50 | 51 | * add home page (all rooms in one page, and live content) 52 | * custom nickname 53 | * create room 54 | * coffee-script 55 | * bugfixes 56 | 57 | ![home](http://blog.leezhong.com/image/comet_home_0.1.png) 58 | ![room](http://blog.leezhong.com/image/comet_room_0.1.gif) 59 | -------------------------------------------------------------------------------- /src/templates/room.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %} 4 | {{room_name}} - {{config['CHAT_NAME']}} 5 | {% endblock %} 6 | 7 | {% block main %} 8 | 9 |
10 |

在线用户

11 |
12 | {% for user in room_online_users %} 13 | {{user}} 14 | {% endfor %} 15 |
16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for item in room_content %} 30 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 | 37 |
usercontenttime
{{item.user|safe}}{{item.content|safe}}{{item.created}}
38 |
39 |
40 | 41 | 42 | 43 |
44 | {% endblock %} 45 | 46 | {% block script %} 47 | 48 | 49 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /src/templates/chat.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %} 4 | {{config['CHAT_NAME']}} 5 | {% endblock %} 6 | 7 | {% block main %} 8 | {% for room in rooms %} 9 |
10 |
11 | {% if is_admin %} 12 |

x

13 | {% endif %} 14 | {{room.title}}({{room.users|count}}人在线) 15 |
16 |
17 | 22 | 23 |
24 |
25 | {% endfor %} 26 | 27 |
28 | 35 | 36 |
+
37 |
38 | {% endblock %} 39 | 40 | {% block script %} 41 | 42 | 43 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /src/static/script/main.coffee: -------------------------------------------------------------------------------- 1 | channel = Math.floor(Math.random()*1000000) 2 | 3 | create_chat_comet = (ts = '') -> 4 | arg = "comet=room_online_users_count_all,room_content_all&channel=#{channel}&ts=#{ts}" 5 | $.getJSON "/comet?uri=#{window.uri}&#{arg}", (result) -> 6 | if result.type == 'room_online_users' 7 | room_online_users_count_all result.data 8 | else if result.type == 'add_content' 9 | room_content_all result.data 10 | create_chat_comet(result.ts) 11 | 12 | create_room_comet = (ts = '') -> 13 | arg = "comet=room_online_users,room_content&channel=#{channel}&room_id=#{window.room_id}&ts=#{ts}" 14 | $.getJSON "/comet?uri=#{window.uri}&#{arg}", (result) -> 15 | if result.type == 'online_users' 16 | online_users result.data 17 | else if result.type == 'room_online_users' 18 | room_online_users result.data 19 | else if result.type == 'add_content' 20 | room_content result.data 21 | create_room_comet(result.ts) 22 | 23 | room_online_users_count_all = (data) -> 24 | $("#room-#{data.room_id} .header span").text("(#{data.users.length}人在线)") 25 | 26 | room_content_all = (data) -> 27 | $body = $("#room-#{data.room_id} .body") 28 | for content in data.content 29 | $body.find('ul').append("
  • #{content.content}
  • ") 30 | while $body.find('ul li').length > 5 31 | $body.find('ul li:first-child').remove() 32 | 33 | room_online_users = (data) -> 34 | html = '' 35 | for item in data.users 36 | html += "#{item}" 37 | $('#room_online_user .user_list').html(html) 38 | 39 | room_content = (data) -> 40 | console.log data 41 | for content in data.content 42 | $msg = $("#msg-#{content.id}") 43 | if not $msg.length 44 | html = " 45 | #{content.user} 46 | #{content.content} 47 | #{content.created} 48 | 49 | " 50 | $('#chat_content table tbody').append(html) 51 | $("#chat_content table tbody tr:last-child").get(0).scrollIntoView() 52 | if not window.entering_content 53 | if document.title.substr(0, 1) != '(' 54 | document.title = "(1) #{document.title}" 55 | else 56 | current_title = document.title 57 | current_count = parseInt(current_title.slice(current_title.indexOf('(')+1, current_title.indexOf(')'))) 58 | new_count = current_count + 1 59 | document.title = current_title.replace("(#{current_count})", "(#{new_count})") 60 | 61 | $ -> 62 | if $('#chat_content tbody tr').length 63 | $('#chat_content tbody tr:last-child').get(0).scrollIntoView() 64 | 65 | $('#post_content').bind 'submit', (evt) -> 66 | evt.preventDefault() 67 | data = $(this).serialize() 68 | if $.trim($(this).find('input[name="content"]').val()) == '' 69 | return false 70 | 71 | $.post( 72 | $(this).attr('action') 73 | data 74 | (result) -> 75 | $('#post_content input[name="content"]').val('') 76 | window.entering_content = true 77 | document.title = document.title.replace(/\([0-9]+\) /, '') 78 | room_content {'content': [result]} 79 | 'json' 80 | ) 81 | 82 | $('#post_content input[name="content"]').bind 'click', (evt) -> 83 | window.entering_content = true 84 | document.title = document.title.replace(/\([0-9]+\) /, '') 85 | 86 | $('#post_content input[name="content"]').bind 'blur', (evt) -> 87 | window.entering_content = false 88 | 89 | $('.add_room').bind 'click', (evt) -> 90 | $('.chat-bubble').toggle() 91 | 92 | $('.header .close').bind 'click', (evt) -> 93 | rs = confirm('do you really want to remove this room?') 94 | if rs 95 | room_info = $(this).parent().parent().attr('id').split('-') 96 | room_id = room_info[room_info.length-1] 97 | $.post( 98 | '/rm_room' 99 | {room_id: room_id} 100 | (result) -> 101 | if result.status == 'ok' 102 | window.location = result.content.url 103 | else 104 | alert result.content.message 105 | 'json' 106 | ) 107 | -------------------------------------------------------------------------------- /src/static/script/main.js: -------------------------------------------------------------------------------- 1 | var channel, create_chat_comet, create_room_comet, room_content, room_content_all, room_online_users, room_online_users_count_all; 2 | channel = Math.floor(Math.random() * 1000000); 3 | create_chat_comet = function(ts) { 4 | var arg; 5 | if (ts == null) { 6 | ts = ''; 7 | } 8 | arg = "comet=room_online_users_count_all,room_content_all&channel=" + channel + "&ts=" + ts; 9 | return $.getJSON("/comet?uri=" + window.uri + "&" + arg, function(result) { 10 | if (result.type === 'room_online_users') { 11 | room_online_users_count_all(result.data); 12 | } else if (result.type === 'add_content') { 13 | room_content_all(result.data); 14 | } 15 | return create_chat_comet(result.ts); 16 | }); 17 | }; 18 | create_room_comet = function(ts) { 19 | var arg; 20 | if (ts == null) { 21 | ts = ''; 22 | } 23 | arg = "comet=room_online_users,room_content&channel=" + channel + "&room_id=" + window.room_id + "&ts=" + ts; 24 | return $.getJSON("/comet?uri=" + window.uri + "&" + arg, function(result) { 25 | if (result.type === 'online_users') { 26 | online_users(result.data); 27 | } else if (result.type === 'room_online_users') { 28 | room_online_users(result.data); 29 | } else if (result.type === 'add_content') { 30 | room_content(result.data); 31 | } 32 | return create_room_comet(result.ts); 33 | }); 34 | }; 35 | room_online_users_count_all = function(data) { 36 | return $("#room-" + data.room_id + " .header span").text("(" + data.users.length + "人在线)"); 37 | }; 38 | room_content_all = function(data) { 39 | var $body, content, _i, _len, _ref, _results; 40 | $body = $("#room-" + data.room_id + " .body"); 41 | _ref = data.content; 42 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 43 | content = _ref[_i]; 44 | $body.find('ul').append("
  • " + content.content + "
  • "); 45 | } 46 | _results = []; 47 | while ($body.find('ul li').length > 5) { 48 | _results.push($body.find('ul li:first-child').remove()); 49 | } 50 | return _results; 51 | }; 52 | room_online_users = function(data) { 53 | var html, item, _i, _len, _ref; 54 | html = ''; 55 | _ref = data.users; 56 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 57 | item = _ref[_i]; 58 | html += "" + item + ""; 59 | } 60 | return $('#room_online_user .user_list').html(html); 61 | }; 62 | room_content = function(data) { 63 | var $msg, content, current_count, current_title, html, new_count, _i, _len, _ref, _results; 64 | console.log(data); 65 | _ref = data.content; 66 | _results = []; 67 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 68 | content = _ref[_i]; 69 | $msg = $("#msg-" + content.id); 70 | _results.push(!$msg.length ? (html = " " + content.user + " " + content.content + " " + content.created + " ", $('#chat_content table tbody').append(html), $("#chat_content table tbody tr:last-child").get(0).scrollIntoView(), !window.entering_content ? document.title.substr(0, 1) !== '(' ? document.title = "(1) " + document.title : (current_title = document.title, current_count = parseInt(current_title.slice(current_title.indexOf('(') + 1, current_title.indexOf(')'))), new_count = current_count + 1, document.title = current_title.replace("(" + current_count + ")", "(" + new_count + ")")) : void 0) : void 0); 71 | } 72 | return _results; 73 | }; 74 | $(function() { 75 | if ($('#chat_content tbody tr').length) { 76 | $('#chat_content tbody tr:last-child').get(0).scrollIntoView(); 77 | } 78 | $('#post_content').bind('submit', function(evt) { 79 | var data; 80 | evt.preventDefault(); 81 | data = $(this).serialize(); 82 | if ($.trim($(this).find('input[name="content"]').val()) === '') { 83 | return false; 84 | } 85 | return $.post($(this).attr('action'), data, function(result) { 86 | $('#post_content input[name="content"]').val(''); 87 | window.entering_content = true; 88 | document.title = document.title.replace(/\([0-9]+\) /, ''); 89 | return room_content({ 90 | 'content': [result] 91 | }); 92 | }, 'json'); 93 | }); 94 | $('#post_content input[name="content"]').bind('click', function(evt) { 95 | window.entering_content = true; 96 | return document.title = document.title.replace(/\([0-9]+\) /, ''); 97 | }); 98 | $('#post_content input[name="content"]').bind('blur', function(evt) { 99 | return window.entering_content = false; 100 | }); 101 | $('.add_room').bind('click', function(evt) { 102 | return $('.chat-bubble').toggle(); 103 | }); 104 | return $('.header .close').bind('click', function(evt) { 105 | var room_id, room_info, rs; 106 | rs = confirm('do you really want to remove this room?'); 107 | if (rs) { 108 | room_info = $(this).parent().parent().attr('id').split('-'); 109 | room_id = room_info[room_info.length - 1]; 110 | return $.post('/rm_room', { 111 | room_id: room_id 112 | }, function(result) { 113 | if (result.status === 'ok') { 114 | return window.location = result.content.url; 115 | } else { 116 | return alert(result.content.message); 117 | } 118 | }, 'json'); 119 | } 120 | }); 121 | }); -------------------------------------------------------------------------------- /src/utils/text.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import re 4 | 5 | def escape_text(txt): 6 | return txt.replace('<', '<').replace('>', '>') 7 | 8 | def make_re_url(): 9 | url = r""" 10 | (?xi) 11 | \b 12 | ( # Capture 1: entire matched URL 13 | (?: 14 | https?:// # http or https protocol 15 | | # or 16 | www\d{0,3}[.] # "www.", "www1.", "www2." … "www999." 17 | | # or 18 | [a-z0-9.\-]+[.][a-z]{2,4}/ # looks like domain name followed by a slash 19 | ) 20 | (?: # One or more: 21 | [^\s()<>]+ # Run of non-space, non-()<> 22 | | # or 23 | \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels 24 | )+ 25 | (?: # End with: 26 | \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels 27 | | # or 28 | [^\s`!()\[\]{};:'".,<>?«»“”‘’] # not a space or one of these punct chars 29 | ) 30 | ) 31 | """ 32 | return re.compile(url, re.VERBOSE | re.MULTILINE) 33 | 34 | RE_URL = make_re_url() 35 | 36 | def linkify(txt, 37 | shorten = True, 38 | target_blank = False, 39 | require_protocol = False, 40 | permitted_protocols = ["http", "https"], 41 | local_domain = None): 42 | """Converts plain txt into HTML with links. back ported from tornado 2.0 43 | 44 | For example: ``linkify("Hello http://tornadoweb.org!")`` would return 45 | ``Hello http://tornadoweb.org!`` 46 | 47 | Parameters: 48 | 49 | shorten: Long urls will be shortened for display. 50 | 51 | extra_params: Extra txt to include in the link tag, 52 | e.g. linkify(txt, extra_params='rel="nofollow" class="external"') 53 | 54 | require_protocol: Only linkify urls which include a protocol. If this is 55 | False, urls such as www.facebook.com will also be linkified. 56 | 57 | permitted_protocols: List (or set) of protocols which should be linkified, 58 | e.g. linkify(txt, permitted_protocols=["http", "ftp", "mailto"]). 59 | It is very unsafe to include protocols such as "javascript". 60 | local_domain: domain link 61 | """ 62 | 63 | if txt is None or not txt.strip(): 64 | return txt 65 | extra_params = ' rel="nofollow"' 66 | 67 | def make_link(m): 68 | tb = target_blank 69 | url = m.group(1) 70 | proto = m.group(2) 71 | if require_protocol and not proto: 72 | return url # not protocol, no linkify 73 | 74 | if proto and proto not in permitted_protocols: 75 | return url # bad protocol, no linkify 76 | 77 | href = m.group(1) 78 | #href = xhtml_unescape(href).strip() 79 | if not proto: 80 | href = "http://" + href # no proto specified, use http 81 | 82 | params = extra_params 83 | if proto: 84 | proto_len = len(proto) + 1 + len(m.group(3) or "") # +1 for : 85 | else: 86 | proto_len = 0 87 | 88 | parts = url[proto_len:].split("/") 89 | 90 | proto_part = url[:proto_len] if proto != 'http' else '' 91 | host_part = parts[0] 92 | 93 | if host_part.startswith('www.'): 94 | host_part = '.'.join(host_part.split('.')[1:]) # add extra idnetification for external link 95 | if not local_domain or not host_part.endswith(local_domain): 96 | params += ' class="external" ' 97 | tb = True 98 | 99 | if tb: 100 | params += 'target="_blank"' 101 | 102 | # clip long urls. max_len is just an approximation 103 | max_len = 30 104 | if shorten and len(url) > max_len: 105 | before_clip = url[proto_len:] 106 | url = proto_part + host_part 107 | #if len(parts) > 2: 108 | # Grab the whole host part plus the first bit of the path 109 | # The path is usually not that interesting once shortened 110 | # (no more slug, etc), so it really just provides a little 111 | # extra indication of shortening. 112 | for n,p in enumerate(parts[1:]): 113 | if n: 114 | cut = 6 115 | else: 116 | cut = 8 117 | url += '/' + p[:cut].split('?')[0].split('.')[0] 118 | if len(p) < 4: 119 | continue 120 | break 121 | #url = proto_part + host_part + "/" + \ 122 | #parts[1][:8].split('?')[0].split('.')[0] 123 | 124 | if len(url) > max_len * 1.5: # still too long 125 | url = url[:max_len] 126 | 127 | if url != before_clip: 128 | amp = url.rfind('&') 129 | # avoid splitting html char entities 130 | if amp > max_len - 5: 131 | url = url[:amp] 132 | url += "..." 133 | 134 | if len(url) >= len(before_clip): 135 | url = before_clip 136 | else: 137 | # full url is visible on mouse-over (for those who don't 138 | # have a status bar, such as Safari by default) 139 | params += ' title="%s"' % href 140 | 141 | return u'%s' % (href, params, url) 142 | 143 | return RE_URL.sub(make_link, txt) 144 | 145 | if __name__ == '__main__': 146 | txt = 'have a link test www.zhihu.com/question/19550224?noti_id=123 how about?' 147 | txt1 = 'hello http://tornadoweb.org!' 148 | print linkify(txt) 149 | -------------------------------------------------------------------------------- /src/static/css/main.css: -------------------------------------------------------------------------------- 1 | p {padding:0; margin:0} 2 | body, html {margin:0; padding:0; color:#222} 3 | body {font:13px/22px 'Helvetica Neue',Helvetica,Arial,Sans-serif} 4 | ul, li {padding:0; margin:0; list-style:none} 5 | 6 | .error {color: red} 7 | 8 | li.new {background: #ffe} 9 | 10 | #header { 11 | border-bottom:1px solid #ccc; 12 | height: 50px; 13 | } 14 | 15 | #header .inner { 16 | width: 960px; 17 | margin: 0 auto; 18 | } 19 | 20 | #header .inner #logo { 21 | float:left; 22 | font-size: 18px; 23 | font-weight: bold; 24 | line-height: 50px; 25 | } 26 | 27 | #header .inner #logo a { 28 | text-decoration: none; 29 | color: #666; 30 | } 31 | 32 | #header .user_info { 33 | float:right; 34 | line-height: 50px; 35 | color: #666; 36 | } 37 | 38 | #main { 39 | width: 960px; 40 | margin: 20px auto; 41 | overflow: hidden; 42 | } 43 | 44 | .add_room { 45 | position: absolute; 46 | left: 41px; 47 | top: 3px; 48 | border: 1px solid #eee; 49 | background: #ffc; 50 | padding: 10px 15px; 51 | -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); 52 | -o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); 53 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); 54 | -webkit-border-radius: 3px; 55 | -moz-border-radius: 3px; 56 | -o-border-radius: 3px; 57 | color: #666; 58 | font-size: 18px; 59 | float: left; 60 | margin: 80px 0px 80px 50px; 61 | cursor: pointer; 62 | } 63 | 64 | .box { 65 | -webkit-border-radius: 3px; 66 | -moz-border-radius: 3px; 67 | -o-border-radius: 3px; 68 | border-radius: 3px; 69 | -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); 70 | -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); 71 | -o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); 72 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15); 73 | background-color: white; 74 | border: 1px solid #eee; 75 | margin: 0 55px 55px 0; 76 | } 77 | 78 | .box .header { 79 | background: #f8f8f8; 80 | padding: 5px; 81 | font-size: 14px; 82 | border-bottom: 1px solid #ccc; 83 | } 84 | 85 | .box .header a { 86 | color: #444; 87 | text-decoration: none; 88 | } 89 | 90 | .box .header a:hover { 91 | text-decoration: underline; 92 | } 93 | 94 | .box .header span { 95 | font-size: 12px; 96 | color: #999; 97 | padding-left: 3px; 98 | } 99 | 100 | .box .body { 101 | height: 160px; 102 | position: relative; 103 | } 104 | 105 | .box .body .chat_line { 106 | } 107 | 108 | .box .body .chat_line li { 109 | padding: 5px; 110 | border-bottom: 1px solid #eee; 111 | color: #666; 112 | height: 21px; 113 | overflow: hidden; 114 | } 115 | 116 | .box .body .chat_line li:last-child { 117 | border:none; 118 | } 119 | 120 | .box .body .single_input { 121 | width: 235px; 122 | margin: 7px 5px; 123 | position: absolute; 124 | bottom: 0; 125 | } 126 | 127 | #post_content { 128 | } 129 | 130 | .user_status {border:1px solid #ccc; padding:0px; overflow:hidden; margin:5px 0 10px 0} 131 | .user_status p {float: left;padding:5px; border-right:1px solid #ccc} 132 | .user_list { 133 | float:left; 134 | color: #666; 135 | } 136 | 137 | .vh_center { 138 | height: 100%; 139 | width: 100%; 140 | overflow: visible; 141 | position:relative; 142 | } 143 | 144 | .vh_center .inner { 145 | position: absolute; 146 | top: 50%; 147 | margin-top: -30px; 148 | left: 50%; 149 | margin-left: -100px; 150 | } 151 | 152 | .user_list span { 153 | color: #105CB6; 154 | background: #E1F0F7; 155 | padding: 0 10px; 156 | -webkit-border-radius: 10px; 157 | -moz-border-radius: 10px; 158 | -o-border-radius: 10px; 159 | border-radius: 10px; 160 | text-decoration: none; 161 | margin: 5px 1px 5px 5px; 162 | display: inline-block; 163 | } 164 | 165 | #chat_content table thead tr { 166 | display: block; 167 | position: relative; 168 | } 169 | 170 | #chat_content table thead th, #chat_content table tbody td { 171 | width: 155px; 172 | } 173 | 174 | #chat_content table thead th + th, #chat_content table tbody td + td { 175 | width: 600px; 176 | } 177 | 178 | #chat_content table thead th + th + th, #chat_content table tbody td + td + td { 179 | width: 150px; 180 | } 181 | 182 | #chat_content table tbody{ 183 | max-height: 400px; 184 | overflow: auto; 185 | width: 100%; 186 | display: block 187 | } 188 | 189 | #chat_content table tbody td + td + td { 190 | color: #999; 191 | } 192 | 193 | table {border-collapse: separate; margin-bottom:15px} 194 | table th {text-align:left; background: #f4f4f4; padding:5px} 195 | table td {padding:5px; border-bottom:1px solid #f4f4f4} 196 | 197 | input[type="text"] {padding:5px;font-size:14px} 198 | input[type="submit"] { 199 | background: #F8F8F9; 200 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f8f8f9',endColorstr='#e6e6e8'); 201 | background: -webkit-gradient(linear,0 0,0 100%,from(#F8F8F9),to(#E6E6E8)); 202 | background: -moz-linear-gradient(top,#F8F8F9,#E6E6E8); 203 | -webkit-box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1); 204 | -moz-box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1); 205 | -o-box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1); 206 | box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1); 207 | text-shadow: 0 1px 0 white; 208 | border: 1px solid #BBB; 209 | color: #666!important; 210 | font: normal 14px/22px "Helvetica Neue",Arial,sans-serif; 211 | text-decoration: none!important; 212 | vertical-align: middle; 213 | display: inline-block; 214 | text-align: center; 215 | padding: 4px 10px; 216 | cursor: pointer; 217 | -webkit-border-radius: 3px; 218 | -moz-border-radius: 3px; 219 | -o-border-radius: 3px; 220 | margin:-4px 0 0 7px; 221 | } 222 | 223 | .chat-bubble { 224 | background-color:#f8f8f8; 225 | border:1px solid #eee; 226 | line-height:1.3em; 227 | margin:10px auto; 228 | padding:10px; 229 | position:relative; 230 | text-align:center; 231 | width:200px; 232 | -moz-border-radius:10px; 233 | -webkit-border-radius:10px; 234 | } 235 | .chat-bubble-arrow-border { 236 | border-color: #eee transparent transparent transparent; 237 | border-style: solid; 238 | border-width: 10px; 239 | height:0; 240 | width:0; 241 | position:absolute; 242 | bottom:-21px; 243 | left:100px; 244 | } 245 | .chat-bubble-arrow { 246 | border-color: #f8f8f8 transparent transparent transparent; 247 | border-style: solid; 248 | border-width: 10px; 249 | height:0; 250 | width:0; 251 | position:absolute; 252 | bottom:-19px; 253 | left:100px; 254 | } 255 | 256 | #add_room_wrapper { 257 | position:relative; 258 | float:left; 259 | height:200px; 260 | } 261 | 262 | .header .close { 263 | float: right; 264 | color: #ddd; 265 | font-size: 14px; 266 | cursor: pointer; 267 | } 268 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | from flask import Flask, request, session, render_template, Response, jsonify, redirect, flash 4 | from gevent.wsgi import WSGIServer 5 | from utils.text import linkify, escape_text 6 | import gevent 7 | import redis 8 | import time 9 | import config 10 | import json 11 | 12 | app = Flask(__name__) 13 | app.config.from_object(config) 14 | app.debug = True 15 | 16 | rc = redis.Redis() 17 | 18 | def is_admin(): 19 | if session.get('admin'): 20 | return True 21 | return False 22 | 23 | def is_duplicate_name(): 24 | user_name = session.get('user', '') 25 | for online_user in rc.zrange(config.ONLINE_USER_CHANNEL, 0, -1): 26 | if online_user == user_name.encode('utf-8'): 27 | flash(u'该名(%s)已被抢占,换一个吧'%user_name, 'error') 28 | session.pop('user', None) 29 | return True 30 | return False 31 | 32 | @app.route('/adm1n') 33 | def admin(): 34 | session['admin'] = 1 35 | return redirect('/chat') 36 | 37 | @app.route('/') 38 | def index(): 39 | if session.get('user'): 40 | return redirect('/chat') 41 | return render_template('index.html') 42 | 43 | @app.route('/change_name') 44 | def change_name(): 45 | session.pop('user', None) 46 | return redirect('/') 47 | 48 | @app.route('/login', methods=['POST']) 49 | def login(): 50 | user_name = request.form.get('user_name', '') 51 | session['user'] = user_name 52 | if is_duplicate_name(): 53 | return redirect('/') 54 | return redirect('/chat') 55 | 56 | @app.route('/chat', methods=['GET', 'POST']) 57 | def chat(): 58 | if not session.get('user'): 59 | return redirect('/') 60 | 61 | if request.method == 'POST': 62 | title = request.form.get('title', '') 63 | if not title: 64 | return jsonify(status='error', message={'title': 'empty title'}) 65 | 66 | room_id = rc.incr(config.ROOM_INCR_KEY) 67 | rc.set(config.ROOM_INFO_KEY.format(room=room_id), 68 | json.dumps({'title': title, 69 | 'room_id': room_id, 70 | 'user': session['user'], 71 | 'created': time.time() 72 | })) 73 | return redirect('/chat') 74 | 75 | rooms = [] 76 | room_info_keys = config.ROOM_INFO_KEY.format(room='*') 77 | for room_info_key in rc.keys(room_info_keys): 78 | room_info = json.loads(rc.get(room_info_key)) 79 | users = rc.zrevrange(config.ROOM_ONLINE_USER_CHANNEL.format(room=room_info['room_id']), 0, -1) 80 | rm_channel_placeholder(users) 81 | rooms.append({ 82 | 'id': room_info['room_id'], 83 | 'creator': room_info['user'], 84 | 'content': map(json.loads, reversed(rc.zrevrange(config.ROOM_CHANNEL.format(room=room_info['room_id']), 0, 4))), 85 | 'title': room_info['title'], 86 | 'users': users, 87 | }) 88 | 89 | return render_template('chat.html', 90 | rooms = rooms, 91 | uri = request.path, 92 | is_admin = is_admin(), 93 | ) 94 | 95 | @app.route('/rm_room', methods=['POST']) 96 | def rm_room(): 97 | if not session.get('user'): 98 | return redirect('/') 99 | 100 | room_id = request.form.get('room_id') 101 | room_key = config.ROOM_INFO_KEY.format(room=room_id) 102 | room_channel = config.ROOM_CHANNEL.format(room=room_id) 103 | room = json.loads(rc.get(room_key)) 104 | if not is_admin(): 105 | return jsonify(status='error', content={'message': 'permission denied'}) 106 | 107 | rc.delete(room_key) 108 | rc.delete(room_channel) 109 | return jsonify(status='ok', content={'url': '/chat'}) 110 | 111 | @app.route('/chat/') 112 | def chat_room(room_id): 113 | if not session.get('user'): 114 | return redirect('/') 115 | 116 | user_name = session['user'] 117 | room = json.loads(rc.get(config.ROOM_INFO_KEY.format(room=room_id))) 118 | room_online_user_channel = config.ROOM_ONLINE_USER_CHANNEL.format(room=room_id) 119 | room_online_user_signal = config.ROOM_ONLINE_USER_SIGNAL.format(room=room_id) 120 | 121 | rc.zadd(config.ONLINE_USER_CHANNEL, user_name, time.time()) 122 | rc.zadd(room_online_user_channel, user_name, time.time()) 123 | rc.publish(config.ONLINE_USER_SIGNAL, '') 124 | rc.publish(room_online_user_signal, json.dumps({'room_id':room_id})) 125 | 126 | room_content = reversed(rc.zrevrange(config.ROOM_CHANNEL.format(room=room_id), 0, 200, withscores=True)) 127 | room_content_list = [] 128 | for item in room_content: 129 | room_content_list.append(json.loads(item[0])) 130 | 131 | room_online_users =[] 132 | for user in rc.zrange(room_online_user_channel, 0, -1): 133 | if user == config.CHANNEL_PLACEHOLDER: 134 | continue 135 | room_online_users.append(user.decode('utf-8')) 136 | 137 | return render_template('room.html', 138 | room_content = room_content_list, 139 | uri = request.path, 140 | room_name = room['title'], 141 | room_id = room_id, 142 | room_online_users = room_online_users) 143 | 144 | @app.route('/post_content', methods=['POST']) 145 | def post_content(): 146 | if not session.get('user'): 147 | return redirect('/') 148 | 149 | room_id = request.form.get('room_id') 150 | data = {'user': session.get('user'), 151 | 'content': linkify(escape_text(request.form.get('content', ''))), 152 | 'created': time.strftime('%m-%d %H:%M:%S'), 153 | 'room_id': room_id, 154 | 'id': rc.incr(config.ROOM_CONTENT_INCR_KEY), 155 | } 156 | rc.zadd(config.ROOM_CHANNEL.format(room=room_id), json.dumps(data), time.time()) 157 | return jsonify(**data) 158 | 159 | @app.route('/comet') 160 | def comet(): 161 | uri = request.args.get('uri', '') 162 | room_id = request.args.get('room_id', '') 163 | comet = request.args.get('comet', '').split(',') 164 | ts = request.args.get('ts', time.time()) 165 | channel = config.CONN_CHANNEL_SET.format(channel=request.args.get('channel')) 166 | 167 | cmt = Comet() 168 | 169 | result = cmt.check(channel, comet, ts, room_id) 170 | if result: 171 | return jsonify(**result) 172 | 173 | passed_time = 0 174 | while passed_time < config.COMET_TIMEOUT: 175 | comet = rc.smembers(config.CONN_CHANNEL_SET.format(channel=channel)) 176 | result = cmt.check(channel, comet, ts, room_id) 177 | if result: 178 | return jsonify(**result) 179 | passed_time += config.COMET_POLL_TIME 180 | gevent.sleep(config.COMET_POLL_TIME) 181 | 182 | if room_id: 183 | room_online_user_channel = config.ROOM_ONLINE_USER_CHANNEL.format(room=room_id) 184 | rc.zadd(room_online_user_channel, session['user'], time.time()) 185 | rc.zadd(config.ONLINE_USER_CHANNEL, session['user'], time.time()) 186 | 187 | return jsonify(ts=time.time()) 188 | 189 | def rm_channel_placeholder(data): 190 | for index, item in enumerate(data): 191 | if item == config.CHANNEL_PLACEHOLDER: 192 | data.pop(index) 193 | 194 | class Comet(object): 195 | def check(self, channel, comet, ts, room_id = 0): 196 | conn_channel_set = config.CONN_CHANNEL_SET.format(channel=channel) 197 | if 'online_users' in comet: 198 | rc.sadd(conn_channel_set, 'online_users') 199 | new_data = rc.zrangebyscore(config.ONLINE_USER_CHANNEL, ts, '+inf') 200 | if new_data: 201 | data=rc.zrevrange(config.ONLINE_USER_CHANNEL, 0, -1) 202 | data.pop(0) if data[0] == config.CHANNEL_PLACEHOLDER else True 203 | return dict(data=data, 204 | ts=time.time(), type='online_users') 205 | 206 | if 'room_online_users' in comet: 207 | rc.sadd(conn_channel_set, 'room_online_users') 208 | room_online_user_channel = config.ROOM_ONLINE_USER_CHANNEL.format(room=room_id) 209 | new_data = rc.zrangebyscore(room_online_user_channel, ts, '+inf') 210 | if new_data: 211 | users=rc.zrevrange(room_online_user_channel, 0, -1) 212 | rm_channel_placeholder(users) 213 | data = {'room_id': room_id, 'users': users} 214 | return dict(data=data, 215 | ts=time.time(), type='room_online_users') 216 | 217 | if 'room_content' in comet: 218 | rc.sadd(conn_channel_set, 'room_content') 219 | room_channel = config.ROOM_CHANNEL.format(room=room_id) 220 | new_data = rc.zrangebyscore(room_channel, ts, '+inf') 221 | if new_data: 222 | data = {'room_id': room_id, 'content':[]} 223 | for item in new_data: 224 | data['content'].append(json.loads(item)) 225 | return dict(data=data, ts=time.time(), type='add_content') 226 | 227 | if 'room_online_users_count_all' in comet: 228 | rc.sadd(conn_channel_set, 'room_online_users_count_all') 229 | room_online_user_channels = config.ROOM_ONLINE_USER_CHANNEL.format(room='*') 230 | for room_online_user_channel in rc.keys(room_online_user_channels): 231 | new_data = rc.zrangebyscore(room_online_user_channel, ts, '+inf') 232 | if new_data: 233 | users=rc.zrevrange(room_online_user_channel, 0, -1) 234 | rm_channel_placeholder(users) 235 | room_id = room_online_user_channel.split('_')[-1] 236 | data = {'room_id': room_id, 'users': users} 237 | return dict(data=data, ts=time.time(), type='room_online_users') 238 | 239 | if 'room_content_all' in comet: 240 | rc.sadd(conn_channel_set, 'room_content_all') 241 | room_channels = config.ROOM_CHANNEL.format(room='*') 242 | for room_channel in rc.keys(room_channels): 243 | new_data = rc.zrangebyscore(room_channel, ts, '+inf') 244 | if new_data: 245 | room_id = room_channel.split('_')[-1] 246 | data = {'room_id': room_id, 'content':[]} 247 | for item in new_data: 248 | data['content'].append(json.loads(item)) 249 | return dict(data=data, ts=time.time(), type='add_content') 250 | 251 | def run(): 252 | http_server = WSGIServer(('', config.PORT), app) 253 | http_server.serve_forever() 254 | #app.run(port=config.PORT) 255 | 256 | if __name__ == '__main__': 257 | run() 258 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Bootstrap a buildout-based project 15 | 16 | Simply run this script in a directory containing a buildout.cfg. 17 | The script accepts buildout command-line options, so you can 18 | use the -c option to specify an alternate configuration file. 19 | """ 20 | 21 | import os, shutil, sys, tempfile, urllib, urllib2, subprocess 22 | from optparse import OptionParser 23 | 24 | if sys.platform == 'win32': 25 | def quote(c): 26 | if ' ' in c: 27 | return '"%s"' % c # work around spawn lamosity on windows 28 | else: 29 | return c 30 | else: 31 | quote = str 32 | 33 | # See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. 34 | stdout, stderr = subprocess.Popen( 35 | [sys.executable, '-Sc', 36 | 'try:\n' 37 | ' import ConfigParser\n' 38 | 'except ImportError:\n' 39 | ' print 1\n' 40 | 'else:\n' 41 | ' print 0\n'], 42 | stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 43 | has_broken_dash_S = bool(int(stdout.strip())) 44 | 45 | # In order to be more robust in the face of system Pythons, we want to 46 | # run without site-packages loaded. This is somewhat tricky, in 47 | # particular because Python 2.6's distutils imports site, so starting 48 | # with the -S flag is not sufficient. However, we'll start with that: 49 | if not has_broken_dash_S and 'site' in sys.modules: 50 | # We will restart with python -S. 51 | args = sys.argv[:] 52 | args[0:0] = [sys.executable, '-S'] 53 | args = map(quote, args) 54 | os.execv(sys.executable, args) 55 | # Now we are running with -S. We'll get the clean sys.path, import site 56 | # because distutils will do it later, and then reset the path and clean 57 | # out any namespace packages from site-packages that might have been 58 | # loaded by .pth files. 59 | clean_path = sys.path[:] 60 | import site # imported because of its side effects 61 | sys.path[:] = clean_path 62 | for k, v in sys.modules.items(): 63 | if k in ('setuptools', 'pkg_resources') or ( 64 | hasattr(v, '__path__') and 65 | len(v.__path__) == 1 and 66 | not os.path.exists(os.path.join(v.__path__[0], '__init__.py'))): 67 | # This is a namespace package. Remove it. 68 | sys.modules.pop(k) 69 | 70 | is_jython = sys.platform.startswith('java') 71 | 72 | setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' 73 | distribute_source = 'http://python-distribute.org/distribute_setup.py' 74 | 75 | 76 | # parsing arguments 77 | def normalize_to_url(option, opt_str, value, parser): 78 | if value: 79 | if '://' not in value: # It doesn't smell like a URL. 80 | value = 'file://%s' % ( 81 | urllib.pathname2url( 82 | os.path.abspath(os.path.expanduser(value))),) 83 | if opt_str == '--download-base' and not value.endswith('/'): 84 | # Download base needs a trailing slash to make the world happy. 85 | value += '/' 86 | else: 87 | value = None 88 | name = opt_str[2:].replace('-', '_') 89 | setattr(parser.values, name, value) 90 | 91 | usage = '''\ 92 | [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] 93 | 94 | Bootstraps a buildout-based project. 95 | 96 | Simply run this script in a directory containing a buildout.cfg, using the 97 | Python that you want bin/buildout to use. 98 | 99 | Note that by using --setup-source and --download-base to point to 100 | local resources, you can keep this script from going over the network. 101 | ''' 102 | 103 | parser = OptionParser(usage=usage) 104 | parser.add_option("-v", "--version", dest="version", 105 | help="use a specific zc.buildout version") 106 | parser.add_option("-d", "--distribute", 107 | action="store_true", dest="use_distribute", default=False, 108 | help="Use Distribute rather than Setuptools.") 109 | parser.add_option("--setup-source", action="callback", dest="setup_source", 110 | callback=normalize_to_url, nargs=1, type="string", 111 | help=("Specify a URL or file location for the setup file. " 112 | "If you use Setuptools, this will default to " + 113 | setuptools_source + "; if you use Distribute, this " 114 | "will default to " + distribute_source + ".")) 115 | parser.add_option("--download-base", action="callback", dest="download_base", 116 | callback=normalize_to_url, nargs=1, type="string", 117 | help=("Specify a URL or directory for downloading " 118 | "zc.buildout and either Setuptools or Distribute. " 119 | "Defaults to PyPI.")) 120 | parser.add_option("--eggs", 121 | help=("Specify a directory for storing eggs. Defaults to " 122 | "a temporary directory that is deleted when the " 123 | "bootstrap script completes.")) 124 | parser.add_option("-t", "--accept-buildout-test-releases", 125 | dest='accept_buildout_test_releases', 126 | action="store_true", default=False, 127 | help=("Normally, if you do not specify a --version, the " 128 | "bootstrap script and buildout gets the newest " 129 | "*final* versions of zc.buildout and its recipes and " 130 | "extensions for you. If you use this flag, " 131 | "bootstrap and buildout will get the newest releases " 132 | "even if they are alphas or betas.")) 133 | parser.add_option("-c", None, action="store", dest="config_file", 134 | help=("Specify the path to the buildout configuration " 135 | "file to be used.")) 136 | 137 | options, args = parser.parse_args() 138 | 139 | # if -c was provided, we push it back into args for buildout's main function 140 | if options.config_file is not None: 141 | args += ['-c', options.config_file] 142 | 143 | if options.eggs: 144 | eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) 145 | else: 146 | eggs_dir = tempfile.mkdtemp() 147 | 148 | if options.setup_source is None: 149 | if options.use_distribute: 150 | options.setup_source = distribute_source 151 | else: 152 | options.setup_source = setuptools_source 153 | 154 | if options.accept_buildout_test_releases: 155 | args.append('buildout:accept-buildout-test-releases=true') 156 | args.append('bootstrap') 157 | 158 | try: 159 | import pkg_resources 160 | import setuptools # A flag. Sometimes pkg_resources is installed alone. 161 | if not hasattr(pkg_resources, '_distribute'): 162 | raise ImportError 163 | except ImportError: 164 | ez_code = urllib2.urlopen( 165 | options.setup_source).read().replace('\r\n', '\n') 166 | ez = {} 167 | exec ez_code in ez 168 | setup_args = dict(to_dir=eggs_dir, download_delay=0) 169 | if options.download_base: 170 | setup_args['download_base'] = options.download_base 171 | if options.use_distribute: 172 | setup_args['no_fake'] = True 173 | ez['use_setuptools'](**setup_args) 174 | if 'pkg_resources' in sys.modules: 175 | reload(sys.modules['pkg_resources']) 176 | import pkg_resources 177 | # This does not (always?) update the default working set. We will 178 | # do it. 179 | for path in sys.path: 180 | if path not in pkg_resources.working_set.entries: 181 | pkg_resources.working_set.add_entry(path) 182 | 183 | cmd = [quote(sys.executable), 184 | '-c', 185 | quote('from setuptools.command.easy_install import main; main()'), 186 | '-mqNxd', 187 | quote(eggs_dir)] 188 | 189 | if not has_broken_dash_S: 190 | cmd.insert(1, '-S') 191 | 192 | find_links = options.download_base 193 | if not find_links: 194 | find_links = os.environ.get('bootstrap-testing-find-links') 195 | if find_links: 196 | cmd.extend(['-f', quote(find_links)]) 197 | 198 | if options.use_distribute: 199 | setup_requirement = 'distribute' 200 | else: 201 | setup_requirement = 'setuptools' 202 | ws = pkg_resources.working_set 203 | setup_requirement_path = ws.find( 204 | pkg_resources.Requirement.parse(setup_requirement)).location 205 | env = dict( 206 | os.environ, 207 | PYTHONPATH=setup_requirement_path) 208 | 209 | requirement = 'zc.buildout' 210 | version = options.version 211 | if version is None and not options.accept_buildout_test_releases: 212 | # Figure out the most recent final version of zc.buildout. 213 | import setuptools.package_index 214 | _final_parts = '*final-', '*final' 215 | 216 | def _final_version(parsed_version): 217 | for part in parsed_version: 218 | if (part[:1] == '*') and (part not in _final_parts): 219 | return False 220 | return True 221 | index = setuptools.package_index.PackageIndex( 222 | search_path=[setup_requirement_path]) 223 | if find_links: 224 | index.add_find_links((find_links,)) 225 | req = pkg_resources.Requirement.parse(requirement) 226 | if index.obtain(req) is not None: 227 | best = [] 228 | bestv = None 229 | for dist in index[req.project_name]: 230 | distv = dist.parsed_version 231 | if _final_version(distv): 232 | if bestv is None or distv > bestv: 233 | best = [dist] 234 | bestv = distv 235 | elif distv == bestv: 236 | best.append(dist) 237 | if best: 238 | best.sort() 239 | version = best[-1].version 240 | if version: 241 | requirement = '=='.join((requirement, version)) 242 | cmd.append(requirement) 243 | 244 | if is_jython: 245 | import subprocess 246 | exitcode = subprocess.Popen(cmd, env=env).wait() 247 | else: # Windows prefers this, apparently; otherwise we would prefer subprocess 248 | exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) 249 | if exitcode != 0: 250 | sys.stdout.flush() 251 | sys.stderr.flush() 252 | print ("An error occurred when trying to install zc.buildout. " 253 | "Look above this message for any errors that " 254 | "were output by easy_install.") 255 | sys.exit(exitcode) 256 | 257 | ws.add_entry(eggs_dir) 258 | ws.require(requirement) 259 | import zc.buildout.buildout 260 | zc.buildout.buildout.main(args) 261 | if not options.eggs: # clean up temporary egg directory 262 | shutil.rmtree(eggs_dir) 263 | --------------------------------------------------------------------------------