├── .gitignore ├── README.md ├── cronjob ├── bind_wordpress.py ├── crontab_list ├── first_sync_timeline.py ├── generate_pdf.py ├── rm_pdf.sh ├── send_refresh_token_mail.py ├── send_reminding.py └── sync_timeline.py ├── dep.txt ├── deploy ├── memcached.conf ├── mysql.conf ├── nginx-sites-available │ ├── default │ ├── linjuly │ ├── sample │ └── simplelife ├── nginx.conf └── nginx_site_default ├── gunicorn.conf ├── jobs.py ├── past ├── __init__.py ├── api │ ├── __init__.py │ ├── douban.py │ ├── error.py │ ├── instagram.py │ ├── oauth2.py │ ├── qqweibo.py │ ├── renren.py │ ├── sina.py │ ├── twitter.py │ └── wordpress.py ├── config.py ├── connect │ ├── __init__.py │ └── view.py ├── consts.py ├── corelib │ ├── __init__.py │ ├── cache.py │ ├── empty.py │ └── format.py ├── dev.sql ├── dev │ ├── __init__.py │ └── view │ │ ├── __init__.py │ │ ├── api.py │ │ └── token.py ├── model │ ├── __init__.py │ ├── data.py │ ├── kv.py │ ├── note.py │ ├── status.py │ ├── user.py │ ├── user_tokens.py │ └── weixin.py ├── schema.sql ├── static │ ├── 500.html │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap-responsive.css │ │ │ ├── bootstrap-responsive.min.css │ │ │ ├── bootstrap.css │ │ │ └── bootstrap.min.css │ │ ├── img │ │ │ ├── glyphicons-halflings-white.png │ │ │ └── glyphicons-halflings.png │ │ └── js │ │ │ ├── bootstrap.js │ │ │ └── bootstrap.min.js │ ├── calendar │ │ ├── bootstrap.calendar.js │ │ └── css │ │ │ └── bootstrap.calendar.css │ ├── css │ │ ├── all.css │ │ ├── magnify.cur │ │ ├── markdown.css │ │ ├── new.css │ │ └── pdf.css │ ├── favicon.ico │ ├── fbtimeline │ │ ├── images │ │ │ ├── .DS_Store │ │ │ ├── active-unit.png │ │ │ ├── icons-2.png │ │ │ ├── icons-3.png │ │ │ ├── icons-4.png │ │ │ ├── icons-5.png │ │ │ ├── icons.png │ │ │ └── line.png │ │ ├── javascripts │ │ │ └── all.js │ │ └── stylesheets │ │ │ ├── all.css │ │ │ ├── fb-buttons.css │ │ │ ├── normalize.css │ │ │ └── timeline.css │ ├── img │ │ ├── ActionIcons10.png │ │ ├── avatar.png │ │ ├── bg.gif │ │ ├── bg.png │ │ ├── bg_top_light.png │ │ ├── close.gif │ │ ├── comt.png │ │ ├── head.png │ │ ├── header_sprite.png │ │ ├── icon_error.png │ │ ├── icon_success.png │ │ ├── login.png │ │ ├── logo.png │ │ ├── nav_arrow.png │ │ ├── nav_st.png │ │ ├── note.png │ │ ├── sns2.png │ │ ├── subnav_bg.png │ │ ├── timeline.png │ │ ├── tips_bg.gif │ │ └── top-bar-white.png │ └── js │ │ ├── cssrefresh.js │ │ ├── date.format.js │ │ ├── jquery.js │ │ ├── jquery.magnifier.js │ │ ├── jquery.masonry.min.js │ │ └── scrollpagination.js ├── store.py ├── templates │ ├── bind_wordpress.html │ ├── blocks.html │ ├── home.html │ ├── layout.html │ ├── mail.html │ ├── note.html │ ├── note_create.html │ ├── past.html │ ├── pdf.html │ ├── post.html │ ├── settings.html │ ├── status.html │ ├── timeline.html │ ├── user_explore.html │ └── v2 │ │ ├── base.html │ │ ├── bind_wordpress.html │ │ ├── explore.html │ │ ├── explore_user.html │ │ ├── note.html │ │ ├── note_create.html │ │ ├── pdf.html │ │ ├── settings.html │ │ ├── timeline_base.html │ │ ├── user.html │ │ ├── user_more.html │ │ └── user_past.html ├── utils │ ├── __init__.py │ ├── escape.py │ ├── filters.py │ ├── logger.py │ ├── pdf.py │ └── sendmail.py ├── view │ ├── __init__.py │ ├── note.py │ ├── pdf_view.py │ ├── settings.py │ ├── user_past.py │ ├── utils.py │ └── views.py └── weixin │ ├── __init__.py │ └── view.py ├── pastme.py ├── run.sh └── tools ├── __init__.py ├── add_all_synctask_for_old_user.py ├── commpress_pdf.py ├── import_status_to_wordpress.py ├── manully_sync_user_timeline.py ├── merge_user.py ├── move_data_from_mongo_to_mysql.py ├── move_data_from_redis_to_mongo.py ├── recover_wrong.py ├── remove_pdf.py ├── remove_user.py ├── repair_sinaweibo_time.py └── update_user_privacy.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *.txt 4 | *.tar.gz 5 | *.xdb 6 | *.swo 7 | 8 | local_config.py 9 | env/ 10 | build/ 11 | third-src/ 12 | third/ 13 | var/ 14 | log 15 | 16 | nohup.out 17 | 18 | *.log 19 | backup/ 20 | cronjob/backup_data.sh 21 | cronjob/remove_cache.sh 22 | cronjob/schema_dump.sql 23 | cronjob/wp_linjuly_data_dump.sql 24 | cronjob/wp_simplelife_data_dump.sql 25 | past/schema_dump.sql 26 | past/static/font/ 27 | past/static/google8224590caf5c39a4.html 28 | past/utils/.pdf.py.swo 29 | tools/del_douban_miniblog.py 30 | tools/del_status_by_user.sh 31 | tools/get_user_email.py 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 你好 旧时光 | 之个人杂志计划 2 | ============= 3 | 4 | 当今的互联网,在高速发展,大家都在努力向前看! 5 | 6 | thepast,希望能帮助我们,时不时的停住脚步,去看看过往的一些事情… 7 | 8 | 9 | **目前已经完成的一些东西**: 10 | 11 | 1. 实时聚合个人在“豆瓣”、“人人”、“新浪微博”、“腾讯微博”、“Twitter”、“wordpress”、“instagram” 等平台的 Timeline(包括历史消息)。 12 | 1. 每天清晨,会邮件提醒你过往的今天都发生了些什么,或许是惊喜,亦或是怀念。 13 | 1. 聚合后的Timeline,生成PDF[预览]版本,供离线阅读或永久保存,打造你自己的个人杂志。 14 | 1. 同步更新微博到多个平台(有些偏离thepast的主旨,是提供给一小部分用户使用的)。 15 | 1. 提供日记功能,“今天写日记,明年再来看”。 16 | 17 | **计划中的一些事情**: 18 | 19 | 1. 导入更多的第三方(evernote、google+、facebook) 20 | 1. 提供多种形式的导出形式(不仅仅是PDF形式,可能会支持导出到google drive,dropbox等) 21 | 1. 个人挑选的历史信息,转为纸质杂志,作为留念,收藏。 22 | 1. 设置个性域名,提供个人名片服务(在个人数据挖掘的基础上提供全面的个人名片)。 23 | 1. Android、iOS移动客户端,提供更好的历史消息回顾体验。 24 | 25 | 26 | 还有一些更多可以做的东西: 27 | ------- 28 | * 对聚合后的消息,提供搜索功能(个人信息的社会化搜索) 29 | * 根据聚合后的timeline,生成更权威的“个人关键字tag云” 30 | * 更好的排版格式,提升在移动设备上(手机,pad)的离线阅读体验,或者发送到kindle上也未尝不可。 31 | * 试想想,这样出一本记录自己,讲述自己的纸质杂志应该还是很令人期待的。 32 | 33 | **目前的运行状况** 34 | 35 | - 注册用户2300+ 36 | - 用户数据500万条 37 | - 绑定的sns帐号,4000+ 38 | 39 | 40 | 技术细节: 41 | ------- 42 | 43 | * [linux(debian6)](http://debian.org) -- `stable and powerfull` 44 | * nginx/uwsgi -- `web server and serve static file` 45 | * mysql 46 | * python 47 | * [flask](http://flask.pocoo.org) -- `python web framework` 48 | * [redis](http://redis.io) -- `nosqldb, store text,img etc, and used for cache instead of memcached` 49 | * memcached -- `之前使用redis代替memcached,不过redis在小内存情况下表现较差,所以选择使用memcached` 50 | * mongodb -- `data storage` 51 | * [xhtml2pdf](https://github.com/chrisglass/xhtml2pdf) -- `convert html to pdf` 52 | * [scws](http://www.ftphp.com/scws) -- `simple chinese word segment` 53 | * git/github -- `code version control` 54 | * v2ex -- `thanks for v2ex and css of v2ex^^` 55 | 56 | 项目地址: 57 | ------- 58 | 59 | https://github.com/laiwei/thepast 60 | 61 | 官方主页: 62 | ------- 63 | 64 | http://thepast.me 65 | 66 | 67 | 作为开源项目,欢迎前端工程师 和 iOS工程师,一起来完善,也欢迎吐槽。 68 | 69 | 贡献者列表: 70 | ------- 71 | * [laiwei](https://github.com/laiwei) --`项目发起者` 72 | * [lmm214](https://github.com/lmm214) --`设计,修改了首页timeline的展示方式` 73 | 74 | 75 | ChangeList: 76 | ------- 77 | * `2012-10-03`: 支持绑定instagram了:) 78 | * `2012-10-02`: 支持绑定人人了:) 79 | * `2012-09-11`: 数据存储从mongodb转向了mysql,节省硬盘空间,[参考](http://laiwei.net/2012/09/15/mongodb和mysql的一个小小的取舍比较) 80 | * `2012-09-09`: “时间线”重构,使用flask的blueprint,松耦合;时间线支持reverse order 81 | * `2012-07-24`: 支持从thepast发送消息到多个第三方了 82 | * `2012-06-09`: 日记支持markdown格式,这样写日记方便好多了:) 83 | * `2012-04-22`: 增加了隐私设置,可以选择公开、仅登录用户可见、仅自己可见,保护用户的隐私; 增加了邮件退订功能。 84 | * `2012-04-18`: PDF归档,修改为按月归档,每个月生成一个独立的文件,代替了之前整理为一个PDF文件。 85 | * `2012-04-14`: 增加了[“时间线”](http://thepast.me/visual)栏目,以一种别样的视角看过去。 86 | * `2012-04-10`: 支持绑定自己的独立wordpress博客:) 87 | * `2012-04-05`: 增加了"我的过去"栏目,提供有意思的回忆功能 88 | * `2012-04-04`: 提供补充email功能,以便在PDF文件生成之后,通知用户或者直接发送附件 89 | * `2012-04-01`: redis在内存比较小的情况下,效率比较低,而且在分配的内存耗尽,没有及时淘汰掉key时,会造成写入失败,于是改用了memcached 90 | * `2012-04-01`: mongodb坏掉了,原因是在32位系统下,mongodb存在数据文件不能超过2G的限制,见[官方说明](http://blog.mongodb.org/post/137788967/32-bit-limitations); 于是将系统升级为64位debian,重新安装了64位版本mongodb,恢复了数据 91 | * `2012-03-31`: 加上了sidebar,用来展示用户的自我介绍,个人关键字等 92 | * `2012-03-30`: 恢复了早期新浪微博用户的status时间差了12小时的数据 93 | * `2012-03-25`: 增加了个人关键字提取功能,根据timeline的信息提取个人关键字,使用了[scws](http://www.ftphp.com/scws/),thanks 94 | * `2012-03-10`: 新的匿名用户首页和timeline页面,from木木[lmm214] 95 | * `2012-03-04`: 使用mongodb代替redis做数据持久化存储,并将redis中的37万条数据转存到mongodb中 96 | * `2012-03-04`: 使用豆瓣新广播的api,代替旧的miniblog API 97 | * `2012-03-01`: mysql connect增加了mysql gone away之后的重试机制 98 | * `2012-02-28`: 使用了新的logo,感谢木木[lmm214](https://github.com/lmm214)的设计 99 | * `2012-02-24`: 支持同步腾讯微博(使用腾讯微博的朋友看过来^^) 100 | * `2012-02-22`: 屏蔽搜索引擎收录(因为隐私还是很重要的) 101 | * `2012-02-18`: 加cache,使用redis充当memcache,提高访问速度,降低机器负载 102 | * `2012-02-17`: 优化PDF文件的下载效率,使用nginx来承担文件下载任务 103 | * `2012-02-16`: 优化代码解决生成PDF的效率(因为内存不够用了^^) 104 | * `2012-02-15`: 增加了个人杂志计划成员展示页 105 | * `2012-02-14`: 在v2ex社区介绍个人杂志计划,共有40人加入! 106 | * `2012-02-13`: 增加保存个人内容为排版后的PDF功能 107 | * `2012-02-12`: 开源项目,个人杂志计划上线 108 | 109 | thanks 110 | 111 | by laiwei 112 | -------------------------------------------------------------------------------- /cronjob/bind_wordpress.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append("../") 5 | 6 | from past.store import mc 7 | from past.model.user import UserAlias, User 8 | from past.model.status import SyncTask, TaskQueue 9 | from past import config 10 | 11 | 12 | def bind(uid, feed_uri): 13 | user = User.get(uid) 14 | if not user: 15 | print 'no user' 16 | return 17 | ua = UserAlias.bind_to_exists_user(user, 18 | config.OPENID_TYPE_DICT[config.OPENID_WORDPRESS], feed_uri) 19 | if not ua: 20 | print "no user alias" 21 | else: 22 | ##添加同步任务 23 | t = SyncTask.add(config.CATE_WORDPRESS_POST, user.id) 24 | t and TaskQueue.add(t.id, t.kind) 25 | ##删除confiration记录 26 | mc.delete("wordpress_bind:%s" %user.id) 27 | 28 | if __name__ == "__main__": 29 | print sys.argv 30 | if len(sys.argv) == 3: 31 | bind(sys.argv[1], sys.argv[2]) 32 | else: 33 | print "bind uid feed" 34 | 35 | -------------------------------------------------------------------------------- /cronjob/crontab_list: -------------------------------------------------------------------------------- 1 | #10 06 * * * cd /home/work/proj/thepast/cronjob && sh rm_pdf.sh 2 | 3 | ##每60分钟同步timeline 4 | 0 * * * * cd /home/work/proj/thepast/cronjob; /home/work/proj/thepast/env/bin/python sync_timeline.py >>/home/work/proj/thepast/var/cron_sync_timeline.log 2>&1 5 | 6 | ##每天早晨6点10分,生成所有用户的pdf(19:1) 7 | #10 06 * * * cd /home/work/proj/thepast/cronjob; /home/work/proj/thepast/env/bin/python generate_pdf.py >>/home/work/proj/thepast/var/cron_generate_pdf.log 2>&1 8 | 9 | ##每5分钟同步新添加用户的任务 10 | 0,5,10,15,20,25,30,35,40,45,50,55 * * * * cd /home/work/proj/thepast/cronjob; /home/work/proj/thepast/env/bin/python first_sync_timeline.py >>first_sync.log 2>&1 11 | 12 | ##每天数据备份 13 | 10 04 * * * cd /home/work/proj/thepast/cronjob; bash backup_data.sh >>/home/work/proj/thepast/var/cron_backup_data.log 2>&1 14 | -------------------------------------------------------------------------------- /cronjob/first_sync_timeline.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append("../") 5 | 6 | import os 7 | import time 8 | import datetime 9 | import commands 10 | 11 | activate_this = '../env/bin/activate_this.py' 12 | execfile(activate_this, dict(__file__=activate_this)) 13 | 14 | import past 15 | import jobs 16 | from past.model.status import TaskQueue, SyncTask, Status 17 | from past import config 18 | 19 | if __name__ == "__main__": 20 | 21 | try: 22 | queue_ids = TaskQueue.get_all_ids() 23 | print '%s queue length: %s' %(datetime.datetime.now(),len(queue_ids)) 24 | for qid in queue_ids: 25 | queue = TaskQueue.get(qid) 26 | if queue and queue.task_kind == config.K_SYNCTASK: 27 | print 'syncing task id:', queue.task_id 28 | sync_task = SyncTask.get(queue.task_id) 29 | if not sync_task: 30 | continue 31 | 32 | ## 现在不同步豆瓣日记 33 | if str(sync_task.category) == str(config.CATE_DOUBAN_NOTE): 34 | continue 35 | 36 | ## 同步wordpress rss 37 | if str(sync_task.category) == str(config.CATE_WORDPRESS_POST): 38 | jobs.sync_wordpress(sync_task) 39 | queue.remove() 40 | continue 41 | 42 | max_sync_times = 0 43 | min_id = Status.get_min_origin_id(sync_task.category, sync_task.user_id) 44 | if sync_task: 45 | while True: 46 | if max_sync_times >= 20: 47 | break 48 | r = jobs.sync(sync_task, old=True) 49 | new_min_id = Status.get_min_origin_id(sync_task.category, sync_task.user_id) 50 | if r == 0 or new_min_id == min_id: 51 | break 52 | min_id = new_min_id 53 | max_sync_times += 1 54 | queue.remove() 55 | time.sleep(1) 56 | time.sleep(1) 57 | except Exception, e: 58 | print e 59 | -------------------------------------------------------------------------------- /cronjob/generate_pdf.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append("../") 5 | 6 | import time 7 | import datetime 8 | import calendar 9 | import commands 10 | 11 | activate_this = '../env/bin/activate_this.py' 12 | execfile(activate_this, dict(__file__=activate_this)) 13 | 14 | from past.utils.pdf import generate_pdf, get_pdf_filename, is_pdf_file_exists, get_pdf_full_filename 15 | from past.model.user import User, UserAlias, PdfSettings 16 | from past.model.status import Status 17 | from past import config 18 | 19 | 20 | def generate(user_id, date, order='asc'): 21 | try: 22 | uas = UserAlias.gets_by_user_id(user_id) 23 | if not uas: 24 | return 25 | 26 | start_date = datetime.datetime(date.year, date.month, 1) 27 | end_date = datetime.datetime(date.year, date.month, 28 | calendar.monthrange(date.year, date.month)[1], 23, 59, 59) 29 | 30 | pdf_filename = get_pdf_filename(user_id, date.strftime("%Y%m"), "") 31 | pdf_filename_compressed = get_pdf_filename(user_id, date.strftime("%Y%m")) 32 | print '----generate pdf:', start_date, ' to ', end_date, ' file is', pdf_filename 33 | 34 | if is_pdf_file_exists(pdf_filename_compressed): 35 | print '---- %s exists, so ignore...' % pdf_filename_compressed 36 | return 37 | 38 | status_ids = Status.get_ids_by_date(user_id, start_date, end_date)[:900] 39 | if order == 'asc': 40 | status_ids = status_ids[::-1] 41 | if not status_ids: 42 | print '----- status ids is none', status_ids 43 | return 44 | generate_pdf(pdf_filename, user_id, status_ids) 45 | 46 | if not is_pdf_file_exists(pdf_filename): 47 | print '----%s generate pdf for user:%s fail' % (datetime.datetime.now(), user_id) 48 | else: 49 | commands.getoutput("cd %s && tar -zcf %s %s && rm %s" %(config.PDF_FILE_DOWNLOAD_DIR, 50 | pdf_filename_compressed, pdf_filename, pdf_filename)) 51 | print '----%s generate pdf for user:%s succ' % (datetime.datetime.now(), user_id) 52 | except Exception, e: 53 | import traceback 54 | print '%s %s' % (datetime.datetime.now(), traceback.format_exc()) 55 | 56 | def generate_pdf_by_user(user_id): 57 | user = User.get(user_id) 58 | if not user: 59 | return 60 | 61 | #XXX:暂时只生成2012年的(uid从98开始的用户) 62 | #XXX:暂时只生成2012年3月份的(uid从166开始的用户) 63 | start_date = Status.get_oldest_create_time(None, user_id) 64 | if not start_date: 65 | return 66 | now = datetime.datetime.now() 67 | now = datetime.datetime(now.year, now.month, now.day) - datetime.timedelta(days = calendar.monthrange(now.year, now.month)[1]) 68 | 69 | d = start_date 70 | while d <= now: 71 | generate(user_id, d) 72 | 73 | days = calendar.monthrange(d.year, d.month)[1] 74 | d += datetime.timedelta(days=days) 75 | d = datetime.datetime(d.year, d.month, 1) 76 | 77 | 78 | if __name__ == "__main__": 79 | for uid in PdfSettings.get_all_user_ids(): 80 | #print '------begin generate pdf of user:', uid 81 | #generate_pdf_by_user(uid) 82 | 83 | now = datetime.datetime.now() 84 | last_mongth = datetime.datetime(now.year, now.month, now.day) \ 85 | - datetime.timedelta(days = calendar.monthrange(now.year, now.month)[1]) 86 | print '----- generate last month pdf of user:', last_mongth, uid 87 | generate(uid, last_mongth) 88 | -------------------------------------------------------------------------------- /cronjob/rm_pdf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /home/work/proj/thepast/var/down/pdf && { 4 | find ./ -name "*.pdf" -ctime +1 -exec rm {} \; 5 | } 6 | -------------------------------------------------------------------------------- /cronjob/send_refresh_token_mail.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append("../") 5 | 6 | activate_this = '../env/bin/activate_this.py' 7 | execfile(activate_this, dict(__file__=activate_this)) 8 | 9 | import time 10 | from past.store import db_conn 11 | from send_reminding import send_reconnect 12 | 13 | if __name__ == "__main__": 14 | cursor = db_conn.execute("select max(id) from user") 15 | row = cursor.fetchone() 16 | cursor and cursor.close() 17 | max_uid = row and row[0] 18 | max_uid = int(max_uid) 19 | t = 0 20 | for uid in xrange(4,max_uid + 1): 21 | #for uid in xrange(4, 5): 22 | if t >= 100: 23 | t = 0 24 | time.sleep(5) 25 | send_reconnect(uid) 26 | time.sleep(1) 27 | t += 1 28 | sys.stdout.flush() 29 | 30 | -------------------------------------------------------------------------------- /cronjob/sync_timeline.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append("../") 5 | 6 | import os 7 | import time 8 | import commands 9 | 10 | activate_this = '../env/bin/activate_this.py' 11 | execfile(activate_this, dict(__file__=activate_this)) 12 | 13 | if __name__ == "__main__": 14 | for c in [100, 200, 300, 400, 500, 700, 702, 703, 704, 800,]: 15 | for t in ['old', 'new']: 16 | print commands.getoutput("../env/bin/python ../jobs.py -t %s -c %s -n 1" %(t, c)) 17 | -------------------------------------------------------------------------------- /dep.txt: -------------------------------------------------------------------------------- 1 | 2 | sudo apt-get install python-virtualenv python-pip 3 | sudo apt-get install git ipython 4 | sudo apt-get install mysql-server 5 | #root password:password 6 | mysql_secure_installation 7 | 8 | sudo apt-get install python-mysqldb 9 | #cd env/lib/python2.6/ && ln -s /usr/lib/pymodules/python2.6/MySQLdb/ MySQLdb && ln -s /usr/lib/pymodules/python2.6/_mysql_exceptions.py _mysql_exceptions.py && ln -s /usr/lib/pymodules/python2.6/_mysql.so _mysql.so 10 | 11 | ##cd env/lib/python2.7 12 | ##ln -s /usr/lib/python2.7/dist-packages/MySQLdb MySQLdb 13 | ##ln -s /usr/lib/python2.7/dist-packages/_mysql_exceptions.py _mysql_exceptions.py 14 | ##ln -s /usr/lib/python2.7/dist-packages/_mysql.so _mysql.so 15 | 16 | 17 | #redis not use any more, instead by memcached and mongodb 18 | #sudo apt-get install redis-server 19 | 20 | git clone git://github.com/laiwei/thepast.git 21 | #virtualenv --no-site-packages env 22 | virtualenv env 23 | 24 | pip install flask redis tweepy httplib2 25 | 26 | 27 | ## mysql dump schema 28 | mysqldump -u root -pmypassword test_database --no-data=true --add-drop-table=false > schema_dump.sql 29 | 30 | ## mysql dump data 31 | mysqldump -u root -pmypassword test_database --add-drop-table=false > data_dump.sql 32 | 33 | ## import table 34 | create database `thepast` 35 | mysql -u root -ppassword thepast < past/schema.sql 36 | 37 | ##install nginx 38 | ###http://wiki.nginx.org/Install 39 | sudo -s 40 | nginx=stable # use nginx=development for latest development version 41 | echo "deb http://ppa.launchpad.net/nginx/$nginx/ubuntu lucid main" > /etc/apt/sources.list.d/nginx-$nginx-lucid.list 42 | apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C 43 | 44 | apt-get update 45 | apt-get install nginx 46 | 47 | ##deploy with nginx and uwsgi 48 | ###http://projects.unbit.it/uwsgi/wiki/Quickstart 49 | apt-get install build-essential python-dev libxml2-dev 50 | pip install uwsgi 51 | 52 | ## run app 53 | #uwsgi --socket 127.0.0.1:3031 --file /home/work/proj/thepast/pastme.py --callable app --processes 2 54 | #or 55 | #uwsgi -s /tmp/uwsgi.sock --file /home/work/proj/thepast/pastme.py --callable app --processes 2 56 | #recommend 57 | nohup uwsgi -s /tmp/uwsgi.sock --file /home/work/proj/thepast/pastme.py --callable app --processes 2 & 58 | 59 | ## xhtml2pdf 60 | git clone git://github.com/chrisglass/xhtml2pdf.git 61 | cd xhtml2pdf/ 62 | #modify requirements.xml, == to >= 63 | pip install -r requirements.xml 64 | python setup.py install 65 | 66 | ### for test 67 | sudo apt-get install python-nose 68 | nosetests --with-coverage 69 | 70 | 71 | ## PIL setup 72 | 73 | sudo apt-get install libfreetype6-dev libjpeg8-dev 74 | sudo ln -s /usr/lib/i386-linux-gnu/libz.so /usr/lib/ 75 | sudo ln -s /usr/lib/i386-linux-gnu/libfreetype.so.6 /usr/lib/ 76 | sudo ln -s /usr/lib/i386-linux-gnu/libjpeg.so /usr/lib/ 77 | 78 | #sudo ln -s /usr/lib/x86_64-linux-gnu/libfreetype.so /usr/lib/ 79 | #sudo ln -s /usr/lib/x86_64-linux-gnu/libz.so /usr/lib/ 80 | #sudo ln -s /usr/lib/x86_64-linux-gnu/libjpeg.so /usr/lib/ 81 | 82 | #pip install -U PIL 83 | pip install --no-index -f http://dist.plone.org/thirdparty/ -U PIL 84 | 85 | 86 | ## config cache 87 | ## use redis instead memcached (no use any more) 88 | ### /etc/redis_7379.conf include ^/deploy/redis_cache.conf 89 | ### cp /etc/init.d/redis_6379 /etc/init.d/redis_7379 90 | ### sudo update-rc.d redis_7379 defaults 91 | 92 | ##mongo db 93 | ## sudo apt-get install mongodb-server 94 | wget http://fastdl.mongodb.org/linux/mongodb-linux-i686-2.0.3.tgz 95 | sudo mkdir -p /data/db 96 | tar xzf mongodb-linux-i686-2.0.3.tgz 97 | ./mongodb-xxxx/bin/mongod & 98 | 99 | pip install pymongo 100 | 101 | ## about mongodb 102 | http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries 103 | http://www.mongodb.org/display/DOCS/Optimization 104 | ##index 105 | http://www.mongodb.org/display/DOCS/Indexes 106 | http://blog.nosqlfan.com/html/271.html 107 | 108 | db.thepast.getIndexes 109 | db.thepast.ensureIndex({k:1}) 110 | 111 | 112 | ## memcached 113 | sudo apt-get install memcached 114 | ## edit /etc/memcached.conf, -m 100 means max-memory = 100M 115 | ## sudo /etc/init.d/memcached restart 116 | ## telnet 127.0.0.1 11211 "stats" 117 | pip install python-memcached 118 | 119 | 120 | ## rss feed parse 121 | pip install feedparser 122 | #http://stackoverflow.com/questions/2244836/rss-feed-parser-library-in-python 123 | #http://packages.python.org/feedparser/ 124 | 125 | ## add note 126 | pip install markdown2 127 | https://github.com/trentm/python-markdown2 128 | 129 | 130 | -------------------------------------------------------------------------------- /deploy/memcached.conf: -------------------------------------------------------------------------------- 1 | # memcached default config file 2 | # 2003 - Jay Bonci 3 | # This configuration file is read by the start-memcached script provided as 4 | # part of the Debian GNU/Linux distribution. 5 | 6 | # Run memcached as a daemon. This command is implied, and is not needed for the 7 | # daemon to run. See the README.Debian that comes with this package for more 8 | # information. 9 | -d 10 | 11 | # Log memcached's output to /var/log/memcached 12 | logfile /var/log/memcached.log 13 | 14 | # Be verbose 15 | # -v 16 | 17 | # Be even more verbose (print client commands as well) 18 | # -vv 19 | 20 | # Start with a cap of 64 megs of memory. It's reasonable, and the daemon default 21 | # Note that the daemon will grow to this size, but does not start out holding this much 22 | # memory 23 | -m 100 24 | 25 | # Default connection port is 11211 26 | -p 11211 27 | 28 | # Run the daemon as root. The start-memcached will default to running as root if no 29 | # -u command is present in this config file 30 | -u nobody 31 | 32 | # Specify which IP address to listen on. The default is to listen on all IP addresses 33 | # This parameter is one of the only security measures that memcached has, so make sure 34 | # it's listening on a firewalled interface. 35 | -l 127.0.0.1 36 | 37 | # Limit the number of simultaneous incoming connections. The daemon default is 1024 38 | # -c 1024 39 | 40 | # Lock down all paged memory. Consult with the README and homepage before you do this 41 | # -k 42 | 43 | # Return error when memory is exhausted (rather than removing items) 44 | # -M 45 | 46 | # Maximize core file limit 47 | # -r 48 | -------------------------------------------------------------------------------- /deploy/mysql.conf: -------------------------------------------------------------------------------- 1 | # 2 | # The MySQL database server configuration file. 3 | # 4 | # You can copy this to one of: 5 | # - "/etc/mysql/my.cnf" to set global options, 6 | # - "~/.my.cnf" to set user-specific options. 7 | # 8 | # One can use all long options that the program supports. 9 | # Run program with --help to get a list of available options and with 10 | # --print-defaults to see which it would actually understand and use. 11 | # 12 | # For explanations see 13 | # http://dev.mysql.com/doc/mysql/en/server-system-variables.html 14 | 15 | # This will be passed to all mysql clients 16 | # It has been reported that passwords should be enclosed with ticks/quotes 17 | # escpecially if they contain "#" chars... 18 | # Remember to edit /etc/mysql/debian.cnf when changing the socket location. 19 | [client] 20 | port = 3306 21 | socket = /var/run/mysqld/mysqld.sock 22 | 23 | ## laiwei add 24 | default-character-set = utf8 25 | 26 | # Here is entries for some specific programs 27 | # The following values assume you have at least 32M ram 28 | 29 | # This was formally known as [safe_mysqld]. Both versions are currently parsed. 30 | [mysqld_safe] 31 | socket = /var/run/mysqld/mysqld.sock 32 | nice = 0 33 | 34 | [mysqld] 35 | # 36 | # * Basic Settings 37 | # 38 | user = mysql 39 | pid-file = /var/run/mysqld/mysqld.pid 40 | socket = /var/run/mysqld/mysqld.sock 41 | port = 3306 42 | basedir = /usr 43 | datadir = /var/lib/mysql 44 | tmpdir = /tmp 45 | language = /usr/share/mysql/english 46 | skip-external-locking 47 | 48 | ## laiwei add 49 | default-character-set = utf8 50 | # 51 | # Instead of skip-networking the default is now to listen only on 52 | # localhost which is more compatible and is not less secure. 53 | bind-address = 127.0.0.1 54 | # 55 | # * Fine Tuning 56 | # 57 | key_buffer = 16M 58 | max_allowed_packet = 16M 59 | thread_stack = 192K 60 | thread_cache_size = 8 61 | # This replaces the startup script and checks MyISAM tables if needed 62 | # the first time they are touched 63 | myisam-recover = BACKUP 64 | #max_connections = 100 65 | #table_cache = 64 66 | #thread_concurrency = 10 67 | # 68 | # * Query Cache Configuration 69 | # 70 | query_cache_limit = 1M 71 | query_cache_size = 16M 72 | # 73 | # * Logging and Replication 74 | # 75 | # Both location gets rotated by the cronjob. 76 | # Be aware that this log type is a performance killer. 77 | # As of 5.1 you can enable the log at runtime! 78 | #general_log_file = /var/log/mysql/mysql.log 79 | #general_log = 1 80 | # 81 | # Error logging goes to syslog due to /etc/mysql/conf.d/mysqld_safe_syslog.cnf. 82 | # 83 | # Here you can see queries with especially long duration 84 | #log_slow_queries = /var/log/mysql/mysql-slow.log 85 | #long_query_time = 2 86 | #log-queries-not-using-indexes 87 | # 88 | # The following can be used as easy to replay backup logs or for replication. 89 | # note: if you are setting up a replication slave, see README.Debian about 90 | # other settings you may need to change. 91 | #server-id = 1 92 | #log_bin = /var/log/mysql/mysql-bin.log 93 | expire_logs_days = 10 94 | max_binlog_size = 100M 95 | #binlog_do_db = include_database_name 96 | #binlog_ignore_db = include_database_name 97 | # 98 | # * InnoDB 99 | # 100 | # InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/. 101 | # Read the manual for more InnoDB related options. There are many! 102 | # 103 | # * Security Features 104 | # 105 | # Read the manual, too, if you want chroot! 106 | # chroot = /var/lib/mysql/ 107 | # 108 | # For generating SSL certificates I recommend the OpenSSL GUI "tinyca". 109 | # 110 | # ssl-ca=/etc/mysql/cacert.pem 111 | # ssl-cert=/etc/mysql/server-cert.pem 112 | # ssl-key=/etc/mysql/server-key.pem 113 | 114 | ##laiwei add 115 | transaction-isolation = READ-COMMITTED 116 | 117 | [mysqldump] 118 | quick 119 | quote-names 120 | max_allowed_packet = 16M 121 | 122 | [mysql] 123 | #no-auto-rehash # faster start of mysql but no tab completition 124 | 125 | [isamchk] 126 | key_buffer = 16M 127 | 128 | # 129 | # * IMPORTANT: Additional settings that can override those from this file! 130 | # The files must end with '.cnf', otherwise they'll be ignored. 131 | # 132 | !includedir /etc/mysql/conf.d/ 133 | -------------------------------------------------------------------------------- /deploy/nginx-sites-available/default: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; ## listen for ipv4 4 | ##listen [::]:80 default ipv6only=on; ## listen for ipv6 5 | 6 | server_name thepast.me; 7 | 8 | 9 | access_log /var/log/nginx/thepast.me.access.log; 10 | 11 | location = /favicon.ico { 12 | return 204; 13 | } 14 | 15 | location /down/ { 16 | internal; 17 | root /home/work/proj/thepast/var; 18 | } 19 | 20 | location / { try_files $uri @pastme; } 21 | location /static/ { 22 | #add_header Cache-Control public; 23 | #expires max; 24 | root /home/work/proj/thepast/past; 25 | } 26 | location @pastme { 27 | include uwsgi_params; 28 | uwsgi_pass unix:/tmp/uwsgi.sock; 29 | } 30 | 31 | error_page 500 502 503 504 /500.html; 32 | location = /500.html { 33 | root /home/work/proj/thepast/past/static; 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /deploy/nginx-sites-available/linjuly: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; ## listen for ipv4 4 | 5 | server_name linjuly.com www.linjuly.com; 6 | 7 | 8 | access_log /var/log/nginx/linjuly.com.access.log; 9 | error_log /var/log/nginx/linjuly.com.error.log info; 10 | 11 | index index.php index.html; 12 | 13 | location / { 14 | root /home/work/linjuly_blog; 15 | 16 | if (-f $request_filename) { 17 | expires 10d; 18 | break; 19 | } 20 | if (!-e $request_filename) { 21 | rewrite ^(.+)$ /index.php?q=$1 last; 22 | } 23 | } 24 | 25 | 26 | location ~ \.php$ { 27 | 28 | fastcgi_pass 127.0.0.1:12306; 29 | fastcgi_index index.php; 30 | 31 | fastcgi_param SCRIPT_FILENAME /home/work/linjuly_blog/$fastcgi_script_name; 32 | 33 | fastcgi_param QUERY_STRING $query_string; 34 | fastcgi_param REQUEST_METHOD $request_method; 35 | fastcgi_param CONTENT_TYPE $content_type; 36 | fastcgi_param CONTENT_LENGTH $content_length; 37 | 38 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 39 | fastcgi_param REQUEST_URI $request_uri; 40 | fastcgi_param DOCUMENT_URI $document_uri; 41 | fastcgi_param DOCUMENT_ROOT $document_root; 42 | fastcgi_param SERVER_PROTOCOL $server_protocol; 43 | 44 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 45 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 46 | 47 | fastcgi_param REMOTE_ADDR $remote_addr; 48 | fastcgi_param REMOTE_PORT $remote_port; 49 | fastcgi_param SERVER_ADDR $server_addr; 50 | fastcgi_param SERVER_PORT $server_port; 51 | fastcgi_param SERVER_NAME $server_name; 52 | 53 | # required if PHP was built with --enable-force-cgi-redirect 54 | fastcgi_param REDIRECT_STATUS 200; 55 | } 56 | 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /deploy/nginx-sites-available/sample: -------------------------------------------------------------------------------- 1 | # You may add here your 2 | # server { 3 | # ... 4 | # } 5 | # statements for each of your virtual hosts to this file 6 | 7 | ## 8 | # You should look at the following URL's in order to grasp a solid understanding 9 | # of Nginx configuration files in order to fully unleash the power of Nginx. 10 | # http://wiki.nginx.org/Pitfalls 11 | # http://wiki.nginx.org/QuickStart 12 | # http://wiki.nginx.org/Configuration 13 | # 14 | # Generally, you will want to move this file somewhere, and start with a clean 15 | # file but keep this around for reference. Or just disable in sites-enabled. 16 | # 17 | # Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. 18 | ## 19 | 20 | server { 21 | #listen 80; ## listen for ipv4; this line is default and implied 22 | #listen [::]:80 default ipv6only=on; ## listen for ipv6 23 | 24 | root /usr/share/nginx/www; 25 | index index.html index.htm; 26 | 27 | # Make site accessible from http://localhost/ 28 | server_name localhost; 29 | 30 | location / { 31 | # First attempt to serve request as file, then 32 | # as directory, then fall back to index.html 33 | try_files $uri $uri/ /index.html; 34 | } 35 | 36 | location /doc/ { 37 | alias /usr/share/doc; 38 | autoindex on; 39 | allow 127.0.0.1; 40 | deny all; 41 | } 42 | 43 | #error_page 404 /404.html; 44 | 45 | # redirect server error pages to the static page /50x.html 46 | # 47 | #error_page 500 502 503 504 /50x.html; 48 | #location = /50x.html { 49 | # root /usr/share/nginx/www; 50 | #} 51 | 52 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 53 | # 54 | #location ~ \.php$ { 55 | # fastcgi_split_path_info ^(.+\.php)(/.+)$; 56 | # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini 57 | # 58 | # fastcgi_pass 127.0.0.1:9000; 59 | # fastcgi_index index.php; 60 | # include fastcgi_params; 61 | #} 62 | 63 | # deny access to .htaccess files, if Apache's document root 64 | # concurs with nginx's one 65 | # 66 | #location ~ /\.ht { 67 | # deny all; 68 | #} 69 | } 70 | 71 | 72 | # another virtual host using mix of IP-, name-, and port-based configuration 73 | # 74 | #server { 75 | # listen 8000; 76 | # listen somename:8080; 77 | # server_name somename alias another.alias; 78 | # root html; 79 | # index index.html index.htm; 80 | # 81 | # location / { 82 | # try_files $uri $uri/ /index.html; 83 | # } 84 | #} 85 | 86 | 87 | # HTTPS server 88 | # 89 | #server { 90 | # listen 443; 91 | # server_name localhost; 92 | # 93 | # root html; 94 | # index index.html index.htm; 95 | # 96 | # ssl on; 97 | # ssl_certificate cert.pem; 98 | # ssl_certificate_key cert.key; 99 | # 100 | # ssl_session_timeout 5m; 101 | # 102 | # ssl_protocols SSLv3 TLSv1; 103 | # ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; 104 | # ssl_prefer_server_ciphers on; 105 | # 106 | # location / { 107 | # try_files $uri $uri/ /index.html; 108 | # } 109 | #} 110 | -------------------------------------------------------------------------------- /deploy/nginx-sites-available/simplelife: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; ## listen for ipv4 4 | 5 | server_name www.simplelife.cn; 6 | 7 | 8 | access_log /var/log/nginx/simplelife.cn.access.log; 9 | error_log /var/log/nginx/simplelife.cn.error.log info; 10 | 11 | index index.php index.html; 12 | 13 | location / { 14 | root /home/haidong/simplelife_blog; 15 | 16 | if (-f $request_filename) { 17 | expires 10d; 18 | break; 19 | } 20 | if (!-e $request_filename) { 21 | rewrite ^(.+)$ /index.php?q=$1 last; 22 | } 23 | } 24 | 25 | 26 | location ~ \.php$ { 27 | 28 | fastcgi_pass 127.0.0.1:12306; 29 | fastcgi_index index.php; 30 | 31 | fastcgi_param SCRIPT_FILENAME /home/haidong/simplelife_blog/$fastcgi_script_name; 32 | 33 | fastcgi_param QUERY_STRING $query_string; 34 | fastcgi_param REQUEST_METHOD $request_method; 35 | fastcgi_param CONTENT_TYPE $content_type; 36 | fastcgi_param CONTENT_LENGTH $content_length; 37 | 38 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 39 | fastcgi_param REQUEST_URI $request_uri; 40 | fastcgi_param DOCUMENT_URI $document_uri; 41 | fastcgi_param DOCUMENT_ROOT $document_root; 42 | fastcgi_param SERVER_PROTOCOL $server_protocol; 43 | 44 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 45 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 46 | 47 | fastcgi_param REMOTE_ADDR $remote_addr; 48 | fastcgi_param REMOTE_PORT $remote_port; 49 | fastcgi_param SERVER_ADDR $server_addr; 50 | fastcgi_param SERVER_PORT $server_port; 51 | fastcgi_param SERVER_NAME $server_name; 52 | 53 | # required if PHP was built with --enable-force-cgi-redirect 54 | fastcgi_param REDIRECT_STATUS 200; 55 | } 56 | 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | #user www-data; 2 | user work; 3 | worker_processes 3; 4 | pid /var/run/nginx.pid; 5 | 6 | events { 7 | worker_connections 64; 8 | # multi_accept on; 9 | } 10 | 11 | http { 12 | 13 | ## 14 | # Basic Settings 15 | ## 16 | 17 | sendfile on; 18 | tcp_nopush on; 19 | tcp_nodelay on; 20 | keepalive_timeout 300 300; 21 | #keepalive_timeout 180; 22 | types_hash_max_size 2048; 23 | # server_tokens off; 24 | 25 | # server_names_hash_bucket_size 64; 26 | # server_name_in_redirect off; 27 | 28 | include /etc/nginx/mime.types; 29 | default_type application/octet-stream; 30 | ##XXX 31 | #client_max_body_size 5M; 32 | 33 | ## 34 | # Logging Settings 35 | ## 36 | 37 | access_log /var/log/nginx/access.log; 38 | error_log /var/log/nginx/error.log; 39 | 40 | ## 41 | # Gzip Settings 42 | ## 43 | 44 | gzip on; 45 | gzip_disable "msie6"; 46 | 47 | # gzip_vary on; 48 | # gzip_proxied any; 49 | # gzip_comp_level 6; 50 | # gzip_buffers 16 8k; 51 | # gzip_http_version 1.1; 52 | # gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 53 | 54 | ## 55 | # If HTTPS, then set a variable so it can be passed along. 56 | ## 57 | 58 | map $scheme $server_https { 59 | default off; 60 | https on; 61 | } 62 | 63 | ## 64 | # Virtual Host Configs 65 | ## 66 | 67 | include /etc/nginx/conf.d/*.conf; 68 | include /etc/nginx/sites-enabled/*; 69 | } 70 | 71 | 72 | #mail { 73 | # # See sample authentication script at: 74 | # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript 75 | # 76 | # # auth_http localhost/auth.php; 77 | # # pop3_capabilities "TOP" "USER"; 78 | # # imap_capabilities "IMAP4rev1" "UIDPLUS"; 79 | # 80 | # server { 81 | # listen localhost:110; 82 | # protocol pop3; 83 | # proxy on; 84 | # } 85 | # 86 | # server { 87 | # listen localhost:143; 88 | # protocol imap; 89 | # proxy on; 90 | # } 91 | #} 92 | -------------------------------------------------------------------------------- /deploy/nginx_site_default: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; ## listen for ipv4 4 | listen [::]:80 default ipv6only=on; ## listen for ipv6 5 | 6 | server_name thepast.me; 7 | 8 | 9 | access_log /var/log/nginx/thepast.me.access.log; 10 | 11 | location = /favicon.ico { 12 | return 204; 13 | } 14 | 15 | location /down/ { 16 | internal; 17 | root /home/work/proj/thepast/var; 18 | } 19 | 20 | location / { try_files $uri @pastme; } 21 | location /static/ { 22 | #add_header Cache-Control public; 23 | #expires max; 24 | root /home/work/proj/thepast/past; 25 | } 26 | location @pastme { 27 | include uwsgi_params; 28 | uwsgi_pass unix:/tmp/uwsgi.sock; 29 | } 30 | 31 | error_page 500 502 503 504 /500.html; 32 | location = /500.html { 33 | root /home/work/proj/thepast/past/static; 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /gunicorn.conf: -------------------------------------------------------------------------------- 1 | workers = 4 2 | bind = '127.0.0.1:8888' 3 | #bind = '0.0.0.0:8888' 4 | proc_name = 'thepast.me' 5 | pidfile = '/tmp/thepast.pid' 6 | -------------------------------------------------------------------------------- /past/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from flask import Flask 4 | 5 | #-- create app -- 6 | app = Flask(__name__) 7 | app.config.from_object("past.config") 8 | 9 | ##-- register blueprint -- 10 | from past.connect import blue_print as connect_bp 11 | from past.dev import blue_print as dev_bp 12 | from past.weixin import blue_print as weixin_bp 13 | app.register_blueprint(connect_bp, url_prefix="/connect") 14 | app.register_blueprint(dev_bp, url_prefix="/dev") 15 | app.register_blueprint(weixin_bp, url_prefix="/weixin") 16 | 17 | import view 18 | 19 | from utils import filters 20 | from utils import wrap_long_line 21 | from utils import markdownize 22 | 23 | app.jinja_env.filters['nl2br'] = filters.nl2br 24 | app.jinja_env.filters['linkify'] = filters.linkify 25 | app.jinja_env.filters['html_parse'] = filters.html_parse 26 | app.jinja_env.filters['wrap_long_line'] = wrap_long_line 27 | app.jinja_env.filters['markdownize'] = markdownize 28 | app.jinja_env.filters['isstr'] = lambda x: isinstance(x, basestring) 29 | app.jinja_env.filters['stream_time'] = filters.stream_time 30 | -------------------------------------------------------------------------------- /past/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/api/__init__.py -------------------------------------------------------------------------------- /past/api/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from tweepy.error import TweepError 4 | from past.model.user import User 5 | 6 | class OAuthError(Exception): 7 | def __init__(self, msg_type, user_id, openid_type, msg): 8 | self.msg_type = msg_type 9 | self.user_id = user_id 10 | self.openid_type = openid_type 11 | self.msg = msg 12 | 13 | def __str__(self): 14 | return "OAuthError: user:%s, openid_type:%s, %s, %s" % \ 15 | (self.user_id, self.openid_type, self.msg_type, self.msg) 16 | __repr__ = __str__ 17 | 18 | def set_the_profile(self, flush=False): 19 | if self.user_id: 20 | u = User.get(self.user_id) 21 | if u: 22 | if flush: 23 | u.set_thirdparty_profile_item(self.openid_type, self.msg_type, datetime.datetime.now()) 24 | else: 25 | p = u.get_thirdparty_profile(self.openid_type) 26 | t = p and p.get(self.msg_type) 27 | u.set_thirdparty_profile_item(self.openid_type, self.msg_type, t or datetime.datetime.now()) 28 | 29 | def clear_the_profile(self): 30 | if self.user_id: 31 | u = User.get(self.user_id) 32 | if u: 33 | u.set_thirdparty_profile_item(self.openid_type, self.msg_type, "") 34 | 35 | def is_exception_exists(self): 36 | if self.user_id: 37 | u = User.get(self.user_id) 38 | p = u and u.get_thirdparty_profile(self.openid_type) 39 | return p and p.get(self.msg_type) 40 | 41 | 42 | class OAuthTokenExpiredError(OAuthError): 43 | TYPE = "expired" 44 | def __init__(self, user_id=None, openid_type=None, msg=""): 45 | super(OAuthTokenExpiredError, self).__init__( 46 | OAuthTokenExpiredError.TYPE, user_id, openid_type, msg) 47 | 48 | class OAuthAccessError(OAuthError): 49 | TYPE = "access_error" 50 | def __init__(self, user_id=None, openid_type=None, msg=""): 51 | super(OAuthAccessError, self).__init__( 52 | OAuthAccessError.TYPE, user_id, openid_type, msg) 53 | 54 | 55 | class OAuthLoginError(OAuthError): 56 | TYPE = "login" 57 | def __init__(self, user_id=None, openid_type=None, msg=""): 58 | if isinstance(msg, TweepError): 59 | msg = "%s:%s" %(msg.reason, msg.response) 60 | super(OAuthLoginError, self).__init__( 61 | OAuthLoginError.TYPE, user_id, openid_type, msg) 62 | 63 | -------------------------------------------------------------------------------- /past/api/instagram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import urllib 4 | import urlparse 5 | from past import config 6 | from past.utils.logger import logging 7 | from past.utils.escape import json_decode 8 | from past.utils import httplib2_request 9 | 10 | from past.model.user import User, UserAlias, OAuth2Token 11 | from past.model.data import InstagramUser 12 | from past.model.data import InstagramStatusData 13 | 14 | from .oauth2 import OAuth2 15 | from .error import OAuthLoginError, OAuthTokenExpiredError 16 | 17 | log = logging.getLogger(__file__) 18 | 19 | class Instagram(OAuth2): 20 | 21 | authorize_uri = 'https://api.instagram.com/oauth/authorize/' 22 | access_token_uri = 'https://api.instagram.com/oauth/access_token' 23 | api_host = 'https://api.instagram.com' 24 | 25 | def __init__(self, alias=None, access_token=None, refresh_token=None, api_version="v1"): 26 | self.api_version = api_version 27 | d = config.APIKEY_DICT[config.OPENID_INSTAGRAM] 28 | super(Instagram, self).__init__(provider = config.OPENID_INSTAGRAM, 29 | apikey = d["key"], 30 | apikey_secret = d["secret"], 31 | redirect_uri = d["redirect_uri"], 32 | scope = "basic likes comments relationships", 33 | alias=alias, 34 | access_token=access_token, 35 | refresh_token=refresh_token) 36 | 37 | @classmethod 38 | def get_client(cls, user_id): 39 | alias = UserAlias.get_by_user_and_type(user_id, 40 | config.OPENID_TYPE_DICT[config.OPENID_INSTAGRAM]) 41 | if not alias: 42 | return None 43 | 44 | token = OAuth2Token.get(alias.id) 45 | if not token: 46 | return None 47 | 48 | return cls(alias.alias, token.access_token, token.refresh_token) 49 | 50 | def _request(self, api, method="GET", extra_dict=None): 51 | uri = urlparse.urljoin(self.api_host, api) 52 | if extra_dict is None: 53 | extra_dict = {} 54 | 55 | params = { 56 | "access_token": self.access_token, 57 | } 58 | params.update(extra_dict) 59 | qs = urllib.urlencode(params) 60 | uri = "%s?%s" % (uri, qs) 61 | 62 | log.info('getting %s...' % uri) 63 | resp, content = httplib2_request(uri, method) 64 | if resp.status == 200: 65 | return json_decode(content) if content else None 66 | else: 67 | log.warn("get %s fail, status code=%s, msg=%s" \ 68 | % (uri, resp.status, content)) 69 | 70 | def get_user_info(self, uid=None): 71 | uid = uid or self.user_alias.alias or "self" 72 | jdata = self._request("/v1/users/%s" % uid, "GET") 73 | if jdata and isinstance(jdata, dict): 74 | return InstagramUser(jdata.get("data")) 75 | 76 | def get_timeline(self, uid=None, min_id=None, max_id=None, count=100): 77 | d = {} 78 | d["count"] = count 79 | if min_id: 80 | d["min_id"] = min_id 81 | if max_id: 82 | d["max_id"] = max_id 83 | uid = uid or self.alias or "self" 84 | 85 | contents = self._request("/v1/users/%s/media/recent" %uid, "GET", d) 86 | ##debug 87 | if contents and isinstance(contents, dict): 88 | code = str(contents.get("meta", {}).get("code", "")) 89 | if code == "200": 90 | data = contents.get("data", []) 91 | print '---get instagram feed succ, result length is:', len(data) 92 | return [InstagramStatusData(c) for c in data] 93 | 94 | def get_home_timeline(self, uid=None, min_id=None, max_id=None, count=100): 95 | d = {} 96 | d["count"] = count 97 | if min_id: 98 | d["min_id"] = min_id 99 | if max_id: 100 | d["max_id"] = max_id 101 | uid = uid or self.alias or "self" 102 | 103 | contents = self._request("/v1/users/%s/feed" %uid, "GET", d) 104 | ##debug 105 | if contents and isinstance(contents, dict): 106 | code = str(contents.get("meta", {}).get("code", "")) 107 | if code == "200": 108 | data = contents.get("data", []) 109 | print '---get instagram home_timeline succ, result length is:', len(data) 110 | return [InstagramStatusData(c) for c in data] 111 | -------------------------------------------------------------------------------- /past/api/oauth2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import urllib 4 | from past import config 5 | from past.model.user import UserAlias 6 | from past.utils.escape import json_decode 7 | from past.utils import httplib2_request 8 | 9 | from .error import OAuthLoginError 10 | 11 | class OAuth2(object): 12 | 13 | authorize_uri = '' 14 | access_token_uri = '' 15 | api_host = '' 16 | 17 | def __init__(self, provider=None, apikey=None, apikey_secret=None, redirect_uri=None, 18 | scope=None, state=None, display=None, 19 | alias=None, access_token=None, refresh_token=None): 20 | 21 | self.provider = provider 22 | self.apikey = apikey 23 | self.apikey_secret = apikey_secret 24 | self.redirect_uri = redirect_uri 25 | 26 | self.scope = scope 27 | self.state = state 28 | self.display = display 29 | 30 | self.alias = alias 31 | if alias: 32 | self.user_alias = UserAlias.get( 33 | config.OPENID_TYPE_DICT[provider], alias) 34 | else: 35 | self.user_alias = None 36 | self.access_token = access_token 37 | self.refresh_token = refresh_token 38 | 39 | def __repr__(self): 40 | return '' % (self.provider, self.alias, self.access_token, 42 | self.refresh_token, self.api_host) 43 | __str__ = __repr__ 44 | 45 | def login(self): 46 | qs = { 47 | 'client_id' : self.apikey, 48 | 'response_type' : 'code', 49 | 'redirect_uri' : self.redirect_uri, 50 | } 51 | if self.display: 52 | qs['display'] = self.display 53 | if self.scope: 54 | qs['scope'] = self.scope 55 | if self.state: 56 | qs['state'] = self.state 57 | 58 | qs = urllib.urlencode(qs) 59 | uri = '%s?%s' %(self.authorize_uri, qs) 60 | 61 | return uri 62 | 63 | def get_access_token(self, authorization_code): 64 | qs = { 65 | "client_id": self.apikey, 66 | "client_secret": self.apikey_secret, 67 | "redirect_uri": self.redirect_uri, 68 | "grant_type": "authorization_code", 69 | "code": authorization_code, 70 | } 71 | qs = urllib.urlencode(qs) 72 | resp, content = httplib2_request(self.access_token_uri, "POST", body=qs) 73 | excp = OAuthLoginError(msg='get_access_token, status=%s,reason=%s,content=%s' \ 74 | %(resp.status, resp.reason, content)) 75 | if resp.status != 200: 76 | raise excp 77 | 78 | jdata = json_decode(content) if content else None 79 | return jdata 80 | 81 | 82 | def refresh_tokens(self): 83 | qs = { 84 | "grant_type": "refresh_token", 85 | "refresh_token": self.refresh_token, 86 | "client_id": self.apikey, 87 | "client_secret": self.apikey_secret, 88 | "redirect_uri": self.redirect_uri, 89 | } 90 | resp, content = httplib2_request(self.access_token_uri, "POST", 91 | body=urllib.urlencode(qs)) 92 | excp = OAuthLoginError(self.user_alias.user_id, self.provider, 93 | 'refresh_tokens, status=%s,reason=%s,content=%s' \ 94 | %(resp.status, resp.reason, content)) 95 | if resp.status != 200: 96 | raise excp 97 | 98 | jdata = json_decode(content) if content else None 99 | return jdata 100 | 101 | def set_token(self, access_token, refresh_token): 102 | self.access_token = access_token 103 | self.refresh_token = refresh_token 104 | 105 | def get_user_info(self, uid): 106 | raise NotImplementedError 107 | 108 | -------------------------------------------------------------------------------- /past/api/twitter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tweepy 4 | from tweepy.error import TweepError 5 | 6 | from past import config 7 | from past.utils.escape import json_encode, json_decode 8 | from past.utils import httplib2_request 9 | 10 | from past.model.user import User, UserAlias, OAuth2Token 11 | from past.model.user import OAuth2Token 12 | from past.model.data import TwitterUser 13 | from past.model.data import TwitterStatusData 14 | 15 | from .error import OAuthTokenExpiredError 16 | 17 | class TwitterOAuth1(object): 18 | provider = config.OPENID_TWITTER 19 | 20 | def __init__(self, alias=None, 21 | apikey=None, apikey_secret=None, redirect_uri=None, 22 | token=None, token_secret=None): 23 | 24 | d = config.APIKEY_DICT[config.OPENID_TWITTER] 25 | 26 | self.consumer_key = apikey or d['key'] 27 | self.consumer_secret = apikey_secret or d['secret'] 28 | self.callback = redirect_uri or d['redirect_uri'] 29 | 30 | self.token = token 31 | self.token_secret = token_secret 32 | 33 | self.alias = alias 34 | if alias: 35 | self.user_alias = UserAlias.get( 36 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER], alias) 37 | else: 38 | self.user_alias = None 39 | 40 | self.auth = tweepy.OAuthHandler(self.consumer_key, self.consumer_secret, self.callback) 41 | if self.token and self.token_secret and self.auth: 42 | self.auth.set_access_token(self.token, self.token_secret) 43 | 44 | def __repr__(self): 45 | return "" \ 46 | % (self.consumer_key, self.consumer_secret, self.token, self.token_secret) 47 | __str__ = __repr__ 48 | 49 | def login(self): 50 | return self.auth.get_authorization_url() 51 | 52 | def get_access_token(self, verifier=None): 53 | self.auth.get_access_token(verifier) 54 | t = {"access_token":self.auth.access_token.key, 55 | "access_token_secret": self.auth.access_token.secret,} 56 | return t 57 | 58 | def save_request_token_to_session(self, session_): 59 | t = {"key": self.auth.request_token.key, 60 | "secret": self.auth.request_token.secret,} 61 | session_['request_token'] = json_encode(t) 62 | 63 | def get_request_token_from_session(self, session_, delete=True): 64 | t = session_.get("request_token") 65 | token = json_decode(t) if t else {} 66 | if delete: 67 | self.delete_request_token_from_session(session_) 68 | return token 69 | 70 | def delete_request_token_from_session(self, session_): 71 | session_.pop("request_token", None) 72 | 73 | @classmethod 74 | def get_client(cls, user_id): 75 | alias = UserAlias.get_by_user_and_type(user_id, 76 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER]) 77 | if not alias: 78 | return None 79 | 80 | token = OAuth2Token.get(alias.id) 81 | if not token: 82 | return None 83 | 84 | return cls(alias=alias.alias, token=token.access_token, token_secret=token.refresh_token) 85 | 86 | def api(self): 87 | return tweepy.API(self.auth, parser=tweepy.parsers.JSONParser()) 88 | 89 | def get_user_info(self): 90 | user = self.api().me() 91 | return TwitterUser(user) 92 | 93 | def get_timeline(self, since_id=None, max_id=None, count=200): 94 | user_id = self.user_alias and self.user_alias.user_id or None 95 | try: 96 | contents = self.api().user_timeline(since_id=since_id, max_id=max_id, count=count) 97 | excp = OAuthTokenExpiredError(user_id, 98 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER], "") 99 | excp.clear_the_profile() 100 | return [TwitterStatusData(c) for c in contents] 101 | except TweepError, e: 102 | excp = OAuthTokenExpiredError(user_id, 103 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER], 104 | "%s:%s" %(e.reason, e.response)) 105 | excp.set_the_profile() 106 | raise excp 107 | 108 | def post_status(self, text): 109 | user_id = self.user_alias and self.user_alias.user_id or None 110 | try: 111 | self.api().update_status(status=text) 112 | excp = OAuthTokenExpiredError(user_id, 113 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER], "") 114 | excp.clear_the_profile() 115 | except TweepError, e: 116 | excp = OAuthTokenExpiredError(user_id, 117 | config.OPENID_TYPE_DICT[config.OPENID_TWITTER], 118 | "%s:%s" %(e.reason, e.response)) 119 | excp.set_the_profile() 120 | raise excp 121 | 122 | -------------------------------------------------------------------------------- /past/api/wordpress.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from past.store import mc 4 | from past.model.data import WordpressData 5 | from past.utils.logger import logging 6 | 7 | log = logging.getLogger(__file__) 8 | 9 | class Wordpress(object): 10 | 11 | WORDPRESS_ETAG_KEY = "wordpress:etag:%s" 12 | 13 | ## 同步wordpress rss 14 | def __init__(self, alias): 15 | ## alias means wordpress feed uri 16 | self.alias = alias 17 | 18 | def __repr__(self): 19 | return "" %(self.alias) 20 | __str__ = __repr__ 21 | 22 | def get_etag(self): 23 | r = str(Wordpress.WORDPRESS_ETAG_KEY % self.alias) 24 | return mc.get(r) 25 | 26 | def set_etag(self, etag): 27 | r = str(Wordpress.WORDPRESS_ETAG_KEY % self.alias) 28 | mc.set(r, etag) 29 | 30 | def get_feeds(self, refresh=True): 31 | import feedparser 32 | etag = self.get_etag() 33 | if refresh: 34 | d = feedparser.parse(self.alias) 35 | else: 36 | d = feedparser.parse(self.alias, etag=etag) 37 | if not d: 38 | return [] 39 | if not (d.status == 200 or d.status == 301): 40 | log.warning("---get wordpress feeds, status is %s, not valid" % d.status) 41 | return [] 42 | 43 | entries = d.entries 44 | if not entries: 45 | return [] 46 | 47 | if (not refresh) and hasattr(d, 'etag'): 48 | self.set_etag(d.etag) 49 | return [WordpressData(x) for x in entries] 50 | -------------------------------------------------------------------------------- /past/connect/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # blueprint: connect 3 | 4 | from flask import Blueprint 5 | from flask import redirect, render_template, g, abort, flash, url_for 6 | 7 | from past import config, consts 8 | 9 | blue_print = Blueprint("connect", __name__, template_folder="templates", static_folder="static") 10 | 11 | import view 12 | 13 | @blue_print.before_request 14 | def before_request(): 15 | print "--- in connect blue_print" 16 | 17 | 18 | -------------------------------------------------------------------------------- /past/consts.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import datetime 4 | 5 | YESTERDAY = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime("%Y-%m-%d") 6 | TODAY = datetime.datetime.now().strftime("%Y-%m-%d") 7 | TOMORROW = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime("%Y-%m-%d") 8 | 9 | 10 | YES = 'Y' 11 | NO = 'N' 12 | 13 | USER_PRIVACY_PRIVATE = 'X' 14 | USER_PRIVACY_PUBLIC = 'P' 15 | USER_PRIVACY_FRIEND = 'F' 16 | USER_PRIVACY_THEPAST = 'T' 17 | 18 | NOTE_FMT_PLAIN = 'P' 19 | NOTE_FMT_MARKDOWN = 'M' 20 | 21 | STATUS_PRIVACY_PRIVATE = 'X' 22 | STATUS_PRIVACY_PUBLIC = 'P' 23 | STATUS_PRIVACY_FRIEND = 'F' 24 | STATUS_PRIVACY_THEPAST = 'F' 25 | -------------------------------------------------------------------------------- /past/corelib/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from past import config 4 | from past.utils import randbytes 5 | 6 | from past.corelib.cache import cache 7 | 8 | def auth_user_from_session(session_): 9 | user = None 10 | if config.SITE_COOKIE in session_: 11 | cookies = session_[config.SITE_COOKIE] 12 | user_id, session_id = cookies.split(":") 13 | ## 这里存在循环引用,所以放到函数内部,长远看这个函数不应该放在corelib中 14 | from past.model.user import User 15 | _user = User.get(user_id) 16 | if _user and _user.session_id == session_id: 17 | user = _user 18 | 19 | return user 20 | 21 | def set_user_cookie(user, session_): 22 | if not user: 23 | return None 24 | session_id = user.session_id if user.session_id else randbytes(8) 25 | user.update_session(session_id) 26 | session_[config.SITE_COOKIE] = "%s:%s" % (user.id, session_id) 27 | 28 | def logout_user(user): 29 | if not user: 30 | return 31 | user.clear_session() 32 | 33 | def category2provider(cate): 34 | if cate < 200: 35 | return config.OPENID_DOUBAN 36 | elif cate < 300: 37 | return config.OPENID_SINA 38 | elif cate < 400: 39 | return config.OPENID_WORDPRESS 40 | elif cate < 500: 41 | return config.OPENID_TWITTER 42 | elif cate < 600: 43 | return config.OPENID_QQ 44 | elif cate < 700: 45 | return config.OPENID_THEPAST 46 | elif cate < 800: 47 | return config.OPENID_RENREN 48 | elif cate < 900: 49 | return config.OPENID_INSTAGRAM 50 | else: 51 | return None 52 | 53 | -------------------------------------------------------------------------------- /past/corelib/cache.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | '''from douban code, cool ''' 4 | 5 | import inspect 6 | from functools import wraps 7 | import time 8 | 9 | try: 10 | import cPickle as pickle 11 | except: 12 | import pickle 13 | 14 | from .empty import Empty 15 | from .format import format 16 | 17 | from past.store import mc 18 | 19 | # some time consts for mc expire 20 | HALF_HOUR = 1800 21 | ONE_HOUR = 3600 22 | HALF_DAY = ONE_HOUR * 12 23 | ONE_DAY = ONE_HOUR * 24 24 | ONE_WEEK = ONE_DAY * 7 25 | ONE_MONTH = ONE_DAY * 30 26 | 27 | 28 | def gen_key(key_pattern, arg_names, defaults, *a, **kw): 29 | return gen_key_factory(key_pattern, arg_names, defaults)(*a, **kw) 30 | 31 | 32 | def gen_key_factory(key_pattern, arg_names, defaults): 33 | args = dict(zip(arg_names[-len(defaults):], defaults)) if defaults else {} 34 | if callable(key_pattern): 35 | names = inspect.getargspec(key_pattern)[0] 36 | def gen_key(*a, **kw): 37 | aa = args.copy() 38 | aa.update(zip(arg_names, a)) 39 | aa.update(kw) 40 | if callable(key_pattern): 41 | key = key_pattern(*[aa[n] for n in names]) 42 | else: 43 | key = format(key_pattern, *[aa[n] for n in arg_names], **aa) 44 | return key and key.replace(' ','_'), aa 45 | return gen_key 46 | 47 | def cache_(key_pattern, mc, expire=0, max_retry=0): 48 | def deco(f): 49 | arg_names, varargs, varkw, defaults = inspect.getargspec(f) 50 | if varargs or varkw: 51 | raise Exception("do not support varargs") 52 | gen_key = gen_key_factory(key_pattern, arg_names, defaults) 53 | @wraps(f) 54 | def _(*a, **kw): 55 | key, args = gen_key(*a, **kw) 56 | if not key: 57 | return f(*a, **kw) 58 | if isinstance(key, unicode): 59 | key = key.encode("utf8") 60 | r = mc.get(key) 61 | 62 | # anti miss-storm 63 | retry = max_retry 64 | while r is None and retry > 0: 65 | time.sleep(0.1) 66 | r = mc.get(key) 67 | retry -= 1 68 | r = pickle.loads(r) if r else None 69 | 70 | if r is None: 71 | r = f(*a, **kw) 72 | if r is not None: 73 | mc.set(key, pickle.dumps(r), expire) 74 | 75 | if isinstance(r, Empty): 76 | r = None 77 | return r 78 | _.original_function = f 79 | return _ 80 | return deco 81 | 82 | def pcache_(key_pattern, mc, count=300, expire=0, max_retry=0): 83 | def deco(f): 84 | arg_names, varargs, varkw, defaults = inspect.getargspec(f) 85 | if varargs or varkw: 86 | raise Exception("do not support varargs") 87 | if not ('limit' in arg_names): 88 | raise Exception("function must has 'limit' in args") 89 | gen_key = gen_key_factory(key_pattern, arg_names, defaults) 90 | @wraps(f) 91 | def _(*a, **kw): 92 | key, args = gen_key(*a, **kw) 93 | start = args.pop('start', 0) 94 | limit = args.pop('limit') 95 | start = int(start) 96 | limit = int(limit) 97 | if not key or limit is None or start+limit > count: 98 | return f(*a, **kw) 99 | if isinstance(key, unicode): 100 | key = key.encode("utf8") 101 | r = mc.get(key) 102 | 103 | # anti miss-storm 104 | retry = max_retry 105 | while r is None and retry > 0: 106 | time.sleep(0.1) 107 | r = mc.get(key) 108 | retry -= 1 109 | r = pickle.loads(r) if r else None 110 | 111 | if r is None: 112 | r = f(limit=count, **args) 113 | mc.set(key, pickle.dumps(r), expire) 114 | return r[start:start+limit] 115 | 116 | _.original_function = f 117 | return _ 118 | return deco 119 | 120 | def delete_cache_(key_pattern, mc): 121 | def deco(f): 122 | arg_names, varargs, varkw, defaults = inspect.getargspec(f) 123 | if varargs or varkw: 124 | raise Exception("do not support varargs") 125 | gen_key = gen_key_factory(key_pattern, arg_names, defaults) 126 | @wraps(f) 127 | def _(*a, **kw): 128 | key, args = gen_key(*a, **kw) 129 | r = f(*a, **kw) 130 | mc.delete(key) 131 | return r 132 | return _ 133 | _.original_function = f 134 | return deco 135 | 136 | def create_decorators(mc): 137 | 138 | def _cache(key_pattern, expire=0, mc=mc, max_retry=0): 139 | return cache_(key_pattern, mc, expire=expire, max_retry=max_retry) 140 | 141 | def _pcache(key_pattern, count=300, expire=0, max_retry=0): 142 | return pcache_(key_pattern, mc, count=count, expire=expire, max_retry=max_retry) 143 | 144 | def _delete_cache(key_pattern): 145 | return delete_cache_(key_pattern, mc=mc) 146 | 147 | return dict(cache=_cache, pcache=_pcache, delete_cache=_delete_cache) 148 | 149 | 150 | globals().update(create_decorators(mc)) 151 | 152 | -------------------------------------------------------------------------------- /past/corelib/empty.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | empty.py 4 | from douban code, cool 5 | """ 6 | 7 | class Empty(object): 8 | def __call__(self, *a, **kw): 9 | return empty 10 | def __nonzero__(self): 11 | return False 12 | def __contains__(self, item): 13 | return False 14 | def __repr__(self): 15 | return '' 16 | def __str__(self): 17 | return '' 18 | def __eq__(self, v): 19 | return isinstance(v, Empty) 20 | def __getattr__(self, name): 21 | if not name.startswith('__'): 22 | return empty 23 | raise AttributeError(name) 24 | def __len__(self): 25 | return 0 26 | def __getitem__(self, key): 27 | return empty 28 | def __setitem__(self, key, value): 29 | pass 30 | def __delitem__(self, key): 31 | pass 32 | def __iter__(self): 33 | return self 34 | def next(self): 35 | raise StopIteration 36 | 37 | empty = Empty() 38 | -------------------------------------------------------------------------------- /past/corelib/format.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | 3 | import re 4 | 5 | old_pattern = re.compile(r'%\w') 6 | new_pattern = re.compile(r'\{(\w+(\.\w+|\[\w+\])?)\}') 7 | 8 | __formaters = {} 9 | 10 | def format(text, *a, **kw): 11 | f = __formaters.get(text) 12 | if f is None: 13 | f = formater(text) 14 | __formaters[text] = f 15 | return f(*a, **kw) 16 | #return formater(text)(*a, **kw) 17 | 18 | def formater(text): 19 | """ 20 | >>> format('%s %s', 3, 2, 7, a=7, id=8) 21 | '3 2' 22 | >>> format('%(a)d %(id)s', 3, 2, 7, a=7, id=8) 23 | '7 8' 24 | >>> format('{1} {id}', 3, 2, a=7, id=8) 25 | '2 8' 26 | >>> class Obj: id = 3 27 | >>> format('{obj.id} {0.id}', Obj(), obj=Obj()) 28 | '3 3' 29 | """ 30 | # def arg(k,a,kw): 31 | # if k.isdigit(): 32 | # return a[int(k)] 33 | # return kw[k] 34 | def translator(k): 35 | if '.' in k: 36 | name,attr = k.split('.') 37 | if name.isdigit(): 38 | k = int(name) 39 | return lambda *a, **kw: getattr(a[k], attr) 40 | return lambda *a, **kw: getattr(kw[name], attr) 41 | # elif '[' in k and k.endswith(']'): 42 | # name,index = k[:k.index('[')],k[k.index('[')+1:-1] 43 | # def _(*a, **kw): 44 | # if index.isdigit(): 45 | # return arg(name,a,kw)[int(index)] 46 | # return arg(name,a,kw)[index] 47 | # return _ 48 | else: 49 | if k.isdigit(): 50 | return lambda *a, **kw: a[int(k)] 51 | return lambda *a, **kw: kw[k] 52 | args = [translator(k) for k,_1 in new_pattern.findall(text)] 53 | if args: 54 | if old_pattern.findall(text): 55 | raise Exception('mixed format is not allowed') 56 | f = new_pattern.sub('%s', text) 57 | def _(*a, **kw): 58 | return f % tuple([k(*a,**kw) for k in args]) 59 | return _ 60 | elif '%(' in text: 61 | return lambda *a, **kw: text % kw 62 | else: 63 | n = len(old_pattern.findall(text)) 64 | return lambda *a, **kw: text % tuple(a[:n]) 65 | 66 | if __name__ == '__main__': 67 | import doctest 68 | doctest.testmod() 69 | -------------------------------------------------------------------------------- /past/dev.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP TABLE IF EXISTS `confirmation`; 3 | 4 | CREATE TABLE `confirmation` ( 5 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 6 | `random_id` varchar(16) NOT NULL, 7 | `text` varchar(128) NOT NULL, 8 | `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 9 | PRIMARY KEY (`id`), 10 | UNIQUE KEY `uniq_idx_random` (`random_id`) 11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='confirmation'; 12 | -------------------------------------------------------------------------------- /past/dev/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # blueprint: dev 3 | 4 | from flask import Blueprint 5 | from flask import redirect, render_template, g, abort, flash, url_for 6 | 7 | from past import config, consts 8 | 9 | blue_print = Blueprint("dev", __name__, template_folder="templates", static_folder="static") 10 | 11 | from .view import token 12 | from .view import api 13 | 14 | @blue_print.before_request 15 | def before_request(): 16 | print "--- in dev blue_print" 17 | 18 | 19 | -------------------------------------------------------------------------------- /past/dev/view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/dev/view/__init__.py -------------------------------------------------------------------------------- /past/dev/view/api.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # blueprint: dev -> view -> api 3 | 4 | from past.dev import blue_print 5 | 6 | from past.utils.logger import logging 7 | log = logging.getLogger(__file__) 8 | 9 | @blue_print.route("/api") 10 | def api_index(): 11 | return "ok" 12 | 13 | -------------------------------------------------------------------------------- /past/dev/view/token.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # blueprint: dev -> view -> token 3 | 4 | from past.dev import blue_print 5 | 6 | from past.utils.logger import logging 7 | log = logging.getLogger(__file__) 8 | 9 | @blue_print.route("/token") 10 | def token_index(): 11 | return "ok" 12 | 13 | -------------------------------------------------------------------------------- /past/model/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /past/model/kv.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from MySQLdb import IntegrityError 4 | 5 | from past.corelib.cache import cache 6 | from past.store import db_conn, mc 7 | from past.utils.escape import json_encode, json_decode 8 | 9 | class Kv(object): 10 | def __init__(self, key_, val, time): 11 | self.key_ = key_ 12 | self.val = val 13 | self.time = time 14 | 15 | @classmethod 16 | def clear_cache(cls, key_): 17 | mc.delete("mc_kv:%s" %key_) 18 | 19 | @classmethod 20 | @cache("mc_kv:{key_}") 21 | def get(cls, key_): 22 | cursor = db_conn.execute('''select `key`, value, time from kv 23 | where `key`=%s''', key_) 24 | row = cursor.fetchone() 25 | if row: 26 | return cls(*row) 27 | cursor and cursor.close() 28 | 29 | @classmethod 30 | def set(cls, key_, val): 31 | cursor = None 32 | val = json_encode(val) if not isinstance(val, basestring) else val 33 | 34 | try: 35 | cursor = db_conn.execute('''replace into kv(`key`, value) 36 | values(%s,%s)''', (key_, val)) 37 | db_conn.commit() 38 | cls.clear_cache(key_) 39 | except IntegrityError: 40 | db_conn.rollback() 41 | 42 | cursor and cursor.close() 43 | 44 | @classmethod 45 | def remove(cls, key_): 46 | cursor = None 47 | try: 48 | cursor = db_conn.execute('''delete from kv where `key` = %s''', key_) 49 | db_conn.commit() 50 | cls.clear_cache(key_) 51 | except IntegrityError: 52 | db_conn.rollback() 53 | cursor and cursor.close() 54 | 55 | class UserProfile(object): 56 | def __init__(self, user_id, val, time): 57 | self.user_id = user_id 58 | self.val = val 59 | self.time = time 60 | 61 | @classmethod 62 | def clear_cache(cls, user_id): 63 | mc.delete("mc_user_profile:%s" %user_id) 64 | 65 | @classmethod 66 | @cache("mc_user_profile:{user_id}") 67 | def get(cls, user_id): 68 | cursor = db_conn.execute('''select user_id, profile, time from user_profile 69 | where user_id=%s''', user_id) 70 | row = cursor.fetchone() 71 | if row: 72 | return cls(*row) 73 | cursor and cursor.close() 74 | 75 | @classmethod 76 | def set(cls, user_id, val): 77 | cursor = None 78 | val = json_encode(val) if not isinstance(val, basestring) else val 79 | 80 | try: 81 | cursor = db_conn.execute('''replace into user_profile (user_id, profile) 82 | values(%s,%s)''', (user_id, val)) 83 | db_conn.commit() 84 | cls.clear_cache(user_id) 85 | except IntegrityError: 86 | db_conn.rollback() 87 | 88 | cursor and cursor.close() 89 | 90 | @classmethod 91 | def remove(cls, user_id): 92 | cursor = None 93 | try: 94 | cursor = db_conn.execute('''delete from user_profile where user_id= %s''', user_id) 95 | db_conn.commit() 96 | cls.clear_cache(user_id) 97 | except IntegrityError: 98 | db_conn.rollback() 99 | cursor and cursor.close() 100 | 101 | class RawStatus(object): 102 | def __init__(self, status_id, text, raw, time): 103 | self.status_id = status_id 104 | self.text = text 105 | self.raw = raw 106 | self.time = time 107 | 108 | @classmethod 109 | def clear_cache(cls, status_id): 110 | mc.delete("mc_raw_status:%s" %status_id) 111 | 112 | @classmethod 113 | @cache("mc_raw_status:{status_id}") 114 | def get(cls, status_id): 115 | cursor = db_conn.execute('''select status_id, text, raw, time from raw_status 116 | where status_id=%s''', status_id) 117 | row = cursor.fetchone() 118 | if row: 119 | return cls(*row) 120 | cursor and cursor.close() 121 | 122 | @classmethod 123 | def set(cls, status_id, text, raw): 124 | cursor = None 125 | text = json_encode(text) if not isinstance(text, basestring) else text 126 | raw = json_encode(raw) if not isinstance(raw, basestring) else raw 127 | 128 | try: 129 | cursor = db_conn.execute('''replace into raw_status (status_id, text, raw) 130 | values(%s,%s,%s)''', (status_id, text, raw)) 131 | db_conn.commit() 132 | cls.clear_cache(status_id) 133 | except IntegrityError: 134 | db_conn.rollback() 135 | 136 | cursor and cursor.close() 137 | 138 | @classmethod 139 | def remove(cls, status_id): 140 | cursor = None 141 | try: 142 | cursor = db_conn.execute('''delete from raw_status where status_id = %s''', status_id ) 143 | db_conn.commit() 144 | cls.clear_cache(status_id) 145 | except IntegrityError: 146 | db_conn.rollback() 147 | cursor and cursor.close() 148 | -------------------------------------------------------------------------------- /past/model/note.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import markdown2 4 | from MySQLdb import IntegrityError 5 | import datetime 6 | 7 | from past.store import db_conn, mc 8 | from past.corelib.cache import cache, pcache, HALF_HOUR 9 | from past.utils.escape import json_encode, json_decode 10 | from past import consts 11 | from past import config 12 | 13 | class Note(object): 14 | 15 | def __init__(self, id, user_id, title, content, create_time, update_time, fmt, privacy): 16 | self.id = id 17 | self.user_id = str(user_id) 18 | self.title = title 19 | self.content = content 20 | self.create_time = create_time 21 | self.update_time = update_time 22 | self.fmt = fmt 23 | self.privacy = privacy 24 | 25 | @classmethod 26 | def _clear_cache(cls, user_id, note_id): 27 | if user_id: 28 | mc.delete("note_ids:%s" % user_id) 29 | mc.delete("note_ids_asc:%s" % user_id) 30 | if note_id: 31 | mc.delete("note:%s" % note_id) 32 | 33 | def flush_note(self): 34 | Note._clear_cache(None, self.id) 35 | return Note.get(self.id) 36 | 37 | @classmethod 38 | @cache("note:{id}") 39 | def get(cls, id): 40 | cursor = db_conn.execute('''select id, user_id, title, content, create_time, update_time, fmt, privacy 41 | from note where id = %s''', id) 42 | row = cursor.fetchone() 43 | if row: 44 | return cls(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7]) 45 | 46 | def render_content(self): 47 | if self.fmt == consts.NOTE_FMT_MARKDOWN: 48 | return markdown2.markdown(self.content, extras=["wiki-tables", "code-friendly"]) 49 | else: 50 | return self.content 51 | 52 | @classmethod 53 | def add(cls, user_id, title, content, fmt=consts.NOTE_FMT_PLAIN, privacy=consts.STATUS_PRIVACY_PUBLIC): 54 | cursor = None 55 | try: 56 | cursor = db_conn.execute('''insert into note (user_id, title, content, create_time, fmt, privacy) 57 | values (%s, %s, %s, %s, %s, %s)''', 58 | (user_id, title, content, datetime.datetime.now(), fmt, privacy)) 59 | db_conn.commit() 60 | 61 | note_id = cursor.lastrowid 62 | note = cls.get(note_id) 63 | from past.model.status import Status 64 | Status.add(user_id, note_id, 65 | note.create_time, config.OPENID_TYPE_DICT[config.OPENID_THEPAST], 66 | config.CATE_THEPAST_NOTE, "") 67 | cls._clear_cache(user_id, None) 68 | return note 69 | except IntegrityError: 70 | db_conn.rollback() 71 | finally: 72 | cursor and cursor.close() 73 | 74 | def update(self, title, content, fmt, privacy): 75 | if title and title != self.title or fmt and fmt != self.fmt or content and content != self.content or privacy and privacy != self.privacy: 76 | _fmt = fmt or self.fmt 77 | _title = title or self.title 78 | _content = content or self.content 79 | _privacy = privacy or self.privacy 80 | db_conn.execute('''update note set title = %s, content = %s, fmt = %s, privacy = %s where id = %s''', 81 | (_title, _content, _fmt, _privacy, self.id)) 82 | db_conn.commit() 83 | self.flush_note() 84 | 85 | if title != self.title: 86 | from past.model.status import Status 87 | Status._clear_cache(None, self.get_status_id(), None) 88 | 89 | @cache("note:status_id:{self.id}") 90 | def get_status_id(self): 91 | cursor = db_conn.execute("""select id from status where origin_id = %s and category = %s""", 92 | (self.id, config.CATE_THEPAST_NOTE)) 93 | row = cursor.fetchone() 94 | cursor and cursor.close() 95 | return row and row[0] 96 | 97 | @classmethod 98 | def delete(cls, id): 99 | note = cls.get(id) 100 | if note: 101 | db_conn.execute("""delete from status where id=%s""", id) 102 | db_conn.commit() 103 | cls._clear_cache(note.user_id, note.id) 104 | 105 | @classmethod 106 | @pcache("note_ids:user:{user_id}") 107 | def get_ids_by_user(cls, user_id, start, limit): 108 | return cls._get_ids_by_user(user_id, start, limit) 109 | 110 | @classmethod 111 | @pcache("note_ids_asc:user:{user_id}") 112 | def get_ids_by_user_asc(cls, user_id, start, limit): 113 | return cls._get_ids_by_user(user_id, start, limit, order="create_time asc") 114 | 115 | @classmethod 116 | def _get_ids_by_user(cls, user_id, start=0, limit=20, order="create_time desc"): 117 | sql = """select id from note where user_id=%s order by """ + order \ 118 | + """ limit %s,%s""" 119 | cursor = db_conn.execute(sql, (user_id, start, limit)) 120 | rows = cursor.fetchall() 121 | return [x[0] for x in rows] 122 | 123 | @classmethod 124 | def gets(cls, ids): 125 | return [cls.get(x) for x in ids] 126 | -------------------------------------------------------------------------------- /past/model/user_tokens.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # for dev -> api 3 | 4 | from MySQLdb import IntegrityError 5 | from past.corelib.cache import cache, pcache 6 | from past.store import mc, db_conn 7 | 8 | class UserTokens(object): 9 | def __init__(self, id, user_id, token, device): 10 | self.id = id 11 | self.user_id = str(user_id) 12 | self.token = str(token) 13 | self.device = device 14 | 15 | def __repr__(self): 16 | return "" \ 17 | % (self.id, self.user_id, self.token, self.device) 18 | __str__ = __repr__ 19 | 20 | @classmethod 21 | @cache("user_token:{id}") 22 | def get(cls, id): 23 | return cls._find_by("id", id) 24 | 25 | @classmethod 26 | @cache("user_token:{token}") 27 | def get_by_token(cls, token): 28 | return cls._find_by("token", token) 29 | 30 | @classmethod 31 | @cache("user_token_ids:{user_id}") 32 | def get_ids_by_user_id(cls, user_id): 33 | r = cls._find_by("user_id", user_id, limit=0) 34 | if r: 35 | return [r.id for x in r] 36 | else: 37 | return [] 38 | 39 | @classmethod 40 | def add(cls, user_id, token, device=""): 41 | cursor = None 42 | try: 43 | cursor = db_conn.execute('''insert into user_tokens (user_id, token, device) 44 | values (%s, %s, %s)''', (user_id, token, device)) 45 | id_ = cursor.lastrowid 46 | db_conn.commit() 47 | return cls.get(id_) 48 | except IntegrityError: 49 | db_conn.rollback() 50 | finally: 51 | cursor and cursor.close() 52 | 53 | def remove(self): 54 | db_conn.execute('''delete from user_tokens where id=%s''', self.id) 55 | db_conn.commit() 56 | self._clear_cache() 57 | 58 | def _clear_cache(self): 59 | mc.delete("user_token:%s" % self.id) 60 | mc.delete("user_token:%s" % self.token) 61 | mc.delete("user_token_ids:%s" % self.user_id) 62 | 63 | @classmethod 64 | def _find_by(cls, col, value, start=0, limit=1): 65 | assert limit >= 0 66 | if limit == 0: 67 | cursor = db_conn.execute("""select id, user_id, token, device 68 | from user_tokens where `""" + col + """`=%s""", value) 69 | else: 70 | cursor = db_conn.execute("""select id, user_id, token, device 71 | from user_tokens where `""" + col + """`=%s limit %s, %s""", (value, start, limit)) 72 | if limit == 1: 73 | row = cursor.fetchone() 74 | cursor and cursor.close() 75 | return row and cls(*row) 76 | else: 77 | rows = cursor.fetchall() 78 | cursor and cursor.close() 79 | return [cls(*row) for row in rows] 80 | -------------------------------------------------------------------------------- /past/model/weixin.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from MySQLdb import IntegrityError 4 | from past.corelib.cache import cache, pcache 5 | from past.store import mc, db_conn 6 | 7 | class UserWeixin(object): 8 | def __init__(self, user_id, weixin_name): 9 | self.user_id = str(user_id) 10 | self.weixin_name = weixin_name 11 | 12 | def __repr__(self): 13 | return "" \ 14 | %(self.user_id, self.weixin_name) 15 | __str__ = __repr__ 16 | 17 | @classmethod 18 | @cache("user_weixin:{weixin_name}") 19 | def get_by_weixin(cls, weixin_name): 20 | return cls._find_by("weixin_name", weixin_name) 21 | 22 | @classmethod 23 | def add(cls, user_id, weixin_name): 24 | cursor = None 25 | try: 26 | cursor = db_conn.execute('''insert into user_weixin (user_id, weixin_name) 27 | values (%s, %s) on duplicate key update user_id=%s''', (user_id, weixin_name, user_id)) 28 | db_conn.commit() 29 | cls.clear_cache(user_id, weixin_name) 30 | return cls.get_by_weixin(weixin_name) 31 | except IntegrityError: 32 | db_conn.rollback() 33 | finally: 34 | cursor and cursor.close() 35 | 36 | @classmethod 37 | def clear_cache(cls, user_id, weixin_name): 38 | mc.delete("user_weixin:%s" % weixin_name) 39 | 40 | @classmethod 41 | def _find_by(cls, col, value, start=0, limit=1): 42 | assert limit >= 0 43 | if limit == 0: 44 | cursor = db_conn.execute("""select user_id, weixin_name 45 | from user_weixin where `""" + col + """`=%s""", value) 46 | else: 47 | cursor = db_conn.execute("""select user_id, weixin_name 48 | from user_weixin where `""" + col + """`=%s limit %s, %s""", (value, start, limit)) 49 | if limit == 1: 50 | row = cursor.fetchone() 51 | cursor and cursor.close() 52 | return row and cls(*row) 53 | else: 54 | rows = cursor.fetchall() 55 | cursor and cursor.close() 56 | return [cls(*row) for row in rows] 57 | -------------------------------------------------------------------------------- /past/static/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

thepast正在维护,稍后就回来,有问题可以发邮件到help@thepast.me

7 | 8 | 9 | -------------------------------------------------------------------------------- /past/static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /past/static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /past/static/calendar/css/bootstrap.calendar.css: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * bootstrap-calendar plugin 4 | * Original author: @ahmontero 5 | * Licensed under the MIT license 6 | * 7 | */ 8 | 9 | .calendar body { 10 | background-color: #FFFFFF; 11 | position: relative; 12 | } 13 | 14 | .year { 15 | color: rgba(0, 0, 0, 0.3); 16 | font-size: 15px; 17 | margin: 0px 0px 0px 0px; 18 | } 19 | 20 | .month { 21 | color: rgba(0, 0, 0, 0.3); 22 | font-size: 15px; 23 | margin: -35px 0px 0px 120px; 24 | } 25 | 26 | .calendar { 27 | table-layout:fixed; 28 | } 29 | 30 | .calendar TH { 31 | width: 30px; 32 | height: 30px; 33 | 34 | font-size: 10px; 35 | font-weight: bold; 36 | color: rgba(0, 0, 0, 0.3); 37 | text-align: center; 38 | 39 | -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s; 40 | -moz-transition: background 0.2s linear 0s; 41 | 42 | border-color: rgba(0, 0, 0, 0.1); 43 | border: 1px solid rgba(0, 0, 0, 0.1); 44 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset; 45 | } 46 | 47 | .calendar TD, TD.day{ 48 | height: 30px; 49 | width: 30px; 50 | 51 | font-size: 10px; 52 | font-weight: bold; 53 | color: rgba(0, 0, 0, 0.3); 54 | text-align: center; 55 | 56 | -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s; 57 | -moz-transition: background 0.2s linear 0s; 58 | 59 | border: 1px solid rgba(0, 0, 0, 0.1); 60 | border-color: rgba(0, 0, 0, 0.1); 61 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset; 62 | } 63 | 64 | .calendar TD.day:hover{ 65 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.025) inset, 0 0 10px rgba(0, 0, 0, 0.1); 66 | background: rgba(0, 0, 0, 0.1); 67 | outline: 0 none; 68 | cursor: pointer; 69 | color: #FFFFFF; 70 | } 71 | 72 | .calendar TD.weekend { 73 | color: rgba(0, 0, 0, 0.1); 74 | } 75 | .calendar TD.weekend:hover{ 76 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 10px rgba(82, 168, 236, 0.6); 77 | background: rgba(0, 0, 0, 0.55); 78 | outline: 0 none; 79 | cursor: pointer; 80 | color: #FFFFFF; 81 | } 82 | 83 | .calendar TD.today { 84 | border: 2px solid red; 85 | color: rgba(0, 0, 0, 0.60); 86 | } 87 | 88 | .calendar TD.today:hover{ 89 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 10px rgba(82, 168, 236, 0.6); 90 | background: rgba(0, 0, 0, 0.55); 91 | outline: 0 none; 92 | cursor: pointer; 93 | color: #FFFFFF; 94 | } 95 | 96 | .calendar TD.holiday { 97 | color: red; 98 | } 99 | 100 | .calendar TD.holiday:hover { 101 | cursor: pointer; 102 | } 103 | 104 | .calendar TD SPAN.weekday{ 105 | background-color: rgba(0, 0, 0, 0.1); 106 | border-radius: 14%; 107 | color: #FFFFFF; 108 | font-size: 12px; 109 | font-weight: bold; 110 | padding: 1px 2px 1px; 111 | white-space: nowrap; 112 | } 113 | 114 | .calendar TD SPAN.weekday:hover{ 115 | background-color: rgba(0, 0, 0, 0.25); 116 | } 117 | 118 | .calendar TFOOT, .calendar TFOOT TR TH.sel { 119 | height: 12px; 120 | width: 12px; 121 | 122 | font-size: 10px; 123 | font-weight: bold; 124 | color: rgba(0, 0, 0, 0.3); 125 | text-align: center; 126 | 127 | -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s; 128 | -moz-transition: background 0.2s linear 0s; 129 | 130 | border: 1px solid rgba(0, 0, 0, 0.1); 131 | border-color: rgba(0, 0, 0, 0.1); 132 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset; 133 | 134 | cursor:pointer; 135 | } 136 | 137 | .calendar TFOOT TR TH.sel:hover { 138 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.025) inset, 0 0 10px rgba(0, 0, 0, 0.1); 139 | background: rgba(0, 0, 0, 0.1); 140 | outline: 0 none; 141 | cursor: pointer; 142 | color: #FFFFFF; 143 | } 144 | 145 | .calendar .arrow{ 146 | padding:2px 0px 0px 0px; 147 | } 148 | 149 | -------------------------------------------------------------------------------- /past/static/css/magnify.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/css/magnify.cur -------------------------------------------------------------------------------- /past/static/css/new.css: -------------------------------------------------------------------------------- 1 | body{ 2 | font-size:12px; 3 | } 4 | 5 | .member{ 6 | font-size:13.5px; 7 | width:215px; 8 | height:48px; 9 | float:left; 10 | border:1px solid #FFF; 11 | margin:0 10px 5px 0; 12 | box-shadow:0px 0px 2px #CCC; 13 | line-height:24px; 14 | overflow:hidden; 15 | background-color:rgba(170, 170, 170, 0.07); 16 | } 17 | .member .avatar{ 18 | width:48px; 19 | height:48px; 20 | float:left; 21 | } 22 | .member .avatar img{ 23 | width:48px; 24 | height:48px; 25 | } 26 | .member .entry{ 27 | margin-left:55px; 28 | } 29 | .member .name{ 30 | margin:0 6px 2px 4px; 31 | } 32 | .member .name span{ 33 | float:right; 34 | font-size:12px; 35 | color:#AAA; 36 | } 37 | 38 | .sep20 {height:20px;} 39 | .sep10 {height:10px;} 40 | .sep5 {height:5px;} 41 | .sep3 {height:3px;} 42 | .sep1 {height:1px;} 43 | 44 | .box { 45 | -webkit-border-radius: 2px; 46 | -moz-border-radius: 2px; 47 | -o-border-radius: 2px; 48 | border-radius: 2px; 49 | 50 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); 51 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); 52 | -o-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); 53 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); 54 | 55 | background-color: #fff; 56 | /*background-color: #fff;*/ 57 | border: 0.5px solid #F1EDED; 58 | } 59 | .box .cell { 60 | padding: 10px; 61 | font-size: 12px; 62 | line-height: 16px; 63 | border-bottom: 1px solid #e2e2e2; 64 | } 65 | .box .bar { 66 | font-size: 14px; 67 | line-height: 14px; 68 | color: #667; 69 | background-color: #f0f0f0; 70 | -moz-border-radius: 4px; 71 | -webkit-border-radius: 4px; 72 | padding: 5px; 73 | display: inline-block; 74 | } 75 | .box .inner { 76 | padding: 10px; 77 | font-size: 12px; 78 | line-height: 16px; 79 | } 80 | 81 | .the-center { 82 | margin:0 auto; 83 | text-align:center; 84 | } 85 | 86 | #timeline-container{ 87 | width:901px; 88 | margin:20px auto; 89 | } 90 | -------------------------------------------------------------------------------- /past/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/favicon.ico -------------------------------------------------------------------------------- /past/static/fbtimeline/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/fbtimeline/images/.DS_Store -------------------------------------------------------------------------------- /past/static/fbtimeline/images/active-unit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/fbtimeline/images/active-unit.png -------------------------------------------------------------------------------- /past/static/fbtimeline/images/icons-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/fbtimeline/images/icons-2.png -------------------------------------------------------------------------------- /past/static/fbtimeline/images/icons-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/fbtimeline/images/icons-3.png -------------------------------------------------------------------------------- /past/static/fbtimeline/images/icons-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/fbtimeline/images/icons-4.png -------------------------------------------------------------------------------- /past/static/fbtimeline/images/icons-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/fbtimeline/images/icons-5.png -------------------------------------------------------------------------------- /past/static/fbtimeline/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/fbtimeline/images/icons.png -------------------------------------------------------------------------------- /past/static/fbtimeline/images/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/fbtimeline/images/line.png -------------------------------------------------------------------------------- /past/static/fbtimeline/javascripts/all.js: -------------------------------------------------------------------------------- 1 | //= require_tree . -------------------------------------------------------------------------------- /past/static/fbtimeline/stylesheets/all.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body{ 4 | background:#E7EBF2; 5 | font-size:12px; 6 | line-height:1.4; 7 | font-family:'lucida grande',tahoma,verdana,arial,sans-serif; 8 | } 9 | 10 | .container{ 11 | width:901px; 12 | margin:20px auto; 13 | } 14 | 15 | a, a:visited{ 16 | color:#3B5998; 17 | text-decoration:none; 18 | } 19 | 20 | a:hover, a:focus{ 21 | text-decoration:underline; 22 | } 23 | 24 | img{ 25 | vertical-align:top; 26 | } 27 | 28 | /* Types */ 29 | 30 | p{ 31 | margin:0 0 15px 0; 32 | } 33 | 34 | h4{ 35 | margin:0; 36 | padding:0; 37 | } 38 | 39 | 40 | textarea{ 41 | border:none; 42 | width:100%; 43 | padding:0; 44 | font-size:13px; 45 | } 46 | 47 | /* 48 | * Contain floats: h5bp.com/q 49 | */ 50 | 51 | .clearfix:before, 52 | .clearfix:after { 53 | content: ""; 54 | display: table; 55 | } 56 | 57 | .clearfix:after { 58 | clear: both; 59 | } 60 | 61 | .clearfix { 62 | *zoom: 1; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /past/static/fbtimeline/stylesheets/timeline.css: -------------------------------------------------------------------------------- 1 | /* Timeline */ 2 | .timeline { 3 | background: url(../images/line.png) top repeat-y; 4 | list-style: none; 5 | padding: 0; 6 | margin: 0; 7 | position: relative; 8 | /* Highlight */ 9 | 10 | /* Spine */ 11 | 12 | } 13 | .timeline > li { 14 | float: left; 15 | clear: left; 16 | width: 436px; 17 | margin-bottom: 15px; 18 | position: relative; 19 | } 20 | .timeline > .right { 21 | float: right; 22 | clear: right; 23 | } 24 | .timeline .pointer { 25 | background: url(../images/icons-4.png) -41px -28px no-repeat; 26 | width: 19px; 27 | height: 15px; 28 | position: absolute; 29 | right: -18px; 30 | top: 20px; 31 | } 32 | .timeline .right > .pointer { 33 | left: -18px; 34 | right: auto; 35 | background-position: -61px -28px; 36 | } 37 | .timeline > .left + .right > .pointer { 38 | top: 40px; 39 | } 40 | .timeline > .right + li > .pointer { 41 | top: 40px; 42 | } 43 | .timeline .highlight { 44 | clear: both; 45 | width: auto; 46 | float: none; 47 | } 48 | .timeline .highlight .pointer { 49 | background-image: url(../images/icons-5.png); 50 | background-position: -26px -28px; 51 | height: 21px; 52 | width: 15px; 53 | left: 50%; 54 | top: -20px !important; 55 | margin-left: -7px; 56 | } 57 | .timeline .spine { 58 | position: absolute; 59 | left: 436px; 60 | width: 29px; 61 | height: 100%; 62 | } 63 | .timeline .spine > a, 64 | .timeline .spine a:visited { 65 | display: block; 66 | height: 100%; 67 | } 68 | /* Unit */ 69 | .unit { 70 | background: #fff; 71 | padding: 5px; 72 | border: 1px #C4CDE0 solid; 73 | border-radius: 3px; 74 | } 75 | .storyUnit { 76 | padding: 10px; 77 | } 78 | .imageUnit { 79 | border-bottom: 1px #ccc solid; 80 | padding-bottom: 5px; 81 | margin-bottom: 15px; 82 | font-size: 11px; 83 | } 84 | .imageUnit .imageUnit-content { 85 | display: inline-block; 86 | vertical-align: top; 87 | padding-left: 5px; 88 | } 89 | .imageUnit .imageUnit-content > p { 90 | margin: 0; 91 | } 92 | .formUnit { 93 | border: 1px solid #B4BBCD; 94 | padding: 5px; 95 | margin-top: 5px; 96 | position: relative; 97 | } 98 | .formUnit .active { 99 | background: url(../images/active-unit.png) top left no-repeat; 100 | position: absolute; 101 | left: 10px; 102 | top: -6px; 103 | width: 9px; 104 | height: 6px; 105 | } 106 | .photoUnit { 107 | margin: 5px 0px; 108 | } 109 | .controls { 110 | background: #F2F2F2; 111 | margin: 5px -5px -5px -5px; 112 | list-style: none; 113 | padding: 4px; 114 | border-top: 1px #E6E6E6 solid; 115 | } 116 | .controls > li { 117 | display: inline-block; 118 | } 119 | .controls .post { 120 | float: right; 121 | } 122 | /* Actions */ 123 | .actions { 124 | list-style: none; 125 | padding: 0; 126 | margin: 0; 127 | overflow: hidden; 128 | font-weight: bold; 129 | font-size: 11px; 130 | } 131 | .actions > li { 132 | display: inline; 133 | border-left: 1px #E5E5E5 solid; 134 | float: left; 135 | } 136 | .actions > li:first-child { 137 | border-left: none; 138 | } 139 | .actions > li > a { 140 | padding: 5px 5px; 141 | margin: 0 3px; 142 | display: inline-block; 143 | } 144 | .actions > li > a:hover { 145 | background: #EBEEF4; 146 | text-decoration: none; 147 | } 148 | .actions .active > a { 149 | color: #000; 150 | } 151 | /* Story Actions */ 152 | .storyActions { 153 | background: #EDEFF4; 154 | list-style: none; 155 | padding: 5px 10px; 156 | margin: 10px 0 0 0; 157 | font-size: 11px; 158 | } 159 | .storyActions > li { 160 | display: inline; 161 | } 162 | .storyActions > li:before { 163 | content: " · "; 164 | } 165 | .storyActions > li:first-child:before { 166 | content: ''; 167 | } 168 | /* Icons */ 169 | .icon { 170 | width: 16px; 171 | height: 16px; 172 | background: url(../images/icons.png) top left no-repeat; 173 | display: inline-block; 174 | margin-right: 4px; 175 | vertical-align: middle; 176 | } 177 | .icon-status { 178 | background-position: 0 -308px; 179 | } 180 | .icon-photo { 181 | background-image: url(../images/icons-3.png); 182 | background-position: -68px -118px; 183 | } 184 | .icon-place { 185 | background-image: url(../images/icons-2.png); 186 | background-position: -664px -80px; 187 | } 188 | .icon-event { 189 | background-image: url(../images/icons-2.png); 190 | background-position: -647px -80px; 191 | } 192 | /* Spinner */ 193 | #Spinner { 194 | text-align: center; 195 | padding: 10px 0; 196 | } 197 | 198 | -------------------------------------------------------------------------------- /past/static/img/ActionIcons10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/ActionIcons10.png -------------------------------------------------------------------------------- /past/static/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/avatar.png -------------------------------------------------------------------------------- /past/static/img/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/bg.gif -------------------------------------------------------------------------------- /past/static/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/bg.png -------------------------------------------------------------------------------- /past/static/img/bg_top_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/bg_top_light.png -------------------------------------------------------------------------------- /past/static/img/close.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/close.gif -------------------------------------------------------------------------------- /past/static/img/comt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/comt.png -------------------------------------------------------------------------------- /past/static/img/head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/head.png -------------------------------------------------------------------------------- /past/static/img/header_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/header_sprite.png -------------------------------------------------------------------------------- /past/static/img/icon_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/icon_error.png -------------------------------------------------------------------------------- /past/static/img/icon_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/icon_success.png -------------------------------------------------------------------------------- /past/static/img/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/login.png -------------------------------------------------------------------------------- /past/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/logo.png -------------------------------------------------------------------------------- /past/static/img/nav_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/nav_arrow.png -------------------------------------------------------------------------------- /past/static/img/nav_st.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/nav_st.png -------------------------------------------------------------------------------- /past/static/img/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/note.png -------------------------------------------------------------------------------- /past/static/img/sns2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/sns2.png -------------------------------------------------------------------------------- /past/static/img/subnav_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/subnav_bg.png -------------------------------------------------------------------------------- /past/static/img/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/timeline.png -------------------------------------------------------------------------------- /past/static/img/tips_bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/tips_bg.gif -------------------------------------------------------------------------------- /past/static/img/top-bar-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/past/static/img/top-bar-white.png -------------------------------------------------------------------------------- /past/static/js/cssrefresh.js: -------------------------------------------------------------------------------- 1 | /* 2 | * CSSrefresh v1.0.1 3 | * 4 | * Copyright (c) 2012 Fred Heusschen 5 | * www.frebsite.nl 6 | * 7 | * Dual licensed under the MIT and GPL licenses. 8 | * http://en.wikipedia.org/wiki/MIT_License 9 | * http://en.wikipedia.org/wiki/GNU_General_Public_License 10 | */ 11 | 12 | (function() { 13 | 14 | var phpjs = { 15 | 16 | array_filter: function( arr, func ) 17 | { 18 | var retObj = {}; 19 | for ( var k in arr ) 20 | { 21 | if ( func( arr[ k ] ) ) 22 | { 23 | retObj[ k ] = arr[ k ]; 24 | } 25 | } 26 | return retObj; 27 | }, 28 | filemtime: function( file ) 29 | { 30 | var headers = this.get_headers( file, 1 ); 31 | return ( headers && headers[ 'Last-Modified' ] && Date.parse( headers[ 'Last-Modified' ] ) / 1000 ) || false; 32 | }, 33 | get_headers: function( url, format ) 34 | { 35 | var req = window.ActiveXObject ? new ActiveXObject( 'Microsoft.XMLHTTP' ) : new XMLHttpRequest(); 36 | if ( !req ) 37 | { 38 | throw new Error('XMLHttpRequest not supported.'); 39 | } 40 | 41 | var tmp, headers, pair, i, j = 0; 42 | 43 | try 44 | { 45 | req.open( 'HEAD', url, false ); 46 | req.send( null ); 47 | if ( req.readyState < 3 ) 48 | { 49 | return false; 50 | } 51 | tmp = req.getAllResponseHeaders(); 52 | tmp = tmp.split( '\n' ); 53 | tmp = this.array_filter( tmp, function ( value ) 54 | { 55 | return value.toString().substring( 1 ) !== ''; 56 | }); 57 | headers = format ? {} : []; 58 | 59 | for ( i in tmp ) 60 | { 61 | if ( format ) 62 | { 63 | pair = tmp[ i ].toString().split( ':' ); 64 | headers[ pair.splice( 0, 1 ) ] = pair.join( ':' ).substring( 1 ); 65 | } 66 | else 67 | { 68 | headers[ j++ ] = tmp[ i ]; 69 | } 70 | } 71 | 72 | return headers; 73 | } 74 | catch ( err ) 75 | { 76 | return false; 77 | } 78 | } 79 | }; 80 | 81 | var cssRefresh = function() { 82 | 83 | this.reloadFile = function( links ) 84 | { 85 | for ( var a = 0, l = links.length; a < l; a++ ) 86 | { 87 | var link = links[ a ], 88 | newTime = phpjs.filemtime( this.getRandom( link.href ) ); 89 | 90 | // has been checked before 91 | if ( link.last ) 92 | { 93 | // has been changed 94 | if ( link.last != newTime ) 95 | { 96 | // reload 97 | link.elem.setAttribute( 'href', this.getRandom( this.getHref( link.elem ) ) ); 98 | } 99 | } 100 | 101 | // set last time checked 102 | link.last = newTime; 103 | } 104 | setTimeout( function() 105 | { 106 | this.reloadFile( links ); 107 | }, 1000 ); 108 | }; 109 | 110 | this.getHref = function( f ) 111 | { 112 | return f.getAttribute( 'href' ).split( '?' )[ 0 ]; 113 | }; 114 | this.getRandom = function( f ) 115 | { 116 | return f + '?x=' + Math.random(); 117 | }; 118 | 119 | 120 | var files = document.getElementsByTagName( 'link' ), 121 | links = []; 122 | 123 | for ( var a = 0, l = files.length; a < l; a++ ) 124 | { 125 | var elem = files[ a ], 126 | rel = elem.rel; 127 | if ( typeof rel != 'string' || rel.length == 0 || rel == 'stylesheet' ) 128 | { 129 | links.push({ 130 | 'elem' : elem, 131 | 'href' : this.getHref( elem ), 132 | 'last' : false 133 | }); 134 | } 135 | } 136 | this.reloadFile( links ); 137 | }; 138 | 139 | 140 | cssRefresh(); 141 | 142 | })(); -------------------------------------------------------------------------------- /past/static/js/date.format.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Date Format 1.2.3 3 | * (c) 2007-2009 Steven Levithan 4 | * MIT license 5 | * 6 | * Includes enhancements by Scott Trenda 7 | * and Kris Kowal 8 | * 9 | * Accepts a date, a mask, or a date and a mask. 10 | * Returns a formatted version of the given date. 11 | * The date defaults to the current date/time. 12 | * The mask defaults to dateFormat.masks.default. 13 | */ 14 | 15 | var dateFormat = function () { 16 | var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, 17 | timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, 18 | timezoneClip = /[^-+\dA-Z]/g, 19 | pad = function (val, len) { 20 | val = String(val); 21 | len = len || 2; 22 | while (val.length < len) val = "0" + val; 23 | return val; 24 | }; 25 | 26 | // Regexes and supporting functions are cached through closure 27 | return function (date, mask, utc) { 28 | var dF = dateFormat; 29 | 30 | // You can't provide utc if you skip other args (use the "UTC:" mask prefix) 31 | if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { 32 | mask = date; 33 | date = undefined; 34 | } 35 | 36 | // Passing date through Date applies Date.parse, if necessary 37 | date = date ? new Date(date) : new Date; 38 | if (isNaN(date)) throw SyntaxError("invalid date"); 39 | 40 | mask = String(dF.masks[mask] || mask || dF.masks["default"]); 41 | 42 | // Allow setting the utc argument via the mask 43 | if (mask.slice(0, 4) == "UTC:") { 44 | mask = mask.slice(4); 45 | utc = true; 46 | } 47 | 48 | var _ = utc ? "getUTC" : "get", 49 | d = date[_ + "Date"](), 50 | D = date[_ + "Day"](), 51 | m = date[_ + "Month"](), 52 | y = date[_ + "FullYear"](), 53 | H = date[_ + "Hours"](), 54 | M = date[_ + "Minutes"](), 55 | s = date[_ + "Seconds"](), 56 | L = date[_ + "Milliseconds"](), 57 | o = utc ? 0 : date.getTimezoneOffset(), 58 | flags = { 59 | d: d, 60 | dd: pad(d), 61 | ddd: dF.i18n.dayNames[D], 62 | dddd: dF.i18n.dayNames[D + 7], 63 | m: m + 1, 64 | mm: pad(m + 1), 65 | mmm: dF.i18n.monthNames[m], 66 | mmmm: dF.i18n.monthNames[m + 12], 67 | yy: String(y).slice(2), 68 | yyyy: y, 69 | h: H % 12 || 12, 70 | hh: pad(H % 12 || 12), 71 | H: H, 72 | HH: pad(H), 73 | M: M, 74 | MM: pad(M), 75 | s: s, 76 | ss: pad(s), 77 | l: pad(L, 3), 78 | L: pad(L > 99 ? Math.round(L / 10) : L), 79 | t: H < 12 ? "a" : "p", 80 | tt: H < 12 ? "am" : "pm", 81 | T: H < 12 ? "A" : "P", 82 | TT: H < 12 ? "AM" : "PM", 83 | Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), 84 | o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), 85 | S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] 86 | }; 87 | 88 | return mask.replace(token, function ($0) { 89 | return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); 90 | }); 91 | }; 92 | }(); 93 | 94 | // Some common format strings 95 | dateFormat.masks = { 96 | "default": "ddd mmm dd yyyy HH:MM:ss", 97 | shortDate: "m/d/yy", 98 | mediumDate: "mmm d, yyyy", 99 | longDate: "mmmm d, yyyy", 100 | fullDate: "dddd, mmmm d, yyyy", 101 | shortTime: "h:MM TT", 102 | mediumTime: "h:MM:ss TT", 103 | longTime: "h:MM:ss TT Z", 104 | isoDate: "yyyy-mm-dd", 105 | isoTime: "HH:MM:ss", 106 | isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", 107 | isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" 108 | }; 109 | 110 | // Internationalization strings 111 | dateFormat.i18n = { 112 | dayNames: [ 113 | "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", 114 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" 115 | ], 116 | monthNames: [ 117 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 118 | "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" 119 | ] 120 | }; 121 | 122 | // For convenience... 123 | Date.prototype.format = function (mask, utc) { 124 | return dateFormat(this, mask, utc); 125 | }; 126 | 127 | -------------------------------------------------------------------------------- /past/static/js/jquery.masonry.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Masonry v2.1.05 3 | * A dynamic layout plugin for jQuery 4 | * The flip-side of CSS Floats 5 | * http://masonry.desandro.com 6 | * 7 | * Licensed under the MIT license. 8 | * Copyright 2012 David DeSandro 9 | */ 10 | (function(a,b,c){"use strict";var d=b.event,e;d.special.smartresize={setup:function(){b(this).bind("resize",d.special.smartresize.handler)},teardown:function(){b(this).unbind("resize",d.special.smartresize.handler)},handler:function(a,c){var d=this,f=arguments;a.type="smartresize",e&&clearTimeout(e),e=setTimeout(function(){b.event.handle.apply(d,f)},c==="execAsap"?0:100)}},b.fn.smartresize=function(a){return a?this.bind("smartresize",a):this.trigger("smartresize",["execAsap"])},b.Mason=function(a,c){this.element=b(c),this._create(a),this._init()},b.Mason.settings={isResizable:!0,isAnimated:!1,animationOptions:{queue:!1,duration:500},gutterWidth:0,isRTL:!1,isFitWidth:!1,containerStyle:{position:"relative"}},b.Mason.prototype={_filterFindBricks:function(a){var b=this.options.itemSelector;return b?a.filter(b).add(a.find(b)):a},_getBricks:function(a){var b=this._filterFindBricks(a).css({position:"absolute"}).addClass("masonry-brick");return b},_create:function(c){this.options=b.extend(!0,{},b.Mason.settings,c),this.styleQueue=[];var d=this.element[0].style;this.originalStyle={height:d.height||""};var e=this.options.containerStyle;for(var f in e)this.originalStyle[f]=d[f]||"";this.element.css(e),this.horizontalDirection=this.options.isRTL?"right":"left",this.offset={x:parseInt(this.element.css("padding-"+this.horizontalDirection),10),y:parseInt(this.element.css("padding-top"),10)},this.isFluid=this.options.columnWidth&&typeof this.options.columnWidth=="function";var g=this;setTimeout(function(){g.element.addClass("masonry")},0),this.options.isResizable&&b(a).bind("smartresize.masonry",function(){g.resize()}),this.reloadItems()},_init:function(a){this._getColumns(),this._reLayout(a)},option:function(a,c){b.isPlainObject(a)&&(this.options=b.extend(!0,this.options,a))},layout:function(a,b){for(var c=0,d=a.length;c= $(document).height() - $(target).height(); 39 | if (mayLoadContent){ 40 | if (opts.beforeLoad != null){ 41 | opts.beforeLoad(); 42 | } 43 | $(obj).children().attr('rel', 'loaded'); 44 | $(obj).attr('scrollPagination', 'disabled'); 45 | $.ajax({ 46 | type: 'POST', 47 | url: opts.contentPage, 48 | data: opts.contentData, 49 | success: function(data){ 50 | $(obj).append(data); 51 | var objectsRendered = $(obj).children('[rel!=loaded]'); 52 | 53 | if (opts.afterLoad != null){ 54 | opts.afterLoad(objectsRendered); 55 | } 56 | $(obj).attr('scrollPagination', 'enabled'); 57 | }, 58 | dataType: 'html' 59 | }); 60 | } 61 | 62 | }; 63 | 64 | $.fn.scrollPagination.init = function(obj, opts){ 65 | var target = opts.scrollTarget; 66 | $(obj).attr('scrollPagination', 'enabled'); 67 | 68 | $(target).scroll(function(event){ 69 | if ($(obj).attr('scrollPagination') == 'enabled'){ 70 | $.fn.scrollPagination.loadContent(obj, opts); 71 | } 72 | else { 73 | event.stopPropagation(); 74 | } 75 | }); 76 | 77 | $.fn.scrollPagination.loadContent(obj, opts); 78 | 79 | }; 80 | 81 | $.fn.scrollPagination.defaults = { 82 | 'contentPage' : null, 83 | 'contentData' : {}, 84 | 'beforeLoad': null, 85 | 'afterLoad': null , 86 | 'scrollTarget': null, 87 | 'heightOffset': 0 88 | }; 89 | })( jQuery ); -------------------------------------------------------------------------------- /past/store.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import os 4 | import commands 5 | import datetime 6 | 7 | import MySQLdb 8 | import redis 9 | import memcache 10 | 11 | from past.utils.escape import json_decode, json_encode 12 | from past import config 13 | 14 | def init_db(): 15 | cmd = """mysql -h%s -P%s -u%s -p%s < %s""" \ 16 | % (config.DB_HOST, config.DB_PORT, 17 | config.DB_USER, config.DB_PASSWD, 18 | os.path.join(os.path.dirname(__file__), "schema.sql")) 19 | status, output = commands.getstatusoutput(cmd) 20 | 21 | if status != 0: 22 | print "init_db fail, output is: %s" % output 23 | 24 | return status 25 | 26 | def connect_db(): 27 | try: 28 | conn = MySQLdb.connect( 29 | host=config.DB_HOST, 30 | port=config.DB_PORT, 31 | user=config.DB_USER, 32 | passwd=config.DB_PASSWD, 33 | db=config.DB_NAME, 34 | use_unicode=True, 35 | charset="utf8") 36 | return conn 37 | except Exception, e: 38 | print "connect db fail:%s" % e 39 | return None 40 | 41 | def connect_redis(): 42 | return redis.Redis(config.REDIS_HOST, config.REDIS_PORT) 43 | 44 | def connect_redis_cache(): 45 | return redis.Redis(config.REDIS_CACHE_HOST, config.REDIS_CACHE_PORT) 46 | 47 | def connect_mongo(dbname="thepast"): 48 | import pymongo 49 | conn = pymongo.connection.Connection('localhost') 50 | db = conn.thepast 51 | db = getattr(conn, dbname) 52 | return db and getattr(db, dbname) 53 | 54 | class MongoDB(object): 55 | def __init__(self, dbname="thepast"): 56 | self.dbname = dbname 57 | self._conn = connect_mongo(self.dbname) 58 | 59 | def connect(self): 60 | self._conn = connect_mongo(self.dbname) 61 | return self._conn 62 | 63 | def get(self, k): 64 | d = {"k":k} 65 | r = self._conn.find_one(d) 66 | if r: 67 | return r.get("v") 68 | return None 69 | 70 | def mget(self, keys): 71 | d = {"k": {"$in" : keys}} 72 | rs = self._conn.find(d) 73 | return [r["v"] for r in rs] 74 | 75 | def set(self, k, v): 76 | self._conn.update({"k":k},{"k":k, "v":v}, upsert=True) 77 | 78 | def remove(self, k): 79 | self._conn.remove({"k":k}) 80 | 81 | def get_connection(self): 82 | return self._conn or self.connect() 83 | 84 | class DB(object): 85 | 86 | def __init__(self): 87 | self._conn = connect_db() 88 | 89 | def connect(self): 90 | self._conn = connect_db() 91 | return self._conn 92 | 93 | def execute(self, *a, **kw): 94 | cursor = kw.pop('cursor', None) 95 | try: 96 | cursor = cursor or self._conn.cursor() 97 | cursor.execute(*a, **kw) 98 | except (AttributeError, MySQLdb.OperationalError): 99 | print 'debug, %s re-connect to mysql' % datetime.datetime.now() 100 | self._conn and self._conn.close() 101 | self.connect() 102 | cursor = self._conn.cursor() 103 | cursor.execute(*a, **kw) 104 | return cursor 105 | 106 | def commit(self): 107 | return self._conn and self._conn.commit() 108 | 109 | def rollback(self): 110 | return self._conn and self._conn.rollback() 111 | 112 | def connect_memcached(): 113 | mc = memcache.Client(['%s:%s' % (config.MEMCACHED_HOST, config.MEMCACHED_PORT)], debug=0) 114 | return mc 115 | 116 | db_conn = DB() 117 | mc = redis_cache_conn = connect_memcached() 118 | #redis_conn = connect_redis() 119 | #mongo_conn = MongoDB() 120 | -------------------------------------------------------------------------------- /past/templates/bind_wordpress.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import "blocks.html" as blocks %} 4 | 5 | {% block rightbar_block %} 6 | {{super()}} 7 | {{blocks.rightbar_intros_block(intros)}} 8 | {{blocks.rightbar_feedback_block()}} 9 | {%endblock%} 10 | 11 | {% block middlebar_block %} 12 | 13 | {{ blocks.notification_block() }} 14 | 15 | 16 | {%if wordpress_alias_list%} 17 |
18 |
已绑定的rss列表:
19 |
20 |
    21 | {%for x in wordpress_alias_list%} 22 |
  1. {{x.alias}}
  2. 23 | {%endfor%} 24 |
25 |
26 |
27 |
28 | {%endif%} 29 | 30 | {% if step == '1'%} 31 |
32 |
绑定rss地址
33 |
34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 |
你的wordpress feed地址: 37 | 38 |
39 | 比如: http://xxblog.com/feed/, 填写后不可更改 40 |
46 |
47 | {%else%} 48 |
49 |
绑定rss地址 > 等待验证...
50 |
51 |

你的blog feed地址为:{{feed_uri}}

52 |

你的blog认领验证码为:{{random_id}}

53 |

为了验证blog的主人^^,请发一篇blog,"文章内容"为 {{random_id}},完成该步骤后,请点下一步完成绑定

54 | 55 |
56 |
57 | {%endif%} 58 | 59 |
60 |
61 |
62 |

0. 如果你在绑定过程中遇到任何问题,请直接给管理员捎个话 help@thepast.me,我想没有解决不了的问题^^

63 |

1. 理论来说,支持导入所有的rss feed,而不仅仅是wordpress,注意尽量不要用feedburner的rss地址

64 |

2. 对于wordpress rss feed来讲,只能导入最近的15篇文章,如果需要导入更多,需要去wordpress的控制面板中设置rss输出的文章数目。

65 |
66 |
67 | 68 | 98 | 99 | {% endblock %} 100 | -------------------------------------------------------------------------------- /past/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content_block %} 4 | 5 | 6 |
7 |
8 |
9 |
10 |

你好,旧时光[开源]

11 |
实时聚合你在“豆瓣”、“人人”、“新浪微博”、“腾讯微博”、“Twitter”、“wordpress”、“instagram”等平台的 Timeline。
12 |
每天清晨,会邮件提醒你过往的今天都发生了些什么,或许是惊喜,亦或是怀念。
13 |
聚合后的Timeline,生成PDF[预览]版本,供离线阅读或永久保存,打造你自己的个人杂志。
14 |
同步更新微博到多个平台。
15 |
猛击头部任一图标授权登录! 16 | [?] 17 | 18 | 重要提示:多平台绑定步骤,首页---点击任一图标授权登录---再点击个人信息框内的灰色图标进行授权。
19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 | {% for u in users %} 27 |
28 |
29 | {{u.name}} 30 |
31 |
32 |
{{u.name}} 33 | {%if u.is_pdf_ready()%} 34 | [PDF] 35 | {%endif%} 36 | N.{{u.id}} 37 |
38 | 39 | {%for ua in u.get_alias()%} 40 | {%set homepage_info = ua.get_homepage_url()%} 41 | {%if homepage_info%} 42 | 43 | 44 | 45 | {%endif%} 46 | {%endfor%} 47 | 48 |
49 |
50 | {% endfor %} 51 | 52 |
53 | 54 | {%endblock%} 55 | 56 | -------------------------------------------------------------------------------- /past/templates/note.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import "blocks.html" as blocks %} 4 | 5 | {% block css %} 6 | {{super()}} 7 | 8 | {% endblock %} 9 | 10 | {% block rightbar_block %} 11 | {{super()}} 12 | {{blocks.rightbar_note_block()}} 13 | {{blocks.rightbar_markdown_block()}} 14 | {{blocks.rightbar_feedback_block()}} 15 | {% endblock %} 16 | 17 | {% block middlebar_block %} 18 |
19 |
首页 > 日记 20 | {%if g.user%} 21 | > 写日记 22 | {%endif%} 23 |
24 |
25 |
26 | 27 | {{ blocks.notification_block(g.user) }} 28 | 29 | 30 |
31 |
32 |

{{title}}

33 |
34 | {{create_time.strftime("%Y-%m-%d %H:%M:%S")}} 35 | {%if g.user and g.user.id == note.user_id%} 36 | 编辑 37 | {%else%} 38 | 作者:{{user.name}} 39 | {%endif%} 40 |
41 |
42 |
43 | {%if note.fmt == consts.NOTE_FMT_MARKDOWN%} 44 | {{content|safe}} 45 | {%else%} 46 |
{{content}}
47 | {%endif%} 48 |
49 |
50 | 51 |
52 | {%if not fmt or fmt==consts.NOTE_FMT_PLAIN%} 53 | 格式:文本 54 | {%elif fmt==consts.NOTE_FMT_MARKDOWN %} 55 | 格式:Markdown 56 | {%endif%} 57 |
58 |
59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /past/templates/note_create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import "blocks.html" as blocks %} 4 | 5 | {% block css %} 6 | {{super()}} 7 | 8 | {% endblock %} 9 | 10 | {% block rightbar_block %} 11 | {{super()}} 12 | {{blocks.rightbar_note_block()}} 13 | {{blocks.rightbar_markdown_block()}} 14 | {{blocks.rightbar_feedback_block()}} 15 | {% endblock %} 16 | 17 | {% block middlebar_block %} 18 |
19 |
首页 > 日记 > 编辑
20 |
21 |
22 | 23 | {{ blocks.notification_block() }} 24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 | 格式: 38 |
39 | 43 | 47 | 48 |
49 | 隐私: 50 |
51 | 56 | 61 | 66 |
67 |
68 | 预览 69 | 70 |
71 | 72 |
73 | 74 |
75 |
76 |
77 |
78 | 93 | {% endblock %} 94 | -------------------------------------------------------------------------------- /past/templates/past.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import "status.html" as status_tmpl_helper %} 4 | {% import "blocks.html" as blocks %} 5 | 6 | 7 | {% block css %} 8 | {{super()}} 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block js%} 14 | {{super()}} 15 | 16 | {% endblock %} 17 | 18 | {% block title_block %} {{g.user.name}}{%endblock%} 19 | 20 | {%block content_block%} 21 |
22 | {{self.middlebar_block()}} 23 |
24 | {{blocks.rightbar_intros_block(intros)}} 25 | {{blocks.rightbar_note_block()}} 26 | {{blocks.rightbar_feedback_block()}} 27 |
28 |
29 | 30 | {%endblock%} 31 | 32 | {% block middlebar_block %} 33 |
34 | {%if not history_status%} 35 |
36 |

去年、前年、大前年... 都没有留下什么东西。

37 |

今天记录一下 明年再来看。

38 |
39 | {%else%} 40 | {%for t, status_list in history_status.items()%} 41 |

{{t.decode("utf8")}},找到{{status_list|length}}条往事

42 |
    43 |
  1. 44 | {%set left = True%} 45 | {%for repeated_status in status_list%} 46 | {%if left%} 47 |
  2. 48 | {%else%} 49 |
  3. 50 | {%endif%} 51 |   52 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}} 53 | {%set left = not left%} 54 |
  4. 55 | {%endfor%} 56 | 57 |
58 | {%endfor%} 59 | {%endif%} 60 |
61 | 62 | 63 | 68 | 69 | 74 | 75 | {% endblock %} 76 | 77 | -------------------------------------------------------------------------------- /past/templates/pdf.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import "blocks.html" as blocks %} 4 | 5 | {% block rightbar_block %} 6 | {{super()}} 7 | {{blocks.rightbar_intros_block(intros)}} 8 |
9 | {{blocks.rightbar_feedback_block()}} 10 | {% endblock %} 11 | 12 | {% block middlebar_block %} 13 | 14 | {{ blocks.notification_block(g.user) }} 15 | 16 | 17 |
18 |
首页 > PDF下载 [PDF会按月归档,下载链接如下]
19 |
20 | 21 | {%if not pdf_applyed%} 22 | 23 | 24 |

由于系统资源受限(主要是磁盘),为了给大家提供更好的服务,

25 |

PDF备份功能只开放给切实需要的用户,请点下面的按钮申请(申请后,自动开通!)

26 | 27 | 28 | 29 | 30 | 31 | 32 | {%else%} 33 | 34 | 35 |

由于系统资源受限(主要是磁盘),为了给大家提供更好的服务,

36 |

如果PDF文件你已经下载完毕了,那么请点下面的按钮,系统会在接下里的一天,删除掉生成的PDF!

37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {%endif%} 45 |
46 |
47 | {% for year in files_dict.keys()|sort(reverse=True)%} 48 |
{{year}}年
49 |
50 | 51 | {%for i in [0,1,2,3,4,5]%} 52 | {% if files_dict[year][i]%} 53 | {%set date = files_dict[year][i][0]%} 54 | {%set filename = files_dict[year][i][1]%} 55 | {%set filesize = files_dict[year][i][2]%} 56 | 57 | {% else%} 58 | 59 | {% endif%} 60 | {%endfor%} 61 | 62 | 63 | {%if files_dict[year]|length > 6%} 64 | 65 | {%for i in [6,7,8,9,10,11]%} 66 | {% if files_dict[year][i]%} 67 | {%set date = files_dict[year][i][0]%} 68 | {%set filename = files_dict[year][i][1]%} 69 | {%set filesize = files_dict[year][i][2]%} 70 | 71 | {% else%} 72 | 73 | {% endif%} 74 | {%endfor%} 75 | 76 | {%endif%} 77 |
{{date.month}}月 [{{filesize}}]
{{date.month}}月 [{{filesize}}]
78 | {%endfor%} 79 |
80 | 说明:PDF每月会生成一个单独的文件,如果你想将多个PDF文件合并为一个,请下载各个PDF文件后,使用PDF编辑软件进行编辑合并 81 |
82 |
83 | {% endblock %} 84 | -------------------------------------------------------------------------------- /past/templates/post.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import "blocks.html" as blocks %} 4 | 5 | {% block title_block %} {{status.title}} {% endblock %} 6 | 7 | {% block rightbar_block %} 8 | {{super()}} 9 | {{blocks.rightbar_intros_block(intros)}} 10 | {{blocks.rightbar_feedback_block()}} 11 | {% endblock %} 12 | 13 | {% block middlebar_block %} 14 | 15 | {{ blocks.notification_block() }} 16 | 17 |
18 |
首页 > 文章
19 |
20 |
21 | 22 |
23 |
24 | {{status.title}} 25 |
26 | {{status.create_time.strftime("%Y-%m-%d %H:%M:%S")}} 27 | 作者:{{user.name}} 28 |
29 |
30 | {%if status.category == config.CATE_WORDPRESS_POST%} 31 | {{status.text|safe}} 32 | {%else%} 33 | {{status.text|safe}} 34 | {%endif%} 35 |
36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /past/templates/timeline.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import "status.html" as status_tmpl_helper %} 4 | {% import "blocks.html" as blocks %} 5 | 6 | {% block css %} 7 | {{super()}} 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block js%} 13 | {{super()}} 14 | 15 | {% endblock %} 16 | 17 | {% block title_block %} {{user.name}}{%endblock%} 18 | 19 | {%block content_block%} 20 |
21 | {{self.middlebar_block()}} 22 |
23 | {{blocks.rightbar_intros_block(intros)}} 24 | {{blocks.rightbar_note_block()}} 25 | {{blocks.rightbar_feedback_block()}} 26 |
27 |
28 | 29 | {%endblock%} 30 | 31 | {% block middlebar_block %} 32 |
33 |
    34 |
  1. 35 | 36 | {%if g.cate == g.config.CATE_THEPAST_NOTE and 0%} 37 |
  2. 38 |   40 |
    41 |
      42 |
    1. Status
    2. 43 |
    44 | 45 | 46 |
    47 |
    48 | 49 | 51 | 52 |
      53 |
    1. 54 | 55 |
    2. 56 |
    57 |
    58 |
    59 | 60 |
    61 |
  3. 62 | {%set left = False%} 63 | {%else%} 64 | {%if not status_list and g.start == 0%} 65 |
    66 |

    你还没在thepast上写过日记呢。

    67 |

    今天记录一下 明年再来看。

    68 |

    >写日记

    69 |
    70 | {%endif%} 71 | {%set left = True%} 72 | {%endif%} 73 | 74 | {%for repeated_status in status_list%} 75 | {%if left%} 76 |
  4. 77 | {%else%} 78 |
  5. 79 | {%endif%} 80 |   81 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}} 82 | {%set left = not left%} 83 |
  6. 84 | {%endfor%} 85 | 86 |
87 |
88 |
89 |
    90 | {%if g.start>0%} 91 | 94 | {%endif%} 95 | 98 |
99 |
100 | 101 | 102 | 107 | 108 | 113 | 114 | {% endblock %} 115 | 116 | -------------------------------------------------------------------------------- /past/templates/user_explore.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content_block %} 4 |
5 | 6 |
7 |
8 | 9 | {% for u in users %} 10 |
11 |
12 | {{u.name}} 13 |
14 |
15 |
{{u.name}} 16 | {%if u.is_pdf_ready()%} 17 | [PDF] 18 | {%endif%} 19 | N.{{u.id}} 20 |
21 |

22 | {%for ua in u.get_alias()%} 23 | {%set homepage_info = ua.get_homepage_url()%} 24 | {%if homepage_info%} 25 | 26 | 27 | 28 | {%endif%} 29 | {%endfor%} 30 |

31 |
32 |
33 | {% endfor %} 34 |
35 |
36 | 37 |
38 |
39 |
    40 | {%if g.start>0%} 41 | 44 | {%endif%} 45 | 48 |
49 |
50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /past/templates/v2/bind_wordpress.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/base.html" %} 2 | {% import "blocks.html" as blocks_tmpl_helper %} 3 | 4 | {%block title%}绑定博客 | 旧时光{%endblock%} 5 | 6 | {%block main%} 7 |
8 |
9 |
10 | {%if wordpress_alias_list%} 11 |
12 |
已绑定的rss列表:
13 |
14 |
    15 | {%for x in wordpress_alias_list%} 16 |
  1. {{x.alias}}
  2. 17 | {%endfor%} 18 |
19 |
20 |
21 |
22 | {%endif%} 23 | 24 | {% if step == '1'%} 25 |
26 |
绑定rss地址
27 |
28 | 29 | 30 | 35 | 36 | 37 | 38 | 39 |
你的wordpress feed地址: 31 | 32 |
33 | 比如: http://xxblog.com/feed/, 填写后不可更改 34 |
40 |
41 | {%else%} 42 |
43 |
绑定rss地址 > 等待验证...
44 |
45 |

你的blog feed地址为:{{feed_uri}}

46 |

你的blog认领验证码为:{{random_id}}

47 |

为了验证blog的主人^^,请发一篇blog,"文章内容"为 {{random_id}},完成该步骤后,请点下一步完成绑定

48 | 49 |
50 |
51 | {%endif%} 52 | 53 |
54 |
55 |
56 |

0. 如果你在绑定过程中遇到任何问题,请直接给管理员捎个话 help@thepast.me,我想没有解决不了的问题^^

57 |

1. 理论来说,支持导入所有的rss feed,而不仅仅是wordpress,注意尽量不要用feedburner的rss地址

58 |

2. 对于wordpress rss feed来讲,只能导入最近的15篇文章,如果需要导入更多,需要去wordpress的控制面板中设置rss输出的文章数目。

59 |
60 |
61 | 62 | 92 | 93 |
94 |
95 | {{super()}} 96 | {{blocks_tmpl_helper.rightbar_intros_block(intros)}} 97 | {{blocks_tmpl_helper.rightbar_feedback_block()}} 98 |
99 |
100 | {%endblock%} 101 | -------------------------------------------------------------------------------- /past/templates/v2/explore.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/timeline_base.html" %} 2 | 3 | {%block title%}{{user.name}} | 旧时光{%endblock%} 4 | 5 | {%block hero_unit%} 6 |
7 |
8 |
9 |

你好,旧时光[开源]

10 |
实时聚合你在“豆瓣”、“人人”、“新浪微博”、“腾讯微博”、“Twitter”、“wordpress”、“instagram”等平台的 Timeline。
11 |
每天清晨,会邮件提醒你过往的今天都发生了些什么,或许是惊喜,亦或是怀念。
12 |
聚合后的Timeline,生成PDF[预览]版本,供离线阅读或永久保存,打造你自己的个人杂志。
13 |
同步更新微博到多个平台。
14 |
15 |
16 |
17 | {%endblock%} 18 | 19 | {%block more_js%} 20 | {{super()}} 21 | 26 | {%endblock%} 27 | -------------------------------------------------------------------------------- /past/templates/v2/explore_user.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/base.html" %} 2 | {% import "status.html" as status_tmpl_helper %} 3 | {% import "blocks.html" as blocks_tmpl_helper %} 4 | 5 | {%block title%}浏览用户 | 旧时光{%endblock%} 6 | 7 | {%block main%} 8 |
9 | {% for u in users %} 10 |
11 |
12 | {{u.name}} 13 |
14 |
15 |
{{u.name}} 16 | {%if u.is_pdf_ready()%} 17 | [PDF] 18 | {%endif%} 19 | N.{{u.id}} 20 |
21 |

22 | {%for ua in u.get_alias()%} 23 | {%set homepage_info = ua.get_homepage_url()%} 24 | {%if homepage_info%} 25 | 26 | 27 | 28 | {%endif%} 29 | {%endfor%} 30 |

31 |
32 |
33 | {% endfor %} 34 | 35 |
36 |
    37 | {%if g.start>0%} 38 | 41 | {%endif%} 42 | 45 |
46 | {%endblock%} 47 | -------------------------------------------------------------------------------- /past/templates/v2/note.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/base.html" %} 2 | {% import "status.html" as status_tmpl_helper %} 3 | {% import "blocks.html" as blocks_tmpl_helper %} 4 | 5 | 6 | {%block title%}日记本{{user.name}} | 旧时光{%endblock%} 7 | 8 | {%block middlebar%} 9 |
10 |
11 | 作者:{{user.name}}
12 | 题目:{{title}} 13 |
14 | 15 | {%if note.fmt == consts.NOTE_FMT_MARKDOWN%} 16 |
17 | {{content|safe}} 18 |
19 | {%else%} 20 |
21 |
22 | {{content}} 23 |
24 |
25 | {%endif%} 26 |
27 |
28 | {%endblock%} 29 | 30 | {%block more_js%} 31 | {{super()}} 32 | 37 | {%endblock%} 38 | -------------------------------------------------------------------------------- /past/templates/v2/note_create.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/base.html" %} 2 | {% import "status.html" as status_tmpl_helper %} 3 | {% import "blocks.html" as blocks_tmpl_helper %} 4 | 5 | 6 | {%block title%}写日记{{user.name}} | 旧时光{%endblock%} 7 | 8 | {%block middlebar%} 9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 | 22 | 24 |
25 | 26 | 31 | 36 | 41 | 42 | 43 |
44 |
45 |
46 |
47 |

48 | thepast友情提醒,每天只能记一篇日记,写好了就不能再修改。 49 |

50 |
51 |
52 | {%endblock%} 53 | 54 | {%block more_js%} 55 | {{super()}} 56 | 77 | {%endblock%} 78 | -------------------------------------------------------------------------------- /past/templates/v2/pdf.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/base.html" %} 2 | {% import "blocks.html" as blocks_tmpl_helper %} 3 | 4 | {%block title%}PDF-{{user.name}} | 旧时光{%endblock%} 5 | 6 | {%block main%} 7 |
8 |
9 |
10 | 11 |
12 |
首页 > PDF下载 [PDF会按月归档,下载链接如下]
13 |
14 | 15 | {%if not pdf_applyed%} 16 | 17 | 18 |

由于系统资源受限(主要是磁盘),为了给大家提供更好的服务,

19 |

PDF备份功能只开放给切实需要的用户,请点下面的按钮申请(申请后,自动开通!)

20 | 21 | 22 | 23 | 24 | 25 | 26 | {%else%} 27 | 28 | 29 |

由于系统资源受限(主要是磁盘),为了给大家提供更好的服务,

30 |

如果PDF文件你已经下载完毕了,那么请点下面的按钮,系统会在接下里的一天,删除掉生成的PDF!

31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {%endif%} 39 |
40 |
41 | {% for year in files_dict.keys()|sort(reverse=True)%} 42 |
{{year}}年
43 |
44 | 45 | {%for i in [0,1,2,3,4,5]%} 46 | {% if files_dict[year][i]%} 47 | {%set date = files_dict[year][i][0]%} 48 | {%set filename = files_dict[year][i][1]%} 49 | {%set filesize = files_dict[year][i][2]%} 50 | 51 | {% else%} 52 | 53 | {% endif%} 54 | {%endfor%} 55 | 56 | 57 | {%if files_dict[year]|length > 6%} 58 | 59 | {%for i in [6,7,8,9,10,11]%} 60 | {% if files_dict[year][i]%} 61 | {%set date = files_dict[year][i][0]%} 62 | {%set filename = files_dict[year][i][1]%} 63 | {%set filesize = files_dict[year][i][2]%} 64 | 65 | {% else%} 66 | 67 | {% endif%} 68 | {%endfor%} 69 | 70 | {%endif%} 71 |
{{date.month}}月 [{{filesize}}]
{{date.month}}月 [{{filesize}}]
72 | {%endfor%} 73 |
74 | 说明:PDF每月会生成一个单独的文件,如果你想将多个PDF文件合并为一个,请下载各个PDF文件后,使用PDF编辑软件进行编辑合并 75 |
76 |
77 |
78 |
79 | {{super()}} 80 | {{blocks_tmpl_helper.rightbar_intros_block(intros)}} 81 | {{blocks_tmpl_helper.rightbar_feedback_block()}} 82 |
83 |
84 | {%endblock%} 85 | 86 | -------------------------------------------------------------------------------- /past/templates/v2/timeline_base.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/base.html" %} 2 | {% import "status.html" as status_tmpl_helper %} 3 | {% import "blocks.html" as blocks_tmpl_helper %} 4 | 5 | {%block css%} 6 | {{super()}} 7 | 8 | {%endblock%} 9 | 10 | {% block js%} 11 | {{super()}} 12 | 13 | {% endblock %} 14 | 15 | {%block sidebar%} 16 |
17 | 85 |
86 | {%endblock%} 87 | 88 | {%block rightbar%} 89 |
90 |
91 | {%if not status_list%} 92 |
93 |

欢迎加入旧时光,thepast正在从第三方同步你的消息,请稍等(最多5分钟哦)

94 |

趁着这块空闲,你可以随机看看别人的页面

95 |

或者

96 |

看看laiwei历史上的每天

97 |
98 | {%else%} 99 |
    100 |
  1. 101 | {%set left = True%} 102 | {%for repeated_status in status_list%} 103 | {%if left%} 104 |
  2. 105 | {%else%} 106 |
  3. 107 | {%endif%} 108 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}} 109 |
  4. 110 | {%set left=not left%} 111 | {%endfor%} 112 |
113 |
正在努力加载更多中...
114 | {%endif%} 115 |
116 |
117 | {%endblock%} 118 | 119 | -------------------------------------------------------------------------------- /past/templates/v2/user.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/timeline_base.html" %} 2 | 3 | 4 | {%block title%}{{user.name}} | 旧时光{%endblock%} 5 | 6 | {%block more_js%} 7 | {{super()}} 8 | 62 | {%endblock%} 63 | -------------------------------------------------------------------------------- /past/templates/v2/user_more.html: -------------------------------------------------------------------------------- 1 | {% import "status.html" as status_tmpl_helper %} 2 | 3 | {%set left = True%} 4 | {%for repeated_status in status_list%} 5 | {%if left%} 6 |
  • 7 | {%else%} 8 |
  • 9 | {%endif%} 10 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}} 11 |
  • 12 | {%set left=not left%} 13 | {%endfor%} 14 | -------------------------------------------------------------------------------- /past/templates/v2/user_past.html: -------------------------------------------------------------------------------- 1 | {% extends "v2/timeline_base.html" %} 2 | {% import "status.html" as status_tmpl_helper %} 3 | 4 | {%block css%} 5 | {{super()}} 6 | 7 | {%endblock%} 8 | 9 | {% block js%} 10 | {{super()}} 11 | 12 | {% endblock %} 13 | 14 | {%block sidebox%} 15 | 23 | {{super()}} 24 | {%endblock%} 25 | 26 | {%block rightbar%} 27 |
    28 |
    29 | {%if not history_status%} 30 |
    31 |

    去年、前年、大前年的今天... 都没有留下什么东西。

    32 |

    今天记录一下 明年再来看。

    33 |

    或者

    34 |

    看看历史上的昨天和前天吧:)

    35 |
    36 | {%else%} 37 | {%for t in history_status.keys()|sort(reverse=True)%} 38 | {%set status_list = history_status.get(t)%} 39 |
    40 |

    {{t.decode("utf8")}}

    41 |

    共找到 {{status_list|length}} 条往事。

    42 |
    43 | 44 |
      45 |
    1. 46 | {%set left = True%} 47 | {%for repeated_status in status_list%} 48 | {%if left%} 49 |
    2. 50 | {%else%} 51 |
    3. 52 | {%endif%} 53 | {{status_tmpl_helper.story_unit(g, repeated_status, sync_list)}} 54 |
    4. 55 | {%set left=not left%} 56 | {%endfor%} 57 |
    58 | {%endfor%} 59 | 60 | {%endif%} 61 |
    62 |
    63 | {%endblock%} 64 | 65 | {%block more_js%} 66 | {{super()}} 67 | 90 | {%endblock%} 91 | -------------------------------------------------------------------------------- /past/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import os 3 | import re 4 | import time 5 | import datetime 6 | import imghdr 7 | import httplib2 8 | import mimetypes 9 | import random 10 | import string 11 | import markdown2 12 | from past import config 13 | 14 | def randbytes(bytes_): 15 | return ''.join(random.sample(string.ascii_letters + string.digits, bytes_)) 16 | 17 | 18 | def random_string (length): 19 | return ''.join(random.choice(string.letters) for ii in range (length + 1)) 20 | 21 | def encode_multipart_data (data, files): 22 | boundary = random_string (30) 23 | 24 | def get_content_type (filename): 25 | return mimetypes.guess_type (filename)[0] or 'application/octet-stream' 26 | 27 | def encode_field(field_name): 28 | return ('--' + boundary, 29 | 'Content-Disposition: form-data; name="%s"' % field_name, 30 | '', str (data [field_name])) 31 | 32 | def encode_file(field_name): 33 | filename = files[field_name] 34 | return ('--' + boundary, 35 | 'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, filename), 36 | 'Content-Type: %s' % get_content_type(filename), 37 | '', open (filename, 'rb').read ()) 38 | 39 | lines = [] 40 | for name in data: 41 | lines.extend (encode_field (name)) 42 | for name in files: 43 | lines.extend (encode_file (name)) 44 | lines.extend (('--%s--' % boundary, '')) 45 | body = '\r\n'.join (lines) 46 | 47 | headers = {'content-type': 'multipart/form-data; boundary=' + boundary, 48 | 'content-length': str (len (body))} 49 | 50 | return body, headers 51 | 52 | def httplib2_request(uri, method="GET", body='', headers=None, 53 | redirections=httplib2.DEFAULT_MAX_REDIRECTS, 54 | connection_type=None, disable_ssl_certificate_validation=True): 55 | 56 | DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded' 57 | 58 | if not isinstance(headers, dict): 59 | headers = {} 60 | 61 | if method == "POST": 62 | headers['Content-Type'] = headers.get('Content-Type', 63 | DEFAULT_POST_CONTENT_TYPE) 64 | 65 | return httplib2.Http(disable_ssl_certificate_validation=disable_ssl_certificate_validation).\ 66 | request(uri, method=method, body=body, 67 | headers=headers, redirections=redirections, 68 | connection_type=connection_type) 69 | 70 | def wrap_long_line(text, max_len=60): 71 | if len(text) <= max_len: 72 | return text 73 | out = "" 74 | parts = text.split("\n") 75 | parts_out = [] 76 | for x in parts: 77 | parts_out.append( _wrap_long_line(x, max_len) ) 78 | return "\n".join(parts_out) 79 | 80 | def _wrap_long_line(text, max_len): 81 | out_text = "" 82 | times = len(text)*1.0 / max_len 83 | if times > int(times): 84 | times = int(times) + 1 85 | else: 86 | times = int(times) 87 | 88 | i = 0 89 | index = 0 90 | while i < times: 91 | s = text[index:index+max_len] 92 | out_text += s 93 | if not ('<' in s or '>' in s): 94 | out_text += "\n" 95 | index += max_len 96 | i += 1 97 | 98 | return out_text 99 | 100 | def datetime2timestamp(datetime_): 101 | if not isinstance(datetime_, datetime.datetime): 102 | return 0 103 | 104 | return datetime_ and int(time.mktime(datetime_.timetuple())) 105 | 106 | EMAILRE = re.compile(r'^[_\.0-9a-zA-Z+-]+@([0-9a-zA-Z]+[0-9a-zA-Z-]*\.)+[a-zA-Z]{2,4}$') 107 | def is_valid_email(email): 108 | if len(email) >= 6: 109 | return EMAILRE.match(email) != None 110 | return False 111 | 112 | def is_valid_image(content): 113 | return content and imghdr.what(content) in \ 114 | [ 'rgb' ,'gif' ,'pbm' ,'pgm' , 115 | 'ppm' ,'tiff' ,'rast' ,'xbm' ,'jpeg' ,'bmp' ,'png'] 116 | 117 | def sizeof_fmt(num): 118 | for x in ['bytes','KB','MB','GB','TB']: 119 | if num < 1024.0: 120 | return "%3.1f%s" % (num, x) 121 | num /= 1024.0 122 | 123 | def markdownize(content): 124 | return markdown2.markdown(content, extras=["wiki-tables", "code-friendly"]) 125 | 126 | -------------------------------------------------------------------------------- /past/utils/filters.py: -------------------------------------------------------------------------------- 1 | #!-*- coding:utf8 -*- 2 | import re 3 | from datetime import datetime, timedelta 4 | from past.utils import escape 5 | 6 | _paragraph_re = re.compile(r'(?:\r\n|\r|\n)') 7 | 8 | def nl2br(value): 9 | result = u"
    \n".join(_paragraph_re.split(value)) 10 | return result 11 | 12 | def linkify(text): 13 | return escape.linkify(text) 14 | 15 | def html_parse(s, preserve): 16 | return escape.MyHTMLParser.parse(text, preserve) 17 | 18 | def stream_time(d): 19 | now = datetime.now() 20 | delta = now -d 21 | 22 | #duration = delta.total_seconds() ##python2.7 23 | duration = delta.days * 365 * 86400 + delta.seconds 24 | if duration < 0: 25 | return u'穿越了...' 26 | elif duration <= 60: 27 | return u'%s秒前' %int(duration) 28 | elif duration <= 3600: 29 | return u'%s分钟前' %int(duration/60) 30 | elif duration <= 3600*12: 31 | return u'%s小时前' %int(duration/3600) 32 | elif d.year==now.year and d.month==now.month and d.day == now.day: 33 | return u'今天 %s' %d.strftime("%H:%M") 34 | elif d.year==now.year and d.month==now.month and d.day + 1 == now.day: 35 | return u'昨天 %s' %d.strftime("%H:%M") 36 | elif d.year==now.year and d.month==now.month and d.day + 2 == now.day: 37 | return u'前天 %s' %d.strftime("%H:%M") 38 | elif d.year == now.year: 39 | return u'今年 %s' %d.strftime("%m-%d %H:%M") 40 | elif d.year + 1 == now.year: 41 | return u'去年 %s' %d.strftime("%m-%d %H:%M") 42 | elif d.year + 2 == now.year: 43 | return u'前年 %s' %d.strftime("%m-%d %H:%M") 44 | elif d.year + 3 == now.year: 45 | return u'三年前 %s' %d.strftime("%m-%d %H:%M") 46 | elif d.year + 4 == now.year: 47 | return u'四年前 %s' %d.strftime("%m-%d %H:%M") 48 | elif d.year + 5 == now.year: 49 | return u'五年前 %s' %d.strftime("%m-%d %H:%M") 50 | elif d.year + 6 == now.year: 51 | return u'六年前 %s' %d.strftime("%m-%d %H:%M") 52 | else: 53 | return d.strftime("%Y-%m-%d %H:%M:%S") 54 | 55 | -------------------------------------------------------------------------------- /past/utils/logger.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import logging 4 | logging.basicConfig( 5 | format='%(asctime)s %(levelname)s:%(message)s', 6 | datefmt="%Y-%m-%d %H:%M:%S", 7 | level=logging.DEBUG) 8 | 9 | -------------------------------------------------------------------------------- /past/utils/sendmail.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import smtplib 4 | from email.MIMEMultipart import MIMEMultipart 5 | from email.MIMEBase import MIMEBase 6 | from email.MIMEText import MIMEText 7 | from email.Utils import COMMASPACE, formatdate 8 | from email import Encoders 9 | import os 10 | 11 | from past import config 12 | 13 | _TO_UNICODE_TYPES = (unicode, type(None)) 14 | def to_unicode(value): 15 | if isinstance(value, _TO_UNICODE_TYPES): 16 | return value 17 | assert isinstance(value, bytes) 18 | return value.decode("utf-8") 19 | 20 | 21 | def send_mail(to, fro, subject, text, html, files=None, 22 | server=config.SMTP_SERVER, 23 | user=config.SMTP_USER, password=config.SMTP_PASSWORD): 24 | if to and not isinstance(to, list): 25 | to = [to,] 26 | assert type(to)==list 27 | 28 | if files is None: 29 | files = [] 30 | assert type(files)==list 31 | 32 | # Create message container - the correct MIME type is multipart/alternative. 33 | msg = MIMEMultipart('alternative') 34 | msg['From'] = fro 35 | msg['To'] = COMMASPACE.join(to) 36 | msg['Date'] = formatdate(localtime=True) 37 | msg['Subject'] = to_unicode(subject) 38 | 39 | if text: 40 | msg.attach( MIMEText(text, 'plain', 'utf-8' )) 41 | if html: 42 | msg.attach( MIMEText(html, 'html', 'utf-8')) 43 | 44 | for file in files: 45 | part = MIMEBase('application', "octet-stream") 46 | part.set_payload( open(file,"rb").read() ) 47 | Encoders.encode_base64(part) 48 | part.add_header('Content-Disposition', 'attachment; filename="%s"' 49 | % os.path.basename(file)) 50 | msg.attach(part) 51 | 52 | smtp = smtplib.SMTP(server) 53 | if user and password: 54 | smtp.login(user, password) 55 | smtp.sendmail(fro, to, msg.as_string() ) 56 | smtp.close() 57 | 58 | if __name__ == "__main__": 59 | send_mail(['laiwei_ustc '], 60 | 'today of the past', 61 | 'thepast.me | 历史上的今天', 62 | 'http://thepast.me个人杂志计划', 'html内容', 63 | ['/home/work/proj/thepast/past/static/img/avatar.png']) 64 | 65 | -------------------------------------------------------------------------------- /past/view/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | from flask import g, session, request, \ 3 | redirect, url_for, abort, render_template, flash 4 | 5 | from past import app 6 | from past import config 7 | from past.store import db_conn 8 | from past.model.user import User, UserAlias 9 | from past.corelib import auth_user_from_session 10 | 11 | import settings, pdf_view, note, user_past, views 12 | 13 | @app.before_request 14 | def before_request(): 15 | g.config = config 16 | g.user = auth_user_from_session(session) 17 | #g.user = User.get(2) 18 | g.user_alias = UserAlias.gets_by_user_id(g.user.id) if g.user else None 19 | 20 | if request.method == 'POST': 21 | try: 22 | g.start = int(request.form.get('start', 0)) 23 | except ValueError: 24 | g.start = 0 25 | try: 26 | g.count = int(request.form.get('count', 24)) 27 | except ValueError: 28 | g.count = 0 29 | g.cate = request.form.get("cate", "") 30 | else: 31 | try: 32 | g.start = int(request.args.get('start', 0)) 33 | except ValueError: 34 | g.start = 0 35 | try: 36 | g.count = int(request.args.get('count', 24)) 37 | except ValueError: 38 | g.count = 0 39 | g.cate = request.args.get("cate", "") 40 | g.cate = int(g.cate) if g.cate.isdigit() else "" 41 | 42 | if g.user: 43 | g.binds = [ua.type for ua in g.user.get_alias()] 44 | unbinded = list(set(config.OPENID_TYPE_DICT.values()) - 45 | set(g.binds) - set([config.OPENID_TYPE_DICT[config.OPENID_THEPAST]])) 46 | tmp = {} 47 | for k, v in config.OPENID_TYPE_DICT.items(): 48 | tmp[v] = k 49 | g.unbinded = [[x, tmp[x], config.OPENID_TYPE_NAME_DICT[x]] for x in unbinded] 50 | 51 | expired_providers = [] 52 | for t in [ua.type for ua in g.user.get_alias()]: 53 | p = g.user.get_thirdparty_profile(t) 54 | if p and p.get("expired"): 55 | _ = [t, config.OPENID_TYPE_DICT_REVERSE.get(t), config.OPENID_TYPE_NAME_DICT.get(t, "")] 56 | expired_providers.append(_) 57 | g.expired = expired_providers 58 | if expired_providers: 59 | msg = " ".join([x[-1] for x in expired_providers]) 60 | flash(u"你的 %s 授权已经过期了,会影响数据同步,你可以重新授权 :)" % msg, "tip") 61 | else: 62 | g.unbinded = None 63 | 64 | @app.teardown_request 65 | def teardown_request(exception): 66 | #http://stackoverflow.com/questions/9318347/why-are-some-mysql-connections-selecting-old-data-the-mysql-database-after-a-del 67 | db_conn.commit() 68 | -------------------------------------------------------------------------------- /past/view/note.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | #past.view.note 4 | 5 | import markdown2 6 | from flask import g, flash, request, render_template, redirect, abort, url_for 7 | from past import app 8 | 9 | from past.utils.escape import json_encode 10 | from past.utils import randbytes 11 | from past.store import mc 12 | from past.model.user import User 13 | from past.model.note import Note 14 | from past import consts 15 | from past import config 16 | 17 | from .utils import require_login, check_access_note 18 | 19 | @app.route("/notes", methods=["GET"]) 20 | @require_login() 21 | def my_notes(): 22 | return redirect("/%s?cate=%s" % (g.user.uid, config.CATE_THEPAST_NOTE)) 23 | 24 | @app.route("//notes", methods=["GET"]) 25 | def user_notes(uid): 26 | user = User.get(uid) 27 | if not user: 28 | abort(403, "no_such_user") 29 | 30 | return redirect("/%s?cate=%s" % (uid, config.CATE_THEPAST_NOTE)) 31 | 32 | @app.route("/note/", methods=["GET",]) 33 | def note(nid): 34 | note = Note.get(nid) 35 | if not note: 36 | abort(404, "no such note") 37 | 38 | r = check_access_note(note) 39 | if r: 40 | flash(r[1].decode("utf8"), "tip") 41 | return redirect(url_for("home")) 42 | 43 | title = note.title 44 | content = note.content 45 | fmt = note.fmt 46 | if fmt == consts.NOTE_FMT_MARKDOWN: 47 | content = markdown2.markdown(note.content, extras=["wiki-tables", "code-friendly"]) 48 | create_time = note.create_time 49 | user = User.get(note.user_id) 50 | return render_template("v2/note.html", consts=consts, **locals()) 51 | 52 | @app.route("/note/edit/", methods=["GET", "POST"]) 53 | @require_login() 54 | def note_edit(nid): 55 | note = Note.get(nid) 56 | if not note: 57 | abort(404, "no such note") 58 | 59 | if g.user.id != note.user_id: 60 | abort(403, "not edit privileges") 61 | 62 | error = "" 63 | if request.method == "GET": 64 | title = note.title 65 | content = note.content 66 | fmt = note.fmt 67 | privacy = note.privacy 68 | return render_template("v2/note_create.html", consts=consts, **locals()) 69 | 70 | elif request.method == "POST": 71 | # edit 72 | title = request.form.get("title", "") 73 | content = request.form.get("content", "") 74 | fmt = request.form.get("fmt", consts.NOTE_FMT_PLAIN) 75 | privacy = request.form.get("privacy", consts.STATUS_PRIVACY_PUBLIC) 76 | 77 | if request.form.get("cancel"): 78 | return redirect("/note/%s" % note.id) 79 | 80 | if request.form.get("submit"): 81 | error = check_note(title, content) 82 | if not error: 83 | note.update(title, content, fmt, privacy) 84 | flash(u"日记修改成功", "tip") 85 | return redirect("/note/%s" % note.id) 86 | else: 87 | flash(error.decode("utf8"), "error") 88 | return render_template("v2/note_create.html", consts=consts, **locals()) 89 | 90 | else: 91 | return redirect("/note/%s" % note.id) 92 | 93 | @app.route("/note/create", methods=["GET", "POST"]) 94 | @require_login(msg="先登录才能写日记") 95 | def note_create(): 96 | user = g.user 97 | error = "" 98 | if request.method == "POST": 99 | 100 | title = request.form.get("title", "") 101 | content = request.form.get("content", "") 102 | fmt = request.form.get("fmt", consts.NOTE_FMT_PLAIN) 103 | privacy = request.form.get("privacy", consts.STATUS_PRIVACY_PUBLIC) 104 | 105 | if request.form.get("cancel"): 106 | return redirect("/i") 107 | 108 | # submit 109 | error = check_note(title, content) 110 | 111 | if not error: 112 | note = Note.add(g.user.id, title, content, fmt, privacy) 113 | if note: 114 | flash(u"日记写好了,看看吧", "tip") 115 | return redirect("/note/%s" % note.id) 116 | else: 117 | error = "添加日记的时候失败了,真不走运,再试试吧^^" 118 | if error: 119 | flash(error.decode("utf8"), "error") 120 | return render_template("v2/note_create.html", consts=consts, **locals()) 121 | 122 | elif request.method == "GET": 123 | return render_template("v2/note_create.html", consts=consts, **locals()) 124 | 125 | else: 126 | abort("wrong_http_method") 127 | 128 | @app.route("/note/preview", methods=["POST"]) 129 | def note_preview(): 130 | r = {} 131 | content = request.form.get("content", "") 132 | fmt = request.form.get("fmt", consts.NOTE_FMT_PLAIN) 133 | if fmt == consts.NOTE_FMT_MARKDOWN: 134 | r['data'] = markdown2.markdown(content, extras=["wiki-tables", "code-friendly"]) 135 | else: 136 | r['data'] = content 137 | 138 | return json_encode(r) 139 | 140 | def check_note(title, content): 141 | error = "" 142 | if not title: 143 | error = "得有个标题^^" 144 | elif not content: 145 | error = "写点内容撒^^" 146 | elif len(title) > 120: 147 | error = "标题有些太长了" 148 | elif len(content) > 102400: 149 | error = "正文也太长了吧" 150 | 151 | return error 152 | -------------------------------------------------------------------------------- /past/view/pdf_view.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import os 3 | from datetime import datetime, timedelta 4 | import calendar 5 | import time 6 | from collections import defaultdict 7 | 8 | from flask import g, request, redirect, url_for, abort, render_template,\ 9 | make_response, flash 10 | 11 | from past import app 12 | from past import config 13 | from past.model.user import User, PdfSettings 14 | from past.model.status import Status 15 | 16 | from past.utils import sizeof_fmt 17 | from past.utils.pdf import is_pdf_file_exists, get_pdf_filename, get_pdf_full_filename 18 | from past.utils.escape import json_encode 19 | from past import consts 20 | from .utils import require_login, check_access_user, statuses_timelize, get_sync_list 21 | 22 | @app.route("/pdf") 23 | @require_login() 24 | def mypdf(): 25 | if not g.user: 26 | return redirect(url_for("pdf", uid=config.MY_USER_ID)) 27 | else: 28 | return redirect(url_for("pdf", uid=g.user.id)) 29 | 30 | @app.route("/pdf/apply", methods=["POST"]) 31 | @require_login() 32 | def pdf_apply(): 33 | delete = request.form.get("delete") 34 | if delete: 35 | PdfSettings.remove_user_id(g.user.id) 36 | flash(u"删除PDF的请求提交成功,系统会在接下来的一天里删除掉PDF文件!", "tip") 37 | return redirect("/pdf") 38 | else: 39 | PdfSettings.add_user_id(g.user.id) 40 | flash(u"申请已通过,请明天早上来下载数据吧!", "tip") 41 | return redirect("/pdf") 42 | 43 | @app.route("/demo-pdf") 44 | def demo_pdf(): 45 | pdf_filename = "demo.pdf" 46 | full_file_name = os.path.join(config.PDF_FILE_DOWNLOAD_DIR, pdf_filename) 47 | resp = make_response() 48 | resp.headers['Cache-Control'] = 'no-cache' 49 | resp.headers['Content-Type'] = 'application/pdf' 50 | resp.headers['Content-Disposition'] = 'attachment; filename=%s' % pdf_filename 51 | resp.headers['Content-Length'] = os.path.getsize(full_file_name) 52 | redir = '/down/pdf/' + pdf_filename 53 | resp.headers['X-Accel-Redirect'] = redir 54 | return resp 55 | 56 | #PDF只允许登录用户查看 57 | @app.route("//pdf") 58 | @require_login() 59 | def pdf(uid): 60 | user = User.get(uid) 61 | if not user: 62 | abort(404, "No such user") 63 | 64 | if uid != g.user.id and user.get_profile_item('user_privacy') == consts.USER_PRIVACY_PRIVATE: 65 | flash(u"由于该用户设置了仅自己可见的权限,所以,我们就看不到了", "tip") 66 | return redirect("/") 67 | 68 | intros = [g.user.get_thirdparty_profile(x).get("intro") for x in config.OPENID_TYPE_DICT.values()] 69 | intros = filter(None, intros) 70 | 71 | pdf_files = [] 72 | start_date = Status.get_oldest_create_time(None, user.id) 73 | now = datetime.now() 74 | d = start_date 75 | while d and d <= now: 76 | pdf_filename = get_pdf_filename(user.id, d.strftime("%Y%m")) 77 | if is_pdf_file_exists(pdf_filename): 78 | full_file_name = get_pdf_full_filename(pdf_filename) 79 | pdf_files.append([d, pdf_filename, sizeof_fmt(os.path.getsize(full_file_name))]) 80 | 81 | days = calendar.monthrange(d.year, d.month)[1] 82 | d += timedelta(days=days) 83 | d = datetime(d.year, d.month, 1) 84 | files_dict = defaultdict(list) 85 | for date, filename, filesize in pdf_files: 86 | files_dict[date.year].append([date, filename, filesize]) 87 | 88 | pdf_applyed = PdfSettings.is_user_id_exists(g.user.id) 89 | return render_template("v2/pdf.html", **locals()) 90 | 91 | @app.route("/pdf/") 92 | @require_login() 93 | def pdf_down(filename): 94 | pdf_filename = filename 95 | if not is_pdf_file_exists(pdf_filename): 96 | abort(404, "Please wait one day to download the PDF version, because the vps memory is limited") 97 | 98 | user_id = pdf_filename.split('_')[1] 99 | u = User.get(user_id) 100 | if not u: 101 | abort(400, 'Bad request') 102 | 103 | if user_id != g.user.id and u.get_profile_item('user_privacy') == consts.USER_PRIVACY_PRIVATE: 104 | abort(403, 'Not allowed') 105 | 106 | full_file_name = get_pdf_full_filename(pdf_filename) 107 | resp = make_response() 108 | resp.headers['Cache-Control'] = 'no-cache' 109 | resp.headers['Content-Type'] = 'text/html' 110 | resp.headers['Content-Encoding'] = 'gzip' 111 | resp.headers['Content-Disposition'] = 'attachment; filename=%s' % pdf_filename 112 | resp.headers['Content-Length'] = os.path.getsize(full_file_name) 113 | redir = '/down/pdf/' + pdf_filename 114 | resp.headers['X-Accel-Redirect'] = redir 115 | return resp 116 | 117 | -------------------------------------------------------------------------------- /past/view/utils.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from functools import wraps 4 | from flask import g, flash, redirect, url_for, abort 5 | 6 | from past.model.user import User 7 | from past.model.note import Note 8 | from past import consts 9 | from past import config 10 | 11 | def require_login(msg="", redir=""): 12 | def _(f): 13 | @wraps(f) 14 | def __(*a, **kw): 15 | if not g.user: 16 | flash(msg and msg.decode("utf8") or u"为了保护用户的隐私,请先登录^^", "tip") 17 | return redirect(redir or "/home") 18 | return f(*a, **kw) 19 | return __ 20 | return _ 21 | 22 | def check_access_user(user): 23 | user_privacy = user.get_profile_item('user_privacy') 24 | if user_privacy == consts.USER_PRIVACY_PRIVATE and not (g.user and g.user.id == user.id): 25 | return (403, "由于该用户设置了仅自己可见的权限,所以,我们就看不到了") 26 | elif user_privacy == consts.USER_PRIVACY_THEPAST and not g.user: 27 | return (403, "由于用户设置了仅登录用户可见的权限,所以,需要登录后再看") 28 | 29 | def check_access_note(note): 30 | if note.privacy == consts.STATUS_PRIVACY_PRIVATE and not (g.user and g.user.id == note.user_id): 31 | return (403, "由于该日记设置了仅自己可见的权限,所以,我们就看不到了") 32 | elif note.privacy == consts.STATUS_PRIVACY_THEPAST and not g.user: 33 | return (403, "由于该日记设置了仅登录用户可见的权限,所以,需要登录后再看") 34 | 35 | ## 把status_list构造为month,day的层级结构 36 | def statuses_timelize(status_list): 37 | 38 | hashed = {} 39 | for s in status_list: 40 | hash_s = hash(s) 41 | if hash_s not in hashed: 42 | hashed[hash_s] = RepeatedStatus(s) 43 | else: 44 | hashed[hash_s].status_list.append(s) 45 | 46 | return sorted(hashed.values(), key=lambda x:x.create_time, reverse=True) 47 | 48 | class RepeatedStatus(object): 49 | def __init__(self, status): 50 | self.create_time = status.create_time 51 | self.status_list = [status] 52 | 53 | def get_sync_list(user): 54 | print '------user:',user 55 | user_binded_providers = [ua.type for ua in user.get_alias() if ua.type in config.CAN_SHARED_OPENID_TYPE] 56 | 57 | sync_list = [] 58 | for t in user_binded_providers: 59 | p = user.get_thirdparty_profile(t) 60 | if p and p.get("share") == "Y": 61 | sync_list.append([t, "Y"]) 62 | else: 63 | sync_list.append([t, "N"]) 64 | return sync_list 65 | -------------------------------------------------------------------------------- /past/weixin/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # blueprint: connect 3 | 4 | from flask import Blueprint 5 | from flask import redirect, render_template, g, abort, flash, url_for 6 | 7 | from past import config, consts 8 | 9 | blue_print = Blueprint("weixin", __name__, template_folder="templates", static_folder="static") 10 | 11 | import view 12 | 13 | @blue_print.before_request 14 | def before_request(): 15 | print "--- in weixin blue_print" 16 | 17 | 18 | -------------------------------------------------------------------------------- /pastme.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import os 4 | 5 | activate_this = '%s/env/bin/activate_this.py' % os.path.dirname(os.path.abspath(__file__)) 6 | execfile(activate_this, dict(__file__=activate_this)) 7 | 8 | from werkzeug.contrib.fixers import ProxyFix 9 | from past import app 10 | app.wsgi_app = ProxyFix(app.wsgi_app) 11 | 12 | if __name__ == "__main__": 13 | app.run(port=80) 14 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #hash uwsgi 2>&- || { echo >&2 "I require uwsgi but it's not installed. Aborting."; exit 1; } 4 | # 5 | #killall uwsgi 6 | #sleep 2 7 | ##nohup uwsgi -s /tmp/uwsgi.sock --file `pwd`/pastme.py --callable app & 8 | #nohup uwsgi -s /tmp/uwsgi.sock --lazy --file `pwd`/pastme.py --callable app --processes 4 & 9 | 10 | ## use gunicorn 11 | 12 | hash gunicorn 2>&- || { echo >&2 "I require gunicorn but it's not installed. Aborting."; exit 1; } 13 | killall gunicorn 14 | sleep 2 15 | gunicorn -c gunicorn.conf pastme:app -D --error-logfile ./app.log 16 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laiwei/thepast/b892511380fd99761d952ab61a683e2bb221dff8/tools/__init__.py -------------------------------------------------------------------------------- /tools/add_all_synctask_for_old_user.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append('../') 5 | 6 | import past 7 | from past import config 8 | from past.model.user import UserAlias 9 | from past.model.status import SyncTask 10 | 11 | all_alias_ids = UserAlias.get_ids() 12 | for id_ in all_alias_ids: 13 | print id_ 14 | ua = UserAlias.get_by_id(id_) 15 | if not ua: 16 | continue 17 | print ua 18 | 19 | if ua.type == 'D': 20 | SyncTask.add(config.CATE_DOUBAN_STATUS, ua.user_id) 21 | #SyncTask.add(config.CATE_DOUBAN_MINIBLOG, ua.user_id) 22 | 23 | #if ua.type == 'S': 24 | # SyncTask.add(config.CATE_SINA_STATUS, ua.user_id) 25 | 26 | #if ua.type == 'T': 27 | # SyncTask.add(config.CATE_TWITTER_STATUS, ua.user_id) 28 | 29 | -------------------------------------------------------------------------------- /tools/commpress_pdf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') 3 | import os 4 | import commands 5 | 6 | def file_visitor(args, dir_, files): 7 | if not isinstance(files, list): 8 | return 9 | for f in files: 10 | if not (f.startswith("thepast.me_") and f.endswith(".pdf")): 11 | continue 12 | cmd = "cd ../var/down/pdf/ && tar -zcvf %s.tar.gz %s && rm %s" %(f, f, f) 13 | print "-----", cmd 14 | print commands.getoutput(cmd) 15 | 16 | os.path.walk("../var/down/pdf/", file_visitor, None) 17 | -------------------------------------------------------------------------------- /tools/import_status_to_wordpress.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append('../') 5 | 6 | from datetime import timedelta 7 | import MySQLdb 8 | 9 | import past 10 | from past import config 11 | from past.model.status import Status 12 | 13 | def connect_db(): 14 | try: 15 | conn = MySQLdb.connect( 16 | host=config.DB_HOST, 17 | port=config.DB_PORT, 18 | user=config.DB_USER, 19 | passwd=config.DB_PASSWD, 20 | db="wp_linjuly", 21 | use_unicode=True, 22 | charset="utf8") 23 | return conn 24 | except Exception, e: 25 | print "connect db fail:%s" % e 26 | return None 27 | db_conn = connect_db() 28 | 29 | user_id = 34 30 | limit = 250 31 | 32 | status_ids = Status.get_ids(user_id, limit=limit, order="create_time desc") 33 | 34 | for s in Status.gets(status_ids): 35 | try: 36 | _t = ''.join( [x for x in s.text] ) 37 | 38 | retweeted_data = s.get_retweeted_data() 39 | if retweeted_data: 40 | if isinstance(retweeted_data, basestring): 41 | _t += retweeted_data 42 | else: 43 | _t += retweeted_data.get_content() 44 | print '---sid:', s.id 45 | post_author = 1 46 | post_date = s.create_time 47 | post_date_gmt = s.create_time - timedelta(hours=8) 48 | post_content = _t 49 | post_title = u"%s" %post_content[:10] 50 | post_modified = post_date 51 | post_modified_gmt = post_date_gmt 52 | post_type = "post" 53 | 54 | post_excerpt = "" 55 | to_ping = "" 56 | pinged = "" 57 | post_content_filtered = "" 58 | 59 | cursor = None 60 | try: 61 | cursor = db_conn.cursor() 62 | cursor.execute('''insert into wp_posts (post_author, post_date, post_date_gmt, post_content, 63 | post_excerpt, to_ping, pinged, post_content_filtered, 64 | post_title, post_modified, post_modified_gmt, post_type) 65 | values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)''', 66 | (post_author, post_date, post_date_gmt, post_content, 67 | post_excerpt, to_ping, pinged, post_content_filtered, 68 | post_title, post_modified, post_date_gmt, post_type)) 69 | post_id = cursor.lastrowid 70 | cursor.execute('''update wp_posts set guid = %s''', 71 | "http://www.linjuly.com/?p=%s" %post_id) 72 | cursor.execute('''insert into wp_term_relationships values(%s,3,0)''', post_id) 73 | db_conn.commit() 74 | except Exception, e: 75 | import traceback; print traceback.format_exc() 76 | db_conn.rollback() 77 | finally: 78 | cursor and cursor.close() 79 | except Exception, e: 80 | import traceback; print traceback.format_exc() 81 | 82 | 83 | #*************************** 1. row *************************** 84 | # ID: 8 85 | # post_author: 1 86 | # post_date: 2012-01-01 22:29:57 87 | # post_date_gmt: 2012-01-01 14:29:57 88 | # post_content: 2011,其实是蛮惨的一年。。。 89 | # post_title: 我的2011 90 | # post_excerpt: 91 | # post_status: publish 92 | # comment_status: open 93 | # ping_status: open 94 | # post_password: 95 | # post_name: %e6%88%91%e7%9a%842011 96 | # to_ping: 97 | # pinged: 98 | # post_modified: 2012-03-29 23:31:37 99 | # post_modified_gmt: 2012-03-29 15:31:37 100 | #post_content_filtered: 101 | # post_parent: 0 102 | # guid: http://www.linjuly.com/?p=8 103 | # menu_order: 0 104 | # post_type: post 105 | # post_mime_type: 106 | # comment_count: 0 107 | #1 row in set (0.00 sec) 108 | # 109 | -------------------------------------------------------------------------------- /tools/manully_sync_user_timeline.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append("../") 5 | 6 | activate_this = '../env/bin/activate_this.py' 7 | execfile(activate_this, dict(__file__=activate_this)) 8 | 9 | import datetime 10 | from past import config 11 | from past.model.status import SyncTask 12 | from past.model.user import User 13 | import jobs 14 | 15 | if __name__ == '__main__': 16 | user = User.get(sys.argv[1]) 17 | old = sys.argv[2] == "old" 18 | 19 | if not user: 20 | print "no such user" 21 | exit(1) 22 | 23 | ts = SyncTask.gets_by_user(user) 24 | if not ts: 25 | print "no sync tasks" 26 | 27 | for t in ts: 28 | try: 29 | if t.category == config.CATE_WORDPRESS_POST: 30 | jobs.sync_wordpress(t) 31 | else: 32 | jobs.sync(t, old=old) 33 | except Exception, e: 34 | import traceback 35 | print "%s %s" % (datetime.datetime.now(), traceback.format_exc()) 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tools/merge_user.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') 3 | 4 | from past.store import db_conn 5 | 6 | def merge_a2b(del_uid, merged_uid): 7 | 8 | 9 | print "-------update status:%s 2 %s" % (del_uid, merged_uid) 10 | db_conn.execute("update status set user_id=%s where user_id=%s", (merged_uid, del_uid)) 11 | 12 | print "-------update alias:%s 2 %s" % (del_uid, merged_uid) 13 | db_conn.execute("update user_alias set user_id=%s where user_id=%s", (merged_uid, del_uid)) 14 | 15 | print "-------update synctask:%s 2 %s" % (del_uid, merged_uid) 16 | db_conn.execute("update sync_task set user_id=%s where user_id=%s", (merged_uid, del_uid)) 17 | 18 | db_conn.commit() 19 | 20 | -------------------------------------------------------------------------------- /tools/move_data_from_mongo_to_mysql.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append('../') 5 | import datetime 6 | import os 7 | from MySQLdb import IntegrityError 8 | from past.store import mongo_conn, db_conn 9 | from past.model.kv import UserProfile, RawStatus, Kv 10 | from past.utils.escape import json_decode, json_encode 11 | 12 | def move_user_profile(): 13 | RAW_USER_REDIS_KEY = "/user/raw/%s" 14 | 15 | cursor = db_conn.execute("select id from user order by id") 16 | rows = cursor.fetchall() 17 | cursor and cursor.close() 18 | for row in rows: 19 | print '--------user raw id:', row[0] 20 | sys.stdout.flush() 21 | r1 = mongo_conn.get(RAW_USER_REDIS_KEY % row[0]) 22 | if r1: 23 | print "r1" 24 | #UserProfile.set(row[0], r1) 25 | Kv.set('/profile/%s' %row[0], r1) 26 | r2 = mongo_conn.get("/profile/%s" % row[0]) 27 | if r2: 28 | #Kv.set('/profile/%s' %row[0], r2) 29 | UserProfile.set(row[0], r2) 30 | 31 | def myset(status_id, text, raw): 32 | cursor = None 33 | text = json_encode(text) if not isinstance(text, basestring) else text 34 | raw = json_encode(raw) if not isinstance(raw, basestring) else raw 35 | 36 | db_conn.execute('''replace into raw_status (status_id, text, raw) 37 | values(%s,%s,%s)''', (status_id, text, raw)) 38 | 39 | 40 | def move_status(): 41 | STATUS_REDIS_KEY = "/status/text/%s" 42 | RAW_STATUS_REDIS_KEY = "/status/raw/%s" 43 | 44 | start = 3720000 45 | limit = 100000 46 | #r =db_conn.execute("select count(1) from status") 47 | #total = r.fetchone()[0] 48 | total = 4423725 49 | print '----total status:', total 50 | sys.stdout.flush() 51 | 52 | ef = open("error.log", "a") 53 | #cf = open("cmd.txt", "w") 54 | while (start <= int(total)): 55 | f = open("./midfile.txt", "w") 56 | print '-------start ', start 57 | sys.stdout.flush() 58 | cursor = db_conn.execute("select id from status order by id limit %s,%s", (start, limit)) 59 | rows = cursor.fetchall() 60 | for row in rows: 61 | text = mongo_conn.get(STATUS_REDIS_KEY % row[0]) 62 | raw = mongo_conn.get(RAW_STATUS_REDIS_KEY% row[0]) 63 | if text and raw: 64 | text = json_encode(text) if not isinstance(text, basestring) else text 65 | raw = json_encode(raw) if not isinstance(raw, basestring) else raw 66 | 67 | db_conn.execute('''replace into raw_status (status_id, text, raw) 68 | values(%s,%s,%s)''', (row[0], text, raw)) 69 | db_conn.commit() 70 | start += limit 71 | 72 | 73 | move_user_profile() 74 | move_status() 75 | -------------------------------------------------------------------------------- /tools/move_data_from_redis_to_mongo.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append('../') 5 | import datetime 6 | 7 | from past.store import mongo_conn, db_conn 8 | from past.utils.escape import json_decode, json_encode 9 | 10 | def move_user_profile(): 11 | RAW_USER_REDIS_KEY = "/user/raw/%s" 12 | 13 | cursor = db_conn.execute("select id from user order by id") 14 | rows = cursor.fetchall() 15 | cursor and cursor.close() 16 | for row in rows: 17 | print '--------user raw id:', row[0] 18 | sys.stdout.flush() 19 | r = redis_conn.get(RAW_USER_REDIS_KEY % row[0]) 20 | if r: 21 | mongo_conn.set(RAW_USER_REDIS_KEY % row[0], r) 22 | r2 = redis_conn.get("/profile/%s" % row[0]) 23 | if r2: 24 | mongo_conn.set("/profile/%s" % row[0], r2) 25 | 26 | def move_status(): 27 | STATUS_REDIS_KEY = "/status/text/%s" 28 | RAW_STATUS_REDIS_KEY = "/status/raw/%s" 29 | 30 | start = 318003 31 | limit = 2500 32 | r =db_conn.execute("select count(1) from status") 33 | total = r.fetchone()[0] 34 | print '----total status:', total 35 | sys.stdout.flush() 36 | 37 | while (start <= int(total)): 38 | print '-------start ', start 39 | sys.stdout.flush() 40 | cursor = db_conn.execute("select id from status order by id limit %s,%s", (start, limit)) 41 | rows = cursor.fetchall() 42 | if rows: 43 | keys = [STATUS_REDIS_KEY % row[0] for row in rows] 44 | values = redis_conn.mget(*keys) 45 | print '+++ mget text:', datetime.datetime.now() 46 | docs = [] 47 | for i in xrange(0, len(keys)): 48 | if values[i]: 49 | docs.append({"k":keys[i], "v":values[i]}) 50 | mongo_conn.get_connection().insert(docs) 51 | ##mongo_conn.set(keys[i], values[i]) 52 | print '+++ inserted text:', datetime.datetime.now() 53 | 54 | keys = [RAW_STATUS_REDIS_KEY % row[0] for row in rows] 55 | values = redis_conn.mget(*keys) 56 | print '+++ mget raw:', datetime.datetime.now() 57 | docs = [] 58 | for i in xrange(0, len(keys)): 59 | if values[i]: 60 | docs.append({"k":keys[i], "v":values[i]}) 61 | mongo_conn.get_connection().insert(docs) 62 | print '+++ inserted raw:', datetime.datetime.now() 63 | 64 | start += limit 65 | 66 | #move_user_profile() 67 | move_status() 68 | -------------------------------------------------------------------------------- /tools/recover_wrong.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append('../') 5 | 6 | from datetime import timedelta 7 | 8 | from past.store import db_conn 9 | from past.model.kv import RawStatus 10 | 11 | with open("ids.txt") as f: 12 | for id_ in f: 13 | id_ = id_.rstrip("\n") 14 | 15 | print id_ 16 | cursor = db_conn.execute("delete from status where id=%s", id_) 17 | db_conn.commit() 18 | 19 | RawStatus.remove(id_) 20 | 21 | #cursor = db_conn.execute("select * from status where id=%s", id_) 22 | #print cursor.fetchone() 23 | -------------------------------------------------------------------------------- /tools/remove_pdf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') 3 | import os 4 | 5 | from past.store import db_conn 6 | 7 | user_ids = [] 8 | cursor = db_conn.execute('''select user_id from pdf_settings''') 9 | if cursor: 10 | rows = cursor.fetchall() 11 | user_ids = [row[0] for row in rows] 12 | cursor and cursor.close() 13 | 14 | print user_ids, len(user_ids) 15 | 16 | def file_visitor(args, dir_, files): 17 | #print "-------", dir_, files 18 | pendding = set() 19 | if not isinstance(files, list): 20 | return 21 | for f in files: 22 | if not (f.startswith("thepast.me") and f.endswith(".pdf.tar.gz")): 23 | continue 24 | user_id = int(f.split("_")[1]) 25 | if user_id not in user_ids: 26 | pendding.add(user_id) 27 | print pendding, len(pendding) 28 | for user_id in pendding: 29 | print '---deleting pdf of', user_id 30 | os.popen("rm ../var/down/pdf/thepast.me_%s_2*.pdf.tar.gz" %user_id) 31 | 32 | os.path.walk("../var/down/pdf/", file_visitor, None) 33 | -------------------------------------------------------------------------------- /tools/remove_user.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') 3 | 4 | from past.store import db_conn 5 | from past.model.user import User 6 | from past.model.status import Status 7 | from past.model.kv import RawStatus 8 | from past import consts 9 | from past import config 10 | 11 | from past.utils.logger import logging 12 | log = logging.getLogger(__file__) 13 | 14 | suicide_log = logging.getLogger(__file__) 15 | suicide_log.addHandler(logging.FileHandler(config.SUICIDE_LOG)) 16 | 17 | def remove_user(uid, clear_status=True): 18 | user = User.get(uid) 19 | if not user: 20 | print '---no user:%s' % uid 21 | 22 | suicide_log.info("---- delete from user, uid=%s" %uid) 23 | db_conn.execute("delete from user where id=%s", uid) 24 | db_conn.commit() 25 | User._clear_cache(uid) 26 | 27 | if clear_status: 28 | cursor = db_conn.execute("select id from status where user_id=%s", uid) 29 | if cursor: 30 | rows = cursor.fetchall() 31 | for row in rows: 32 | sid = row[0] 33 | suicide_log.info("---- delete status text, sid=%s" % sid) 34 | RawStatus.remove(sid) 35 | 36 | suicide_log.info("---- delete from status, uid=" %uid) 37 | db_conn.execute("delete from status where user_id=%s", uid) 38 | db_conn.commit() 39 | Status._clear_cache(uid, None) 40 | 41 | suicide_log.info("---- delete from passwd, uid=%s" %uid) 42 | db_conn.execute("delete from passwd where user_id=%s", uid) 43 | suicide_log.info("---- delete from sync_task, uid=%s" % uid) 44 | db_conn.execute("delete from sync_task where user_id=%s", uid) 45 | suicide_log.info("---- delete from user_alias, uid=%s" % uid) 46 | db_conn.execute("delete from user_alias where user_id=%s", uid) 47 | db_conn.commit() 48 | 49 | 50 | def remove_status(uid): 51 | cursor = db_conn.execute("select id from status where user_id=%s", uid) 52 | if cursor: 53 | rows = cursor.fetchall() 54 | for row in rows: 55 | sid = row[0] 56 | print "---- delete mongo text, sid=", sid 57 | RawStatus.remove(sid) 58 | 59 | print "---- delete from status, uid=", uid 60 | db_conn.execute("delete from status where user_id=%s", uid) 61 | db_conn.commit() 62 | Status._clear_cache(uid, None) 63 | 64 | if __name__ == "__main__": 65 | a = sys.argv 66 | uids = a[1:] 67 | for uid in uids: 68 | print "----- remove user:", uid 69 | remove_user(uid) 70 | print "----- remove status of user:", uid 71 | remove_status(uid) 72 | 73 | 74 | -------------------------------------------------------------------------------- /tools/repair_sinaweibo_time.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | sys.path.append('../') 5 | 6 | import datetime 7 | import past 8 | from past.store import db_conn 9 | from past.utils.escape import json_decode 10 | from past.model.kv import RawStatus 11 | 12 | cursor = db_conn.execute("select id from status where category=200") 13 | rows = cursor.fetchall() 14 | cursor and cursor.close() 15 | ids = [x[0] for x in rows] 16 | 17 | for x in ids: 18 | try: 19 | r = RawStatus.get(x) 20 | raw = r.raw if r else "" 21 | if raw: 22 | print x 23 | data = json_decode(raw) 24 | t = data.get("created_at") 25 | created_at = datetime.datetime.strptime(t, "%a %b %d %H:%M:%S +0800 %Y") 26 | db_conn.execute("update status set create_time = %s where id=%s", (created_at, x)) 27 | db_conn.commit() 28 | except: 29 | import traceback 30 | print traceback.format_exc() 31 | sys.stdout.flush() 32 | -------------------------------------------------------------------------------- /tools/update_user_privacy.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../') 3 | 4 | from past.store import db_conn 5 | from past.model.user import User 6 | from past import consts 7 | 8 | def update_p2t(): 9 | cursor = db_conn.execute("select id from user") 10 | rows = cursor and cursor.fetchall() 11 | cursor and cursor.close() 12 | for row in rows: 13 | uid = row[0] 14 | user = User.get(uid) 15 | if not user: 16 | print '---no user %s' %uid 17 | return 18 | 19 | old_p = user.get_profile_item("user_privacy") 20 | if not old_p or old_p == consts.USER_PRIVACY_PUBLIC: 21 | user.set_profile_item("user_privacy", consts.USER_PRIVACY_THEPAST) 22 | print '---updated user %s' %uid 23 | 24 | update_p2t() 25 | --------------------------------------------------------------------------------