├── googleplay ├── __init__.py └── api.py ├── .gitignore ├── README.md └── bot.py /googleplay/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore precompiled python files 2 | *.pyc 3 | 4 | # ignore downloaded apps 5 | *.apk 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Google Play Telegram Bot 2 | ============ 3 | A Telegram Bot that let you download .apk from Google Play 4 | 5 | ## Credits 6 | This project uses a custom and updated version of the [Google Play Unofficial Python API](https://github.com/egirault/googleplay-api). 7 | 8 | ## Requirements 9 | * [Python 2.7](http://www.python.org) 10 | * [Protocol Buffers](http://code.google.com/p/protobuf/) 11 | * [Requests](http://docs.python-requests.org/) 12 | * [Telepot](https://github.com/nickoala/telepot) 13 | 14 | ## Configuration 15 | You must edit `config.py` before running the bot. 16 | 17 | You need to provide: 18 | * a valid [Telegram Bot](https://core.telegram.org/bots) authentication token 19 | * your phone's `androidID` 20 | * your Google Play Store login and password, or a valid subAuthToken 21 | 22 | To get your `androidID`, use `*#*#8255#*#*` on your phone to start *Gtalk Monitor*. The hex string listed after `aid` is your `androidID`. 23 | 24 | ## Quick Start 25 | 26 | When the configuration is complete you can install the dependencies with: 27 | 28 | $ pip install protobuf requests telepot 29 | 30 | And run the bot with: 31 | 32 | $ python bot.py 33 | 34 | ## Usage 35 | With the bot running you can send him a text with the **package name** (i.e. _*org.telegram.messenger*_) and it will respond with the requested .apk file. 36 | 37 | I only tested with free apps, but I guess it should work as well with non-free as soon as you have enough money on your Google account. 38 | 39 | ## License 40 | This project is licensed under the terms of the **MIT** license. 41 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import telepot 2 | import time 3 | 4 | from googleplay.api import GooglePlayAPI 5 | 6 | TELEGRAM_TOKEN = "" 7 | 8 | ANDROID_ID = "" 9 | GOOGLE_USER = "" 10 | GOOGLE_PWD = "" 11 | GOOGLE_TOKEN = None 12 | 13 | 14 | def sizeof_fmt(size): 15 | """ 16 | Get human readable version of file size 17 | """ 18 | for x in ['bytes', 'KB', 'MB', 'GB', 'TB']: 19 | if size < 1024.0: 20 | return "%3.1f%s" % (size, x) 21 | size /= 1024.0 22 | 23 | 24 | class GooglePlayBot(telepot.Bot): 25 | def handle(self, msg): 26 | content_type, chat_type, chat_id = telepot.glance(msg) 27 | 28 | if content_type == 'text': 29 | text = msg['text'] 30 | 31 | if text.startswith('/download'): 32 | try: 33 | command, package = text.split(' ') 34 | filename = "{}.apk".format(package) 35 | except ValueError: 36 | msg = "Package name malformed or missing" 37 | return self.sendMessage(chat_id, msg) 38 | 39 | # Init API and log in 40 | api = GooglePlayAPI(ANDROID_ID) 41 | logged = api.login(GOOGLE_USER, GOOGLE_PWD, GOOGLE_TOKEN) 42 | if not logged: 43 | msg = "Login error, check your credentials" 44 | return self.sendMessage(chat_id, msg) 45 | 46 | # Get version code and offer type from the app details 47 | doc = api.details(package).docV2 48 | version = doc.details.appDetails.versionCode 49 | if not doc.offer: 50 | msg = "Package not found, check for typos" 51 | return self.sendMessage(chat_id, msg) 52 | 53 | offer = doc.offer[0].offerType 54 | 55 | try: 56 | # Download 57 | size = sizeof_fmt(doc.details.appDetails.installationSize) 58 | msg = "Downloading: {}\nSize: {}".format(doc.title, size) 59 | self.sendMessage(chat_id, msg) 60 | 61 | # Upload 62 | data = api.download(package, version, offer) 63 | open(filename, "wb").write(data) 64 | self.sendMessage(chat_id, "Done, sending..") 65 | self.sendDocument(chat_id, open(filename, 'rb')) 66 | 67 | except Exception: 68 | self.sendMessage(chat_id, "Error, retry") 69 | 70 | 71 | bot = GooglePlayBot(TELEGRAM_TOKEN) 72 | bot.message_loop() 73 | 74 | # Keep the bot running. 75 | while 1: 76 | time.sleep(10) 77 | -------------------------------------------------------------------------------- /googleplay/api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import proto 3 | 4 | 5 | class LoginError(Exception): 6 | def __init__(self, value): 7 | self.value = value 8 | 9 | def __str__(self): 10 | return repr(self.value) 11 | 12 | 13 | class RequestError(Exception): 14 | def __init__(self, value): 15 | self.value = value 16 | 17 | def __str__(self): 18 | return repr(self.value) 19 | 20 | 21 | class GooglePlayAPI(object): 22 | """ 23 | Google Play Unofficial API Class 24 | 25 | Usual APIs methods are login(), details(), download(). 26 | """ 27 | 28 | LANG = "en_US" 29 | SERVICE = "androidmarket" 30 | URL_LOGIN = "https://android.clients.google.com/auth" 31 | ACCOUNT_TYPE_HOSTED_OR_GOOGLE = "HOSTED_OR_GOOGLE" 32 | authSubToken = None 33 | 34 | def __init__(self, androidId): 35 | self.preFetch = {} 36 | self.androidId = androidId 37 | 38 | def _try_register_preFetch(self, protoObj): 39 | fields = [i.name for (i, _) in protoObj.ListFields()] 40 | if ("preFetch" in fields): 41 | for p in protoObj.preFetch: 42 | self.preFetch[p.url] = p.response 43 | 44 | def setAuthSubToken(self, authSubToken): 45 | self.authSubToken = authSubToken 46 | 47 | def login(self, email=None, password=None, authSubToken=None): 48 | """ 49 | Login to your Google Account. You must provide either: 50 | - an email and password 51 | - a valid Google authSubToken 52 | """ 53 | if (authSubToken is not None): 54 | self.setAuthSubToken(authSubToken) 55 | else: 56 | if (email is None or password is None): 57 | return False 58 | 59 | params = {"Email": email, 60 | "Passwd": password, 61 | "service": self.SERVICE, 62 | "accountType": self.ACCOUNT_TYPE_HOSTED_OR_GOOGLE, 63 | "has_permission": "1", 64 | "source": "android", 65 | "androidId": self.androidId, 66 | "app": "com.android.vending", 67 | "device_country": "fr", 68 | "operatorCountry": "fr", 69 | "lang": "fr", 70 | "sdk_version": "16"} 71 | 72 | headers = { 73 | "Accept-Encoding": "", 74 | } 75 | 76 | response = requests.post(self.URL_LOGIN, data=params, 77 | headers=headers, verify=False) 78 | data = response.text.split() 79 | params = {} 80 | for d in data: 81 | try: 82 | if "=" not in d: 83 | continue 84 | s = d.split("=", 1) 85 | params[s[0].strip().lower()] = s[1].strip() 86 | except: 87 | return False 88 | if "auth" in params: 89 | self.setAuthSubToken(params["auth"]) 90 | elif "error" in params: 91 | return False 92 | return True 93 | 94 | def executeRequestApi2(self, path, datapost=None): 95 | post_content_type = "application/x-www-form-urlencoded; charset=UTF-8" 96 | if (datapost is None and path in self.preFetch): 97 | data = self.preFetch[path] 98 | else: 99 | headers = {"Accept-Language": self.LANG, 100 | "Authorization": "GoogleLogin auth=%s" % self.authSubToken, 101 | "X-DFE-Enabled-Experiments": "cl:billing.select_add_instrument_by_default", 102 | "X-DFE-Unsupported-Experiments": "nocache:billing.use_charging_poller,market_emails,buyer_currency,prod_baseline,checkin.set_asset_paid_app_field,shekel_test,content_ratings,buyer_currency_in_app,nocache:encrypted_apk,recent_changes", 103 | "X-DFE-Device-Id": self.androidId, 104 | "X-DFE-Client-Id": "am-android-google", 105 | "User-Agent": "Android-Finsky/3.7.13 (api=3,versionCode=8013013,sdk=16,device=crespo,hardware=herring,product=soju)", 106 | "X-DFE-SmallestScreenWidthDp": "320", 107 | "X-DFE-Filter-Level": "3", 108 | "Accept-Encoding": "", 109 | "Host": "android.clients.google.com"} 110 | 111 | if datapost is not None: 112 | headers["Content-Type"] = post_content_type 113 | 114 | url = "https://android.clients.google.com/fdfe/%s" % path 115 | if datapost is not None: 116 | response = requests.post(url, data=datapost, headers=headers, 117 | verify=False) 118 | else: 119 | response = requests.get(url, headers=headers, verify=False) 120 | data = response.content 121 | 122 | message = proto.ResponseWrapper.FromString(data) 123 | self._try_register_preFetch(message) 124 | 125 | return message 126 | 127 | ##################################### 128 | # Google Play API Methods 129 | ##################################### 130 | 131 | def search(self, query, nb_results=None, offset=None): 132 | """ 133 | Search for apps. 134 | """ 135 | path = "search?c=3&q=%s" % requests.utils.quote(query) 136 | if (nb_results is not None): 137 | path += "&n=%d" % int(nb_results) 138 | if (offset is not None): 139 | path += "&o=%d" % int(offset) 140 | 141 | message = self.executeRequestApi2(path) 142 | return message.payload.searchResponse 143 | 144 | def details(self, packageName): 145 | """ 146 | Get app details from a package name. 147 | """ 148 | path = "details?doc=%s" % requests.utils.quote(packageName) 149 | message = self.executeRequestApi2(path) 150 | return message.payload.detailsResponse 151 | 152 | def download(self, packageName, versionCode, offerType=1): 153 | """ 154 | Download an app and return its raw data (APK file). 155 | """ 156 | path = "purchase" 157 | data = "ot=%d&doc=%s&vc=%d" % (offerType, packageName, versionCode) 158 | message = self.executeRequestApi2(path, data) 159 | status = message.payload.buyResponse.purchaseStatusResponse 160 | 161 | url = status.appDeliveryData.downloadUrl 162 | cookie = status.appDeliveryData.downloadAuthCookie[0] 163 | 164 | cookies = { 165 | str(cookie.name): str(cookie.value) 166 | } 167 | 168 | headers = {"User-Agent": "AndroidDownloadManager/4.1.1 (Linux; U; Android 4.1.1; Nexus S Build/JRO03E)", 169 | "Accept-Encoding": ""} 170 | 171 | response = requests.get(url, headers=headers, cookies=cookies, verify=False) 172 | return response.content 173 | --------------------------------------------------------------------------------