├── testforum ├── cron.txt ├── media │ ├── index.html │ ├── avatars │ │ ├── index.html │ │ ├── Nature │ │ │ ├── index.html │ │ │ ├── rabbit.jpg │ │ │ ├── serval.jpg │ │ │ ├── baby_fox.jpg │ │ │ ├── blackbird.jpg │ │ │ └── arctic_fox.jpg │ │ ├── Space │ │ │ ├── index.html │ │ │ ├── andromeda.jpg │ │ │ ├── messier_74.jpg │ │ │ ├── ngc_1672.jpg │ │ │ ├── ngc_4414.jpg │ │ │ ├── antennae_galaxies.jpg │ │ │ └── barred_spiral_galaxy.jpg │ │ └── blank.png │ ├── mred.png │ └── misago.png ├── testforum │ ├── __init__.py │ ├── local.example.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── readme.md ├── avatar_store │ ├── blank │ │ ├── 100.png │ │ ├── 150.png │ │ ├── 20.png │ │ ├── 200.png │ │ ├── 30.png │ │ ├── 50.png │ │ └── 64.png │ ├── c │ │ ├── 1 │ │ │ ├── 2_100.png │ │ │ ├── 2_150.png │ │ │ ├── 2_20.png │ │ │ ├── 2_200.png │ │ │ ├── 2_30.png │ │ │ ├── 2_400.png │ │ │ ├── 2_50.png │ │ │ └── 2_64.png │ │ └── c │ │ │ ├── 1_100.png │ │ │ ├── 1_150.png │ │ │ ├── 1_20.png │ │ │ ├── 1_200.png │ │ │ ├── 1_30.png │ │ │ ├── 1_400.png │ │ │ ├── 1_50.png │ │ │ └── 1_64.png │ ├── e │ │ └── c │ │ │ ├── 3_100.png │ │ │ ├── 3_150.png │ │ │ ├── 3_20.png │ │ │ ├── 3_200.png │ │ │ ├── 3_30.png │ │ │ ├── 3_400.png │ │ │ ├── 3_50.png │ │ │ └── 3_64.png │ └── README.txt ├── theme │ ├── templates │ │ └── README.txt │ └── static │ │ └── README.txt ├── attachments │ └── README.txt ├── manage.py ├── requirements.txt └── .gitignore ├── wechat_bot ├── __init__.py ├── kinto_cli_cookie.json ├── readme.md ├── requirements.txt ├── qa_bot.py ├── forum_client.py ├── wx_test.py ├── localuser.py ├── message_tool_delete_after_read.py ├── message_tool_use_timestamp.py ├── connect_bot.py ├── wx_itchat_test.py ├── paperweeklybot_3group.py ├── paperweeklybot.py └── wxbot.py ├── todo.md ├── redis_sub.py ├── redis_pub.py ├── conf ├── kinto_server └── paper ├── kinto_cli ├── readme.md └── message_tool.py ├── .gitignore ├── readme.md └── doc ├── ubuntu_install.md └── mac_dev.md /testforum/cron.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wechat_bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testforum/media/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testforum/testforum/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testforum/media/avatars/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testforum/media/avatars/Nature/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testforum/media/avatars/Space/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testforum/readme.md: -------------------------------------------------------------------------------- 1 | # paperweekly's forum 2 | 为paperweekly做的论坛 3 | -------------------------------------------------------------------------------- /wechat_bot/kinto_cli_cookie.json: -------------------------------------------------------------------------------- 1 | {"lastest_thread_timestamp":0} 2 | -------------------------------------------------------------------------------- /testforum/media/mred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/mred.png -------------------------------------------------------------------------------- /wechat_bot/readme.md: -------------------------------------------------------------------------------- 1 | # wechat_bot 2 | 这一块应当独立成库,bot和forum是分开部署的 3 | 4 | 5 | # todo 6 | 改为message_bot 7 | -------------------------------------------------------------------------------- /testforum/media/misago.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/misago.png -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | * 使wechat bot和论坛服务分离,在不同机器上,之后可以把wechat bot到树莓派上 3 | * redis分布式访问 4 | * 论坛restful接口 5 | -------------------------------------------------------------------------------- /wechat_bot/requirements.txt: -------------------------------------------------------------------------------- 1 | itchat==1.1.5 2 | requests==2.9.2 3 | kinto-http==6.2.1 4 | Pillow 5 | tinydb 6 | -------------------------------------------------------------------------------- /testforum/media/avatars/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/blank.png -------------------------------------------------------------------------------- /testforum/avatar_store/blank/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/blank/100.png -------------------------------------------------------------------------------- /testforum/avatar_store/blank/150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/blank/150.png -------------------------------------------------------------------------------- /testforum/avatar_store/blank/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/blank/20.png -------------------------------------------------------------------------------- /testforum/avatar_store/blank/200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/blank/200.png -------------------------------------------------------------------------------- /testforum/avatar_store/blank/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/blank/30.png -------------------------------------------------------------------------------- /testforum/avatar_store/blank/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/blank/50.png -------------------------------------------------------------------------------- /testforum/avatar_store/blank/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/blank/64.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/1/2_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/1/2_100.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/1/2_150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/1/2_150.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/1/2_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/1/2_20.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/1/2_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/1/2_200.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/1/2_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/1/2_30.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/1/2_400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/1/2_400.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/1/2_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/1/2_50.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/1/2_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/1/2_64.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/c/1_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/c/1_100.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/c/1_150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/c/1_150.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/c/1_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/c/1_20.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/c/1_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/c/1_200.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/c/1_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/c/1_30.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/c/1_400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/c/1_400.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/c/1_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/c/1_50.png -------------------------------------------------------------------------------- /testforum/avatar_store/c/c/1_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/c/c/1_64.png -------------------------------------------------------------------------------- /testforum/avatar_store/e/c/3_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/e/c/3_100.png -------------------------------------------------------------------------------- /testforum/avatar_store/e/c/3_150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/e/c/3_150.png -------------------------------------------------------------------------------- /testforum/avatar_store/e/c/3_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/e/c/3_20.png -------------------------------------------------------------------------------- /testforum/avatar_store/e/c/3_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/e/c/3_200.png -------------------------------------------------------------------------------- /testforum/avatar_store/e/c/3_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/e/c/3_30.png -------------------------------------------------------------------------------- /testforum/avatar_store/e/c/3_400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/e/c/3_400.png -------------------------------------------------------------------------------- /testforum/avatar_store/e/c/3_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/e/c/3_50.png -------------------------------------------------------------------------------- /testforum/avatar_store/e/c/3_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/avatar_store/e/c/3_64.png -------------------------------------------------------------------------------- /testforum/media/avatars/Nature/rabbit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Nature/rabbit.jpg -------------------------------------------------------------------------------- /testforum/media/avatars/Nature/serval.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Nature/serval.jpg -------------------------------------------------------------------------------- /testforum/media/avatars/Nature/baby_fox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Nature/baby_fox.jpg -------------------------------------------------------------------------------- /testforum/media/avatars/Nature/blackbird.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Nature/blackbird.jpg -------------------------------------------------------------------------------- /testforum/media/avatars/Space/andromeda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Space/andromeda.jpg -------------------------------------------------------------------------------- /testforum/media/avatars/Space/messier_74.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Space/messier_74.jpg -------------------------------------------------------------------------------- /testforum/media/avatars/Space/ngc_1672.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Space/ngc_1672.jpg -------------------------------------------------------------------------------- /testforum/media/avatars/Space/ngc_4414.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Space/ngc_4414.jpg -------------------------------------------------------------------------------- /testforum/media/avatars/Nature/arctic_fox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Nature/arctic_fox.jpg -------------------------------------------------------------------------------- /testforum/theme/templates/README.txt: -------------------------------------------------------------------------------- 1 | You can use this directory to replace default templates with custom ones as well as add new ones to your theme. 2 | -------------------------------------------------------------------------------- /testforum/attachments/README.txt: -------------------------------------------------------------------------------- 1 | This directory is used by Misago to store uploaded posts attachments. 2 | 3 | Make sure its not accessible from outside! 4 | -------------------------------------------------------------------------------- /testforum/media/avatars/Space/antennae_galaxies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Space/antennae_galaxies.jpg -------------------------------------------------------------------------------- /testforum/avatar_store/README.txt: -------------------------------------------------------------------------------- 1 | This directory is used by Misago avatar server to cache final user avatars. 2 | 3 | Make sure its not accessible from outside! 4 | -------------------------------------------------------------------------------- /testforum/media/avatars/Space/barred_spiral_galaxy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwj718/paperweekly_forum/HEAD/testforum/media/avatars/Space/barred_spiral_galaxy.jpg -------------------------------------------------------------------------------- /testforum/theme/static/README.txt: -------------------------------------------------------------------------------- 1 | You can use this directory to replace default assets with custom ones or add new ones to your site. Remember to use collectstatic command with "-c" argument to make your changes visible. 2 | -------------------------------------------------------------------------------- /testforum/testforum/local.example.py: -------------------------------------------------------------------------------- 1 | EMAIL_BACKEND = 'xx' 2 | EMAIL_HOST = 'xx' 3 | EMAIL_HOST_PASSWORD = 'xx' #my gmail password 4 | EMAIL_HOST_USER = 'xx' #my gmail username 5 | EMAIL_PORT = 25 6 | DEFAULT_FROM_EMAIL = EMAIL_HOST_USER 7 | DATABASES_PASSWORD = 'xx' 8 | -------------------------------------------------------------------------------- /testforum/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testforum.settings") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /redis_sub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import redis 4 | #import wdb #本地调试比ipdb方便 5 | r = redis.Redis() 6 | pubsub = r.pubsub() 7 | channels = ['test'] 8 | pubsub.subscribe(channels) 9 | messages = pubsub.get_message() 10 | # 发布的时候必须在线? 11 | # 客户端先在线 然后等待发布,客户端先等待 12 | # 订阅者要先在线 13 | 14 | #ipdb.set_trace() 15 | #wdb.set_trace() 16 | messages #每次只取一条,消息是序列化后的, json 17 | # {'pattern': None, 'type': 'message', 'channel': 'test', 'data': '{"foo": "var"}'} 18 | -------------------------------------------------------------------------------- /testforum/requirements.txt: -------------------------------------------------------------------------------- 1 | django~=1.9.6 2 | djangorestframework==3.3.3 3 | beautifulsoup4==4.4.1 4 | bleach==1.4.3 5 | django-debug-toolbar==1.5 6 | django-crispy-forms==1.6.0 7 | django-htmlmin==0.9.1 8 | django-mptt==0.8.4 9 | fake-factory~=0.5.7 10 | html5lib<0.99999999,>=0.999 11 | markdown==2.6.6 12 | path.py==8.2.1 13 | pillow==3.2.0 14 | psycopg2==2.6.1 15 | pytz 16 | requests<3 17 | unidecode 18 | django-oauth-toolkit==0.10.0 19 | gunicorn 20 | kinto-http==6.2.1 21 | -------------------------------------------------------------------------------- /testforum/testforum/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Misago settings for testforum project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testforum.settings") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /redis_pub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import redis 4 | import json 5 | r = redis.Redis() 6 | 7 | test_json = {} 8 | test_json['foo'] = 'var' 9 | 10 | r.publish('test', 'this will reach the listener') 11 | r.publish('test', 'this will reach th listener 2') 12 | r.publish('fail', 'this will not') 13 | 14 | r.publish('test', json.dumps(test_json)) 15 | # 这部分之后由论坛发送信息到redis,数据结构,json 16 | # http://www.wklken.me/posts/2013/10/19/redis-base.html#2 17 | # HSET 18 | # redis不管数据编码 19 | # Redis has no meaning of "objects", all redis gets are bytes, specifically strings! 20 | # 使用python序列化模块 21 | 22 | -------------------------------------------------------------------------------- /conf/kinto_server: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; #端口 3 | server_name kinto.just4fun.site; #访问域名 4 | #root /home/bob/dylan/; 5 | access_log /tmp/access.log; 6 | error_log /tmp/access.log; 7 | location / { 8 | proxy_set_header X-Forward-For $proxy_add_x_forwarded_for; 9 | proxy_set_header Host $http_host; 10 | proxy_redirect off; 11 | if (!-f $request_filename) { 12 | proxy_pass http://127.0.0.1:8888; #这里是flask应用的gunicorn端口 13 | break; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /wechat_bot/qa_bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | # pip install translate 5 | from translate import Translator 6 | import subprocess 7 | 8 | 9 | 10 | def translate_zh_to_en(content): 11 | # from zh to en 12 | translator= Translator(from_lang='zh',to_lang="en") 13 | translation = translator.translate(content) 14 | return translation 15 | 16 | 17 | def howdoi_zh(content_zh): 18 | content_en = translate_zh_to_en(content_zh) 19 | command = ["howdoi","-a",content_en] 20 | answer = subprocess.check_output(command) 21 | #return answer 22 | #print(answer) 23 | #return answer 24 | return format_answer(answer) 25 | 26 | 27 | def format_answer(answer): 28 | if "Answer from" in answer: 29 | content,url = answer.split("Answer from") 30 | if len(content)>200: 31 | content = content[:200] 32 | new_answer = "{}... \n---\nurl:{}".format(content,url) 33 | 34 | return new_answer 35 | 36 | if __name__ == '__main__': 37 | query = u"如何学python" 38 | howdoi_zh(query.encode('utf-8')) 39 | #query = "如何学python" 40 | #howdoi_zh(query) 41 | -------------------------------------------------------------------------------- /wechat_bot/forum_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | import requests 5 | 6 | # post thread 7 | # @bot#Q 测试第一个问题 8 | def post_thread(username,content,title="来自paperweekly的问题"): 9 | headers = {"Authorization": "bearer test", "User-Agent": "wechatClient/0.1 by paperweekly"} 10 | # 做摘要 11 | #title = '「来自paperweekly微信群用户@{}」的讨论:{}'.format(username,content) 12 | # 做摘要 , 摘要 13 | title = '「来自paperweekly微信群用户@{}」的讨论'.format(username) 14 | post_data = {"title": title, "post":content, "category": 3} 15 | threads_url = 'http://paperweekly.just4fun.site/api/threads/' 16 | response = requests.post(threads_url, data=post_data,headers=headers,verify=False) 17 | return response.json() 18 | 19 | 20 | ### 发评论 21 | # @bot#T#2 这是帖子2的答复 正则解析 22 | def post_reply(username,thread_id,content): #用户名 ,用户名就叫paperweekly 23 | headers = {"Authorization": "bearer test", "User-Agent": "wechatClient/0.1 by paperweekly"} 24 | content = '「来自paperweekly微信群用户@{}」的回复:{}'.format(username,content) 25 | post_data = {"post":content} #支持markdown 26 | threads_url = 'http://paperweekly.just4fun.site/api/threads/{id}/posts/'.format(id=thread_id) 27 | response = requests.post(threads_url, data=post_data,headers=headers,verify=False) 28 | return response.json() 29 | -------------------------------------------------------------------------------- /wechat_bot/wx_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | from wxbot import * 5 | import requests 6 | 7 | def post2forum(content,title="来自paperweekly的问题"): 8 | headers = {"Authorization": "bearer test", "User-Agent": "ChangeMeClient/0.1 by YourUsername"} 9 | post_data = {"title": title, "post":content, "category": 3} 10 | threads_url = 'http://127.0.0.1:8000/api/threads/' 11 | response = requests.post(threads_url, data=post_data,headers=headers,verify=False) 12 | print(response.json()) 13 | 14 | class MyWXBot(WXBot): 15 | def handle_msg_all(self, msg): 16 | #if msg['msg_type_id'] == 4 and msg['content']['type'] == 0: 17 | print(msg["content"]["data"]) 18 | if "Question" in msg["content"]["data"]: 19 | print('[Question]') 20 | content = msg["content"]["data"] 21 | post2forum(content) 22 | # self.send_msg_by_uid(u'hi', msg['user']['id']) 23 | #self.send_img_msg_by_uid("img/1.png", msg['user']['id']) 24 | #self.send_file_msg_by_uid("img/1.png", msg['user']['id']) 25 | ''' 26 | def schedule(self): 27 | self.send_msg(u'张三', u'测试') 28 | time.sleep(1) 29 | ''' 30 | 31 | 32 | def main(): 33 | bot = MyWXBot() 34 | bot.DEBUG = True 35 | bot.conf['qr'] = 'png' # tty linux 36 | bot.run() 37 | 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /kinto_cli/readme.md: -------------------------------------------------------------------------------- 1 | # kinto client 2 | 将[kinto](https://github.com/Kinto/kinto)用作消息队列(这里使用消息队列的广义意思,概念上就是存储字符串的一个队列) 3 | 4 | # Why 5 | ### 为何需要消息队列 6 | 我想把wechat bot和forum分布在不同机器上,两边需要通信,决定采用传递消息的方式来通信。消息存储在一个队列里(消息是事件的载体) 7 | 8 | ### 为何不是redis活着RabbitMQ 9 | 我想把wechat bot和forum分布在不同机器上,forum跑在云服务器上,wechat bot跑在我的树莓派里(因为是私人微信,放在本地树莓派里比较有安全感),由此一来它们是分布式的系统。 10 | 11 | redis的subpub可以轻松解决我的需求,实际上我已经实现了基于redis的通信机制,但redis的远程访问,设计很多安全问题,忍痛弃用 12 | 13 | RabbitMQ比较重,需求很轻量,不想引入额外复杂度 14 | 15 | ### 为何选择kinto 16 | 为何选择kinto用python构建,我喜欢python : ) , 此外kinto小而美,所以没采用firebase或是parse 17 | 18 | 关于kinto可以参考我的这篇文章:[如何架空经常500的后端程序员](http://blog.just4fun.site/kinto-note-05-31.html) 19 | 20 | 在这个需求中,kinto作为一个python库,运行起来,消息直接存在内存汇总即可(后期有需要存入postgres数据库), 小而美 :) 21 | 22 | # 依赖 23 | 使用Kinto的python客户端:[kinto-http](https://github.com/Kinto/kinto-http.py) 24 | 25 | # 消息存取流程 26 | * `微信->论坛` 的消息通信机制已经完成,这部分不需要外部消息队列 27 | * kinto主要服务于`论坛->微信`的消息存取,下边描述 28 | 29 | 论坛发生变更(目前主要关注帖子/评论的创建),把消息发送到kinto server上,kinto server是一个JSON storage service 30 | 31 | 32 | # 消息监控 33 | 可以直接使用[kinto-admin](https://kinto.github.io/kinto-admin/) 34 | 35 | 可以把这个网页视为你的client,填入你的server和凭证即可,官方的管理端需要https,无法对接本地http服务 36 | 37 | # todo 38 | 如果kinto-http之后实现了synchronisation机制,利用了客户端缓存,我们就不需要删除record了,每次查询都将获得新的信息(类似pubsub),原理上是通过时间戳完成 39 | 40 | 细节参考:[Synchronisation](http://kinto.readthedocs.io/en/stable/tutorials/synchronisation.html#sync-implementations) 41 | 42 | # 客户端工具 43 | wechat_bot/message_tool.py 44 | 45 | -------------------------------------------------------------------------------- /testforum/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | .venv/ 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # local 10 | local.py 11 | # Distribution / packaging 12 | .Python 13 | temp/ 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # cookie 31 | *.pkl 32 | # local note 33 | note.md 34 | SmartQQBot 35 | 36 | ### Misago 37 | Misago/ 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *,cover 58 | .hypothesis/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # IPython Notebook 82 | .ipynb_checkpoints 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # dotenv 91 | .env 92 | 93 | # virtualenv 94 | .venv/ 95 | venv/ 96 | ENV/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | -------------------------------------------------------------------------------- /testforum/testforum/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include, url 3 | # Serve static and media files in development 4 | from django.conf.urls.static import static 5 | # Setup Django admin to work with Misago auth 6 | from django.contrib import admin 7 | 8 | # Register default views 9 | from misago.core.views import javascript_catalog, momentjs_catalog 10 | from misago.users.forms.auth import AdminAuthenticationForm 11 | 12 | 13 | admin.autodiscover() 14 | admin.site.login_form = AdminAuthenticationForm 15 | 16 | 17 | 18 | urlpatterns = [ 19 | url(r'^', include('misago.urls', namespace='misago')), 20 | 21 | # wwj 22 | url(r'^admin/', admin.site.urls), 23 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), 24 | 25 | # Javascript translations 26 | url(r'^django-i18n.js$', javascript_catalog), 27 | url(r'^moment-i18n.js$', momentjs_catalog), 28 | 29 | # Uncomment next line if you plan to use Django admin for 3rd party apps 30 | #url(r'^django-admin/', include(admin.site.urls)), 31 | 32 | # Uncomment next line if you plan to use browseable API 33 | #url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) 34 | ] 35 | 36 | 37 | 38 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 39 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 40 | 41 | 42 | # Error Handlers 43 | # Misago needs those handlers to deal with errors raised by it's middlewares 44 | # If you replace those handlers with custom ones, make sure you decorate them 45 | # functions with shared_403_exception_handler or shared_404_exception_handler 46 | # decorators that are defined in misago.views.errorpages module! 47 | handler403 = 'misago.core.errorpages.permission_denied' 48 | handler404 = 'misago.core.errorpages.page_not_found' 49 | -------------------------------------------------------------------------------- /wechat_bot/localuser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | # 构建一个用户模型 5 | # 消息来得时候构建用户 6 | from tinydb import TinyDB, where,Query 7 | import time 8 | 9 | class LocalUserTool(object): 10 | ''' 11 | 数据库作为类属性 12 | 是一个工具类 13 | 实质上是数据存储/检索类 14 | ''' 15 | DB = TinyDB("localuser"+".json") 16 | # 删掉旧的 17 | #TABLE= DB.table("localuser"+str(int(time.time()))) # 加上时间戳 18 | DB.purge_tables() # 移除所有表格 19 | TABLE= DB.table("localuser") # 加上时间戳 20 | 21 | def __init__(self): 22 | """TODO: to be defined1. 23 | :userid: TODO 24 | """ 25 | pass 26 | def get_actual_user_name(self,at_id): 27 | ''' 28 | 根据at_id获取用户昵称 , 用于at 29 | ''' 30 | #userid = msg["ActualUserName"] # 用户在群里的名字 , 可at 31 | Record = Query() 32 | localuser = self.TABLE.get(Record.at_id==at_id) # dict 33 | if localuser: 34 | return localuser.get("actual_user_name") 35 | def get_at_id(self,actual_user_name): 36 | Record = Query() 37 | localuser = self.TABLE.get(Record.actual_user_name == actual_user_name) # dict 38 | if localuser: 39 | return localuser.get("at_id") 40 | def set_at_id(self,actual_user_name,groupid=None): 41 | ''' 42 | 设置用户at_id 43 | ''' 44 | #检验msg["ActualUserName"]是够已分配at_id,如果没有则分配,如果有则 45 | #new_record["actual_user_name"] = userid 46 | localuser = {} 47 | localuser["actual_user_name"] = actual_user_name 48 | localuser["groupid"] = groupid 49 | localuser["at_id"] = len(self.TABLE.all())+1 # 自增,从1开始 50 | self.TABLE.insert(localuser) 51 | return localuser["at_id"] 52 | # 获取用户信息,如果存在则获取,不存在则添加 53 | 54 | def main(): 55 | localuser_tool = LocalUserTool() 56 | print(localuser_tool.get_actual_user_name(10)) 57 | at_id = localuser_tool.get_at_id("@abc") 58 | if not at_id: 59 | localuser_tool.set_at_id("@abc") 60 | at_id = localuser_tool.get_at_id("@abc") 61 | print(localuser_tool.get_actual_user_name(at_id)) 62 | 63 | 64 | if __name__ == '__main__': 65 | main() 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # paperweekly's forum 2 | 为[paperweekly](https://zhuanlan.zhihu.com/paperweekly#!)构建的论坛 3 | 4 | ps:群里进行头脑风暴,需求确定得很快,用了一晚大概实现了骨架,源码粗糙,见笑,欢迎改进 : ) 5 | 6 | # 提醒 7 | 消息机器人最近被独立出去,单独维护了,仅仅想使用消息机器人的同学,可以移步到这个项目:[paperweekly_bot](https://github.com/wwj718/paperweekly_bot) 8 | 9 | # 描述 10 | 项目由3个组件构成: 11 | 12 | * 论坛(forum/bbs) 13 | * 微信机器人(wechat_bot) 14 | * 消息服务 15 | 16 | 实现paperweekly微信群<==>论坛双向通信(方便问题讨论与归档整理),消息同时可以被多个client订阅,支持推送到QQ群,允许被任意多得消息订阅点订阅(假设不考虑服务器压力) 17 | 18 | # 场景 19 | 当大家在微信群中交流时,消息可以被推送到论坛中以便归档。当论坛有新的讨论时,将自动推送到微信群,大家可以据此展开讨论,并将讨论结果推往论坛以解答问题。 20 | 21 | 设想这种场景:进行头脑风暴时,大家在微信群中漫谈、碰撞、擦出火花,任何成员看到亮点即可使用:`/bot/q xxx`将点子推往论坛做记录,观点争论问题也是如此。 22 | 23 | 当群成员看到来自论坛的问题,使用:`/bot/t/(id) xxx`即可对问题及时作出回答,论坛那头在线急等的小伙伴便可看到 24 | 25 | 同时更多的微信群和QQ群可以订阅讨论的结果,华山论剑,天下观之 26 | 27 | # 架构 28 | ![](https://raw.githubusercontent.com/wwj718/gif_bed/master/paperweekly_architecture.png) 29 | 30 | # 测试站点 31 | http://paperweekly.just4fun.site/ 32 | 33 | ![](https://raw.githubusercontent.com/wwj718/gif_bed/master/paperweekly_all.jpg) 34 | 35 | 36 | # 依赖 37 | * Nginx 38 | * Gunicorn 39 | * virtualenv 40 | * supervisor 41 | * PostgreSQL 42 | * redis 43 | * Misago 44 | * ItChat 45 | * Kinto 46 | 47 | # todo 48 | - [x] 在服务器部署论坛: paperweekly.just4fun.site 49 | - [x] 微信发送帖子到论坛 50 | - [x] 论坛发送帖子到微信群 51 | - [x] bot的交互界面(help:/bot/h,question:/bot/q,thread reply:/bot/t/(id)) 52 | - [x] 迁移论坛到新的服务器 53 | - [ ] 重新设计user interface,更友好的交互方式, 诸如使用表情:`[疑问]`来激活bot 54 | - [x] 整合论坛机器人和1,2群转发机器人(我这里基于itchat实现了一个,@碱馒头兄也有一个版本,我比较偏好itchat就自己实现了) 55 | - [ ] 撰写教程和开发者文档 56 | - [x] 在markdown中支持数学公式 57 | - [x] 与qq群对接 58 | - [x] 回复时增加@的功能 59 | - [x] 从stackoverflow搜索最佳答案 60 | - [x] 支持转发图片和sharing格式信息 61 | 62 | ### 来自paperweekly群的建议 63 | - [x] @张俊:帖子内容支持放图片(方便提问) 64 | - [ ] @guangbao: 有帖子的新消息,@发帖人 ( 功能已在开发环境完成,尚未集成) 65 | - [ ] @碱馒头: 精简帖子创建成功的消息,突出id 66 | - [ ] @张源源: 消息内容的组织需要重新排版。群消息和bbs消息要有区分度 67 | - [ ] @侯月源:希望论坛地址变成帖子地址(地址建议采用ip而不是域名,否则体验不好),能直接跳转近帖子里看历史讨论. 68 | 69 | # 感谢 70 | * [Misago](https://github.com/rafalp/Misago) 71 | * 我fork了一个分支,对源码做了调整:[wwj718/Misago](https://github.com/wwj718/Misago/tree/wwj_master),之后维护这个分支 72 | * [ItChat](https://github.com/littlecodersh/ItChat) 73 | * [kinto](https://github.com/Kinto/kinto) 74 | -------------------------------------------------------------------------------- /conf/paper: -------------------------------------------------------------------------------- 1 | upstream forum_server { 2 | # fail_timeout=0 means we always retry an upstream even if it failed 3 | # to return a good HTTP response (in case the Unicorn master nukes a 4 | # single worker for timing out). 5 | 6 | server 127.0.0.1:8001; 7 | } 8 | 9 | server { 10 | 11 | listen 80; 12 | server_name paperweekly.just4fun.site paperweekly.club; 13 | 14 | client_max_body_size 4G; 15 | 16 | access_log /tmp/nginx-access.log; 17 | error_log /tmp/nginx-error.log; 18 | 19 | location /static/ { 20 | alias /home/wwj/paperweekly_forum/testforum/static/; 21 | } 22 | 23 | location /media/ { 24 | alias /home/paperweekly_forum/testforum/media/; 25 | } 26 | 27 | location / { 28 | # an HTTP header important enough to have its own Wikipedia entry: 29 | # http://en.wikipedia.org/wiki/X-Forwarded-For 30 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 31 | 32 | # enable this if and only if you use HTTPS, this helps Rack 33 | # set the proper protocol for doing redirects: 34 | # proxy_set_header X-Forwarded-Proto https; 35 | 36 | # pass the Host: header from the client right along so redirects 37 | # can be set properly within the Rack application 38 | proxy_set_header Host $http_host; 39 | 40 | # we don't want nginx trying to do something clever with 41 | # redirects, we set the Host: header above already. 42 | proxy_redirect off; 43 | 44 | # set "proxy_buffering off" *only* for Rainbows! when doing 45 | # Comet/long-poll stuff. It's also safe to set if you're 46 | # using only serving fast clients with Unicorn + nginx. 47 | # Otherwise you _want_ nginx to buffer responses to slow 48 | # clients, really. 49 | # proxy_buffering off; 50 | 51 | # Try to serve static files from nginx, no point in making an 52 | # *application* server like Unicorn/Rainbows! serve static files. 53 | if (!-f $request_filename) { 54 | proxy_pass http://forum_server; 55 | break; 56 | } 57 | } 58 | 59 | # Error pages 60 | #error_page 500 502 503 504 /500.html; 61 | #location = /500.html { 62 | # root /webapps/hello_django/static/; 63 | #} 64 | } 65 | -------------------------------------------------------------------------------- /kinto_cli/message_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from kinto_http import Client # 也可以用requests手动实现 4 | credentials = ('wwj', 'wwj-test') 5 | server_url = 'http://localhost:8888/v1' 6 | client = Client(server_url= server_url,auth=credentials) 7 | collection = 'forum2wechat_todo' #forum2wechat_todo # forum2wechat_done 8 | #写到配置文件里 9 | bucket = 'paperweekly' 10 | 11 | 12 | def push_thread(thread_id,username,title,content): #使用魔法参数 13 | data={'thread_id': thread_id, 'username':username,'title':title,'content': content} 14 | client.create_record(data=data,collection=collection, bucket=bucket) 15 | 16 | def get_threads(): 17 | # 获取 18 | records = client.get_records(collection=collection, bucket=bucket) 19 | if records: 20 | client.delete_records(collection=collection,bucket=bucket) #获取即焚 21 | print(records) 22 | return records 23 | #for item in records: 24 | # print(item) 25 | 26 | 27 | 28 | #record = client.delete_record(id='8c3b1c8c-ed5b-46d9-a9f0-681f3debb68c',collection=collection, bucket=bucket) 29 | 30 | 31 | # F10 vim本地运行python代码,或者分屏,在jupyter里做吧 32 | # http://localhost:8888/v1/buckets/default/collections/tasks/records 33 | # http://localhost:8888/v1/buckets/paperweekly/collections/forum2wechat_todo/records 有记录 34 | # kinto-admin本地有问题 35 | 36 | if __name__ == '__main__': 37 | # 只运行一次 38 | client.create_bucket(bucket) 39 | client.create_collection(collection, bucket=bucket) 40 | 41 | # 创建记录 数据单元 42 | #client.create_record(data={'status': 'todo', 'title': 'Todo #2'}, 43 | # collection=collection, bucket=bucket) 44 | # 获取 45 | push_thread('thread_id','username','title','content') 46 | push_thread('thread_id2','username','title','content') 47 | get_threads() 48 | #records = client.get_records(collection=collection, bucket=bucket) 49 | 50 | #client.delete_records(collection=collection,bucket=bucket) #获取即焚 51 | #for item in records: 52 | # print(item) 53 | #这样每次只有新创建的 54 | ''' 55 | [{u'username': u'username', u'title': u'title', u'content': u'content', u'thread_id': u'thread_id2', u'last_modified': 1474377043828, u'id': u'd8663a05-e864-4b81-9cf3-99cb03232327'}, {u'username': u'username', u'title': u'title', u'content': u'content', u'thread_id': u'thread_id', u'last_modified': 1474377043820, u'id': u'8f3f7646-9601-4b02-b364-fc8c1b31450d'}] 56 | ''' 57 | 58 | 59 | -------------------------------------------------------------------------------- /wechat_bot/message_tool_delete_after_read.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from kinto_http import Client # 也可以用requests手动实现 4 | credentials = ('wwj', 'wwj-test') 5 | server_url = 'http://paperweekly.just4fun.site:8888/v1' 6 | client = Client(server_url= server_url,auth=credentials) 7 | collection = 'forum2wechat_todo' #forum2wechat_todo # forum2wechat_done 8 | #写到配置文件里 9 | bucket = 'paperweekly' 10 | 11 | 12 | def push_thread(thread_id,username,title,content): #使用魔法参数 13 | data={'thread_id': thread_id, 'username':username,'title':title,'content': content} 14 | client.create_record(data=data,collection=collection, bucket=bucket) 15 | 16 | def get_threads(): 17 | # 获取 18 | records = client.get_records(collection=collection, bucket=bucket) 19 | if records: 20 | client.delete_records(collection=collection,bucket=bucket) #获取即焚 21 | print(records) 22 | return records 23 | #for item in records: 24 | # print(item) 25 | 26 | 27 | 28 | #record = client.delete_record(id='8c3b1c8c-ed5b-46d9-a9f0-681f3debb68c',collection=collection, bucket=bucket) 29 | 30 | 31 | # F10 vim本地运行python代码,或者分屏,在jupyter里做吧 32 | # http://localhost:8888/v1/buckets/default/collections/tasks/records 33 | # http://localhost:8888/v1/buckets/paperweekly/collections/forum2wechat_todo/records 有记录 34 | # kinto-admin本地有问题 35 | 36 | if __name__ == '__main__': 37 | # 只运行一次 38 | client.create_bucket(bucket) 39 | client.create_collection(collection, bucket=bucket) 40 | 41 | # 创建记录 数据单元 42 | #client.create_record(data={'status': 'todo', 'title': 'Todo #2'}, 43 | # collection=collection, bucket=bucket) 44 | # 获取 45 | push_thread('thread_id','username','title','content') 46 | push_thread('thread_id2','username','title','content') 47 | get_threads() 48 | #records = client.get_records(collection=collection, bucket=bucket) 49 | 50 | #client.delete_records(collection=collection,bucket=bucket) #获取即焚 51 | #for item in records: 52 | # print(item) 53 | #这样每次只有新创建的 54 | ''' 55 | [{u'username': u'username', u'title': u'title', u'content': u'content', u'thread_id': u'thread_id2', u'last_modified': 1474377043828, u'id': u'd8663a05-e864-4b81-9cf3-99cb03232327'}, {u'username': u'username', u'title': u'title', u'content': u'content', u'thread_id': u'thread_id', u'last_modified': 1474377043820, u'id': u'8f3f7646-9601-4b02-b364-fc8c1b31450d'}] 56 | ''' 57 | 58 | 59 | -------------------------------------------------------------------------------- /doc/ubuntu_install.md: -------------------------------------------------------------------------------- 1 | # 在ubuntu下部署 2 | 网站示例:http://paperweekly.club/ 3 | 4 | 当前论坛部署在ubuntu14.04,其他版本的系统应该也适用 5 | 6 | ### 系统依赖 7 | 8 | ```bash 9 | sudo apt-get install libpq-dev python-dev libjpeg-dev libfreetype6-dev 10 | ``` 11 | 12 | 13 | ### 组件依赖 14 | * Nginx 15 | * Gunicorn 16 | * virtualenv 17 | * supervisor 18 | * PostgreSQL 19 | * redis 20 | * Misago 21 | * ItChat 22 | * Kinto 23 | 24 | # 安装论坛 25 | 论坛使用[Misago](https://github.com/rafalp/Misago) 26 | 27 | 28 | 29 | ### 安装PostgreSQL 30 | 建议采用9.5 31 | 32 | ```bash 33 | # install PostgreSQL 9.5, 早期版本不支持jsonb 34 | sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' 35 | wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | sudo apt-key add - 36 | sudo art-get update 37 | sudo apt-get install postgresql postgresql-contrib 38 | # sudo su - postgres 39 | # psql 40 | # \password postgres 41 | ``` 42 | 43 | ### 配置论坛服务 44 | ```bash 45 | #git clone https://github.com/wwj718/Misago --depth=1 # 我fork了自己的版本(2016.09.18),之后的定制基于这个版本,--depth=1表示只克隆最新的版本 46 | # 克隆我的分支 47 | git clone https://github.com/wwj718/Misago -b wwj_master 48 | pip install -e ./Misago # -e是edit模式,对源码的修改即时生效 , 源码安装, __version__ = '0.6a1.dev1', 49 | git clone https://github.com/wwj718/paperweekly_forum 50 | pip install -r paperweekly_forum/testforum/requirements.txt 51 | cd paperweekly_forum/testforum 52 | python manage.py migrate 53 | python manage.py createsuperuser 54 | #python manage.py runserver #开发 55 | # 收集静态文件 56 | python manage.py collectstatic 57 | gunicorn testforum.wsgi:application --bind 127.0.0.1:8001 -w 4 58 | ``` 59 | 60 | 61 | ### 配置kinto server(消息服务) 62 | ```bash 63 | mkdir ~/kinto_server && cd ~/kinto_server 64 | virtualenv env 65 | source env/bin/activate 66 | pip install kinto 67 | kinto init 68 | kinto migrate 69 | kinto start 70 | ``` 71 | 72 | ### 配置nginx 73 | ``` 74 | sudo apt install nginx 75 | sudo ln -s /home/ubuntu/paperweekly_forum/conf/paper /etc/nginx/sites-enabled/ 76 | sudo ln -s /home/ubuntu/paperweekly_forum/conf/kinto_server /etc/nginx/sites-enabled/ 77 | sudo service nginx restart 78 | ``` 79 | 80 | ###Supervisor 81 | 可参考我的这篇文章:[使用Supervisor来管理进程](http://blog.just4fun.site/process-control-system-supervisor.html) 82 | 83 | 84 | 85 | # todo 86 | * ansible/docker 87 | 88 | 89 | 90 | # 参考 91 | * [How to thoroughly purge and reinstall postgresql on ubuntu?](:http://stackoverflow.com/questions/2748607/how-to-thoroughly-purge-and-reinstall-postgresql-on-ubuntu) 92 | * [How to Install PostgreSQL 9.5 on Ubuntu (12.04 - 15.10)](https://www.howtoforge.com/tutorial/how-to-install-postgresql-95-on-ubuntu-12_04-15_10/) 93 | * [Setting up Django with Nginx, Gunicorn, virtualenv, supervisor and PostgreSQL](http://michal.karzynski.pl/blog/2013/06/09/django-nginx-gunicorn-virtualenv-supervisor/) 94 | 95 | -------------------------------------------------------------------------------- /wechat_bot/message_tool_use_timestamp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from kinto_http import Client # 也可以用requests手动实现 4 | import requests 5 | import time 6 | import json 7 | credentials = ('wwj', 'wwj-test') 8 | server_url = 'http://kinto.just4fun.site/v1' #不能有/ 9 | client = Client(server_url= server_url,auth=credentials) 10 | collection = 'forum2wechat_todo' #forum2wechat_todo # forum2wechat_done 11 | #写到配置文件里 12 | bucket = 'paperweekly' 13 | lastest_thread_timestamp = None # 作为session存储 14 | threads_records_pattern = "/buckets/{bucket}/collections/{collection}/records".format(bucket=bucket,collection=collection) 15 | threads_records_url = "{}{}".format(server_url,threads_records_pattern) 16 | with open("./kinto_cli_cookie.json") as kinto_cli_cookie: 17 | cookie_data = json.loads(kinto_cli_cookie.read()) 18 | now_timestamp = str(int(time.time()))+"000" 19 | lastest_thread_timestamp = cookie_data['lastest_thread_timestamp'] if cookie_data['lastest_thread_timestamp'] else now_timestamp #如果为空则从现在开始 20 | 21 | def push_thread(thread_id,username,title,content): #使用魔法参数 22 | data={'thread_id': thread_id, 'username':username,'title':title,'content': content} 23 | client.create_record(data=data,collection=collection, bucket=bucket) 24 | 25 | def get_threads(): 26 | # 获取 27 | global lastest_thread_timestamp 28 | if not lastest_thread_timestamp: 29 | # 到本地查看是否有文件,cookie, dump,挂掉与激活, kinto_cli_cookie.json 30 | url = threads_records_url 31 | else: 32 | url = threads_records_url+"?_since={}".format(lastest_thread_timestamp) 33 | #records = client.get_records(collection=collection, bucket=bucket) 34 | # try 直接保护起来 35 | try: 36 | response = requests.get(url,auth=credentials) 37 | print response.json() 38 | records = response.json()['data'] 39 | except: 40 | records = [] 41 | # get_records里好像有实现etag,在同义次中应该不会反复请求 ,缓存在哪呢 ,并未缓存 42 | if records: 43 | print(records) 44 | # 找到最大timestamp 45 | lastest_thread_timestamp = max(record['last_modified'] for record in records) 46 | with open("./kinto_cli_cookie.json",'w') as kinto_cli_cookie: 47 | cookie_data = {"lastest_thread_timestamp":lastest_thread_timestamp} 48 | kinto_cli_cookie.write(json.dumps(cookie_data)) 49 | # 存入cookie,理想状态下,只在程序崩溃才存 50 | print("len(records):",len(records)) 51 | #client.delete_records(collection=collection,bucket=bucket) #获取即焚 52 | # 每次不删除而是读取timestamp,获取最大的 53 | for i in records: 54 | print(i) 55 | return records 56 | #for item in records: 57 | # print(item) 58 | 59 | 60 | 61 | #record = client.delete_record(id='8c3b1c8c-ed5b-46d9-a9f0-681f3debb68c',collection=collection, bucket=bucket) 62 | 63 | 64 | # F10 vim本地运行python代码,或者分屏,在jupyter里做吧 65 | # http://localhost:8888/v1/buckets/default/collections/tasks/records 66 | # http://localhost:8888/v1/buckets/paperweekly/collections/forum2wechat_todo/records 有记录 67 | # kinto-admin本地有问题 68 | 69 | if __name__ == '__main__': 70 | # 只运行一次 71 | client.create_bucket(bucket) 72 | client.create_collection(collection, bucket=bucket) 73 | 74 | # 创建记录 数据单元 75 | #client.create_record(data={'status': 'todo', 'title': 'Todo #2'}, 76 | # collection=collection, bucket=bucket) 77 | # 获取 78 | ''' 79 | for i in range(3): 80 | #push_thread('thread_id2','username','title','content') 81 | get_threads() 82 | time.sleep(2) 83 | push_thread('thread_id','username','title','content') 84 | ''' 85 | #records = client.get_records(collection=collection, bucket=bucket) 86 | 87 | #client.delete_records(collection=collection,bucket=bucket) #获取即焚 88 | #for item in records: 89 | # print(item) 90 | #这样每次只有新创建的 91 | ''' 92 | [{u'username': u'username', u'title': u'title', u'content': u'content', u'thread_id': u'thread_id2', u'last_modified': 1474377043828, u'id': u'd8663a05-e864-4b81-9cf3-99cb03232327'}, {u'username': u'username', u'title': u'title', u'content': u'content', u'thread_id': u'thread_id', u'last_modified': 1474377043820, u'id': u'8f3f7646-9601-4b02-b364-fc8c1b31450d'}] 93 | ''' 94 | 95 | 96 | -------------------------------------------------------------------------------- /wechat_bot/connect_bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from __future__ import unicode_literals 4 | import itchat # 另一个微信库:https://github.com/littlecodersh/ItChat 5 | from itchat.content import TEXT 6 | #import redis 7 | #ipdb.set_trace() 8 | import thread 9 | import time 10 | import datetime 11 | import re 12 | 13 | ######### 14 | #log 15 | import logging 16 | LOG_FILE = "/tmp/wechat_log.log" 17 | logging.basicConfig(filename=LOG_FILE,level=logging.INFO) 18 | logger = logging.getLogger(__name__) 19 | handler=logging.FileHandler(LOG_FILE) 20 | logger.addHandler(handler) 21 | logger.setLevel(logging.INFO) 22 | ######### 23 | 24 | 25 | 26 | 27 | # todo : group1 和group2硬编码部分抽象为函数 28 | # todo:targetGroupIds = [] 29 | group1_id = None 30 | group2_id = None 31 | group1 = 'gtest' 32 | group2 = 'paper测试' 33 | group1_msg_list=[] 34 | group2_msg_list=[] 35 | #paperweeklyGroupName = 'PaperWeekly交流群' 36 | 37 | 38 | 39 | 40 | def change_function(): 41 | global group1_msg_list 42 | global group2_msg_list 43 | global group1_id 44 | global group2_id 45 | 46 | #threads = message_tool_use_timestamp.get_threads() 47 | if group1_msg_list and group1_id: # 全局变量paperweeklyGroupId ,初始化为None 48 | print(group1_msg_list) 49 | for msg in group1_msg_list: 50 | message = '@{}:\n{}'.format(msg['ActualNickName'],msg['Text']) 51 | itchat.send_msg(message,group2_id) #完成主动推送 52 | group1_msg_list = [] 53 | if group2_msg_list and group2_id: # 全局变量paperweeklyGroupId ,初始化为None 54 | print(group2_msg_list) 55 | for msg in group2_msg_list: 56 | message = '@{}:\n{}'.format(msg['ActualNickName'],msg['Text']) 57 | itchat.send_msg(message,group1_id) #完成主动推送 58 | group2_msg_list = [] 59 | @itchat.msg_register(TEXT, isGroupChat=True) # 群聊,TEXT , 可视为已经完成的filter 60 | def simple_reply(msg): 61 | global group1_msg_list 62 | global group2_msg_list 63 | global group1_id 64 | global group2_id 65 | #itchat.send(u'@%s\u2005I received: %s' % (msg['ActualNickName'], msg['Content']), msg['FromUserName']) 66 | # 需要判断是否处理消息,只处理目标群消息 67 | if msg['FromUserName'] == group1_id: #针对性处理消息 68 | print('微信群{}连接完毕'.format(group1)) 69 | #response = handle_group_msg(msg) # type 70 | # 来自群1消息,加入消息队列 71 | if '/bot/h' in msg["Text"]: 72 | response='Hi @{}:\nmessage bot是个信使机器人,将使1、2群消息互通\nhave a nice weekend :)\n源码已开放:https://github.com/wwj718/paperweekly_forum'.format(msg['ActualNickName']) 73 | itchat.send_msg(response,group1_id) 74 | else: 75 | group1_msg_list.append(msg) 76 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 77 | logger.info((now,group1,msg['ActualNickName'],msg["Text"])) 78 | 79 | if not group1_id: 80 | #如果找到群id就不找,否则每条消息来都找一下,维护一个群列表,全局 81 | group1_instance = itchat.search_chatrooms(name=group1) #本地测试群 82 | if group1_instance: 83 | group1_id = group1_instance[0]['UserName'] 84 | itchat.send_msg('发现{}id,信使机器人已激活: )'.format(group1),group1_id) 85 | 86 | if msg['FromUserName'] == group2_id: 87 | print('微信群{}连接完毕'.format(group2)) 88 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 89 | if '/bot/h' in msg["Text"]: 90 | response='Hi @{}:\nmessage bot是个信使机器人,将使1、2群消息互通\nhave a nice weekend :)\n源码已开放:https://github.com/wwj718/paperweekly_forum'.format(msg['ActualNickName']) 91 | itchat.send_msg(response,group2_id) 92 | else: 93 | group2_msg_list.append(msg) 94 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 95 | logger.info((now,group2,msg['ActualNickName'],msg["Text"])) 96 | if not group2_id: 97 | group2_instance = itchat.search_chatrooms(name=group2) 98 | if group2_instance: 99 | group2_id = group2_instance[0]['UserName'] 100 | itchat.send_msg('发现{}id,信使机器人已激活: )'.format(group2),group2_id) 101 | 102 | 103 | 104 | 105 | itchat.auto_login(enableCmdQR=2,hotReload=True) #调整宽度:enableCmdQR=2 106 | thread.start_new_thread(itchat.run, ()) 107 | 108 | while 1: 109 | change_function() 110 | time.sleep(1) 111 | 112 | -------------------------------------------------------------------------------- /wechat_bot/wx_itchat_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from __future__ import unicode_literals 4 | import itchat # 另一个微信库:https://github.com/littlecodersh/ItChat 5 | from itchat.content import TEXT 6 | #import redis 7 | #ipdb.set_trace() 8 | import thread 9 | import time 10 | import forum_client 11 | import re 12 | import message_tool_use_timestamp 13 | # 需要在主循环中,有一个轮询机制,而不是回调,目前只能是回调,使用多线程.微信有任何消息,都会查一次 , 需要有一个消息队列, redis 14 | # 发布订阅模型 PubSub 15 | 16 | # http://itchat.readthedocs.io/zh/latest/3.Handler/ 17 | # https://gist.github.com/jobliz/2596594 18 | 19 | # 论坛发往微信 20 | # 如何主动往微信推送 21 | 22 | # http://itchat.readthedocs.io/zh/latest/6.Member%20stuff/ 23 | # todo:targetGroupIds = [] 24 | paperweeklyGroupId = None #目标群id,每次登陆都不同,同一次登录不变 25 | #paperweeklyGroupName = 'paperweekly bbs' #目标群id,每次登陆都不同,同一次登录不变 26 | paperweeklyGroupName = 'gtest' 27 | #paperweeklyGroupName = 'PaperWeekly交流群' 28 | 29 | 30 | def handle_group_msg(msg): 31 | #forum_client.post_thread 32 | #forum_client.post_reply 33 | # forum_client.post_reply('wwj','9',u'测试回复.') 34 | print(msg) 35 | username = msg['ActualNickName'] # 发言者 36 | content = msg['Text'] 37 | if '/bot/q' in content: 38 | clean_content = re.split(r'/bot/q', content)[-1] 39 | response = forum_client.post_thread(username,clean_content) 40 | return {'type':'q','response':response} 41 | 42 | # /bot/q 测试第一个问题 43 | # /bot/t/9 这是帖子9的答复 44 | if '/bot/t' in content: 45 | # 正则获取 46 | thread_id,clean_content = re.split(r'/bot/t/(?P\d+)', content)[-2:] 47 | response = forum_client.post_reply(username,thread_id,clean_content) 48 | return {'type':'t','response':response} 49 | 50 | if '/bot/h' in content: 51 | # 正则获取 52 | response='paperweekly_bot使用说明:帮助:/bot/h\n发帖:/bot/q 帖子内容\n回帖:/bot/t/(id) 回复内容' 53 | 54 | return {'type':'h','response':response} 55 | return {'type':None,'response':None} 56 | 57 | def change_function(): 58 | global paperweeklyGroupId 59 | 60 | #data_list = pubsub.listen() 61 | #for item in data_list: # The last for section will block,使用多线程,处理阻塞问题 62 | # 到kinto上轮询 63 | threads = message_tool_use_timestamp.get_threads() 64 | if threads and paperweeklyGroupId: # 全局变量paperweeklyGroupId ,初始化为None 65 | print(threads) 66 | # message是json,data值为序列化后的json数据,需要做反序列化,可以参考test文件 67 | #print("paperweeklyGroupId:", paperweeklyGroupId) 68 | # 成功发送 69 | for item in threads: 70 | # thread_id,username,title,content 71 | thread_message = '新的讨论:\n帖子id:{}\n发帖人:{}\n标题:{}\n内容:{}\n论坛地址:http://paperweekly.just4fun.site'.format(item['thread_id'],item['username'],item['title'],item['content']) 72 | 73 | itchat.send_msg(thread_message, paperweeklyGroupId) #完成主动推送 74 | #print('主动推送:',threads) 75 | @itchat.msg_register(TEXT, isGroupChat=True) # 群聊,TEXT , 可视为已经完成的filter 76 | def simple_reply(msg): 77 | # @ 78 | #itchat.send(u'@%s\u2005I received: %s' % (msg['ActualNickName'], msg['Content']), msg['FromUserName']) 79 | # 需要判断是否处理消息,只处理目标群消息 80 | global paperweeklyGroupId 81 | if msg['FromUserName'] == paperweeklyGroupId: 82 | print('处理群gtest消息') 83 | # 业务逻辑 , 回调handle 84 | response = handle_group_msg(msg) # type 85 | print response 86 | if response['type'] == 'q': # 发送帖子 87 | pass # 论坛乎触发 88 | #to_wechat_msg = '帖子发送成功 \n 帖子id:{} \n 使用 /bot/t/(id) 可回复'.format(response['response']['id']) 89 | #itchat.send_msg(to_wechat_msg, paperweeklyGroupId) 90 | 91 | if response['type'] == 't': #回复帖子 92 | to_wechat_msg = '帖子回复成功 : )' 93 | itchat.send_msg(to_wechat_msg, paperweeklyGroupId) 94 | if response['type'] == 'h': #回复帖子 95 | to_wechat_msg = response['response'] 96 | itchat.send_msg(to_wechat_msg, paperweeklyGroupId) 97 | # 做个日志记录 98 | if not paperweeklyGroupId: 99 | #如果找到群id就不找,否则每条消息来都找一下,维护一个群列表,全局 100 | gtest = itchat.search_chatrooms(name=paperweeklyGroupName) #本地测试群 101 | if gtest: 102 | paperweeklyGroupId = gtest[0]['UserName'] 103 | itchat.send_msg('发现群id,微信<=>论坛机器人已激活:)', paperweeklyGroupId) 104 | #print(msg) 105 | #print('test:', msg['Content']) 106 | #print("search_chatrooms:", 107 | # itchat.search_chatrooms(name='gtest')) 108 | # NickName, PYQuanPin(全拼) 109 | # 消息来自的用户:msg['ActualNickName'] 110 | #print('get_chatrooms:',itchat.get_chatrooms()) 111 | 112 | 113 | 114 | itchat.auto_login(enableCmdQR=2,hotReload=True) #调整宽度:enableCmdQR=2 115 | thread.start_new_thread(itchat.run, ()) 116 | # 多线程,线程共享了内存,可以考虑协程 117 | # 当前代码是主线程 118 | # thread模块的start_new_thread方法,在线程中运行一个函数,但获得函数返回值极为困难,Python官方不推荐 119 | #itchat.run() 120 | 121 | while 1: 122 | change_function() 123 | time.sleep(1) 124 | 125 | # https://github.com/Urinx/WeixinBot/issues/68 ,主动推送 126 | # https://github.com/Urinx/WeixinBot/blob/da0b2ff1995db97fa7233693cd42ec697785c58d/weixin.py#L300 127 | -------------------------------------------------------------------------------- /testforum/testforum/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Misago settings for testforum project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.9/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.9/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | 14 | from misago.conf.defaults import * 15 | import local # ./local.py store private info 16 | 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: don't run with debug turned on in production! 24 | 25 | DEBUG = TEMPLATE_DEBUG = False 26 | # Hosts allowed to POST to your site 27 | # If you are unsure, just enter here your host name, eg. 'mysite.com' 28 | 29 | #ALLOWED_HOSTS = [] 30 | 31 | 32 | # Database 33 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 34 | 35 | DATABASES = { 36 | 'default': { 37 | # Only PostgreSQL is supported 38 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 39 | 'NAME': 'exampledb', 40 | 'USER': 'dbuser', 41 | 'HOST': 'localhost', 42 | 'PASSWORD': local.DATABASES_PASSWORD, 43 | 'PORT': 5432, 44 | } 45 | } 46 | 47 | # wwj 48 | ''' 49 | DATABASES = { 50 | 'default': { 51 | 'ENGINE': 'django.db.backends.sqlite3', 52 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 53 | } 54 | } 55 | ''' 56 | #LANGUAGE_CODE = 'en' 57 | 58 | TIME_ZONE = 'Asia/Shanghai' 59 | 60 | # Cache 61 | # https://docs.djangoproject.com/en/1.9/ref/settings/#caches 62 | 63 | CACHES = { 64 | 'default': { 65 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 66 | } 67 | } 68 | 69 | 70 | # Site language 71 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 72 | 73 | #LANGUAGE_CODE = 'en-us' 74 | 75 | # Fallback Timezone 76 | # Used to format dates on server, that are then 77 | # presented to clients with disabled JS 78 | # Consult http://en.wikipedia.org/wiki/List_of_tz_database_time_zones TZ column 79 | # for valid values 80 | 81 | TIME_ZONE = 'Asia/Shanghai' 82 | 83 | 84 | # Path used to access static files (CSS, JavaScript, Images) 85 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 86 | 87 | STATIC_URL = '/static/' 88 | 89 | # Path used to access uploaded media (Avatars and Profile Backgrounds, ect.) 90 | # This is NOT path used to serve posts attachments. 91 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 92 | MEDIA_URL = '/media/' 93 | 94 | 95 | # Automatically setup default paths to media and attachments directories 96 | MISAGO_ATTACHMENTS_ROOT = os.path.join(BASE_DIR, 'attachments') 97 | MISAGO_AVATAR_STORE = os.path.join(BASE_DIR, 'avatar_store') 98 | 99 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 100 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 101 | 102 | 103 | # Automatically setup default paths for static and template directories 104 | # You can use those directories to easily customize and add your own 105 | # assets and templates to your site 106 | STATICFILES_DIRS = ( 107 | os.path.join(BASE_DIR, 'theme', 'static'), 108 | ) + STATICFILES_DIRS 109 | 110 | TEMPLATE_DIRS = ( 111 | os.path.join(BASE_DIR, 'theme', 'templates'), 112 | ) + TEMPLATE_DIRS 113 | 114 | 115 | # SECURITY WARNING: keep the secret key used in production secret! 116 | SECRET_KEY = 'ib(6tv9*8skdccpjm3*xxtgovvxc73u+cvls#6h&&r@^yy_jfb' 117 | 118 | 119 | # X-Sendfile support 120 | # X-Sendfile is feature provided by Http servers that allows web apps to 121 | # delegate serving files over to the better performing server instead of 122 | # doing it within app. 123 | # If your server supports X-Sendfile or its variation, enter header name here. 124 | # For example if you are using Nginx with X-accel enabled, set this setting 125 | # to "X-Accel-Redirect". 126 | # Leave this setting empty to Django fallback instead 127 | MISAGO_SENDFILE_HEADER = '' 128 | 129 | # Allows you to use location feature of your Http server 130 | # For example, if you have internal location /mymisago/avatar_cache/ 131 | # that points at /home/myweb/misagoforum/avatar_cache/, set this setting 132 | # to "mymisago". 133 | MISAGO_SENDFILE_LOCATIONS_PATH = '' 134 | 135 | 136 | # Application definition 137 | # Don't edit those settings unless you know what you are doing 138 | ROOT_URLCONF = 'testforum.urls' 139 | WSGI_APPLICATION = 'testforum.wsgi.application' 140 | 141 | # wwj 142 | ALLOWED_HOSTS = ['*'] 143 | 144 | 145 | REST_FRAMEWORK = { 146 | 'DEFAULT_PERMISSION_CLASSES': ( 147 | 'misago.users.rest_permissions.IsAuthenticatedOrReadOnly', 148 | ), 149 | 'EXCEPTION_HANDLER': 'misago.core.exceptionhandler.handle_api_exception', 150 | 'UNAUTHENTICATED_USER': 'misago.users.models.AnonymousUser', 151 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 152 | 'rest_framework.authentication.SessionAuthentication', 153 | 'rest_framework.authentication.TokenAuthentication', 154 | 'oauth2_provider.ext.rest_framework.OAuth2Authentication', 155 | ), 156 | 'URL_FORMAT_OVERRIDE': None, 157 | } 158 | INSTALLED_APPS += ( 159 | 'oauth2_provider', 160 | ) 161 | 162 | # smtp 163 | #EMAIL_USE_TLS = True 164 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 165 | EMAIL_HOST = local.EMAIL_HOST 166 | EMAIL_HOST_PASSWORD = local.EMAIL_HOST_PASSWORD 167 | EMAIL_HOST_USER = local.EMAIL_HOST_USER 168 | EMAIL_PORT = 25 169 | DEFAULT_FROM_EMAIL = EMAIL_HOST_USER 170 | -------------------------------------------------------------------------------- /doc/mac_dev.md: -------------------------------------------------------------------------------- 1 | # mac下开发 2 | 我自己在mac下开发,记录开发的环境搭建以及笔记 3 | 4 | 5 | # 安装论坛 6 | 官方推荐真是使用使用0.5版,0.6版还在开发中,不稳定,新版有许多吸引人的特性,我看了源码和项目的架构,觉得hold得住,自己fork个版本来继续开发也没啥问题,于是决心,吃新鲜螃蟹. 7 | 8 | 项目文档不大完备,安装起来比较折腾,如果你熟悉django,就没啥问题,工程化方面做得不好(开发者少) 9 | 10 | 如果你按照官方文档安装,可能会遇到一些坑。我把我遇到的坑列出 11 | 12 | Misago0.6版目前只支持PostgreSQL。我们先安装数据库 13 | 14 | ## 论坛选型 15 | 群里熟悉python的小伙伴居多,选型上放弃了discourse。一番筛选下来,决定使用[Misago](https://github.com/rafalp/Misago) 16 | 17 | > Misago is fully featured forum application written in Python and ES6, powered by Django and React.js 18 | 19 | #### 介绍 20 | 我们引用该项目首页的介绍: 21 | 22 | > Misago aims to be complete, featured and modern forum solution that has no fear to say 'NO' to common and outdated opinions about how forum software should be made and what it should do. 23 | 24 | 该项目主要由波兰程序员[Rafał Pitoń](https://github.com/rafalp)推进,他完成了绝大多数的代码实现 25 | 26 | 技术栈很新,用的多是当下正流行的开源组件,折腾起来很有意思. 27 | 28 | #### 特性 29 | 就论坛应用而言,这个项目的设计很现代 30 | 31 | * 基于web的管理界面(和discourse类似): `/admincp/` 32 | 33 | ![](http://oav6fgfj1.bkt.clouddn.com/paperweekly862d225b.png) 34 | 35 | * 对markdown的支持 36 | * todo 37 | * 学霸多。支持数学公式 38 | 39 | 40 | 41 | ### 项目依赖 42 | ##### PostgreSQL数据库 43 | 这是个强依赖,无法替换为其他数据库。PostgreSQL是个十分优秀的开源数据库 44 | 45 | 我们先配置好数据库环境,如果你对PostgreSQL不熟悉,可以先阅读:[PostgreSQL新手入门](http://www.ruanyifeng.com/blog/2013/12/getting_started_with_postgresql.html) 46 | 47 | ##### 安装数据库 48 | ```bash 49 | brew cask install postgres # 装好后是postgres.app ,是9.5 , 有图形界面 50 | brew install postgres 51 | # sudo pip install psycopg2 52 | ``` 53 | 54 | 数据库跑起来后,使用`psql`进入数据库,接下来创建用户和数据库并分配好权限 55 | 56 | ``` 57 | :::text 58 | \password wwj # 我的当前用户是wwj,为当前用户用户设置一个密码 59 | CREATE USER dbuser WITH PASSWORD 'password'; # 创建数据库用户dbuser(刚才的是系统用户),并设置密码 60 | CREATE DATABASE exampledb OWNER dbuser; # 创建用户数据库,这里为exampledb,并指定所有者为dbuser 61 | GRANT ALL PRIVILEGES ON DATABASE exampledb to dbuser; # 将exampledb数据库的所有权限都赋予dbuser 62 | \q # 退出控制台(也可以直接按ctrl+D) 63 | ``` 64 | 65 | [PostgreSQL新手入门](http://www.ruanyifeng.com/blog/2013/12/getting_started_with_postgresql.html)一问中给出了其他方法,大家你可以参考 66 | 67 | 在mac下postgres的服务端使用postgres.app,比较好控制。至于为何还要用`brew install postgres`,主要是为了满足python客户端依赖 68 | 69 | #### postgres笔记 70 | 登录数据库:`psql -U dbuser -d exampledb -h 127.0.0.1 -p 5432` 71 | 72 | 另外推荐一个很受欢迎的命令行工具:[pgcli](https://github.com/dbcli/pgcli),使用pgcli连接数据库:`pgcli -U dbuser -d exampledb -h 127.0.0.1 -p 5432` 73 | 74 | 还有个工具:[sandman2](https://github.com/jeffknupp/sandman2),用于提供数据库的rest接口,方面我们直接侵入拓展,暴力美学 75 | 76 | sandman2ctl postgresql+psycopg2://dbuser:wwjtest@localhost/exampledb. 访问:`http://127.0.0.1:5000/admin/` 77 | 78 | 79 | ### 论坛配置文件 80 | forum项目中,数据库相关的设置如下: 81 | 82 | ``` 83 | DATABASES = { 84 | 'default': { 85 | # Only PostgreSQL is supported 86 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 87 | 'NAME': 'exampledb', 88 | 'USER': 'dbuser', 89 | 'HOST': 'localhost', 90 | 'PASSWORD': 'password', 91 | 'PORT': 5432, 92 | } 93 | } 94 | ``` 95 | 96 | ### 安装论坛 97 | 推荐方法二 98 | 99 | #### 方法一 100 | 安装官方的做法,应该这样: 101 | 102 | ```bash 103 | git clone https://github.com/rafalp/Misago 104 | cd Misago 105 | # 使用virtualenv建立虚拟环境 106 | python setup.py install 107 | pip install -r misago/project_template/requirements.txt 108 | misago-start.py testforum # 项目本身有坑,你可以直接使用我建立好的论坛: https://github.com/wwj718/paperweekly_forum 109 | # 构建项目依赖 110 | cd testforum 111 | python manage.py migrate 112 | python manage.py createsuperuser 113 | python manage.py runserver 114 | ``` 115 | 116 | 在此解释下Misago个testforum的关系,这就类似于,django和django site的关系,testforum可以看做Misago的示例,Misago里是作者写好的论坛的核心部件,使用misago-start.py新建的工程,将直接使用Misago的部件来够构建论坛,这样的好处是,用的定制化内容可以放在自建项目里,而核心组件由社区推动,这是开源社区常见做法 117 | 118 | 119 | 120 | #### 方法二 121 | 我已经把项目剥离到github,如果你安装我的版本,坑可能少些 122 | 123 | ``` 124 | # 使用virtualenv建立虚拟环境 125 | # 先把pip升级到1.8以上: pip install --upgrade pip 126 | 127 | git clone https://github.com/wwj718/Misago --depth=1 # 我fork了自己的版本(2016.09.18),之后的定制基于这个版本,--depth=1表示只克隆最新的版本 128 | pip install -e ./Misago # -e是edit模式,对源码的修改即时生效 , 源码安装, __version__ = '0.6a1.dev1', 129 | git clone https://github.com/wwj718/paperweekly_forum 130 | pip install -r paperweekly_forum/testforum/requirements.txt 131 | python manage.py migrate 132 | python manage.py createsuperuser 133 | python manage.py runserver 134 | ``` 135 | 136 | ##### 我的调整 137 | * 使用OAuth2Authentication(django-oauth-toolkit),引出restful接口与外部bot通信 138 | * 开启/admin 139 | * 支持跨域请求 140 | 141 | 默认的设置:[conf/defaults](https://github.com/rafalp/Misago/blob/master/misago/conf/defaults.py),这是django层面的默认设置,可以在forum项目的settings.py中自行覆盖 142 | 143 | 144 | # 跑起来试试 145 | 测试工具分别使用[httpie](https://github.com/jkbrzt/httpie)和[requests](),当然你也可以使用postman或者curl 146 | 147 | 测试网站为`paperweekly.just4fun.site` (目前论坛已经迁往 paperweekly.club) 148 | 149 | #### 发帖 150 | `http post http://paperweekly.just4fun.site/api/threads/ title='from httpie' category=3 post="from httpie" "Authorization: Bearer xxx" ` 返回: 151 | 152 | ```json 153 | { 154 | "id": 2, 155 | "title": "from httpie", 156 | "url": "/thread/from-httpie-2/" 157 | } 158 | ``` 159 | 160 | 下边用[requests](https://github.com/kennethreitz/requests)测试 161 | 162 | ``` 163 | def post2forum(content,title="来自paperweekly的问题"): 164 | threads_url = "http://paperweekly.just4fun.site/api/threads/" 165 | headers = {"Authorization": "bearer test", "User-Agent": "wechatClient/0.1 by paperweekly"} 166 | post_data = {"title": title, "post":content, "category": 3} 167 | response = requests.post(threads_url, data=post_data,headers=headers,verify=False) 168 | return response.json() #json 169 | print post2forum("from requests") 170 | ``` 171 | 172 | ### 发评论 173 | `http post http://paperweekly.just4fun.site/api/threads/2/posts/ post='test2' "Authorization: Bearer test"` 174 | 175 | 176 | 177 | ### 论坛管理 178 | * /admincp/:管理界面入口(需要管理员权限) 179 | * /admincp/settings/basic/ : 论坛基本设置,名称等 180 | * /admincp/settings/users/ : 注册机制设置 181 | 182 | 183 | 184 | 185 | # 与微信通信 186 | 我们使用[wxBot](https://github.com/liuwons/wxBot)来与微信通信,视为微信的io即可 187 | 188 | 为了在linux命令行中使用,需要设置 WXBot 对象的 conf['qr'] 为 tty ,如此一来二维码直接在终端打印出 189 | 190 | 出现1102错误,之后换用:[ItChat](https://github.com/littlecodersh/ItChat) 191 | 192 | 193 | ### 将微信群中的问题类消息发往论坛 194 | 源码参考:[wx_test.py](https://github.com/wwj718/paperweekly_forum/blob/master/wx_test.py) 195 | 196 | 197 | # 开发笔记 198 | 199 | ### 顶部警告 200 | 'Warning: This is unreleased version of Misago. There's no support or update path available for it!' 201 | 202 | Misago中misago/templates/misago/base.html 203 | 204 | 205 | ### smtplib 配置 206 | 207 | ``` 208 | # smtp 209 | #EMAIL_USE_TLS = True 210 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 211 | EMAIL_HOST = 'smtp.qq.com' #使用qq邮箱 212 | EMAIL_HOST_PASSWORD = 'xx' #my gmail password 213 | EMAIL_HOST_USER = 'xx@qq.com' #my gmail username 214 | EMAIL_PORT = 25 215 | DEFAULT_FROM_EMAIL = EMAIL_HOST_USER 216 | ``` 217 | 218 | 219 | ### postgres备份 220 | ``` 221 | pg_dump exampledb > /tmp/exampledb.sql 222 | #恢复 223 | psql exampledb < /tmp/exampledb.sql 224 | ``` 225 | 226 | ### 数据迁移 227 | ``` 228 | pg_dump -d exampledb -U dbuser -W -h 127.0.0.1 -p 5432 > /tmp/exampledb.sql 229 | scp /tmp/exampledb.sql wwj@139.162.234.107:/tmp 230 | sudo su - postgres 231 | psql exampledb < /tmp/exampledb.sql #先删除原有数据 232 | # 备份到git里,大文件 233 | pg_dump -Fc -d exampledb -U dbuser -W -h 127.0.0.1 -p 5432 > /tmp/exampledb.bak # compressed binary format 234 | ``` 235 | 236 | ### 论坛翻译 237 | 238 | ``` 239 | django-admin makemessages -l zh_CN 240 | django-admin compilemessages 241 | ``` 242 | 243 | ### 中文翻译(年代久远) 244 | https://github.com/fooying/misago-trans-cn 245 | 246 | 247 | ### 论坛markdown部分 248 | * bleach : Bleach is a whitelist-based HTML sanitizing library that escapes or strips markup and attributes. 漂白剂这个名字很贴切 249 | * [markdown](https://github.com/waylan/Python-Markdown)==2.6.6 250 | * [文档](https://pythonhosted.org/Markdown/) 251 | * mathjax: 直接在前端解析多好(纯js) 252 | 253 | 254 | # 论坛发送消息(视为webhook output) 255 | https://github.com/rafalp/Misago/blob/master/misago/threads/api/threads.py#L90 256 | 257 | 258 | 259 | # 参考 260 | * [postgres Backup and Restore](http://postgresguide.com/utilities/backup-restore.html) 261 | -------------------------------------------------------------------------------- /wechat_bot/paperweeklybot_3group.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from __future__ import unicode_literals 4 | import threading 5 | import time 6 | import random 7 | import re 8 | import datetime 9 | import thread 10 | #import tinydb 11 | from localuser import LocalUserTool 12 | 13 | ''' 14 | # 重构 15 | * 3个小组 16 | * 先完成转发部分 17 | * 采用心想事成法(sicp) 18 | * send_message 假设存在,发给其他两个群 19 | * 两个群订阅即可,类中有on emit方法(on 方法是并行) 20 | * 消息本身有身份,如果合适就listen on 21 | * 基于事件驱动/多线程 22 | * Queue/blinker 线程安全 23 | ''' 24 | 25 | # itchat for wechat 26 | import itchat # 另一个微信库:https://github.com/littlecodersh/ItChat 27 | from itchat.content import TEXT, PICTURE, RECORDING, ATTACHMENT, VIDEO, SHARING # RECORDING 语音 28 | 29 | ######### 30 | #log 31 | import logging 32 | LOG_FILE = "/tmp/wechat_3group.log" 33 | logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG) 34 | logger = logging.getLogger(__name__) 35 | handler = logging.FileHandler(LOG_FILE) 36 | logger.addHandler(handler) 37 | logger.setLevel(logging.DEBUG) 38 | 39 | ######### 40 | 41 | 42 | #class GroupBot(threading.Thread): # 作为threading类 43 | class GroupBot(object): # 没必要多线程 44 | """Docstring for GroupBot. """ 45 | 46 | def __init__(self, group_name): 47 | """TODO: to be defined1. 48 | 49 | :group_name: TODO 50 | 51 | """ 52 | #threading.Thread.__init__(self) 53 | self._group_name = group_name 54 | self._group_id = None 55 | 56 | def __str__(self): 57 | return self._group_name 58 | def set_id(self,group_id): 59 | self._group_id = group_id 60 | 61 | 62 | def __repr__(self): 63 | return self._group_name 64 | #@itchat.msg_register([TEXT,SHARING,PICTURE], isGroupChat=True) # 群聊,TEXT , 可视为已经完成的filter 65 | def simple_reply(msg): 66 | print("reply message!") # 消息接受在主进程中接受一次即可,没必要多线程,需要一个只能的send_message 67 | 68 | def run(self): 69 | wait_time = random.randrange(1, 3) 70 | print("thread {}(group_name:{}) will wait {}s".format( 71 | self.name, self._group_name, wait_time)) # 默认的名字:Thread-1 72 | time.sleep(wait_time) 73 | print("thread {} finished".format(self.name)) 74 | 75 | 76 | def forward_message(msg,src_group,target_groups): 77 | '''按类型发消息''' 78 | if msg["Type"] == 'Text': 79 | ''' 80 | print(itchat.get_friends()) 81 | # 消息入口 82 | logger.debug(msg) 83 | username = msg["ActualUserName"] # 发言用户id 群id:FromUserName 84 | user = itchat.search_friends(userName=username) 85 | ''' 86 | logger.debug(msg) # log 87 | # 跨群@ , 至于私聊 可以截图发微信号 88 | #把用户都存下,多给msg一个属性 at_id 89 | actual_user_name = msg["ActualNickName"] 90 | localuser_tool = LocalUserTool() 91 | at_id = localuser_tool.get_at_id(actual_user_name) 92 | if not at_id: 93 | at_id = localuser_tool.set_at_id(actual_user_name) 94 | # 改造消息属性,使其多一个at_id 95 | msg["at_id"] = at_id 96 | match_at_message = re.match(r'at *(?P\d+) *(?P.*)', msg["Text"]) 97 | 98 | 99 | 100 | ''' 101 | if 1==2:#msg["Text"].startswith('[疑问]') or msg["Text"].startswith('[闭嘴]') or msg["Text"].startswith('[得意]') or msg["Text"].startswith('[惊讶]'): 102 | # 这些是系统功能不转发 103 | #response = handle_text_msg(msg) # type 104 | response = {'type':None,'response':None} 105 | if response['type'] == 'q': # 发送帖子 106 | pass # 论坛会触发到两个群 107 | if response['type'] == 'qa': # 发送帖子 108 | to_wechat_msg = response['response'] 109 | itchat.send_msg(to_wechat_msg,src_group) 110 | 111 | if response['type'] == 't': #回复帖子 112 | to_wechat_msg = '@{} 帖子回复成功 : )'.format(msg['ActualNickName']) 113 | itchat.send_msg(to_wechat_msg,src_group) 114 | if response['type'] == 'h': #回复帖子 115 | to_wechat_msg = response['response'] 116 | itchat.send_msg(to_wechat_msg,src_group) 117 | # 写一个跨群at,先检测消息 at 1 你好 118 | ''' 119 | if match_at_message: 120 | groupdict = match_at_message.groupdict() 121 | message_at_id = groupdict.get("message_at_id") 122 | message_text = groupdict.get("message_text") 123 | actual_user_name = localuser_tool.get_actual_user_name(int(message_at_id)) 124 | 125 | for group in target_groups: 126 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 127 | logger.info((now, group._group_name, msg['ActualNickName'], msg["Text"])) 128 | message = u'@{} \n{}-at_id:{} 发言 :\n{}'.format(actual_user_name,msg['ActualNickName'],msg['at_id'],message_text) 129 | #message = u'@{}\u2005\n : {}'.format(actual_user_name,message_text) 130 | itchat.send(message,group._group_id) 131 | else: 132 | #普通文本消息 133 | for group in target_groups: 134 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 135 | logger.info((now, group._group_name, msg['ActualNickName'], msg["Text"])) 136 | #if group._group_id: 137 | message = '{}-at_id:{} 发言 :\n{}'.format(msg['ActualNickName'],msg['at_id'],msg['Text']) 138 | itchat.send(message,group._group_id) 139 | if msg["Type"] == 'Picture': 140 | msg['Text'](msg['FileName']) #下载 141 | for group in target_groups: 142 | itchat.send_image(msg['FileName'], group._group_id) 143 | if msg['Type'] == 'Sharing': 144 | share_message = "@{}分享\n{} {}".format( 145 | msg['ActualNickName'], msg["Url"].replace("amp;", ""), msg["Text"]) 146 | for group in target_groups: 147 | itchat.send_msg(share_message, group._group_id) 148 | 149 | 150 | def get_target_groups(src_group, groups): 151 | ''' 152 | src_group 是 对象 153 | groups: 所有的微信组,全集 154 | ''' 155 | list_groups = list(groups) 156 | list_groups.remove(src_group) # target groups 157 | return list_groups 158 | #print("from {} to {}".format(src_group._group_name,",".join([group._group_name for group in list_groups]))) 159 | 160 | def handle_text_msg(msg): 161 | #username = msg['ActualNickName'] # 发言者 162 | content = msg['Text'] 163 | 164 | if '[疑问]' in content: 165 | clean_content = re.split(r'\[疑问\]', content)[-1] 166 | response = "response"#forum_client.post_thread(username,clean_content) 167 | return {'type':'q','response':response} 168 | if '[惊讶]' in content: 169 | clean_content = re.split(r'\[惊讶\]', content)[-1] 170 | answer = "qabot"#qa_bot.howdoi_zh(clean_content.encode('utf-8')) 171 | response = "@{}\n".format(msg['ActualNickName'])+answer 172 | return {'type':'qa','response':response} 173 | #if '/bot/t' in content: 174 | if content.startswith('[得意]'): 175 | #判断下正则是够合格 176 | thread_id,clean_content = re.split(r'\[得意\].*?(?P\d+)', content)[-2:] 177 | response = "response"#forum_client.post_reply(username,thread_id,clean_content) 178 | return {'type':'t','response':response} 179 | 180 | #if '/bot/h' in content: 181 | if '[闭嘴]' in content: 182 | response='Hi @{} 使用说明如下:\n帮助:[闭嘴]\n发帖:[疑问] 帖子内容\n回帖:[得意](id) 回复内容\n搜索:[惊讶] 问题内容'.format(msg['ActualNickName']) 183 | return {'type':'h','response':response} 184 | return {'type':None,'response':None} 185 | 186 | 187 | 188 | 189 | # 全局设置 190 | group1_name = 'paper测试1' 191 | group2_name = 'paper测试2' 192 | group3_name = '测试m' 193 | #group1_name = 'PaperWeekly交流群' 194 | #group2_name = 'PaperWeekly交流二群' 195 | #group3_name = 'PaperWeekly交流三群' 196 | #print "Start main threading" 197 | group1 = GroupBot(group_name=group1_name) 198 | group2 = GroupBot(group_name=group2_name) 199 | group3 = GroupBot(group_name=group3_name) 200 | groups = (group1, group2, group3) #list原有结构会被改变 ,内部元素是够会不可变 201 | 202 | 203 | 204 | def main(): 205 | #group1_name = 'PaperWeekly交流群' 206 | #group2_name = 'PaperWeekly交流二群' 207 | #group3_name = 'PaperWeekly交流三群' 208 | #forward_message("test", group1, groups) 209 | 210 | @itchat.msg_register([TEXT,SHARING,PICTURE], isGroupChat=True) # 群聊,TEXT , 可视为已经完成的filter 211 | def simple_reply(msg): 212 | #设置为nolocal 213 | global groups 214 | print("simple_reply begin msg") 215 | for group in groups: 216 | if msg['FromUserName'] == group._group_id: 217 | src_group = group 218 | target_groups = get_target_groups(src_group, tuple(groups)) 219 | # 筛选出已激活的 220 | active_target_groups = [group for group in target_groups if group._group_id] 221 | forward_message(msg,src_group,active_target_groups) 222 | if not group._group_id: 223 | print(group._group_id) #None 224 | # 不存在的时候 225 | #如果找到群id就不找,否则每条消息来都找一下,维护一个群列表,全局 226 | group_instance = itchat.search_chatrooms(name=group._group_name 227 | ) #本地测试群 228 | if group_instance: 229 | group.set_id(group_instance[0]['UserName']) #没有设置成功? 230 | print("{}激活,group_id:{}".format(group._group_name,group._group_id)) 231 | itchat.send_msg('机器人已激活: )', group._group_id) 232 | 233 | print "End Main function" 234 | 235 | itchat.auto_login(enableCmdQR=2,hotReload=True) #调整宽度:enableCmdQR=2 236 | #thread.start_new_thread(sync_thread, ()) 237 | thread.start_new_thread(itchat.run, ()) 238 | 239 | while 1: 240 | main() 241 | time.sleep(1) 242 | 243 | -------------------------------------------------------------------------------- /wechat_bot/paperweeklybot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from __future__ import unicode_literals 4 | import itchat # 另一个微信库:https://github.com/littlecodersh/ItChat 5 | from itchat.content import TEXT,PICTURE,RECORDING, ATTACHMENT, VIDEO,SHARING 6 | #import redis 7 | #ipdb.set_trace() 8 | import thread 9 | import time 10 | import datetime 11 | import re 12 | import message_tool_use_timestamp 13 | import forum_client 14 | ######### 15 | #log 16 | import logging 17 | LOG_FILE = "/tmp/wechat_log.log" 18 | logging.basicConfig(filename=LOG_FILE,level=logging.INFO) 19 | logger = logging.getLogger(__name__) 20 | handler=logging.FileHandler(LOG_FILE) 21 | logger.addHandler(handler) 22 | logger.setLevel(logging.INFO) 23 | ######### 24 | 25 | import qa_bot 26 | 27 | 28 | # todo : group1 和group2硬编码部分抽象为函数 29 | # todo:targetGroupIds = [] 30 | group1_id = None 31 | group2_id = None 32 | #group1 = 'gtest' 33 | #group2 = 'paper测试' 34 | #group2 = 'paperweekly bbs' 35 | group1_msg_list=[] 36 | group2_msg_list=[] 37 | group1 = 'PaperWeekly交流群' 38 | group2 = 'PaperWeekly交流二群' 39 | 40 | 41 | def sync_thread(): 42 | # 没有被执行 43 | print('begin sync_thread') 44 | global group1_id 45 | global group2_id 46 | threads = message_tool_use_timestamp.get_threads() 47 | print("threads:",threads) 48 | print("ids:",group1_id,group2_id) 49 | if threads and group1_id: 50 | for item in threads: 51 | # url 52 | thread_message = '新的讨论:\n帖子id:{}\n发帖人:{}\n标题:{}\n内容:{}\n论坛地址:http://paperweekly.club{}'.format(item['thread_id'],item['username'],item['title'],item['content'],item['url']) 53 | itchat.send_msg(thread_message,group1_id) #主动推送 54 | if threads and group2_id: 55 | for item in threads: 56 | thread_message = '新的讨论:\n帖子id:{}\n发帖人:{}\n标题:{}\n内容:{}\n论坛地址:http://paperweekly.club{}'.format(item['thread_id'],item['username'],item['title'],item['content'],item['url']) 57 | itchat.send_msg(thread_message,group2_id) 58 | print('end sync_thread') 59 | 60 | 61 | def change_function(): 62 | print("begin change_function") 63 | global group1_msg_list 64 | global group2_msg_list 65 | global group1_id 66 | global group2_id 67 | 68 | sync_thread() 69 | # get thread and post it ,同步帖子 70 | #sync_thread() #todo:单独作为线程 71 | if group1_msg_list and group1_id: # 全局变量paperweeklyGroupId ,初始化为None 72 | print(group1_msg_list) 73 | # 可以不需要队列,直接发送即可,考虑到3个群的问题 74 | for msg in group1_msg_list: 75 | message = '@{} 发言:\n{}'.format(msg['ActualNickName'],msg['Text']) 76 | itchat.send_msg(message,group2_id) #完成主动推送 77 | group1_msg_list.remove(msg) 78 | if group2_msg_list and group2_id: # 全局变量paperweeklyGroupId ,初始化为None 79 | print(group2_msg_list) 80 | for msg in group2_msg_list: 81 | message = '@{}发言:\n{}'.format(msg['ActualNickName'],msg['Text']) 82 | itchat.send_msg(message,group1_id) #完成主动推送 83 | group2_msg_list.remove(msg) 84 | @itchat.msg_register([TEXT,SHARING,PICTURE], isGroupChat=True) # 群聊,TEXT , 可视为已经完成的filter 85 | def simple_reply(msg): 86 | global group1_msg_list 87 | global group2_msg_list 88 | global group1_id 89 | global group2_id 90 | #itchat.send(u'@%s\u2005I received: %s' % (msg['ActualNickName'], msg['Content']), msg['FromUserName']) 91 | # 需要判断是否处理消息,只处理目标群消息 92 | print("simple_reply begin msg") 93 | if msg['FromUserName'] == group1_id: #针对性处理消息 94 | print('微信群{}连接完毕'.format(group1)) 95 | print(msg) 96 | #response = handle_group_msg(msg) # type 97 | # 来自群1消息,加入消息队列 98 | #if '/bot' in msg["Text"] or '[疑问]' in msg["Text"]: 99 | # 多一个分支,if msg["Type"] == 'Picture' 100 | if msg["Type"] == 'Text': 101 | if msg["Text"].startswith('[疑问]') or msg["Text"].startswith('[闭嘴]') or msg["Text"].startswith('[得意]') or msg["Text"].startswith('[惊讶]'): 102 | # 这些是系统功能不转发 103 | response = handle_group_msg(msg) # type 104 | if response['type'] == 'q': # 发送帖子 105 | pass # 论坛会触发到两个群 106 | if response['type'] == 'qa': # 发送帖子 107 | to_wechat_msg = response['response'] 108 | itchat.send_msg(to_wechat_msg,group1_id) 109 | 110 | if response['type'] == 't': #回复帖子 111 | to_wechat_msg = '@{} 帖子回复成功 : )'.format(msg['ActualNickName']) 112 | itchat.send_msg(to_wechat_msg,group1_id) 113 | if response['type'] == 'h': #回复帖子 114 | to_wechat_msg = response['response'] 115 | itchat.send_msg(to_wechat_msg,group1_id) 116 | else: 117 | #普通文本消息 118 | group1_msg_list.append(msg) 119 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 120 | logger.info((now,group1,msg['ActualNickName'],msg["Text"])) 121 | if msg["Type"] == 'Picture': 122 | msg['Text'](msg['FileName']) #下载 123 | #group2_id = group2_id or None 124 | itchat.send_image(msg['FileName'],group2_id) 125 | #itchat.send_image(msg['FileName'],group2_id) 126 | if msg['Type'] == 'Sharing': 127 | #group2_id = group2_id or None 128 | share_message = "@{}分享\n{} {}".format(msg['ActualNickName'],msg["Url"].replace("amp;",""),msg["Text"]) 129 | itchat.send_msg(share_message,group2_id) 130 | #print "share" 131 | if not group1_id: 132 | #如果找到群id就不找,否则每条消息来都找一下,维护一个群列表,全局 133 | group1_instance = itchat.search_chatrooms(name=group1) #本地测试群 134 | if group1_instance: 135 | group1_id = group1_instance[0]['UserName'] 136 | itchat.send_msg('发现{}id,信使机器人已激活: )'.format(group1),group1_id) 137 | 138 | 139 | if msg['FromUserName'] == group2_id: #针对性处理消息 140 | if msg["Type"] == 'Text': 141 | print('微信群{}连接完毕'.format(group2)) 142 | #response = handle_group_msg(msg) # type 143 | if msg["Text"].startswith('[疑问]') or msg["Text"].startswith('[闭嘴]') or msg["Text"].startswith('[得意]') or msg["Text"].startswith('[惊讶]'): 144 | response = handle_group_msg(msg) # type 145 | if response['type'] == 'q': # 发送帖子 146 | pass # 论坛会触发到两个群 147 | if response['type'] == 'qa': # 发送帖子 148 | to_wechat_msg = response['response'] 149 | itchat.send_msg(to_wechat_msg,group2_id) 150 | 151 | if response['type'] == 't': #回复帖子 152 | to_wechat_msg = '@{} 帖子回复成功 : )'.format(msg['ActualNickName']) 153 | itchat.send_msg(to_wechat_msg,group2_id) 154 | if response['type'] == 'h': #回复帖子 155 | to_wechat_msg = response['response'] 156 | itchat.send_msg(to_wechat_msg,group2_id) 157 | # 来自群1消息,加入消息队列 158 | else: 159 | group2_msg_list.append(msg) 160 | now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 161 | logger.info((now,group2,msg['ActualNickName'],msg["Text"])) 162 | 163 | if msg["Type"] == 'Picture': 164 | msg['Text'](msg['FileName']) #下载 165 | #group1_id = group1_id or None 166 | itchat.send_image(msg['FileName'],group1_id) 167 | #itchat.send_image(msg['FileName'],group2_id) 168 | if msg['Type'] == 'Sharing': 169 | #group1_id = group1_id or None 170 | share_message = "@{}分享\n{} {}".format(msg['ActualNickName'],msg["Url"].replace("amp;",""),msg["Text"]) 171 | itchat.send_msg(share_message,group1_id) 172 | #print "share" 173 | 174 | if not group2_id: 175 | #如果找到群id就不找,否则每条消息来都找一下,维护一个群列表,全局 176 | group2_instance = itchat.search_chatrooms(name=group2) #本地测试群 177 | if group2_instance: 178 | group2_id = group2_instance[0]['UserName'] 179 | itchat.send_msg('发现{}id,信使机器人已激活: )'.format(group2),group2_id) 180 | print("end change_function") 181 | 182 | 183 | 184 | 185 | def handle_group_msg(msg): 186 | # 有多种消息 187 | logger.info(msg) 188 | username = msg['ActualNickName'] # 发言者 189 | content = msg['Text'] 190 | print('handle_group_msg',handle_group_msg) 191 | ''' 192 | if '/bot/q' in content: 193 | clean_content = re.split(r'/bot/q', content)[-1] 194 | response = forum_client.post_thread(username,clean_content) 195 | return {'type':'q','response':response} 196 | 197 | ''' 198 | if '[疑问]' in content: 199 | clean_content = re.split(r'\[疑问\]', content)[-1] 200 | response = forum_client.post_thread(username,clean_content) 201 | return {'type':'q','response':response} 202 | if '[惊讶]' in content: 203 | clean_content = re.split(r'\[惊讶\]', content)[-1] 204 | answer = qa_bot.howdoi_zh(clean_content.encode('utf-8')) 205 | response = "@{}\n".format(msg['ActualNickName'])+answer 206 | return {'type':'qa','response':response} 207 | #if '/bot/t' in content: 208 | if content.startswith('[得意]'): 209 | #判断下正则是够合格 210 | thread_id,clean_content = re.split(r'\[得意\].*?(?P\d+)', content)[-2:] 211 | response = forum_client.post_reply(username,thread_id,clean_content) 212 | return {'type':'t','response':response} 213 | 214 | #if '/bot/h' in content: 215 | if '[闭嘴]' in content: 216 | response='Hi @{} 使用说明如下:\n帮助:[闭嘴]\n发帖:[疑问] 帖子内容\n回帖:[得意](id) 回复内容\n搜索:[惊讶] 问题内容'.format(msg['ActualNickName']) 217 | return {'type':'h','response':response} 218 | return {'type':None,'response':None} 219 | 220 | 221 | 222 | 223 | 224 | itchat.auto_login(enableCmdQR=2,hotReload=True) #调整宽度:enableCmdQR=2 225 | #thread.start_new_thread(sync_thread, ()) 226 | thread.start_new_thread(itchat.run, ()) 227 | 228 | while 1: 229 | change_function() 230 | time.sleep(1) 231 | 232 | -------------------------------------------------------------------------------- /wechat_bot/wxbot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import os 5 | import sys 6 | import traceback 7 | import webbrowser 8 | import pyqrcode 9 | import requests 10 | import mimetypes 11 | import json 12 | import xml.dom.minidom 13 | import urllib 14 | import time 15 | import re 16 | import random 17 | from traceback import format_exc 18 | from requests.exceptions import ConnectionError, ReadTimeout 19 | import HTMLParser 20 | 21 | UNKONWN = 'unkonwn' 22 | SUCCESS = '200' 23 | SCANED = '201' 24 | TIMEOUT = '408' 25 | 26 | 27 | def show_image(file_path): 28 | """ 29 | 跨平台显示图片文件 30 | :param file_path: 图片文件路径 31 | """ 32 | if sys.version_info >= (3, 3): 33 | from shlex import quote 34 | else: 35 | from pipes import quote 36 | 37 | if sys.platform == "darwin": 38 | command = "open -a /Applications/Preview.app %s&" % quote(file_path) 39 | os.system(command) 40 | else: 41 | webbrowser.open(os.path.join(os.getcwd(),'temp',file_path)) 42 | 43 | 44 | class SafeSession(requests.Session): 45 | def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, 46 | timeout=None, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None, 47 | json=None): 48 | for i in range(3): 49 | try: 50 | return super(SafeSession, self).request(method, url, params, data, headers, cookies, files, auth, 51 | timeout, 52 | allow_redirects, proxies, hooks, stream, verify, cert, json) 53 | except Exception as e: 54 | print e.message, traceback.format_exc() 55 | continue 56 | 57 | 58 | class WXBot: 59 | """WXBot功能类""" 60 | 61 | def __init__(self): 62 | self.DEBUG = False 63 | self.uuid = '' 64 | self.base_uri = '' 65 | self.redirect_uri = '' 66 | self.uin = '' 67 | self.sid = '' 68 | self.skey = '' 69 | self.pass_ticket = '' 70 | self.device_id = 'e' + repr(random.random())[2:17] 71 | self.base_request = {} 72 | self.sync_key_str = '' 73 | self.sync_key = [] 74 | self.sync_host = '' 75 | 76 | #文件缓存目录 77 | self.temp_pwd = os.path.join(os.getcwd(),'temp') 78 | if os.path.exists(self.temp_pwd) == False: 79 | os.makedirs(self.temp_pwd) 80 | 81 | self.session = SafeSession() 82 | self.session.headers.update({'User-Agent': 'Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5'}) 83 | self.conf = {'qr': 'png'} 84 | 85 | self.my_account = {} # 当前账户 86 | 87 | # 所有相关账号: 联系人, 公众号, 群组, 特殊账号 88 | self.member_list = [] 89 | 90 | # 所有群组的成员, {'group_id1': [member1, member2, ...], ...} 91 | self.group_members = {} 92 | 93 | # 所有账户, {'group_member':{'id':{'type':'group_member', 'info':{}}, ...}, 'normal_member':{'id':{}, ...}} 94 | self.account_info = {'group_member': {}, 'normal_member': {}} 95 | 96 | self.contact_list = [] # 联系人列表 97 | self.public_list = [] # 公众账号列表 98 | self.group_list = [] # 群聊列表 99 | self.special_list = [] # 特殊账号列表 100 | self.encry_chat_room_id_list = [] # 存储群聊的EncryChatRoomId,获取群内成员头像时需要用到 101 | 102 | self.file_index = 0 103 | 104 | @staticmethod 105 | def to_unicode(string, encoding='utf-8'): 106 | """ 107 | 将字符串转换为Unicode 108 | :param string: 待转换字符串 109 | :param encoding: 字符串解码方式 110 | :return: 转换后的Unicode字符串 111 | """ 112 | if isinstance(string, str): 113 | return string.decode(encoding) 114 | elif isinstance(string, unicode): 115 | return string 116 | else: 117 | raise Exception('Unknown Type') 118 | 119 | def get_contact(self): 120 | """获取当前账户的所有相关账号(包括联系人、公众号、群聊、特殊账号)""" 121 | url = self.base_uri + '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' \ 122 | % (self.pass_ticket, self.skey, int(time.time())) 123 | r = self.session.post(url, data='{}') 124 | r.encoding = 'utf-8' 125 | if self.DEBUG: 126 | with open(os.path.join(self.temp_pwd,'contacts.json'), 'w') as f: 127 | f.write(r.text.encode('utf-8')) 128 | dic = json.loads(r.text) 129 | self.member_list = dic['MemberList'] 130 | 131 | special_users = ['newsapp', 'fmessage', 'filehelper', 'weibo', 'qqmail', 132 | 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle', 133 | 'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 134 | 'blogapp', 'facebookapp', 'masssendapp', 'meishiapp', 135 | 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder', 136 | 'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 137 | 'officialaccounts', 'notification_messages', 'wxid_novlwrv3lqwv11', 138 | 'gh_22b87fa7cb3c', 'wxitil', 'userexperience_alarm', 'notification_messages'] 139 | 140 | self.contact_list = [] 141 | self.public_list = [] 142 | self.special_list = [] 143 | self.group_list = [] 144 | 145 | for contact in self.member_list: 146 | if contact['VerifyFlag'] & 8 != 0: # 公众号 147 | self.public_list.append(contact) 148 | self.account_info['normal_member'][contact['UserName']] = {'type': 'public', 'info': contact} 149 | elif contact['UserName'] in special_users: # 特殊账户 150 | self.special_list.append(contact) 151 | self.account_info['normal_member'][contact['UserName']] = {'type': 'special', 'info': contact} 152 | elif contact['UserName'].find('@@') != -1: # 群聊 153 | self.group_list.append(contact) 154 | self.account_info['normal_member'][contact['UserName']] = {'type': 'group', 'info': contact} 155 | elif contact['UserName'] == self.my_account['UserName']: # 自己 156 | self.account_info['normal_member'][contact['UserName']] = {'type': 'self', 'info': contact} 157 | else: 158 | self.contact_list.append(contact) 159 | self.account_info['normal_member'][contact['UserName']] = {'type': 'contact', 'info': contact} 160 | 161 | self.batch_get_group_members() 162 | 163 | for group in self.group_members: 164 | for member in self.group_members[group]: 165 | if member['UserName'] not in self.account_info: 166 | self.account_info['group_member'][member['UserName']] = \ 167 | {'type': 'group_member', 'info': member, 'group': group} 168 | 169 | if self.DEBUG: 170 | with open(os.path.join(self.temp_pwd,'contact_list.json'), 'w') as f: 171 | f.write(json.dumps(self.contact_list)) 172 | with open(os.path.join(self.temp_pwd,'special_list.json'), 'w') as f: 173 | f.write(json.dumps(self.special_list)) 174 | with open(os.path.join(self.temp_pwd,'group_list.json'), 'w') as f: 175 | f.write(json.dumps(self.group_list)) 176 | with open(os.path.join(self.temp_pwd,'public_list.json'), 'w') as f: 177 | f.write(json.dumps(self.public_list)) 178 | with open(os.path.join(self.temp_pwd,'member_list.json'), 'w') as f: 179 | f.write(json.dumps(self.member_list)) 180 | with open(os.path.join(self.temp_pwd,'group_users.json'), 'w') as f: 181 | f.write(json.dumps(self.group_members)) 182 | with open(os.path.join(self.temp_pwd,'account_info.json'), 'w') as f: 183 | f.write(json.dumps(self.account_info)) 184 | return True 185 | 186 | def batch_get_group_members(self): 187 | """批量获取所有群聊成员信息""" 188 | url = self.base_uri + '/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s' % (int(time.time()), self.pass_ticket) 189 | params = { 190 | 'BaseRequest': self.base_request, 191 | "Count": len(self.group_list), 192 | "List": [{"UserName": group['UserName'], "EncryChatRoomId": ""} for group in self.group_list] 193 | } 194 | r = self.session.post(url, data=json.dumps(params)) 195 | r.encoding = 'utf-8' 196 | dic = json.loads(r.text) 197 | group_members = {} 198 | encry_chat_room_id = {} 199 | for group in dic['ContactList']: 200 | gid = group['UserName'] 201 | members = group['MemberList'] 202 | group_members[gid] = members 203 | encry_chat_room_id[gid] = group['EncryChatRoomId'] 204 | self.group_members = group_members 205 | self.encry_chat_room_id_list = encry_chat_room_id 206 | 207 | def get_group_member_name(self, gid, uid): 208 | """ 209 | 获取群聊中指定成员的名称信息 210 | :param gid: 群id 211 | :param uid: 群聊成员id 212 | :return: 名称信息,类似 {"display_name": "test_user", "nickname": "test", "remark_name": "for_test" } 213 | """ 214 | if gid not in self.group_members: 215 | return None 216 | group = self.group_members[gid] 217 | for member in group: 218 | if member['UserName'] == uid: 219 | names = {} 220 | if 'RemarkName' in member and member['RemarkName']: 221 | names['remark_name'] = member['RemarkName'] 222 | if 'NickName' in member and member['NickName']: 223 | names['nickname'] = member['NickName'] 224 | if 'DisplayName' in member and member['DisplayName']: 225 | names['display_name'] = member['DisplayName'] 226 | return names 227 | return None 228 | 229 | def get_contact_info(self, uid): 230 | return self.account_info['normal_member'].get(uid) 231 | 232 | 233 | def get_group_member_info(self, uid): 234 | return self.account_info['group_member'].get(uid) 235 | 236 | def get_contact_name(self, uid): 237 | info = self.get_contact_info(uid) 238 | if info is None: 239 | return None 240 | info = info['info'] 241 | name = {} 242 | if 'RemarkName' in info and info['RemarkName']: 243 | name['remark_name'] = info['RemarkName'] 244 | if 'NickName' in info and info['NickName']: 245 | name['nickname'] = info['NickName'] 246 | if 'DisplayName' in info and info['DisplayName']: 247 | name['display_name'] = info['DisplayName'] 248 | if len(name) == 0: 249 | return None 250 | else: 251 | return name 252 | 253 | @staticmethod 254 | def get_contact_prefer_name(name): 255 | if name is None: 256 | return None 257 | if 'remark_name' in name: 258 | return name['remark_name'] 259 | if 'nickname' in name: 260 | return name['nickname'] 261 | if 'display_name' in name: 262 | return name['display_name'] 263 | return None 264 | 265 | @staticmethod 266 | def get_group_member_prefer_name(name): 267 | if name is None: 268 | return None 269 | if 'remark_name' in name: 270 | return name['remark_name'] 271 | if 'display_name' in name: 272 | return name['display_name'] 273 | if 'nickname' in name: 274 | return name['nickname'] 275 | return None 276 | 277 | def get_user_type(self, wx_user_id): 278 | """ 279 | 获取特定账号与自己的关系 280 | :param wx_user_id: 账号id: 281 | :return: 与当前账号的关系 282 | """ 283 | for account in self.contact_list: 284 | if wx_user_id == account['UserName']: 285 | return 'contact' 286 | for account in self.public_list: 287 | if wx_user_id == account['UserName']: 288 | return 'public' 289 | for account in self.special_list: 290 | if wx_user_id == account['UserName']: 291 | return 'special' 292 | for account in self.group_list: 293 | if wx_user_id == account['UserName']: 294 | return 'group' 295 | for group in self.group_members: 296 | for member in self.group_members[group]: 297 | if member['UserName'] == wx_user_id: 298 | return 'group_member' 299 | return 'unknown' 300 | 301 | def is_contact(self, uid): 302 | for account in self.contact_list: 303 | if uid == account['UserName']: 304 | return True 305 | return False 306 | 307 | def is_public(self, uid): 308 | for account in self.public_list: 309 | if uid == account['UserName']: 310 | return True 311 | return False 312 | 313 | def is_special(self, uid): 314 | for account in self.special_list: 315 | if uid == account['UserName']: 316 | return True 317 | return False 318 | 319 | def handle_msg_all(self, msg): 320 | """ 321 | 处理所有消息,请子类化后覆盖此函数 322 | msg: 323 | msg_id -> 消息id 324 | msg_type_id -> 消息类型id 325 | user -> 发送消息的账号id 326 | content -> 消息内容 327 | :param msg: 收到的消息 328 | """ 329 | pass 330 | 331 | @staticmethod 332 | def proc_at_info(msg): 333 | if not msg: 334 | return '', [] 335 | segs = msg.split(u'\u2005') 336 | str_msg_all = '' 337 | str_msg = '' 338 | infos = [] 339 | if len(segs) > 1: 340 | for i in range(0, len(segs) - 1): 341 | segs[i] += u'\u2005' 342 | pm = re.search(u'@.*\u2005', segs[i]).group() 343 | if pm: 344 | name = pm[1:-1] 345 | string = segs[i].replace(pm, '') 346 | str_msg_all += string + '@' + name + ' ' 347 | str_msg += string 348 | if string: 349 | infos.append({'type': 'str', 'value': string}) 350 | infos.append({'type': 'at', 'value': name}) 351 | else: 352 | infos.append({'type': 'str', 'value': segs[i]}) 353 | str_msg_all += segs[i] 354 | str_msg += segs[i] 355 | str_msg_all += segs[-1] 356 | str_msg += segs[-1] 357 | infos.append({'type': 'str', 'value': segs[-1]}) 358 | else: 359 | infos.append({'type': 'str', 'value': segs[-1]}) 360 | str_msg_all = msg 361 | str_msg = msg 362 | return str_msg_all.replace(u'\u2005', ''), str_msg.replace(u'\u2005', ''), infos 363 | 364 | def extract_msg_content(self, msg_type_id, msg): 365 | """ 366 | content_type_id: 367 | 0 -> Text 368 | 1 -> Location 369 | 3 -> Image 370 | 4 -> Voice 371 | 5 -> Recommend 372 | 6 -> Animation 373 | 7 -> Share 374 | 8 -> Video 375 | 9 -> VideoCall 376 | 10 -> Redraw 377 | 11 -> Empty 378 | 99 -> Unknown 379 | :param msg_type_id: 消息类型id 380 | :param msg: 消息结构体 381 | :return: 解析的消息 382 | """ 383 | mtype = msg['MsgType'] 384 | content = HTMLParser.HTMLParser().unescape(msg['Content']) 385 | msg_id = msg['MsgId'] 386 | 387 | msg_content = {} 388 | if msg_type_id == 0: 389 | return {'type': 11, 'data': ''} 390 | elif msg_type_id == 2: # File Helper 391 | return {'type': 0, 'data': content.replace('
', '\n')} 392 | elif msg_type_id == 3: # 群聊 393 | sp = content.find('
') 394 | uid = content[:sp] 395 | content = content[sp:] 396 | content = content.replace('
', '') 397 | uid = uid[:-1] 398 | name = self.get_contact_prefer_name(self.get_contact_name(uid)) 399 | if not name: 400 | name = self.get_group_member_prefer_name(self.get_group_member_name(msg['FromUserName'], uid)) 401 | if not name: 402 | name = 'unknown' 403 | msg_content['user'] = {'id': uid, 'name': name} 404 | else: # Self, Contact, Special, Public, Unknown 405 | pass 406 | 407 | msg_prefix = (msg_content['user']['name'] + ':') if 'user' in msg_content else '' 408 | 409 | if mtype == 1: 410 | if content.find('http://weixin.qq.com/cgi-bin/redirectforward?args=') != -1: 411 | r = self.session.get(content) 412 | r.encoding = 'gbk' 413 | data = r.text 414 | pos = self.search_content('title', data, 'xml') 415 | msg_content['type'] = 1 416 | msg_content['data'] = pos 417 | msg_content['detail'] = data 418 | if self.DEBUG: 419 | print ' %s[Location] %s ' % (msg_prefix, pos) 420 | else: 421 | msg_content['type'] = 0 422 | if msg_type_id == 3 or (msg_type_id == 1 and msg['ToUserName'][:2] == '@@'): # Group text message 423 | msg_infos = self.proc_at_info(content) 424 | str_msg_all = msg_infos[0] 425 | str_msg = msg_infos[1] 426 | detail = msg_infos[2] 427 | msg_content['data'] = str_msg_all 428 | msg_content['detail'] = detail 429 | msg_content['desc'] = str_msg 430 | else: 431 | msg_content['data'] = content 432 | if self.DEBUG: 433 | try: 434 | print ' %s[Text] %s' % (msg_prefix, msg_content['data']) 435 | except UnicodeEncodeError: 436 | print ' %s[Text] (illegal text).' % msg_prefix 437 | elif mtype == 3: 438 | msg_content['type'] = 3 439 | msg_content['data'] = self.get_msg_img_url(msg_id) 440 | msg_content['img'] = self.session.get(msg_content['data']).content.encode('hex') 441 | if self.DEBUG: 442 | image = self.get_msg_img(msg_id) 443 | print ' %s[Image] %s' % (msg_prefix, image) 444 | elif mtype == 34: 445 | msg_content['type'] = 4 446 | msg_content['data'] = self.get_voice_url(msg_id) 447 | msg_content['voice'] = self.session.get(msg_content['data']).content.encode('hex') 448 | if self.DEBUG: 449 | voice = self.get_voice(msg_id) 450 | print ' %s[Voice] %s' % (msg_prefix, voice) 451 | elif mtype == 37: 452 | msg_content['type'] = 37 453 | msg_content['data'] = msg['RecommendInfo'] 454 | if self.DEBUG: 455 | print ' %s[useradd] %s' % (msg_prefix,msg['RecommendInfo']['NickName']) 456 | elif mtype == 42: 457 | msg_content['type'] = 5 458 | info = msg['RecommendInfo'] 459 | msg_content['data'] = {'nickname': info['NickName'], 460 | 'alias': info['Alias'], 461 | 'province': info['Province'], 462 | 'city': info['City'], 463 | 'gender': ['unknown', 'male', 'female'][info['Sex']]} 464 | if self.DEBUG: 465 | print ' %s[Recommend]' % msg_prefix 466 | print ' -----------------------------' 467 | print ' | NickName: %s' % info['NickName'] 468 | print ' | Alias: %s' % info['Alias'] 469 | print ' | Local: %s %s' % (info['Province'], info['City']) 470 | print ' | Gender: %s' % ['unknown', 'male', 'female'][info['Sex']] 471 | print ' -----------------------------' 472 | elif mtype == 47: 473 | msg_content['type'] = 6 474 | msg_content['data'] = self.search_content('cdnurl', content) 475 | if self.DEBUG: 476 | print ' %s[Animation] %s' % (msg_prefix, msg_content['data']) 477 | elif mtype == 49: 478 | msg_content['type'] = 7 479 | if msg['AppMsgType'] == 3: 480 | app_msg_type = 'music' 481 | elif msg['AppMsgType'] == 5: 482 | app_msg_type = 'link' 483 | elif msg['AppMsgType'] == 7: 484 | app_msg_type = 'weibo' 485 | else: 486 | app_msg_type = 'unknown' 487 | msg_content['data'] = {'type': app_msg_type, 488 | 'title': msg['FileName'], 489 | 'desc': self.search_content('des', content, 'xml'), 490 | 'url': msg['Url'], 491 | 'from': self.search_content('appname', content, 'xml'), 492 | 'content': msg.get('Content') # 有的公众号会发一次性3 4条链接一个大图,如果只url那只能获取第一条,content里面有所有的链接 493 | } 494 | if self.DEBUG: 495 | print ' %s[Share] %s' % (msg_prefix, app_msg_type) 496 | print ' --------------------------' 497 | print ' | title: %s' % msg['FileName'] 498 | print ' | desc: %s' % self.search_content('des', content, 'xml') 499 | print ' | link: %s' % msg['Url'] 500 | print ' | from: %s' % self.search_content('appname', content, 'xml') 501 | print ' | content: %s' % (msg.get('content')[:20] if msg.get('content') else "unknown") 502 | print ' --------------------------' 503 | 504 | elif mtype == 62: 505 | msg_content['type'] = 8 506 | msg_content['data'] = content 507 | if self.DEBUG: 508 | print ' %s[Video] Please check on mobiles' % msg_prefix 509 | elif mtype == 53: 510 | msg_content['type'] = 9 511 | msg_content['data'] = content 512 | if self.DEBUG: 513 | print ' %s[Video Call]' % msg_prefix 514 | elif mtype == 10002: 515 | msg_content['type'] = 10 516 | msg_content['data'] = content 517 | if self.DEBUG: 518 | print ' %s[Redraw]' % msg_prefix 519 | elif mtype == 10000: # unknown, maybe red packet, or group invite 520 | msg_content['type'] = 12 521 | msg_content['data'] = msg['Content'] 522 | if self.DEBUG: 523 | print ' [Unknown]' 524 | else: 525 | msg_content['type'] = 99 526 | msg_content['data'] = content 527 | if self.DEBUG: 528 | print ' %s[Unknown]' % msg_prefix 529 | return msg_content 530 | 531 | def handle_msg(self, r): 532 | """ 533 | 处理原始微信消息的内部函数 534 | msg_type_id: 535 | 0 -> Init 536 | 1 -> Self 537 | 2 -> FileHelper 538 | 3 -> Group 539 | 4 -> Contact 540 | 5 -> Public 541 | 6 -> Special 542 | 99 -> Unknown 543 | :param r: 原始微信消息 544 | """ 545 | for msg in r['AddMsgList']: 546 | user = {'id': msg['FromUserName'], 'name': 'unknown'} 547 | if msg['MsgType'] == 51: # init message 548 | msg_type_id = 0 549 | user['name'] = 'system' 550 | elif msg['MsgType'] == 37: # friend request 551 | msg_type_id = 37 552 | pass 553 | # content = msg['Content'] 554 | # username = content[content.index('fromusername='): content.index('encryptusername')] 555 | # username = username[username.index('"') + 1: username.rindex('"')] 556 | # print u'[Friend Request]' 557 | # print u' Nickname:' + msg['RecommendInfo']['NickName'] 558 | # print u' 附加消息:'+msg['RecommendInfo']['Content'] 559 | # # print u'Ticket:'+msg['RecommendInfo']['Ticket'] # Ticket添加好友时要用 560 | # print u' 微信号:'+username #未设置微信号的 腾讯会自动生成一段微信ID 但是无法通过搜索 搜索到此人 561 | elif msg['FromUserName'] == self.my_account['UserName']: # Self 562 | msg_type_id = 1 563 | user['name'] = 'self' 564 | elif msg['ToUserName'] == 'filehelper': # File Helper 565 | msg_type_id = 2 566 | user['name'] = 'file_helper' 567 | elif msg['FromUserName'][:2] == '@@': # Group 568 | msg_type_id = 3 569 | user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id'])) 570 | elif self.is_contact(msg['FromUserName']): # Contact 571 | msg_type_id = 4 572 | user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id'])) 573 | elif self.is_public(msg['FromUserName']): # Public 574 | msg_type_id = 5 575 | user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id'])) 576 | elif self.is_special(msg['FromUserName']): # Special 577 | msg_type_id = 6 578 | user['name'] = self.get_contact_prefer_name(self.get_contact_name(user['id'])) 579 | else: 580 | msg_type_id = 99 581 | user['name'] = 'unknown' 582 | if not user['name']: 583 | user['name'] = 'unknown' 584 | user['name'] = HTMLParser.HTMLParser().unescape(user['name']) 585 | 586 | if self.DEBUG and msg_type_id != 0: 587 | print u'[MSG] %s:' % user['name'] 588 | content = self.extract_msg_content(msg_type_id, msg) 589 | message = {'msg_type_id': msg_type_id, 590 | 'msg_id': msg['MsgId'], 591 | 'content': content, 592 | 'to_user_id': msg['ToUserName'], 593 | 'user': user} 594 | self.handle_msg_all(message) 595 | 596 | def schedule(self): 597 | """ 598 | 做任务型事情的函数,如果需要,可以在子类中覆盖此函数 599 | 此函数在处理消息的间隙被调用,请不要长时间阻塞此函数 600 | """ 601 | pass 602 | 603 | def proc_msg(self): 604 | self.test_sync_check() 605 | while True: 606 | check_time = time.time() 607 | try: 608 | [retcode, selector] = self.sync_check() 609 | # print '[DEBUG] sync_check:', retcode, selector 610 | if retcode == '1100': # 从微信客户端上登出 611 | break 612 | elif retcode == '1101': # 从其它设备上登了网页微信 613 | break 614 | elif retcode == '0': 615 | if selector == '2': # 有新消息 616 | r = self.sync() 617 | if r is not None: 618 | self.handle_msg(r) 619 | elif selector == '3': # 未知 620 | r = self.sync() 621 | if r is not None: 622 | self.handle_msg(r) 623 | elif selector == '4': # 通讯录更新 624 | r = self.sync() 625 | if r is not None: 626 | self.get_contact() 627 | elif selector == '6': # 可能是红包 628 | r = self.sync() 629 | if r is not None: 630 | self.handle_msg(r) 631 | elif selector == '7': # 在手机上操作了微信 632 | r = self.sync() 633 | if r is not None: 634 | self.handle_msg(r) 635 | elif selector == '0': # 无事件 636 | pass 637 | else: 638 | print '[DEBUG] sync_check:', retcode, selector 639 | r = self.sync() 640 | if r is not None: 641 | self.handle_msg(r) 642 | else: 643 | print '[DEBUG] sync_check:', retcode, selector 644 | self.schedule() 645 | except: 646 | print '[ERROR] Except in proc_msg' 647 | print format_exc() 648 | check_time = time.time() - check_time 649 | if check_time < 0.8: 650 | time.sleep(1 - check_time) 651 | 652 | def apply_useradd_requests(self,RecommendInfo): 653 | url = self.base_uri + '/webwxverifyuser?r='+str(int(time.time()))+'&lang=zh_CN' 654 | params = { 655 | "BaseRequest": self.base_request, 656 | "Opcode": 3, 657 | "VerifyUserListSize": 1, 658 | "VerifyUserList": [ 659 | { 660 | "Value": RecommendInfo['UserName'], 661 | "VerifyUserTicket": RecommendInfo['Ticket'] } 662 | ], 663 | "VerifyContent": "", 664 | "SceneListCount": 1, 665 | "SceneList": [ 666 | 33 667 | ], 668 | "skey": self.skey 669 | } 670 | headers = {'content-type': 'application/json; charset=UTF-8'} 671 | data = json.dumps(params, ensure_ascii=False).encode('utf8') 672 | try: 673 | r = self.session.post(url, data=data, headers=headers) 674 | except (ConnectionError, ReadTimeout): 675 | return False 676 | dic = r.json() 677 | return dic['BaseResponse']['Ret'] == 0 678 | 679 | def add_groupuser_to_friend_by_uid(self,uid,VerifyContent): 680 | """ 681 | 主动向群内人员打招呼,提交添加好友请求 682 | uid-群内人员得uid VerifyContent-好友招呼内容 683 | 慎用此接口!封号后果自负!慎用此接口!封号后果自负!慎用此接口!封号后果自负! 684 | """ 685 | if self.is_contact(uid): 686 | return True 687 | url = self.base_uri + '/webwxverifyuser?r='+str(int(time.time()))+'&lang=zh_CN' 688 | params ={ 689 | "BaseRequest": self.base_request, 690 | "Opcode": 2, 691 | "VerifyUserListSize": 1, 692 | "VerifyUserList": [ 693 | { 694 | "Value": uid, 695 | "VerifyUserTicket": "" 696 | } 697 | ], 698 | "VerifyContent": VerifyContent, 699 | "SceneListCount": 1, 700 | "SceneList": [ 701 | 33 702 | ], 703 | "skey": self.skey 704 | } 705 | headers = {'content-type': 'application/json; charset=UTF-8'} 706 | data = json.dumps(params, ensure_ascii=False).encode('utf8') 707 | try: 708 | r = self.session.post(url, data=data, headers=headers) 709 | except (ConnectionError, ReadTimeout): 710 | return False 711 | dic = r.json() 712 | return dic['BaseResponse']['Ret'] == 0 713 | 714 | def add_friend_to_group(self,uid,group_name): 715 | """ 716 | 将好友加入到群聊中 717 | """ 718 | gid = '' 719 | #通过群名获取群id,群没保存到通讯录中的话无法添加哦 720 | for group in self.group_list: 721 | if group['NickName'] == group_name: 722 | gid = group['UserName'] 723 | if gid == '': 724 | return False 725 | #通过群id判断uid是否在群中 726 | for user in self.group_members[gid]: 727 | if user['UserName'] == uid: 728 | #已经在群里面了,不用加了 729 | return True 730 | url = self.base_uri + '/webwxupdatechatroom?fun=addmember&pass_ticket=%s' % self.pass_ticket 731 | params ={ 732 | "AddMemberList": uid, 733 | "ChatRoomName": gid, 734 | "BaseRequest": self.base_request 735 | } 736 | headers = {'content-type': 'application/json; charset=UTF-8'} 737 | data = json.dumps(params, ensure_ascii=False).encode('utf8') 738 | try: 739 | r = self.session.post(url, data=data, headers=headers) 740 | except (ConnectionError, ReadTimeout): 741 | return False 742 | dic = r.json() 743 | return dic['BaseResponse']['Ret'] == 0 744 | 745 | def delete_user_from_group(self,uname,gid): 746 | """ 747 | 将群用户从群中剔除,只有群管理员有权限 748 | """ 749 | uid = "" 750 | for user in self.group_members[gid]: 751 | if user['NickName'] == uname: 752 | uid = user['UserName'] 753 | if uid == "": 754 | return False 755 | url = self.base_uri + '/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % self.pass_ticket 756 | params ={ 757 | "DelMemberList": uid, 758 | "ChatRoomName": gid, 759 | "BaseRequest": self.base_request 760 | } 761 | headers = {'content-type': 'application/json; charset=UTF-8'} 762 | data = json.dumps(params, ensure_ascii=False).encode('utf8') 763 | try: 764 | r = self.session.post(url, data=data, headers=headers) 765 | except (ConnectionError, ReadTimeout): 766 | return False 767 | dic = r.json() 768 | return dic['BaseResponse']['Ret'] == 0 769 | 770 | 771 | def send_msg_by_uid(self, word, dst='filehelper'): 772 | url = self.base_uri + '/webwxsendmsg?pass_ticket=%s' % self.pass_ticket 773 | msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '') 774 | word = self.to_unicode(word) 775 | params = { 776 | 'BaseRequest': self.base_request, 777 | 'Msg': { 778 | "Type": 1, 779 | "Content": word, 780 | "FromUserName": self.my_account['UserName'], 781 | "ToUserName": dst, 782 | "LocalID": msg_id, 783 | "ClientMsgId": msg_id 784 | } 785 | } 786 | headers = {'content-type': 'application/json; charset=UTF-8'} 787 | data = json.dumps(params, ensure_ascii=False).encode('utf8') 788 | try: 789 | r = self.session.post(url, data=data, headers=headers) 790 | except (ConnectionError, ReadTimeout): 791 | return False 792 | dic = r.json() 793 | return dic['BaseResponse']['Ret'] == 0 794 | 795 | def upload_media(self, fpath, is_img=False): 796 | if not os.path.exists(fpath): 797 | print '[ERROR] File not exists.' 798 | return None 799 | url_1 = 'https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' 800 | url_2 = 'https://file2.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json' 801 | flen = str(os.path.getsize(fpath)) 802 | ftype = mimetypes.guess_type(fpath)[0] or 'application/octet-stream' 803 | files = { 804 | 'id': (None, 'WU_FILE_%s' % str(self.file_index)), 805 | 'name': (None, os.path.basename(fpath)), 806 | 'type': (None, ftype), 807 | 'lastModifiedDate': (None, time.strftime('%m/%d/%Y, %H:%M:%S GMT+0800 (CST)')), 808 | 'size': (None, flen), 809 | 'mediatype': (None, 'pic' if is_img else 'doc'), 810 | 'uploadmediarequest': (None, json.dumps({ 811 | 'BaseRequest': self.base_request, 812 | 'ClientMediaId': int(time.time()), 813 | 'TotalLen': flen, 814 | 'StartPos': 0, 815 | 'DataLen': flen, 816 | 'MediaType': 4, 817 | })), 818 | 'webwx_data_ticket': (None, self.session.cookies['webwx_data_ticket']), 819 | 'pass_ticket': (None, self.pass_ticket), 820 | 'filename': (os.path.basename(fpath), open(fpath, 'rb'),ftype.split('/')[1]), 821 | } 822 | self.file_index += 1 823 | try: 824 | r = self.session.post(url_1, files=files) 825 | if json.loads(r.text)['BaseResponse']['Ret'] != 0: 826 | # 当file返回值不为0时则为上传失败,尝试第二服务器上传 827 | r = self.session.post(url_2, files=files) 828 | if json.loads(r.text)['BaseResponse']['Ret'] != 0: 829 | print '[ERROR] Upload media failure.' 830 | return None 831 | mid = json.loads(r.text)['MediaId'] 832 | return mid 833 | except Exception,e: 834 | return None 835 | 836 | def send_file_msg_by_uid(self, fpath, uid): 837 | mid = self.upload_media(fpath) 838 | if mid is None or not mid: 839 | return False 840 | url = self.base_uri + '/webwxsendappmsg?fun=async&f=json&pass_ticket=' + self.pass_ticket 841 | msg_id = str(int(time.time() * 1000)) + str(random.random())[:5].replace('.', '') 842 | data = { 843 | 'BaseRequest': self.base_request, 844 | 'Msg': { 845 | 'Type': 6, 846 | 'Content': ("%s6%s%s%s" % (os.path.basename(fpath).encode('utf-8'), str(os.path.getsize(fpath)), mid, fpath.split('.')[-1])).encode('utf8'), 847 | 'FromUserName': self.my_account['UserName'], 848 | 'ToUserName': uid, 849 | 'LocalID': msg_id, 850 | 'ClientMsgId': msg_id, }, } 851 | try: 852 | r = self.session.post(url, data=json.dumps(data)) 853 | res = json.loads(r.text) 854 | if res['BaseResponse']['Ret'] == 0: 855 | return True 856 | else: 857 | return False 858 | except Exception,e: 859 | return False 860 | 861 | def send_img_msg_by_uid(self, fpath, uid): 862 | mid = self.upload_media(fpath, is_img=True) 863 | if mid is None: 864 | return False 865 | url = self.base_uri + '/webwxsendmsgimg?fun=async&f=json' 866 | data = { 867 | 'BaseRequest': self.base_request, 868 | 'Msg': { 869 | 'Type': 3, 870 | 'MediaId': mid, 871 | 'FromUserName': self.my_account['UserName'], 872 | 'ToUserName': uid, 873 | 'LocalID': str(time.time() * 1e7), 874 | 'ClientMsgId': str(time.time() * 1e7), }, } 875 | if fpath[-4:] == '.gif': 876 | url = self.base_uri + '/webwxsendemoticon?fun=sys' 877 | data['Msg']['Type'] = 47 878 | data['Msg']['EmojiFlag'] = 2 879 | try: 880 | r = self.session.post(url, data=json.dumps(data)) 881 | res = json.loads(r.text) 882 | if res['BaseResponse']['Ret'] == 0: 883 | return True 884 | else: 885 | return False 886 | except Exception,e: 887 | return False 888 | 889 | def get_user_id(self, name): 890 | if name == '': 891 | return None 892 | name = self.to_unicode(name) 893 | for contact in self.contact_list: 894 | if 'RemarkName' in contact and contact['RemarkName'] == name: 895 | return contact['UserName'] 896 | elif 'NickName' in contact and contact['NickName'] == name: 897 | return contact['UserName'] 898 | elif 'DisplayName' in contact and contact['DisplayName'] == name: 899 | return contact['UserName'] 900 | for group in self.group_list: 901 | if 'RemarkName' in group and group['RemarkName'] == name: 902 | return group['UserName'] 903 | if 'NickName' in group and group['NickName'] == name: 904 | return group['UserName'] 905 | if 'DisplayName' in group and group['DisplayName'] == name: 906 | return group['UserName'] 907 | 908 | return '' 909 | 910 | def send_msg(self, name, word, isfile=False): 911 | uid = self.get_user_id(name) 912 | if uid is not None: 913 | if isfile: 914 | with open(word, 'r') as f: 915 | result = True 916 | for line in f.readlines(): 917 | line = line.replace('\n', '') 918 | print '-> ' + name + ': ' + line 919 | if self.send_msg_by_uid(line, uid): 920 | pass 921 | else: 922 | result = False 923 | time.sleep(1) 924 | return result 925 | else: 926 | word = self.to_unicode(word) 927 | if self.send_msg_by_uid(word, uid): 928 | return True 929 | else: 930 | return False 931 | else: 932 | if self.DEBUG: 933 | print '[ERROR] This user does not exist .' 934 | return True 935 | 936 | @staticmethod 937 | def search_content(key, content, fmat='attr'): 938 | if fmat == 'attr': 939 | pm = re.search(key + '\s?=\s?"([^"<]+)"', content) 940 | if pm: 941 | return pm.group(1) 942 | elif fmat == 'xml': 943 | pm = re.search('<{0}>([^<]+)'.format(key), content) 944 | if pm: 945 | return pm.group(1) 946 | return 'unknown' 947 | 948 | def run(self): 949 | self.get_uuid() 950 | self.gen_qr_code(os.path.join(self.temp_pwd,'wxqr.png')) 951 | print '[INFO] Please use WeChat to scan the QR code .' 952 | 953 | result = self.wait4login() 954 | if result != SUCCESS: 955 | print '[ERROR] Web WeChat login failed. failed code=%s' % (result,) 956 | return 957 | 958 | if self.login(): 959 | print '[INFO] Web WeChat login succeed .' 960 | else: 961 | print '[ERROR] Web WeChat login failed .' 962 | return 963 | 964 | if self.init(): 965 | print '[INFO] Web WeChat init succeed .' 966 | else: 967 | print '[INFO] Web WeChat init failed' 968 | return 969 | self.status_notify() 970 | self.get_contact() 971 | print '[INFO] Get %d contacts' % len(self.contact_list) 972 | print '[INFO] Start to process messages .' 973 | self.proc_msg() 974 | 975 | def get_uuid(self): 976 | url = 'https://login.weixin.qq.com/jslogin' 977 | params = { 978 | 'appid': 'wx782c26e4c19acffb', 979 | 'fun': 'new', 980 | 'lang': 'zh_CN', 981 | '_': int(time.time()) * 1000 + random.randint(1, 999), 982 | } 983 | r = self.session.get(url, params=params) 984 | r.encoding = 'utf-8' 985 | data = r.text 986 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"' 987 | pm = re.search(regx, data) 988 | if pm: 989 | code = pm.group(1) 990 | self.uuid = pm.group(2) 991 | return code == '200' 992 | return False 993 | 994 | def gen_qr_code(self, qr_file_path): 995 | string = 'https://login.weixin.qq.com/l/' + self.uuid 996 | qr = pyqrcode.create(string) 997 | if self.conf['qr'] == 'png': 998 | qr.png(qr_file_path, scale=8) 999 | show_image(qr_file_path) 1000 | # img = Image.open(qr_file_path) 1001 | # img.show() 1002 | elif self.conf['qr'] == 'tty': 1003 | print(qr.terminal(quiet_zone=1)) 1004 | 1005 | def do_request(self, url): 1006 | r = self.session.get(url) 1007 | r.encoding = 'utf-8' 1008 | data = r.text 1009 | param = re.search(r'window.code=(\d+);', data) 1010 | code = param.group(1) 1011 | return code, data 1012 | 1013 | def wait4login(self): 1014 | """ 1015 | http comet: 1016 | tip=1, 等待用户扫描二维码, 1017 | 201: scaned 1018 | 408: timeout 1019 | tip=0, 等待用户确认登录, 1020 | 200: confirmed 1021 | """ 1022 | LOGIN_TEMPLATE = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' 1023 | tip = 1 1024 | 1025 | try_later_secs = 1 1026 | MAX_RETRY_TIMES = 10 1027 | 1028 | code = UNKONWN 1029 | 1030 | retry_time = MAX_RETRY_TIMES 1031 | while retry_time > 0: 1032 | url = LOGIN_TEMPLATE % (tip, self.uuid, int(time.time())) 1033 | code, data = self.do_request(url) 1034 | if code == SCANED: 1035 | print '[INFO] Please confirm to login .' 1036 | tip = 0 1037 | elif code == SUCCESS: # 确认登录成功 1038 | param = re.search(r'window.redirect_uri="(\S+?)";', data) 1039 | redirect_uri = param.group(1) + '&fun=new' 1040 | self.redirect_uri = redirect_uri 1041 | self.base_uri = redirect_uri[:redirect_uri.rfind('/')] 1042 | return code 1043 | elif code == TIMEOUT: 1044 | print '[ERROR] WeChat login timeout. retry in %s secs later...' % (try_later_secs,) 1045 | 1046 | tip = 1 # 重置 1047 | retry_time -= 1 1048 | time.sleep(try_later_secs) 1049 | else: 1050 | print ('[ERROR] WeChat login exception return_code=%s. retry in %s secs later...' % 1051 | (code, try_later_secs)) 1052 | tip = 1 1053 | retry_time -= 1 1054 | time.sleep(try_later_secs) 1055 | 1056 | return code 1057 | 1058 | def login(self): 1059 | if len(self.redirect_uri) < 4: 1060 | print '[ERROR] Login failed due to network problem, please try again.' 1061 | return False 1062 | r = self.session.get(self.redirect_uri) 1063 | r.encoding = 'utf-8' 1064 | data = r.text 1065 | doc = xml.dom.minidom.parseString(data) 1066 | root = doc.documentElement 1067 | 1068 | for node in root.childNodes: 1069 | if node.nodeName == 'skey': 1070 | self.skey = node.childNodes[0].data 1071 | elif node.nodeName == 'wxsid': 1072 | self.sid = node.childNodes[0].data 1073 | elif node.nodeName == 'wxuin': 1074 | self.uin = node.childNodes[0].data 1075 | elif node.nodeName == 'pass_ticket': 1076 | self.pass_ticket = node.childNodes[0].data 1077 | 1078 | if '' in (self.skey, self.sid, self.uin, self.pass_ticket): 1079 | return False 1080 | 1081 | self.base_request = { 1082 | 'Uin': self.uin, 1083 | 'Sid': self.sid, 1084 | 'Skey': self.skey, 1085 | 'DeviceID': self.device_id, 1086 | } 1087 | return True 1088 | 1089 | def init(self): 1090 | url = self.base_uri + '/webwxinit?r=%i&lang=en_US&pass_ticket=%s' % (int(time.time()), self.pass_ticket) 1091 | params = { 1092 | 'BaseRequest': self.base_request 1093 | } 1094 | r = self.session.post(url, data=json.dumps(params)) 1095 | r.encoding = 'utf-8' 1096 | dic = json.loads(r.text) 1097 | self.sync_key = dic['SyncKey'] 1098 | self.my_account = dic['User'] 1099 | self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val']) 1100 | for keyVal in self.sync_key['List']]) 1101 | return dic['BaseResponse']['Ret'] == 0 1102 | 1103 | def status_notify(self): 1104 | url = self.base_uri + '/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % self.pass_ticket 1105 | self.base_request['Uin'] = int(self.base_request['Uin']) 1106 | params = { 1107 | 'BaseRequest': self.base_request, 1108 | "Code": 3, 1109 | "FromUserName": self.my_account['UserName'], 1110 | "ToUserName": self.my_account['UserName'], 1111 | "ClientMsgId": int(time.time()) 1112 | } 1113 | r = self.session.post(url, data=json.dumps(params)) 1114 | r.encoding = 'utf-8' 1115 | dic = json.loads(r.text) 1116 | return dic['BaseResponse']['Ret'] == 0 1117 | 1118 | def test_sync_check(self): 1119 | for host in ['webpush', 'webpush2']: 1120 | self.sync_host = host 1121 | retcode = self.sync_check()[0] 1122 | if retcode == '0': 1123 | return True 1124 | return False 1125 | 1126 | def sync_check(self): 1127 | params = { 1128 | 'r': int(time.time()), 1129 | 'sid': self.sid, 1130 | 'uin': self.uin, 1131 | 'skey': self.skey, 1132 | 'deviceid': self.device_id, 1133 | 'synckey': self.sync_key_str, 1134 | '_': int(time.time()), 1135 | } 1136 | url = 'https://' + self.sync_host + '.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?' + urllib.urlencode(params) 1137 | try: 1138 | r = self.session.get(url, timeout=60) 1139 | r.encoding = 'utf-8' 1140 | data = r.text 1141 | pm = re.search(r'window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}', data) 1142 | retcode = pm.group(1) 1143 | selector = pm.group(2) 1144 | return [retcode, selector] 1145 | except: 1146 | return [-1, -1] 1147 | 1148 | def sync(self): 1149 | url = self.base_uri + '/webwxsync?sid=%s&skey=%s&lang=en_US&pass_ticket=%s' \ 1150 | % (self.sid, self.skey, self.pass_ticket) 1151 | params = { 1152 | 'BaseRequest': self.base_request, 1153 | 'SyncKey': self.sync_key, 1154 | 'rr': ~int(time.time()) 1155 | } 1156 | try: 1157 | r = self.session.post(url, data=json.dumps(params), timeout=60) 1158 | r.encoding = 'utf-8' 1159 | dic = json.loads(r.text) 1160 | if dic['BaseResponse']['Ret'] == 0: 1161 | self.sync_key = dic['SyncKey'] 1162 | self.sync_key_str = '|'.join([str(keyVal['Key']) + '_' + str(keyVal['Val']) 1163 | for keyVal in self.sync_key['List']]) 1164 | return dic 1165 | except: 1166 | return None 1167 | 1168 | def get_icon(self, uid, gid=None): 1169 | """ 1170 | 获取联系人或者群聊成员头像 1171 | :param uid: 联系人id 1172 | :param gid: 群id,如果为非None获取群中成员头像,如果为None则获取联系人头像 1173 | """ 1174 | if gid is None: 1175 | url = self.base_uri + '/webwxgeticon?username=%s&skey=%s' % (uid, self.skey) 1176 | else: 1177 | url = self.base_uri + '/webwxgeticon?username=%s&skey=%s&chatroomid=%s' % ( 1178 | uid, self.skey, self.encry_chat_room_id_list[gid]) 1179 | r = self.session.get(url) 1180 | data = r.content 1181 | fn = 'icon_' + uid + '.jpg' 1182 | with open(os.path.join(self.temp_pwd,fn), 'wb') as f: 1183 | f.write(data) 1184 | return fn 1185 | 1186 | def get_head_img(self, uid): 1187 | """ 1188 | 获取群头像 1189 | :param uid: 群uid 1190 | """ 1191 | url = self.base_uri + '/webwxgetheadimg?username=%s&skey=%s' % (uid, self.skey) 1192 | r = self.session.get(url) 1193 | data = r.content 1194 | fn = 'head_' + uid + '.jpg' 1195 | with open(os.path.join(self.temp_pwd,fn), 'wb') as f: 1196 | f.write(data) 1197 | return fn 1198 | 1199 | def get_msg_img_url(self, msgid): 1200 | return self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) 1201 | 1202 | def get_msg_img(self, msgid): 1203 | """ 1204 | 获取图片消息,下载图片到本地 1205 | :param msgid: 消息id 1206 | :return: 保存的本地图片文件路径 1207 | """ 1208 | url = self.base_uri + '/webwxgetmsgimg?MsgID=%s&skey=%s' % (msgid, self.skey) 1209 | r = self.session.get(url) 1210 | data = r.content 1211 | fn = 'img_' + msgid + '.jpg' 1212 | with open(os.path.join(self.temp_pwd,fn), 'wb') as f: 1213 | f.write(data) 1214 | return fn 1215 | 1216 | def get_voice_url(self, msgid): 1217 | return self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) 1218 | 1219 | def get_voice(self, msgid): 1220 | """ 1221 | 获取语音消息,下载语音到本地 1222 | :param msgid: 语音消息id 1223 | :return: 保存的本地语音文件路径 1224 | """ 1225 | url = self.base_uri + '/webwxgetvoice?msgid=%s&skey=%s' % (msgid, self.skey) 1226 | r = self.session.get(url) 1227 | data = r.content 1228 | fn = 'voice_' + msgid + '.mp3' 1229 | with open(os.path.join(self.temp_pwd,fn), 'wb') as f: 1230 | f.write(data) 1231 | return fn 1232 | def set_remarkname(self,uid,remarkname):#设置联系人的备注名 1233 | url = self.base_uri + '/webwxoplog?lang=zh_CN&pass_ticket=%s' \ 1234 | % (self.pass_ticket) 1235 | remarkname = self.to_unicode(remarkname) 1236 | params = { 1237 | 'BaseRequest': self.base_request, 1238 | 'CmdId': 2, 1239 | 'RemarkName': remarkname, 1240 | 'UserName': uid 1241 | } 1242 | try: 1243 | r = self.session.post(url, data=json.dumps(params), timeout=60) 1244 | r.encoding = 'utf-8' 1245 | dic = json.loads(r.text) 1246 | return dic['BaseResponse']['ErrMsg'] 1247 | except: 1248 | return None 1249 | --------------------------------------------------------------------------------