├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.py ├── en └── README.md ├── example.py ├── gvapi ├── GoogleVoice.py └── __init__.py ├── requirements.txt └── setup.py /.dockerignore: -------------------------------------------------------------------------------- 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 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # PyCharm 104 | .idea/ 105 | 106 | # img .png 107 | img/*.png 108 | 109 | # test py file 110 | #test.py 111 | -------------------------------------------------------------------------------- /.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 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # PyCharm 104 | .idea/ 105 | 106 | # img .png 107 | img/*.png 108 | 109 | # test py file 110 | test.py 111 | alpine 112 | Docker -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VERSION=0.1 2 | FROM ubuntu:latest 3 | LABEL author="cvno" 4 | LABEL maintainer="x@cvno.me" 5 | 6 | ARG PHANTOM_JS_VERSION 7 | ENV PHANTOM_JS_VERSION ${PHANTOM_JS_VERSION:-2.1.1-linux-x86_64} 8 | 9 | ENV TZ=Asia/Shanghai 10 | ENV DEBIAN_FRONTEND=noninteractive 11 | 12 | ENV GV_USR 13 | ENV GV_PWD 14 | ENV GVAPI_IS_DEV false 15 | ENV TO_NUMBER 13212969527 16 | 17 | RUN mkdir -p /usr/src/app && \ 18 | mkdir -p /var/log/gunicorn 19 | 20 | COPY . /usr/src/app 21 | WORKDIR /usr/src/app 22 | 23 | RUN set -x \ 24 | && apt-get update \ 25 | && apt-get install -y --no-install-recommends \ 26 | tzdata \ 27 | ca-certificates \ 28 | bzip2 \ 29 | libfontconfig \ 30 | curl \ 31 | python3 \ 32 | python3-pip \ 33 | python3-setuptools \ 34 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 35 | && echo "Asia/Shanghai" > /etc/timezone \ 36 | && mkdir /tmp/phantomjs \ 37 | && curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-${PHANTOM_JS_VERSION}.tar.bz2 \ 38 | | tar -xj --strip-components=1 -C /tmp/phantomjs \ 39 | && mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \ 40 | && pip3 install --no-cache-dir gunicorn \ 41 | && pip3 install --no-cache-dir -r requirements.txt -i https://pypi.doubanio.com/simple \ 42 | && apt-get clean \ 43 | && rm -rf /var/lib/apt/lists/* 44 | 45 | CMD ["/usr/local/bin/gunicorn", "-w", "1", "-b", ":5000", "app:app"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cvno(https://github.com/cvno) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 20200807 不再维护 2 | >为了帮助保护您的帐号,Google 不允许您通过某些浏览器登录。Google 可能会阻止从存在以下情况的浏览器登录: 3 | 4 | - 不支持 JavaScript 或者已关闭 JavaScript。 5 | - 添加了不安全或不受支持的扩展程序。 6 | - **使用自动化测试框架**。 7 | - 嵌入在其他应用中。 8 | 9 | 详情查看: [here](https://support.google.com/accounts/answer/7675428?hl=zh-Hans) 10 | 仅作为学习用途 11 | ```shell script 12 | python setup.py sdist bdist_wheel # 打包成wheel 13 | ``` 14 | 15 | ~~使`Google Voice`号码免于被回收~~ 16 | 17 | ### 封装 Docker 2018/11/11 18 | 封装到 docker, 并每周向指定号码发送信息 19 | 20 | ```sh 21 | # run 22 | docker run -d -p 3280:5000 -e "GV_USR=" -e "GV_PWD=" -e "GVAPI_IS_DEV=true" --restart always cvno/gv 23 | ``` 24 | 25 | 在指令中`GV_USR`和`GV_PWD`填入自己的邮箱和密码,稍等片刻后访问`ip:3280/sms/13212969527/gv:0.1`成功后会返回数据。 26 | 27 | `ip:3280/sms/13212969527/gv:0.1`向`13212969527`这个号码发送`sms`信息,内容为`gv:0.1` 28 | 29 | ```sh 30 | # 自行build 31 | docker build -t /gv:0.1 . 32 | ``` 33 | 34 | **注意**:自行`build`后如果要上传一定要把`docker`仓库设置为私有!!!否则任何人都可以看到你的镜像将暴露你的帐号密码。 35 | 36 | # GV Python API 37 | 38 | 使用 Python3 来操作 Google Voice 的 API。[English](/en)(未完成) 39 | 40 | 功能列表: 41 | 42 | - 发送 SMS 43 | - 拨打电话 44 | - 取消拨打电话 45 | - 标记为已读或未读 46 | - 下载语音留言 47 | - 后台自动检测是否有新信息 48 | - 根据 SMS 的设置自动回复 49 | 50 | 51 | ~~它也可能是一个廉价的短信验证码方案。~~ 52 | 53 | **依赖:** 54 | - 浏览器 [PhantomJS](http://phantomjs.org/download.html) | [geckodriver](https://github.com/mozilla/geckodriver) | [chromedriver](https://chromedriver.chromium.org/) 55 | 56 | # 开发使用 57 | 58 | ```python 59 | from gvapi import Voice 60 | 61 | 62 | # 如果需要设置自定义回复,请重写 _initial 方法,不需要的请忽略 63 | class Example(Voice): 64 | def _initial(self): 65 | ''' 这个函数运行在登录之前, 可以在这里修改一些配置, 66 | 如: 67 | 1. 设置浏览器请求头 68 | 2. 更改登录地址 69 | 3. 设置超时时间 70 | 4. 是否检测新消息 71 | ''' 72 | self.set_match({'TD': '退订成功'}) # 触发关键词 73 | self.status['auto'] = True # 自动回复开关 74 | 75 | 76 | voice = Example('usernmae', 'passwd', True) 77 | # 在调试的时候建议开启 Debug=True ,它会在终端显示运行日志 78 | 79 | # 一定要在这个 flag 为 True 的时候进行操作 80 | while not voice.status['init']: 81 | continue 82 | 83 | # 发送 sms 84 | res1 = voice.send_sms('+18566712333', 'Hello World!') 85 | # {"ok":true,"data":{"code":0}} 86 | 87 | # 拨打电话 88 | res2 = voice.call('+18566712333') 89 | # {"ok":true,"data":{"code":0,"callId":"XXXXXXXXX...."}} 90 | 91 | # 取消拨打 92 | res3 = voice.cancel_call(res2['data']['callId']) 93 | # {"ok" : false} 94 | 95 | # 获取未读的 sms 96 | for i in voice.unsms: # 这个方法返回的是一个 list 97 | res4 = voice.mark(i['id']) # 标记为已读 98 | res5 = voice.mark(i['id'], 0) # 标记为未读 99 | 100 | # 获取未读的 voicemail 101 | for i in voice.voicemail: # 这个方法返回的是一个 list 102 | print(i['ogg_url']) # 语音下载地址 103 | res6 = voice.mark(i['id']) # 标记为已读 104 | ``` 105 | 106 | ***注意:*** 107 | 1. Google Voice 是使用 C2C 模式拨打电话的,也就是说需要转接号码,如果你账号已经绑定了号码,那么程序会自动处理。 108 | 2. 获取新消息处理之后,建议直接删除,或备份到数据库;如果不删除会影响新消息的数据处理。 109 | 110 | # 声明 111 | 如果 Google 更改登录机制,或弃用旧版,本代码可能会不支持,如果你下载此代码,代表您同意自行承担使用风险。 112 | 113 | # 许可 114 | by [Git @kentio](https://github.com/kentio/) 115 | See LICENSE 116 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -* coding: utf-8 -* 3 | # create by cvno on 2018/11/4 12:54 4 | import os 5 | import json 6 | import time 7 | from flask import Flask 8 | from threading import Timer 9 | from datetime import datetime 10 | from gvapi import Voice 11 | 12 | 13 | app = Flask(__name__) 14 | 15 | 16 | voice = Voice(os.environ.get('GV_USR'),os.environ.get('GV_PWD')) 17 | to_number = os.environ.get('TO_NUMBER') 18 | 19 | while not voice.status['init']: # wait... 20 | time.sleep(3) 21 | continue 22 | 23 | @app.route('/sms//', methods=['GET', 'POST']) 24 | def sms(number, content): 25 | # 发送 sms 26 | res = voice.send_sms(number, content) 27 | data = {'number': number,'content': content, 'res': res} 28 | return json.dumps(data, ensure_ascii=False) 29 | 30 | 31 | class Scheduler(object): 32 | # loop 33 | def __init__(self, sleep_time, function): 34 | self.sleep_time = sleep_time 35 | self.function = function 36 | self._t = None 37 | 38 | def start(self): 39 | if self._t is None: 40 | self._t = Timer(self.sleep_time, self._run) 41 | self._t.start() 42 | else: 43 | raise Exception("this timer is already running") 44 | 45 | def _run(self): 46 | self.function() 47 | self._t = Timer(self.sleep_time, self._run) 48 | self._t.start() 49 | 50 | def stop(self): 51 | if self._t is not None: 52 | self._t.cancel() 53 | self._t = None 54 | 55 | 56 | def task(): 57 | voice.send_sms(to_number,'{}'.format(datetime.now().strftime("%Y年%m月%d日, %H时%M分%S秒, 星期%w"))) 58 | 59 | 60 | if __name__ == '__main__': 61 | try: 62 | # start loop 63 | scheduler = Scheduler(604800, task) # 每隔一周发送一次 64 | scheduler.start() 65 | ENV_API = os.environ.get('GVAPI_IS_DEV') 66 | if ENV_API and json.loads(ENV_API): # dev run 67 | app.run(host='0.0.0.0', debug=False) 68 | exit(0) 69 | app.run() 70 | except KeyboardInterrupt as e: 71 | print('Bye.') 72 | -------------------------------------------------------------------------------- /en/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -* coding: utf-8 -* 3 | # create by cvno on 2018/1/15 11:52 4 | 5 | from gvapi import GoogleVoice 6 | 7 | 8 | # 如果需要设置自定义回复,请重写 _initial 方法,不需要的请忽略 9 | class Example(GoogleVoice.Voice): 10 | def _initial(self): 11 | ''' 这个函数运行在登录之前, 可以在这里修改一些配置, 12 | 如: 13 | 1. 设置浏览器请求头 14 | 2. 更改登录地址 15 | 3. 设置超时时间 16 | 4. 是否检测新消息 17 | ''' 18 | self.set_match({'TD': '退订成功'}) # 触发关键词 19 | self.status['auto'] = True # 自动回复开关 20 | 21 | 22 | voice = Example('usernmae', 'passwd', True) 23 | # 在调试的时候建议开启 Debug=True ,它会在终端显示运行日志 24 | 25 | # 一定要在这个 flag 为 True 的时候进行操作 26 | while not voice.status['init']: 27 | continue 28 | 29 | # 发送 sms 30 | res1 = voice.send_sms('6128880182', 'Hello World!') 31 | # {"ok":true,"data":{"code":0}} 32 | 33 | # 拨打电话 34 | res2 = voice.call('6128880182') 35 | # {"ok":true,"data":{"code":0,"callId":"XXXXXXXXX...."}} 36 | 37 | # 取消拨打 38 | res3 = voice.cancel_call(res2['data']['callId']) 39 | # {"ok" : false} 40 | 41 | # 获取未读的 sms 42 | for i in voice.unsms: # 这个方法返回的是一个 list 43 | res4 = voice.mark(i['id']) # 标记为已读 44 | res5 = voice.mark(i['id'], 0) # 标记为未读 45 | 46 | # 获取未读的 voicemail 47 | for i in voice.voicemail: # 这个方法返回的是一个 list 48 | print(i['ogg_url']) # 语音下载地址 49 | res6 = voice.mark(i['id']) # 标记为已读 50 | -------------------------------------------------------------------------------- /gvapi/GoogleVoice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -* coding: utf-8 -* 3 | # create by git@kentio on 2018/1/2 10:23 4 | import re 5 | import ssl 6 | import time 7 | import json 8 | import logging 9 | import traceback 10 | import urllib3 11 | import requests 12 | import threading 13 | from bs4 import BeautifulSoup 14 | from selenium.webdriver.common.by import By 15 | from selenium import webdriver 16 | from selenium.webdriver.support.wait import WebDriverWait 17 | from selenium.webdriver.support import expected_conditions as EC 18 | 19 | # ERROR 20 | from selenium.common.exceptions import * 21 | from requests.exceptions import * 22 | 23 | try: 24 | import xml.etree.cElementTree as ET 25 | except ImportError: 26 | import xml.etree.ElementTree as ET 27 | 28 | # 禁用 HTTPS SSL 安全警告 29 | from urllib3.exceptions import InsecureRequestWarning 30 | 31 | urllib3.disable_warnings(InsecureRequestWarning) 32 | ssl._create_default_https_context = ssl._create_unverified_context # 忽略 ssl 证书 33 | 34 | 35 | class Singleton(object): 36 | '''基类 单例模式''' 37 | 38 | def __new__(cls, *args, **kwargs): 39 | ''' not obj -> create obj, else return obj''' 40 | if not hasattr(cls, "_instance"): 41 | cls._instance = super(Singleton, cls).__new__(cls) 42 | return cls._instance 43 | 44 | 45 | def init(func): 46 | 'yield next(0) 协程初始化' 47 | 48 | def wrapper(*args, **kwargs): 49 | res = func(*args, **kwargs) 50 | next(res) 51 | return res 52 | 53 | return wrapper 54 | 55 | 56 | class NotOpenGoogle(Exception): 57 | '''打不开谷歌''' 58 | pass 59 | 60 | 61 | class ProxyError(Exception): 62 | '''没有使用代理''' 63 | pass 64 | 65 | 66 | class Voice(Singleton): 67 | '''google voice send info''' 68 | check_msg_url = {} 69 | status = {'self': None, # self : 程序自身是否正常 70 | 'login': False, # login: 登录状态 71 | 'init': False, # init: 数据初始化是否完成 72 | 'guard': False, # guard: 是否启动触发过登录状态维护 73 | 'check': False, # check: check参数是否获取成功 74 | 'auto': False # auto: 是否开启自动回复 75 | } # 0: deviant, 1: normal , 2: error 76 | 77 | __login_url = 'https://accounts.google.com/ServiceLogin?service=grandcentral&passive=1209600&continue=https://www.google.com/voice/b/0/redirection/voice&followup=https://www.google.com/voice/b/0/redirection/voice#inbox' 78 | __user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36' 79 | __window_size_width = 1024 80 | __window_size_height = 768 81 | __page_load_timeout = 20 82 | __time_out = 30 83 | __cookie = {} 84 | __intervals = 3 85 | __driver = None 86 | _gc_data = None 87 | __try_time = 2 88 | _match = {} 89 | 90 | def __init__(self, email, passwd, debug=True): 91 | self.__email = email 92 | self.__passwd = passwd 93 | self.debug = debug 94 | self._gc_data_name = '_gcData' 95 | self.__browser_name = 'PhantomJS' # 默认 96 | self.__module = 'selenium.webdriver' 97 | 98 | self.log = self.__voice_log() 99 | if not (email and passwd): 100 | raise TypeError('email or passwd is empty!') 101 | 102 | t = threading.Thread(target=self.start, name='Analog logon') # 启动登录线程 103 | t.setDaemon(True) 104 | t.start() 105 | 106 | def __guard(self): 107 | ''' 守护线程, 如果当前登录的帐号发生异常则重新启动登录流程''' 108 | while self.status['login'] and not self.status['guard']: 109 | time.sleep(2) 110 | continue 111 | self.status['guard'] = True 112 | e = self.__login() 113 | e.send(None) 114 | 115 | def start(self): 116 | '''程序启动''' 117 | if self.status['login']: 118 | pass 119 | self.log.send(('start...',)) 120 | self._initial() # 钩子 121 | e = self.__login() 122 | e.send(None) 123 | 124 | @init 125 | def __login(self): 126 | ''' 127 | login Google 128 | :return: data -> dict 129 | ''' 130 | while True: 131 | yield 132 | if self.status['login']: 133 | pass 134 | self.log.send(('ready...',)) 135 | if self.__driver: 136 | self.log.send(('login again...',)) 137 | self.__driver.quit() 138 | continue 139 | self.__driver = self.__browser() 140 | try: 141 | self.__driver.get(self.__login_url) 142 | # send email 143 | self.log.send(('enter email...',)) 144 | self.__driver.find_element_by_xpath('//*[@id="identifierId"]').send_keys(self.__email) # inp email 145 | # self.screenshots(self.__driver) # debug -> img -> title -> time 146 | 147 | # js -> next 148 | click_js_str = 'document.getElementById("identifierNext").click();' 149 | self.__driver.execute_script(click_js_str) # run js -> user page -> passwd page 150 | # self.screenshots(self.__driver) # debug -> img -> title -> time 151 | 152 | # Wait password input box ... 153 | WebDriverWait(self.__driver, self.__time_out, self.__intervals).until( 154 | EC.visibility_of_element_located((By.XPATH, '//*[@id="password"]'))) 155 | # send password 156 | self.log.send(('enter password...',)) 157 | self.__driver.find_element_by_xpath('//*[@id="password"]/div[1]/div/div[1]/input').send_keys(self.__passwd) 158 | 159 | # js -> next 160 | click_js_str = 'document.getElementById("passwordNext").click();' 161 | self.__driver.execute_script(click_js_str) # run -> js | next -> login 162 | 163 | # Wait page user phoneNumber 164 | WebDriverWait(self.__driver, self.__time_out, self.__intervals).until( 165 | EC.presence_of_element_located((By.ID, 'gc-iframecontainer'))) 166 | self.log.send(('login successful...',)) 167 | # self.screenshots(self.__driver) # debug -> img -> title -> time 168 | 169 | self.status['login'] = True # login successful flag 170 | e = self.__initial() 171 | e.send(None) 172 | except TimeoutException as e: # 如果出现超时,就重试 173 | time.sleep(1) 174 | e = self.__login() 175 | e.send(None) 176 | except NotOpenGoogle as e: # 打不开谷歌 177 | raise NotOpenGoogle('Can not open google, pleasw use VPN, you know...') 178 | except Exception as e: 179 | self.screenshots(self.__driver) # debug -> img -> title -> time 180 | self.status['self'] = 0 181 | self.__debug(e) 182 | 183 | @init 184 | def __voice_log(self, level=0): 185 | ''' 186 | logging 程序运行日志 187 | :param msg: message 188 | :param level: log level (0,info) (1,warning) (2,error) 189 | ''' 190 | logger = logging.getLogger() 191 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 192 | fh = logging.FileHandler('run.log') 193 | fh.setFormatter(formatter) 194 | logger.addHandler(fh) 195 | logger.level = logging.INFO 196 | if self.debug: # to screen 197 | ch = logging.StreamHandler() 198 | ch.setFormatter(formatter) 199 | logger.addHandler(ch) # 输出到屏幕 200 | while True: 201 | log = yield 202 | if len(log) > 1: 203 | level, msg = log 204 | if level == 0: # info 205 | logger.info(log[0]) 206 | if level == 1: # warning 207 | logger.warning(msg) 208 | elif level == 2:# error 209 | logger.error(msg) 210 | else: 211 | pass 212 | 213 | def _initial(self): 214 | ''' 钩子, 自定义一些配置的操作 ''' 215 | pass 216 | 217 | @init 218 | def __initial(self): 219 | ''' initial data 初始化数据 ''' 220 | # get javascript value -> _gcData 221 | while True: 222 | yield 223 | if self.status['login']: 224 | self.log.send(('initialization data...',)) 225 | self._gc_data = self.__driver.execute_script('return %s' % self._gc_data_name) 226 | if self._gc_data is None: # 如果获取不到这个参数就回去重新获取 直到获取到为止 227 | time.sleep(self.__try_time) 228 | continue 229 | # format url 230 | self.__cookie_func(self.__driver.get_cookies()) # process cookie 处理 cookie 231 | self._send_msg_url = '{}/sms/send/'.format(self._gc_data['baseUrl']) # process send msg url 232 | self.__call_url = '{}/call/connect/'.format(self._gc_data['baseUrl']) # 拨打电话的请求地址 233 | self.__call_cancel_url = '{}/call/cancel/'.format(self._gc_data['baseUrl']) # 取消拨打电话的请求地址 234 | self.__mark_url = '{}/inbox/mark/'.format(self._gc_data['baseUrl']) # 标记为已读的请求地址 235 | self.__del_msg_url = '{}/inbox/deleteMessages/'.format(self._gc_data['baseUrl']) # 删除信息 236 | self.__star_url = '{}/inbox/star/'.format(self._gc_data['baseUrl']) # 收藏信息 237 | self.__dow_msg_url = '{}/inbox/recent/'.format(self._gc_data['baseUrl']) # 下载信息 238 | self.__quick_add_url = '{}//phonebook/quickAdd/'.format(self._gc_data['baseUrl']) 239 | self.__voicemail_ogg_str = '{0}/media/send_voicemail_ogg/{1}?read=0' 240 | 241 | if len(self._gc_data['phones']) > 1: # 获取绑定的号码 242 | for k, v in self._gc_data['phones'].items(): 243 | if self._gc_data['phones'][k]['name'] != 'Google Talk': 244 | self.__call_phone_for = self._gc_data['phones'][k] 245 | break 246 | # 是否需要检测新消息 247 | self.driver.quit() 248 | self.status['init'] = True # 初始化完成 249 | # 开启守护线程 250 | t = threading.Thread(target=self.__guard, name='Guard') 251 | t.setDaemon(True) 252 | t.start() 253 | 254 | self.__check_msg_par() # TODO OFF check msg # 去获取检测新消息的参数, 如果不需要这项功能可以取消掉 255 | 256 | else: 257 | self.log.send(('not login', 1)) 258 | 259 | def __cookie_func(self, cookie_list): 260 | '''处理 cookie ''' 261 | self.log.send(('process cookie...',)) 262 | try: 263 | for i in cookie_list: 264 | self.__cookie[i['name']] = i['value'] 265 | return self.__cookie 266 | except Exception as e: 267 | self.__debug(e) 268 | 269 | def __check_msg_par(self): 270 | ''' get checkMessages token (must) 获取检测消息时url的必须参数''' 271 | self.log.send(('get checkMessages token...',)) 272 | 273 | data = {'xpc': {'tp': None, 'osh': None, 'pru': 'https://www.google.com/voice/xpc/relay', 274 | 'ppu': 'https://www.google.com/voice/xpc/blank/', 275 | 'lpu': '{}/voice/xpc/blank/'.format(self._gc_data['xpcUrl'])}} 276 | 277 | url = '{}/voice/xpc/'.format(self._gc_data['xpcUrl']) 278 | r = self._requests(url, params=data) 279 | par = re.findall("\'(.*?)\'", r.text)[0] 280 | self.log.send(('xpc: %s' % par,)) 281 | 282 | # https://clientsx.google.com/voice/xpc/checkMessages?r=xxxxxxx 283 | self.check_msg_url['url'] = '{0}/voice/xpc/checkMessages'.format(self._gc_data['xpcUrl']) 284 | self.check_msg_url['par'] = {'r': par} 285 | self.status['check'] = True 286 | 287 | # 开启检测消息线程 288 | if self.status['auto']: 289 | t = threading.Thread(target=self._check_sms, args=(self.reply_sms,), name='check-new-sms') 290 | t.setDaemon(True) 291 | t.start() 292 | 293 | def __debug(self, e): 294 | self.status['self'] = 0 # 把当前程序状态改为异常 295 | if self.debug: 296 | self.screenshots(self.driver) # 保存当前浏览器截图 297 | self.log.send((2, traceback.format_exc())) 298 | 299 | def set_agent(self, agent): 300 | '''浏览器请求头''' 301 | self.__user_agent = agent 302 | 303 | @property 304 | def current_url(self): 305 | ''' 模拟浏览器当前的url ''' 306 | try: 307 | return self.driver.current_url 308 | except Exception: 309 | pass 310 | 311 | @property 312 | def __headers(self): 313 | '''send post headers 请求头''' 314 | return {'host': 'www.google.com', 'user-agent': self.__user_agent, 315 | 'referer': self._gc_data['baseUrl'], 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8'} 316 | 317 | def _requests(self, url, params=None, data=None, method='get'): 318 | ''' 319 | requests uniform, code reuse 封装请求方法 320 | :param url: request url 请求的地址 321 | :param method: request method get/post 322 | :param params: request get params get 请求的参数 323 | :param data: request post data post 请求的参数 324 | :return: Response对象 325 | ''' 326 | try: 327 | if method == 'get': 328 | r = requests.get(url, params=params, headers=self.__headers, cookies=self.__cookie, verify=False) 329 | return r 330 | elif method == 'post': 331 | r = requests.post(url, data=data, headers=self.__headers, cookies=self.__cookie, verify=False) 332 | return r 333 | else: # not support method #TODO img? 发送图片 334 | pass 335 | except AttributeError as e: 336 | return None 337 | except OSError as e: # 如果是本地测试出现这个报错要更换为全局代理 338 | raise ProxyError('please proxy global') 339 | except Exception as e: 340 | self.__debug(e) 341 | 342 | def check_unread_msg(self): 343 | ''' 344 | Get all of the unread SMS messages in a Google Voice inbox. 345 | 检测有没有新消息 346 | :return dict 347 | ''' 348 | if self.status['login'] and self.status['check']: 349 | r = self._requests(self.check_msg_url['url'], params=self.check_msg_url['par']) # check... 350 | ret = r.json() 351 | return ret 352 | self.log.send((1, 'check msg not ready')) 353 | 354 | def _check_sms(self, func): 355 | '''检测未读 sms, 并做出自定义的操作''' 356 | while self.status['login'] and self.status['check']: 357 | res = self.check_unread_msg() # check sms... 358 | if res['data']['unreadCounts']['sms'] > 0: # have ... 359 | sms_list = self.unsms 360 | for i in sms_list: 361 | if i['text'].strip().upper() in self._match: # 匹配关键字 362 | t = threading.Thread(target=func, args=(i,), name='reply sms') 363 | t.setDaemon(True) 364 | t.start() 365 | else: 366 | time.sleep(10) 367 | self.log.send((1, 'check sms not ready')) 368 | 369 | def reply_sms(self, data): 370 | ''' 371 | 回复这条 sms 372 | :param data: 需要回复的 sms 的数据 373 | ''' 374 | self.log.send(('ready reply sms',)) 375 | r = self.send_sms(data['number'], self._match[data['text']]) 376 | if r['ok']: 377 | self.log.send(('[success] reply sms to: %s' % data['number'],)) 378 | else: 379 | self.log.send((1, '[miss] reply sms to: %s' % data['number'])) 380 | 381 | def __process_xml(self, r): 382 | ''' 383 | 处理 xml ,并转化为 BeautifulSoup 对象 384 | :param r: Response 385 | :param type: (sms 1) / (voicemail 0) 386 | :param read_type: (0 unread) / (1 read) 387 | :return type dict 388 | ''' 389 | data = {} 390 | if self.status['login']: 391 | tree = ET.fromstring(r.content) 392 | for elem in tree.iter(): 393 | if elem.tag == 'json': 394 | data['json'] = json.loads(elem.text) 395 | continue 396 | if elem.tag == 'html': 397 | data['html'] = elem.text 398 | self.log.send(('process new msg...',)) 399 | data['soup'] = BeautifulSoup(data['html'], 'html.parser') 400 | 401 | return data 402 | 403 | @property 404 | def unsms(self): 405 | ''' 406 | 获取未读的 sms 407 | :return: dict 408 | ''' 409 | par = {'v': self._gc_data['v']} # token 410 | if self.status['login']: 411 | sms_list = [] 412 | r = self._requests(self.__dow_msg_url, params=par) 413 | data = self.__process_xml(r) 414 | msg_list = data['soup'].find_all(name='div', class_='gc-message-unread') # 所有的未读短信消息 415 | # data = {'sms': [], 'voicemail': []} 416 | for msg in msg_list: 417 | attr = msg.get('class') 418 | if 'gc-message-sms' in attr: # sms 419 | sms = {} 420 | sms['id'] = msg['id'] 421 | sms['number'] = msg.find(name='span', class_='gc-message-sms-from').text.strip()[:-1] # 去掉空格并切掉最后的冒号 422 | # ----- 处理时间 ----- 423 | time_str = msg.find(name='span', class_='gc-message-sms-time').text.strip() 424 | local_time = time.strftime("%Y-%m-%d", time.localtime()) 425 | sms_time_str = ''.join((local_time, ' ', time_str)) 426 | sms_time = time.strptime(sms_time_str, '%Y-%m-%d %I:%M %p') 427 | sms['time'] = time.strftime("%Y-%m-%d %X", sms_time) 428 | sms['text'] = msg.find(name='span', class_='gc-message-sms-text').text 429 | self.log.send(('[sms] time: {0}; id:{1} .'.format(sms['time'], sms['id']),)) 430 | print(sms) 431 | sms_list.append(sms) 432 | return sms_list 433 | 434 | @property 435 | def read_sms(self): 436 | ''' 437 | 所有的已读读短信消息 438 | :return: dict 439 | ''' 440 | par = {'v': self._gc_data['v']} # token 441 | if self.status['login']: 442 | sms_list = [] 443 | r = self._requests(self.__dow_msg_url, params=par) 444 | data = self.__process_xml(r) 445 | msg_list = data['soup'].select('div.gc-message-sms.gc-message-read') # 所有的已读读短信消息 446 | # data = {'sms': [], 'voicemail': []} 447 | for msg in msg_list: 448 | attr = msg.get('class') 449 | if 'gc-message-sms' in attr: # sms 450 | sms = {} 451 | sms['id'] = msg['id'] 452 | sms['number'] = msg.find(name='span', class_='gc-message-sms-from').text.strip()[:-1] # 去掉空格并切掉最后的冒号 453 | sms['text'] = msg.find(name='span', class_='gc-message-sms-text').text 454 | sms['time'] = msg.find(name='span', class_='gc-message-sms-time').text.strip() 455 | sms_list.append(sms) 456 | return sms_list 457 | 458 | @property 459 | def voicemail(self): 460 | ''' 461 | 获取未读的 voicemail, 包括文本和语音(下载url 地址) 462 | :return: 463 | ''' 464 | par = {'v': self._gc_data['v']} # token 465 | if self.status['login']: 466 | voice_list = [] 467 | r = self._requests(self.__dow_msg_url, params=par) 468 | data = self.__process_xml(r) 469 | msg_list = data['soup'].find_all(name='div', class_='gc-message-unread') # 所有的未读语音消息 470 | for msg in msg_list: 471 | attr = msg.get('class') 472 | if 'gc-message-sms' in attr: 473 | continue 474 | voicemail = {} 475 | voicemail['id'] = msg['id'] 476 | voicemail['number'] = msg.find(name='span', class_='gc-nobold').text 477 | voicemail['time'] = msg.find(name='span', class_='gc-message-time').text 478 | voicemail['text'] = msg.find(name='span', class_='gc-edited-trans-text').text 479 | voicemail['ogg_url'] = self.__voicemail_ogg_str.format(self._gc_data['baseUrl'], voicemail['id']) 480 | 481 | if len(voicemail['text']) < 1: 482 | voicemail['text'] = '[None] - please go to the website...' 483 | voice_list.append(voicemail) 484 | self.log.send(('[voicemail] time: {0}; id:{1} .'.format(voicemail['time'], voicemail['id']),)) 485 | return voice_list 486 | 487 | def dow_voicemail(self, url): 488 | ''' 489 | 下载 voicemail 的音频 490 | :param url: voicemail 下载地址的url 491 | :return: r.content 二进制数据 492 | ''' 493 | r = self._requests(url) 494 | return r.content 495 | 496 | def quick_add(self, name, number, phone_type=0): 497 | ''' 498 | 添加到 google phonebook 499 | :param name: 名字 备注 500 | :param number: 10 位合法的美国手机号码 501 | :param phone_type: 类型 : {0:'MOBILE',1:'WORK',2:'HOME'} 502 | :return: 503 | ''' 504 | phone_type_dict = {0: 'MOBILE', 1: 'WORK', 2: 'HOME'} 505 | if self.status['login']: 506 | data = {'phoneNumber': '+1%s' % number, 507 | 'phoneType': phone_type_dict[phone_type], 508 | '_rnr_se': self._gc_data['_rnr_se'], 509 | 'needsCheck': 1} 510 | r = self._requests(self.__quick_add_url, data=data, method='post') 511 | ret = r.json() 512 | if ret['ok']: 513 | return ret 514 | else: 515 | self.log.send((1, 'abnormal status, log in again')) 516 | self.status['login'] = False # 状态异常 517 | return 518 | 519 | def mark(self, msg_id, read=1): 520 | ''' 521 | 把信息标记为已读 522 | :param msg_id: msg 的唯一 id 它必须是存在的 523 | :param read: (0,未读) (1,已读) 524 | :return: 525 | ''' 526 | if self.status['login']: 527 | data = {'_rnr_se': self._gc_data['_rnr_se'], 'messages': msg_id, 'read': read} 528 | r = self._requests(self.__mark_url, data=data, method='post') 529 | ret = r.json() 530 | if ret['ok']: 531 | return ret 532 | else: 533 | self.log.send((1, 'abnormal status, log in again')) 534 | self.status['login'] = False # 状态异常 535 | return 536 | 537 | def star(self, msg_id): 538 | ''' 把信息标记为收藏 ''' 539 | if self.status['login']: 540 | data = {'_rnr_se': self._gc_data['_rnr_se'], 'messages': msg_id, 'star': 1} 541 | r = self._requests(self.__star_url, data=data, method='post') 542 | ret = r.json() 543 | if ret['ok']: 544 | return ret 545 | else: 546 | self.log.send((1, 'abnormal status, log in again')) 547 | self.status['login'] = False # 状态异常 548 | return 549 | 550 | def unstar(self, msg_id): 551 | ''' 把已经标记收藏的消息取消收藏标记 ''' 552 | if self.status['login']: 553 | data = {'_rnr_se': self._gc_data['_rnr_se'], 'messages': msg_id, 'star': 0} 554 | r = self._requests(self.__star_url, data=data, method='post') 555 | ret = r.json() 556 | if ret['ok']: 557 | return ret 558 | else: 559 | self.log.send((1, 'abnormal status, log in again')) 560 | self.status['login'] = False # 状态异常 561 | return 562 | 563 | def del_msg(self, msg_id): 564 | ''' 删除信息 ''' 565 | if self.status['login']: 566 | data = {'_rnr_se': self._gc_data['_rnr_se'], 'messages': msg_id, 'trash': 1} 567 | r = self._requests(self.__del_msg_url, data=data, method='post') 568 | ret = r.json() 569 | if ret['ok']: 570 | return ret 571 | else: 572 | self.log.send((1, 'abnormal status, log in again')) 573 | self.status['login'] = False # 状态异常 574 | return 575 | 576 | def missed(self): 577 | '''错过的来电''' 578 | pass 579 | 580 | def send_sms(self, number, text): 581 | ''' 582 | 给指定的美国号码发送文本消息 583 | :param number: 符合格式美国号码 +1XXXXXXXXXX 584 | :param text: 要发送的消息 585 | :return: post 结果, 消息是否发送成功 586 | ''' 587 | if self.status['login']: 588 | # 数据格式 589 | msg = {'id': None, 'phoneNumber': number, 'text': text, 'sendErrorSms': 0, 590 | '_rnr_se': self._gc_data['_rnr_se']} 591 | r = self._requests(self._send_msg_url, method='post', data=msg) # post sms 592 | ret = r.json() 593 | if ret['ok']: 594 | return ret 595 | else: 596 | self.log.send((1, 'abnormal status, log in again')) 597 | self.status['login'] = False # 状态异常 598 | return 599 | 600 | def call(self, number): 601 | ''' 602 | 拨打电话到指定美国号码 603 | :param number: 符合格式美国号码 +1XXXXXXXXXX 604 | :return: post 请求的结果, 拨打状态, 以及此次通信的 callId 605 | ''' 606 | if self.status['login']: 607 | # 数据格式 608 | data = {'outgoingNumber': number, 'remember': 0, 'phoneType': self.__call_phone_for['type'], 609 | 'subscriberNumber': self._gc_data['number']['raw'], 610 | 'forwardingNumber': self.__call_phone_for['phoneNumber'], 611 | '_rnr_se': self._gc_data['_rnr_se']} 612 | 613 | r = self._requests(self.__call_url, data=data, method='post') # 拨打 614 | ret = r.json() 615 | if ret['ok']: 616 | return ret 617 | else: 618 | self.log.send((1, 'abnormal status, log in again')) 619 | self.status['login'] = False # 状态异常 620 | return 621 | 622 | def cancel_call(self, call_id): 623 | ''' 624 | 取消拨打 625 | :param call_id: call 方法返回的 callId, 626 | :return: 请求的结果, 取消通话是否成功 627 | ''' 628 | if self.status['login']: 629 | # 数据格式 630 | data = {'outgoingNumber': None, 631 | 'forwardingNumber': None, 632 | 'cancelType': 'C2C', 633 | '_rnr_se': self._gc_data['_rnr_se'], 634 | 'callId': call_id} 635 | r = self._requests(self.__call_cancel_url, data=data, method='post') 636 | return r.json() # {"ok" : false} 637 | return 638 | 639 | def set_time_out(self, sec): 640 | ''' 641 | set page load time out 642 | :param sec: type int 643 | ''' 644 | self.__page_load_timeout = int(sec) 645 | 646 | def set_browser(self, browser_name): 647 | ''' PhantomJS / Chrome / Firefox ''' 648 | self.__browser_name = browser_name 649 | 650 | def set_login_url(self, url): 651 | '''set login url 如果你需要改变登录的地址 ''' 652 | self.__login_url = url 653 | 654 | def set_intervals(self, sec): 655 | '''set get html tag intervals time 设置寻找网页标签的间隔时间''' 656 | self.__intervals = sec 657 | 658 | def set_match(self, data): 659 | '''设置匹配关键字,字典格式''' 660 | self._match = data 661 | 662 | def __createInstance(self, module_name, class_name, *args, **kwargs): 663 | ''' 664 | create user input browser object 动态导入浏览器浏览器类型模块 665 | :return: object 666 | ''' 667 | 668 | headers = {'Accept': '*/*', 669 | 'Accept-Encoding': 'gzip, deflate, sdch', 670 | 'Accept-Language': 'en-US,en;q=0.8', 671 | 'Cache-Control': 'max-age=0', 672 | 'User-Agent': self.__user_agent, 673 | } 674 | # set browser header 675 | for key, value in headers.items(): 676 | webdriver.DesiredCapabilities.PHANTOMJS['phantomjs.page.customHeaders.{}'.format(key)] = value 677 | module_meta = __import__(module_name, globals(), locals(), [class_name]) 678 | try: 679 | class_meta = getattr(module_meta, class_name) 680 | obj = class_meta(*args, **kwargs) 681 | except AttributeError as e: 682 | raise AttributeError('not is %s' % self.__browser_name) 683 | return obj 684 | 685 | def __browser(self): 686 | ''' 687 | browser obj , browser header 创建要模拟的浏览器 688 | :return obj 689 | ''' 690 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 691 | dcap_lib = getattr(DesiredCapabilities, self.__browser_name.upper()) 692 | if not dcap_lib: 693 | raise 694 | dcap = dict(dcap_lib) 695 | dcap["phantomjs.page.settings.userAgent"] = (self.__user_agent) # 设置 userAgent 696 | 697 | driver = self.__createInstance(self.__module, self.__browser_name, 698 | desired_capabilities=dcap, 699 | service_args=['--ignore-ssl-errors=true', '--ssl-protocol=TLSv1']) 700 | driver.set_window_size(self.__window_size_width, self.__window_size_height) # set window size 701 | driver.maximize_window() # set maximize_window 702 | driver.set_page_load_timeout(self.__page_load_timeout) # set time out 703 | return driver 704 | 705 | @property 706 | def driver(self): 707 | ''' 操作句柄 ''' 708 | return self.__driver if self.__driver else None 709 | 710 | def screenshots(self, driver, sleep=None): 711 | ''' 712 | save screenshots 给模拟浏览器截图 713 | :param driver: selenium 714 | :param sleep: sec 715 | :return: 716 | ''' 717 | if sleep: 718 | time.sleep(sleep) 719 | try: 720 | driver.save_screenshot("./img/%s.png" % (time.strftime("%Y-%m-%d %X", time.localtime()))) 721 | except Exception: 722 | pass 723 | 724 | def get_js(self, js_str): 725 | '''在模拟浏览器中执行 javascript''' 726 | return self.__driver.execute_script('return %s' % js_str) if self.__driver else None 727 | 728 | def __del__(self): 729 | ''' 退出模拟的浏览器 quit driver''' 730 | if not isinstance(self.driver, type(None)): 731 | self.driver.quit() 732 | -------------------------------------------------------------------------------- /gvapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .GoogleVoice import * 2 | 3 | 4 | __version__ = '0.0.1' 5 | 6 | __all__ = [ 7 | 'Voice', 8 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=0.12.2 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | """ 4 | ... 5 | """ 6 | 7 | install_requires = [ 8 | 'selenium==3.8.0', 9 | 'urllib3>=1.23', 10 | 'requests>=2.18.1', 11 | 'beautifulsoup4==4.6.0', 12 | ] 13 | 14 | 15 | setup( 16 | name='gvapi', 17 | version='0.0.1', 18 | author='kentio', 19 | author_email='13550898+kentio@users.noreply.github.com', 20 | url='https://github.com/kentio', 21 | packages=find_packages(exclude=('tests',)), 22 | license='LICENSE', 23 | description='Google Voice Python API', 24 | long_description=__doc__, 25 | zip_safe=False, 26 | install_requires=install_requires, 27 | ) 28 | --------------------------------------------------------------------------------