├── setup.cfg ├── MANIFEST.in ├── dev-requirements.txt ├── tests ├── responses │ ├── once.html │ ├── mission.html │ ├── signin.html │ └── balance.html ├── v2ex_config.json ├── test_get_money.py ├── test_last.py ├── v2ex.log ├── test_read.py └── conftest.py ├── Makefile ├── v2ex_daily_mission ├── notifier │ ├── __init__.py │ ├── none.py │ ├── abc.py │ ├── bark.py │ └── slack.py ├── __init__.py ├── v2ex.py └── cli.py ├── .travis.yml ├── tox.ini ├── LICENSE ├── .gitignore ├── setup.py ├── ChangeLog └── README.rst /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [tool:pytest] 5 | pep8maxlinelength = 102 -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | recursive-include v2ex_daily_mission *.py 4 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | click 3 | beautifulsoup4 4 | pytest 5 | pytest-pep8 6 | responses 7 | tox 8 | -------------------------------------------------------------------------------- /tests/responses/once.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @py.test -vs tests/; 3 | @py.test --pep8 tests/ v2ex_daily_mission/; 4 | 5 | create: 6 | @python setup.py sdist bdist_wheel; 7 | 8 | upload: 9 | @python setup.py sdist bdist_wheel upload; 10 | -------------------------------------------------------------------------------- /v2ex_daily_mission/notifier/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from v2ex_daily_mission.notifier.bark import BarkNotifier 6 | from v2ex_daily_mission.notifier.none import NoneNotifier 7 | from v2ex_daily_mission.notifier.slack import SlackNotifier 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | - "3.8" 9 | 10 | 11 | install: 12 | - pip install -r dev-requirements.txt 13 | - pip install -e . 14 | 15 | script: 16 | - make test 17 | 18 | notifications: 19 | email: false 20 | -------------------------------------------------------------------------------- /tests/v2ex_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "cookie": "default_font=font2; locale=zh-CN;", 3 | "log_directory": "./tests/", 4 | "notifier": { 5 | "bark": { 6 | "url": "https://api.day.app/xxx/v2ex_daily_mission/sign failed" 7 | }, 8 | "slack": { 9 | "url": "https://hooks.slack.com/services/xx/yy/zz" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /v2ex_daily_mission/notifier/none.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | 6 | import requests 7 | 8 | from v2ex_daily_mission.notifier.abc import Notifier 9 | 10 | 11 | class NoneNotifier(Notifier): 12 | def __init__(self, config): 13 | self.config = config 14 | 15 | def send_notification(self): 16 | pass 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py27, py35, py36, py37, py38 8 | 9 | [testenv] 10 | commands = 11 | py.test --pep8 -sv tests/ v2ex_daily_mission/ 12 | deps = 13 | pytest 14 | pytest-pep8 15 | responses 16 | -------------------------------------------------------------------------------- /v2ex_daily_mission/notifier/abc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | 6 | import sys 7 | import abc 8 | from abc import abstractmethod 9 | 10 | if sys.version_info >= (3, 4): 11 | ABC = abc.ABC 12 | else: 13 | ABC = abc.ABCMeta('ABC', (), {}) 14 | 15 | 16 | class NotificationSendFailedException(Exception): 17 | pass 18 | 19 | 20 | class Notifier(ABC): 21 | @abstractmethod 22 | def send_notification(self): 23 | pass 24 | -------------------------------------------------------------------------------- /v2ex_daily_mission/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | v2ex_daily_mission 6 | ~~~~~~~~~~~~~~~~~~ 7 | 8 | Complete daily mission, get money, for V2EX: https://www.v2ex.com. 9 | 10 | :copyright: (c) 2014-2015 by lord63. 11 | :license: MIT, see LICENSE for more details. 12 | """ 13 | 14 | __title__ = "v2ex_daily_mission" 15 | __version__ = "0.8.1" 16 | __author__ = "lord63" 17 | __homepage__ = "https://github.com/lord63/v2ex_daily_mission" 18 | __license__ = "MIT" 19 | __copyright__ = "Copyright 2014-2015 lord63" 20 | -------------------------------------------------------------------------------- /tests/test_get_money.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for the `v2ex last` function 6 | """ 7 | 8 | from __future__ import absolute_import 9 | 10 | import pytest 11 | 12 | from v2ex_daily_mission.cli import cli 13 | 14 | 15 | @pytest.mark.usefixtures('mock_api') 16 | class TestGetMoney(): 17 | def test_get_money(self, runner): 18 | result = runner.invoke(cli, ['--config', './tests/v2ex_config.json', 19 | 'sign']) 20 | assert result.exit_code == 0 21 | assert result.output.strip() == ( 22 | 'You have completed the mission today.') 23 | -------------------------------------------------------------------------------- /tests/test_last.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for the `v2ex last` function 6 | """ 7 | 8 | from __future__ import absolute_import 9 | 10 | import re 11 | 12 | import pytest 13 | 14 | from v2ex_daily_mission.cli import cli 15 | 16 | 17 | @pytest.mark.usefixtures('mock_api') 18 | class TestLast(): 19 | def test_last(self, runner): 20 | result = runner.invoke(cli, ['--config', './tests/v2ex_config.json', 21 | 'last']) 22 | day = int(re.search(r'\d+', result.output).group()) 23 | assert result.exit_code == 0 24 | assert day == 334 25 | -------------------------------------------------------------------------------- /tests/v2ex.log: -------------------------------------------------------------------------------- 1 | 2015-06-11 19:12:03,350 [INFO] 20150611 的每日登录奖励 47 铜币 Total:18694.61 2 | 2015-06-12 19:12:04,552 [INFO] 20150612 的每日登录奖励 16 铜币 Total:18710.61 3 | 2015-06-13 19:12:04,075 [INFO] 20150613 的每日登录奖励 1 铜币 Total:18711.61 4 | 2015-06-14 19:12:06,577 [INFO] 20150614 的每日登录奖励 41 铜币 Total:18952.61 5 | 2015-06-15 19:12:03,147 [INFO] 20150615 的每日登录奖励 35 铜币 Total:18987.61 6 | 2015-06-16 19:12:06,053 [INFO] 20150616 的每日登录奖励 45 铜币 Total:19032.61 7 | 2015-06-17 19:12:02,495 [INFO] 20150617 的每日登录奖励 40 铜币 Total:19072.61 8 | 2015-06-18 19:12:04,766 [INFO] 20150618 的每日登录奖励 43 铜币 Total:19115.61 9 | 2015-06-20 19:12:02,332 [INFO] 20150620 的每日登录奖励 47 铜币 Total:19162.61 10 | 2015-06-22 19:12:08,220 [INFO] 20150622 的每日登录奖励 29 铜币 Total:19191.61 -------------------------------------------------------------------------------- /tests/responses/mission.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 |
V2EX  ›  日常任务
13 |
14 |
  •  每日登录奖励已领取
    15 |
    16 | 17 |
    18 |
    已连续登录 334 天
    19 |
    20 |
    21 | 22 | -------------------------------------------------------------------------------- /tests/test_read.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Tests for the `v2ex read` function 6 | """ 7 | 8 | from __future__ import absolute_import 9 | 10 | import pytest 11 | 12 | from v2ex_daily_mission.cli import cli 13 | 14 | 15 | @pytest.mark.usefixtures('mock_api') 16 | class TestRead(): 17 | def test_read_log_file(self, runner): 18 | result = runner.invoke(cli, ['--config', './tests/v2ex_config.json', 19 | 'read']) 20 | assert result.exit_code == 0 21 | assert len(result.output.strip().split('\n')) == 5 22 | 23 | def test_read_log_file_with_parameter(self, runner): 24 | result = runner.invoke(cli, ['--config', './tests/v2ex_config.json', 25 | 'read', '-c', '1']) 26 | assert result.exit_code == 0 27 | assert len(result.output.strip().split('\n')) == 1 28 | -------------------------------------------------------------------------------- /v2ex_daily_mission/notifier/bark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | 6 | import requests 7 | 8 | from v2ex_daily_mission.notifier.abc import Notifier, NotificationSendFailedException 9 | 10 | 11 | class BarkNotifier(Notifier): 12 | def __init__(self, config): 13 | self.config = config 14 | 15 | def send_notification(self): 16 | url = self.config['notifier']['bark']['url'] 17 | try: 18 | response = requests.get(url) 19 | if response.json()['code'] != 200: 20 | raise NotificationSendFailedException( 21 | "bark notification send failed, response: {}".format(response.text) 22 | ) 23 | except requests.RequestException as e: 24 | raise NotificationSendFailedException( 25 | "bark notification send failed, error: {}".format(e) 26 | ) 27 | -------------------------------------------------------------------------------- /v2ex_daily_mission/notifier/slack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | import json 6 | 7 | import requests 8 | 9 | from v2ex_daily_mission.notifier.abc import Notifier, NotificationSendFailedException 10 | 11 | 12 | class SlackNotifier(Notifier): 13 | def __init__(self, config): 14 | self.config = config 15 | 16 | def send_notification(self): 17 | url = self.config['notifier']['slack']['url'] 18 | data = { 19 | "text": "v2ex_daily_mission: sign failed." 20 | } 21 | try: 22 | response = requests.post(url, data=json.dumps(data)) 23 | if response.text != 'ok': 24 | raise NotificationSendFailedException( 25 | "slack notification send failed, response: {}".format(response.text) 26 | ) 27 | except requests.RequestException as e: 28 | raise NotificationSendFailedException( 29 | "slack notification send failed, error: {}".format(e) 30 | ) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014-2015 lord63 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User specified 2 | #################### 3 | 4 | # virtualenv folder 5 | venv*/ 6 | 7 | # pycharm 8 | .idea/ 9 | 10 | # temp folder 11 | temp/ 12 | 13 | # database 14 | *.db 15 | 16 | *.DS_Store 17 | *.json 18 | 19 | # Belows are from https://github.com/github/gitignore/blob/master/Python.gitignore 20 | #################### 21 | # Byte-compiled / optimized / DLL files 22 | __pycache__/ 23 | *.py[cod] 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Distribution / packaging 29 | .Python 30 | env/ 31 | build/ 32 | develop-eggs/ 33 | dist/ 34 | downloads/ 35 | eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .coverage 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | import v2ex_daily_mission 7 | 8 | 9 | with open('README.rst') as f: 10 | long_description = f.read() 11 | 12 | 13 | setup( 14 | name='v2ex_daily_mission', 15 | version=v2ex_daily_mission.__version__, 16 | description='complete mission, get money, from v2ex', 17 | long_description=long_description, 18 | url='https://github.com/lord63/v2ex_daily_mission', 19 | author='lord63', 20 | author_email='lord63.j@gmail.com', 21 | license='MIT', 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Operating System :: POSIX', 25 | 'Operating System :: POSIX :: Linux', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python :: 2', 28 | 'Programming Language :: Python :: 2.7', 29 | 'Programming Language :: Python :: 3', 30 | 'Programming Language :: Python :: 3.5', 31 | 'Programming Language :: Python :: 3.6', 32 | 'Programming Language :: Python :: 3.7', 33 | 'Programming Language :: Python :: 3.8', 34 | ], 35 | keywords='v2ex daily money sign', 36 | packages=['v2ex_daily_mission'], 37 | install_requires=[ 38 | 'click>=5.0', 39 | 'requests>=2.7.0', 40 | 'beautifulsoup4>=4.4.1'], 41 | include_package_data=True, 42 | entry_points={ 43 | 'console_scripts':[ 44 | 'v2ex=v2ex_daily_mission.cli:cli'] 45 | } 46 | ) 47 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | 6 | from os import path 7 | from codecs import open 8 | 9 | import pytest 10 | import responses 11 | from click.testing import CliRunner 12 | 13 | 14 | ROOT = path.join(path.dirname(path.abspath(__file__)), 'responses') 15 | 16 | 17 | @pytest.yield_fixture 18 | def mock_api(): 19 | 20 | with open(path.join(ROOT, 'signin.html'), encoding='utf-8') as f: 21 | mock_signin_body = f.read() 22 | responses.add(responses.POST, 'https://www.v2ex.com/signin', 23 | body=mock_signin_body) 24 | responses.add(responses.GET, 'https://www.v2ex.com/signin', 25 | body=mock_signin_body) 26 | 27 | with open(path.join(ROOT, 'once.html'), encoding='utf-8') as f: 28 | mock_once_body = f.read() 29 | responses.add(responses.GET, 30 | 'https://www.v2ex.com/mission/daily/redeem?once=51947', 31 | body=mock_once_body) 32 | 33 | with open(path.join(ROOT, 'balance.html'), encoding='utf-8') as f: 34 | mock_balance_body = f.read() 35 | responses.add(responses.GET, 'https://www.v2ex.com/balance', 36 | body=mock_balance_body) 37 | 38 | with open(path.join(ROOT, 'mission.html'), encoding='utf-8') as f: 39 | mock_mission_body = f.read() 40 | responses.add(responses.GET, 'https://www.v2ex.com/mission/daily', 41 | body=mock_mission_body) 42 | 43 | responses.start() 44 | yield responses 45 | responses.stop() 46 | 47 | 48 | @pytest.fixture(scope='function') 49 | def runner(mock_api): 50 | return CliRunner() 51 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2020.06.03 v0.8.1 2 | - fix package problem 3 | 4 | 2020.06.03 v0.8.0 5 | - support notify when sign failed 6 | - drop python 3.4, add 3.6+ support 7 | 8 | 2019.06.12 v0.7.0 9 | - use cookie to to tasks 10 | - drop python 3.3 support and add 3.5 and 3.6 11 | - drop windows support 12 | 13 | 2016.05.01 v0.6.2 14 | - fix the encoding problem under python3, #11 15 | 16 | 2016.04.23 v0.6.1 17 | - fix the bug that the login params have been changed, #10 18 | 19 | 2016.02.27 v0.6.0 20 | - replace lxml with beautifulsoup4, #7 21 | 22 | 2015.11.30 v0.5.2 23 | - fix: 24 | - remove unicode_literals, update click >= 5.0, close #5 25 | - wheel support 26 | 27 | 2015.08.23 v0.5.1 28 | - fix: 29 | - hot fix for ``unicode_literals`` warning, #5 30 | 31 | 2015.07.24 v0.5.0 32 | - add: 33 | - now you can init the config file with subcommand `init` 34 | - add tests 35 | - fix: 36 | - short version parameter: "-v" --> "-V" 37 | - use abspath for log directory 38 | 39 | 2015.06.29 v0.4.0 40 | - add: 41 | - add windows support(tested on win thin pc) 42 | - add '--config' option, you can specify the config file now 43 | - change: 44 | - break change, the main command to get money: v2ex -> v2ex sign 45 | - rewrite with click 46 | - depecate: don't need 'count' in your config file any more, default to 5 47 | - drop 2.6 support, although it may works 48 | 49 | 2015.06.24 v0.3.0 50 | - add: 51 | - python 3.x support 52 | 53 | 2015.02.09 v0.2.4 54 | - fix: 55 | - fix importError when install v2ex_daily_mission 56 | 57 | 2015.02.06 v0.2.3 58 | - change: 59 | - break change, change the command: v2ex_daily_mission -> v2ex 60 | 61 | 2015.01.27 v0.2.2 62 | - fix: 63 | - fix the problem that fail to get money, fix #16 64 | 65 | 2014.09.02 v0.2.1 66 | - fix: 67 | - fix InsecureRequestWarning problem, fix #9 68 | 69 | 2014.09.02 v0.2.0 70 | - add: 71 | - new feature: you can check how long you've kept sigining in, 72 | fix #8 73 | 74 | 2014.07.31 v0.1.0 75 | - initial version, you can get money and read the log file, fix #4 76 | -------------------------------------------------------------------------------- /tests/responses/signin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
    用户名
    密码
    你是机器人么?
    我忘记密码了
    32 | 33 | 34 | 35 |
    36 |
    37 | 38 | -------------------------------------------------------------------------------- /v2ex_daily_mission/v2ex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | 6 | import logging 7 | import os 8 | 9 | import requests 10 | from requests.packages import urllib3 11 | from bs4 import BeautifulSoup 12 | 13 | 14 | # Disable urllib3 warning, see lord63/a_bunch_of_code#9. 15 | urllib3.disable_warnings() 16 | 17 | 18 | class V2ex(object): 19 | def __init__(self, config): 20 | self.signin_url = 'https://www.v2ex.com/signin' 21 | self.balance_url = 'https://www.v2ex.com/balance' 22 | self.mission_url = 'https://www.v2ex.com/mission/daily' 23 | self.config = config 24 | self.session = requests.Session() 25 | self.session.headers.update( 26 | {'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux \ 27 | x86_64; rv:28.0) Gecko/20100101 Firefox/28.0'}) 28 | self.cookie = self._make_cookie(config) 29 | logging.basicConfig( 30 | filename=os.path.join(config['log_directory'], 'v2ex.log'), 31 | level='INFO', 32 | format='%(asctime)s [%(levelname)s] %(message)s') 33 | # Disable log message from the requests library. 34 | requests_log = logging.getLogger("requests") 35 | requests_log.setLevel(logging.WARNING) 36 | 37 | def _make_cookie(self, config): 38 | return dict([i.split('=', 1) for i in config["cookie"].split('; ')]) 39 | 40 | def get_money(self): 41 | """Complete daily mission then get the money.""" 42 | response = self.session.get(self.mission_url, verify=False, cookies=self.cookie) 43 | soup = BeautifulSoup(response.text, 'html.parser') 44 | onclick = soup.find('input', class_='super normal button')['onclick'] 45 | url = onclick.split('=', 1)[1][2:-2] 46 | 47 | if url == '/balance': 48 | return "You have completed the mission today." 49 | else: 50 | headers = {'Referer': 'https://www.v2ex.com/mission/daily'} 51 | data = {'once': url.split('=')[-1]} 52 | self.session.get('https://www.v2ex.com'+url, verify=False, 53 | headers=headers, data=data, cookies=self.cookie,) 54 | balance = self._get_balance() 55 | return balance 56 | 57 | def _get_balance(self): 58 | """Get to know how much you totally have and how much you get today.""" 59 | response = self.session.get(self.balance_url, verify=False, cookies=self.cookie) 60 | soup = BeautifulSoup(response.text, 'html.parser') 61 | first_line = soup.select( 62 | "table.data tr:nth-of-type(2)")[0].text.strip().split('\n') 63 | total, today = first_line[-2:] 64 | logging.info('%-26sTotal:%-8s', today, total) 65 | return '\n'.join([u"Today: {0}".format(today), 66 | "Total: {0}".format(total)]) 67 | 68 | def get_last(self): 69 | """Get to know how long you have kept signing in.""" 70 | response = self.session.get(self.mission_url, verify=False, cookies=self.cookie) 71 | soup = BeautifulSoup(response.text, 'html.parser') 72 | last = soup.select('#Main div')[-1].text 73 | return last 74 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | v2ex\_daily\_mission 2 | ==================== 3 | 4 | |Latest Version| |Build Status| 5 | 6 | :: 7 | 8 | _____ _ _ _ _ _ 9 | / __ \ | | (_) | (_) (_) 10 | __ __`' / /' _____ __ __| | __ _ _| |_ _ _ __ ___ _ ___ ___ _ ___ _ __ 11 | \ \ / / / / / _ \ \/ / / _` |/ _` | | | | | | | '_ ` _ \| / __/ __| |/ _ \| '_ \ 12 | \ V / ./ /__| __/> < | (_| | (_| | | | |_| | | | | | | | \__ \__ \ | (_) | | | | 13 | \_/ \_____/\___/_/\_\ \__,_|\__,_|_|_|\__, | |_| |_| |_|_|___/___/_|\___/|_| |_| 14 | __/ | 15 | |___/ 16 | 17 | 基本简介: 18 | ---------- 19 | 20 | 模拟登录 v2ex 完成任务领钱 OvO 21 | 22 | 功能和亮点 23 | ---------- 24 | 25 | - Python 2.7+/3.5+ support 26 | - 签到领钱 27 | - 本地日志记录,查询 28 | - 查询连续登录天数 29 | - 签到失败提醒 30 | 31 | 基本安装 32 | -------- 33 | 34 | :: 35 | 36 | $ (sudo)pip -U install v2ex_daily_mission 37 | 38 | 请确保版本号大于等于 0.7.0,因为 V2EX 增加了验证码,可以看 `issue #13`_ 39 | 40 | 如何使用 41 | -------- 42 | 43 | 获得cookie 44 | ~~~~~~~~~~ 45 | 46 | 1. 登录v2ex 47 | 2. 页面任意一处右键,选择Inspect,然后在弹出的工具栏里选择Network 48 | 3. 刷新页面,选择一个请求,找到Request Headers里的cookie一栏,全部复制,下一步要用 49 | 50 | 配置文件 51 | ~~~~~~~~ 52 | 53 | 使用自带的子命令初始化(可能需要 root 权限或者管理员权限): 54 | 55 | :: 56 | 57 | $ v2ex init 58 | 59 | 按照提示输入cookie和日志路径。日志路径举个例子:``/home/lord63/code/v2ex_daily_mission/``。 60 | 61 | 生成的配置文件的默认地址, Linux 在 ``/usr/local/bin/v2ex_config.json``。你也可以手动指定生成的配置文件的路径: 62 | 63 | :: 64 | 65 | $ v2ex init --directory /home/lord63/code/v2ex_daily_mission 66 | 67 | 另外如果有需要,可以开启签到失败通知提醒,目前支持 bark_ 和 slack 通知,v0.8.0 以上版本支持。 68 | 69 | * bark配置:只支持 iOS,下载安装后,选择喜欢的格式在 ``init`` 的时候选择 bark 的通知方式并填入 url 即可。 70 | * slack配置:前往自定义集成页面(https://youworkspace.slack.com/apps/manage/custom-integrations)并添加一个新的 webhook 配置, 71 | 在 ``init`` 的时候选择 slack 的通知方式并填入 webhook url 即可 72 | 73 | 74 | 开始使用 75 | ~~~~~~~~ 76 | 77 | 完成任务得到钱: 78 | 79 | :: 80 | 81 | $ v2ex sign 82 | 83 | 查看最近的日志情况(默认天数 5): 84 | 85 | :: 86 | 87 | $ v2ex read 88 | 89 | 也可以通过参数来查看最近的情况 90 | 91 | :: 92 | 93 | $ v2ex read -c NUMBER 94 | 95 | 查看已经连续登录多少天 96 | 97 | :: 98 | 99 | $ v2ex last 100 | 101 | 测试失败通知提醒(v0.8.0 以上版本支持) 102 | 103 | :: 104 | 105 | $ v2ex notify 106 | 107 | 以上的是使用默认的配置文件,你也可以自己手动指定配置文件的地址,使用 ``--config`` 参数, 比如在 Linux 下: 108 | 109 | :: 110 | 111 | $ v2ex --config /home/lord63/v2ex_config.json sign 112 | 113 | 通过 ``v2ex -h`` 和各个子命令的帮助文档获得使用更为详细的使用帮助 114 | 115 | Linux 用户建议将任务加入 ``cron`` 定时运行(建议每天执行两次,防止意外失败导致中断签到天数), 比如: 116 | 117 | :: 118 | 119 | 0 9,21 * * * /usr/local/bin/v2ex sign 120 | 121 | 实际使用举例 122 | ------------ 123 | 124 | 首次签到: 125 | 126 | :: 127 | 128 | $ v2ex sign 129 | Today: 20140731 的每日登录奖励 26 铜币 130 | Total: 5439.0 131 | 132 | 如果你已经签到过了: 133 | 134 | :: 135 | 136 | $ v2ex sign 137 | You have completed the mission today. 138 | 139 | 本地日志查询最近签到领钱的情况(默认设置是 5 ): 140 | 141 | :: 142 | 143 | $ v2ex read 144 | 2014-07-27 19:12:03,902 [INFO] 20140727 的每日登录奖励 15 铜币 Total:5346.0 145 | 2014-07-28 19:12:03,751 [INFO] 20140728 的每日登录奖励 28 铜币 Total:5374.0 146 | 2014-07-29 19:12:03,750 [INFO] 20140729 的每日登录奖励 27 铜币 Total:5401.0 147 | 2014-07-30 19:12:03,471 [INFO] 20140730 的每日登录奖励 12 铜币 Total:5413.0 148 | 2014-07-31 19:12:03,417 [INFO] 20140731 的每日登录奖励 26 铜币 Total:5439.0 149 | 150 | 你当然也可以指定显示日志的数量: 151 | 152 | :: 153 | 154 | $ v2ex read -c 1 155 | 2014-07-31 19:12:03,417 [INFO] 20140731 的每日登录奖励 26 铜币 Total:5439.0 156 | 157 | 查询你连续登录的天数: 158 | 159 | :: 160 | 161 | $ v2ex last 162 | 已连续登录 54 天 163 | 164 | Development 165 | ----------- 166 | 167 | 首先安装依赖,推荐使用 virtualenv: 168 | 169 | :: 170 | 171 | $ virtualenv venv 172 | $ . venv/bin/activate 173 | (venv)$ pip install -r dev-requirements.txt 174 | 175 | 安装开发版本下的 v2ex_daily_mission, 方便调试和测试: 176 | 177 | :: 178 | 179 | (venv)$ python setup.py develop 180 | 181 | 运行测试: 182 | 183 | :: 184 | 185 | (venv)$ make test 186 | 187 | 也可以使用 tox 在 python2.7, 3.4+ 运行测试: 188 | 189 | :: 190 | 191 | (venv)$ tox 192 | 193 | License 194 | ------- 195 | 196 | MIT 197 | 198 | .. |Latest Version| image:: http://img.shields.io/pypi/v/v2ex_daily_mission.svg 199 | :target: https://pypi.python.org/pypi/v2ex_daily_mission 200 | .. |Build Status| image:: https://travis-ci.org/lord63/v2ex_daily_mission.svg 201 | :target: https://travis-ci.org/lord63/v2ex_daily_mission 202 | .. _`issue #13`: https://github.com/lord63/v2ex_daily_mission/issues/13 203 | .. _bark: https://apps.apple.com/cn/app/bark-%E7%BB%99%E4%BD%A0%E7%9A%84iphone%E5%8F%91%E6%8E%A8%E9%80%81/id1403753865 204 | -------------------------------------------------------------------------------- /tests/responses/balance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
    2015-06-22 19:11:48 +08:00每日登录奖励29.019191.6120150622 的每日登录奖励 29 铜币
    2015-06-20 19:11:42 +08:00每日登录奖励47.019162.6120150620 的每日登录奖励 47 铜币
    2015-06-18 19:11:47 +08:00每日登录奖励43.019115.6120150618 的每日登录奖励 43 铜币
    2015-06-17 19:11:44 +08:00每日登录奖励40.019072.6120150617 的每日登录奖励 40 铜币
    2015-06-16 19:11:46 +08:00每日登录奖励45.019032.6120150616 的每日登录奖励 45 铜币
    2015-06-15 19:11:45 +08:00每日登录奖励35.018987.6120150615 的每日登录奖励 35 铜币
    2015-06-14 19:11:49 +08:00每日登录奖励41.018952.6120150614 的每日登录奖励 41 铜币
    2015-06-14 19:11:49 +08:00连续登录奖励200.018911.61连续登录每 10 天奖励 200 铜币
    2015-06-13 19:11:43 +08:00每日登录奖励1.018711.6120150613 的每日登录奖励 1 铜币
    2015-06-12 19:11:44 +08:00每日登录奖励16.018710.6120150612 的每日登录奖励 16 铜币
    2015-06-11 19:11:46 +08:00每日登录奖励47.018694.6120150611 的每日登录奖励 47 铜币
    97 | 98 | -------------------------------------------------------------------------------- /v2ex_daily_mission/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | 6 | from collections import deque 7 | import os 8 | from os import path 9 | import sys 10 | import json 11 | from codecs import open 12 | 13 | import click 14 | 15 | from v2ex_daily_mission import __version__ 16 | from v2ex_daily_mission.v2ex import V2ex 17 | from v2ex_daily_mission.notifier import BarkNotifier, NoneNotifier, SlackNotifier 18 | 19 | 20 | class Config(object): 21 | def __init__(self): 22 | self.config = {} 23 | 24 | def load_config(self, config_path): 25 | with open(config_path, encoding='utf-8') as f: 26 | self.config = json.load(f) 27 | 28 | 29 | pass_config = click.make_pass_decorator(Config, ensure=True) 30 | 31 | 32 | def read_config(ctx, param, config_path): 33 | """Callback that is used whenever --config is passed.""" 34 | if sys.argv[1] == 'init': 35 | return 36 | cfg = ctx.ensure_object(Config) 37 | if config_path is None: 38 | config_path = path.join(sys.path[0], 'v2ex_config.json') 39 | if not path.exists(config_path): 40 | sys.exit("Can't find config file at {0}.\nPlease read " 41 | "https://github.com/lord63/v2ex_daily_mission " 42 | "to follow the guide.".format(config_path)) 43 | cfg.load_config(config_path) 44 | return config_path 45 | 46 | 47 | def initialize_nitifier(config): 48 | if 'notifier' not in config: 49 | return NoneNotifier(config) 50 | if 'bark' in config['notifier']: 51 | return BarkNotifier(config) 52 | elif 'slack' in config['notifier']: 53 | return SlackNotifier(config) 54 | return NoneNotifier(config) 55 | 56 | 57 | @click.group(context_settings={'help_option_names': ('-h', '--help')}) 58 | @click.version_option(__version__, '-V', '--version', message='%(version)s') 59 | @click.option('--config', 60 | type=click.Path(exists=True, dir_okay=False, resolve_path=True), 61 | callback=read_config, expose_value=False, 62 | help='Specify the config file path.') 63 | def cli(): 64 | """Complete daily mission, get money, for V2EX.""" 65 | pass 66 | 67 | 68 | @cli.command() 69 | @click.option( 70 | '--directory', default=sys.path[0], 71 | type=click.Path(exists=True, file_okay=False, resolve_path=True), 72 | help='the config file path directory.') 73 | def init(directory): 74 | """Init the config fle.""" 75 | cookie = click.prompt("Input your cookie") 76 | 77 | log_directory = click.prompt("Input your log directory") 78 | if not path.exists(log_directory): 79 | sys.exit("Invalid log directory, please have a check.") 80 | 81 | notifier = click.prompt( 82 | "Input your notifier", 83 | default='none', 84 | type=click.Choice(['bark', 'slack', 'none'], case_sensitive=False), 85 | show_choices=True, 86 | show_default=True 87 | ) 88 | if notifier != 'none': 89 | notifier_url = click.prompt("Input your notifier url") 90 | 91 | config = { 92 | "cookie": cookie, 93 | "log_directory": path.abspath(log_directory) 94 | } 95 | if notifier != 'none': 96 | config['notifier'] = {notifier: {'url': notifier_url}} 97 | 98 | config_file_path = path.join(directory, 'v2ex_config.json') 99 | with open(config_file_path, 'w') as f: 100 | json.dump(config, f) 101 | click.echo("Init the config file at: {0}".format(config_file_path)) 102 | 103 | 104 | @cli.command() 105 | @pass_config 106 | def sign(conf): 107 | """Sign in and get money.""" 108 | notifier = initialize_nitifier(conf.config) 109 | try: 110 | v2ex = V2ex(conf.config) 111 | balance = v2ex.get_money() 112 | click.echo(balance) 113 | except KeyError: 114 | notifier.send_notification() 115 | click.echo('Keyerror, please check your config file.') 116 | except IndexError: 117 | notifier.send_notification() 118 | click.echo('Please check your username and password.') 119 | except Exception as e: 120 | notifier.send_notification() 121 | click.echo('Sign failed, error: {}.'.format(e)) 122 | 123 | 124 | @cli.command() 125 | @click.option('--count', '-c', default=5, help="the count of days.") 126 | @pass_config 127 | def read(conf, count): 128 | """Read log file.""" 129 | file_path = os.path.join(path.abspath(conf.config['log_directory']), 130 | 'v2ex.log') 131 | for line in deque(open(file_path, encoding='utf-8'), int(count)): 132 | click.echo(line, nl=False) 133 | 134 | 135 | @cli.command() 136 | @pass_config 137 | def last(conf): 138 | """How long you have kept signing in.""" 139 | try: 140 | v2ex = V2ex(conf.config) 141 | last_date = v2ex.get_last() 142 | click.echo(last_date) 143 | except KeyError: 144 | click.echo('Keyerror, please check your config file.') 145 | except IndexError: 146 | click.echo('Please check your username and password.') 147 | 148 | 149 | @cli.command() 150 | @pass_config 151 | def notify(conf): 152 | """Test notify send.""" 153 | notifier = initialize_nitifier(conf.config) 154 | if isinstance(notifier, NoneNotifier): 155 | click.echo("There is no notifier configuration.") 156 | return 157 | notifier.send_notification() 158 | click.echo("send notification success.") 159 | --------------------------------------------------------------------------------