#!/usr/bin/env python # -*- coding:utf-8 -*- # project: 12月 # author: NinEveN # date: 2019/12/19 import io import os import plistlib import re import zipfile import qrcode import requests from PIL import Image from androguard.core.bytecodes import apk def bytes2human(n): symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') prefix = {} for i, s in enumerate(symbols): prefix[s] = 1 << (i + 1) * 10 for s in reversed(symbols): if n >= prefix[s]: value = float(n) / prefix[s] return '%.1f%s' % (value, s) return '%sB' % n class AppInfo(object): def __init__(self, app_path): self.app_path = app_path self.result = {} def make_app_png(self, png_path, filename): zf = zipfile.ZipFile(self.app_path) name_list = zf.namelist() if self.app_path.endswith("apk"): # pattern = re.compile(r'res/drawable[^/]*/app_icon.png') iconfile = apk.APK(self.app_path).get_app_icon() size = 999 elif self.app_path.endswith("ipa"): size = 0 pattern = re.compile(r'Payload/[^/]*.app/AppIcon[^/]*[^(ipad)].png') iconfile = "" for path in name_list: m = pattern.match(path) if m is not None: filepath = m.group() fsize = len(zf.read(filepath)) if size == 0: iconfile = filepath if size < fsize: size = fsize iconfile = filepath else: raise Exception("File type error") if size == 0: raise Exception("File size error") if not os.path.isdir(png_path): os.makedirs(png_path) with open(os.path.join(png_path, "%s.png" % filename), 'wb') as f: f.write(zf.read(iconfile)) return "%s.png" % filename def get_app_data(self): if self.app_path.endswith("apk"): return self.__get_android_data() elif self.app_path.endswith("ipa"): return self.__get_ios_data() else: raise Exception("File type error") def __get_ios_info_path(self, ipaobj): infopath_re = re.compile(r'Payload/[^/]*.app/Info.plist') for i in ipaobj.namelist(): m = infopath_re.match(i) if m is not None: return m.group() def __get_android_data(self): try: apkobj = apk.APK(self.app_path) except Exception as err: raise err else: if apkobj.is_valid_APK(): versioncode = apkobj.get_androidversion_code() bundle_id = apkobj.get_package() labelname = apkobj.get_app_name() version = apkobj.get_androidversion_name() sdk_version = apkobj.get_min_sdk_version() self.result = { "labelname": labelname, "bundle_id": bundle_id, "versioncode": versioncode, "version": version, "miniOSversion": sdk_version, "type": "android", } return self.result def __get_ios_data(self): if zipfile.is_zipfile(self.app_path): ipaobj = zipfile.ZipFile(self.app_path) info_path = self.__get_ios_info_path(ipaobj) if info_path: plist_data = ipaobj.read(info_path) plist_root = plistlib.loads(plist_data) labelname = plist_root['CFBundleDisplayName'] versioncode = plist_root['CFBundleVersion'] bundle_id = plist_root['CFBundleIdentifier'] version = plist_root['CFBundleShortVersionString'] miniOSversion = plist_root["MinimumOSVersion"] self.result = { "labelname": labelname, "bundle_id": bundle_id, "versioncode": versioncode, "version": version, "miniOSversion": miniOSversion, "type": "ios", } release_type_info = self.__get_ios_release_type(ipaobj) if info_path: plist_data = ipaobj.read(release_type_info) if b"ProvisionedDevices" in plist_data: self.result["release_type"] = "Adhoc" else: self.result["release_type"] = "Inhouse" return self.result def __get_ios_release_type(self, ipaobj): infopath_re = re.compile(r'Payload/[^/]*.app/embedded.mobileprovision') for i in ipaobj.namelist(): m = infopath_re.match(i) if m is not None: return m.group() class FirApi(object): def __init__(self, api_token): ''' :param api_token: 长度为 32, 用户在 fir 的 api_token ''' self.api_token = api_token self.app_url = "http://api.fir.im/apps" def __request(self, action, *args, **kwargs): fun_re = getattr(requests, action, None) res = fun_re(*args, **kwargs) return res def get_version_by_id(self, id): ''' :param id: 应用ID,可在"应用管理"->"基本信息"查看 :return: ''' url = "%s/latest/%s?api_token=%s" % (self.app_url, id, self.api_token) res = self.__request("get", url) if res.status_code == 200: return res.json() else: raise Exception(res.content) def get_version_by_bundle_id(self, bundle_id, type): ''' :param bundle_id: Bundle ID(iOS) / Package name(Android) :param type: 应用类型 ( ios / android ) :return: ''' url = "%s/latest/%s?api_token=%s&type=%s" % (self.app_url, bundle_id, self.api_token, type) # 使用 bundle_id 请求必填 ( ios / android ) res = self.__request("get", url) if res.status_code == 200: return res.json() else: raise Exception(res.content) def __get_upload_token(self, bundle_id, type): ''' :param bundle_id: App 的 bundleId(发布新应用时必填) :param type: ios 或者 android(发布新应用时必填) :return: ''' data = { "type": type, "bundle_id": bundle_id, "api_token": self.api_token, } res = self.__request("post", self.app_url, data) if res.status_code == 201: return res.json() else: raise Exception(res.content) def upload_app(self, app_path): appobj = AppInfo(app_path) appinfo = appobj.get_app_data() icon_path = appobj.make_app_png() bundle_id = appinfo.get("bundle_id") upcretsdata = self.__get_upload_token(bundle_id, appinfo.get("type")) icon_key = upcretsdata["cert"]["icon"]["key"] icon_token = upcretsdata["cert"]["icon"]["token"] icon_upload_url = upcretsdata["cert"]["icon"]["upload_url"] short_url = upcretsdata["short"] icon_data = { "key": icon_key, "token": icon_token, } files = {'file': (os.path.basename(icon_path), open(icon_path, 'rb'), {'Expires': '0'})} res = self.__request("post", icon_upload_url, data=icon_data, files=files) if res.status_code == 200: print(res.json()) else: raise Exception(res.content) binary_key = upcretsdata["cert"]["binary"]["key"] binary_token = upcretsdata["cert"]["binary"]["token"] binary_upload_url = upcretsdata["cert"]["binary"]["upload_url"] binary_data = { "key": binary_key, "token": binary_token, "x:name": appinfo["labelname"], "x:version": appinfo["version"], "x:build": appinfo["versioncode"], "x:changelog": "" } if appinfo["type"] == "ios": binary_data["x:release_type"] = appinfo.get("release_type", "Adhoc") # Adhoc: 内测版 Inhouse:企业版 files = {'file': (os.path.basename(app_path), open(app_path, 'rb'), {'Expires': '0'})} res = self.__request("post", binary_upload_url, data=binary_data, files=files) if res.status_code == 200: print(res.json()) QrCode.make_logo_qr(os.path.join("https://fir.im/", short_url), icon_path, "fir_%s.png" % (appinfo["labelname"])) else: raise Exception(res.content) def get_app_list(self): url = "%s?api_token=%s" % (self.app_url, self.api_token) res = self.__request("get", url) if res.status_code == 200: return res.json() else: raise Exception(res.content) def get_app_infos_by_id(self, id): ''' :param id: 应用id,可在"应用管理"->"基本信息"查看 :return: ''' url = "%s/%s?api_token=%s" % (self.app_url, id, self.api_token) res = self.__request("get", url) if res.status_code == 200: return res.json() else: raise Exception(res.content) def modify_app_info(self, id, name=None, desc=None, short=None, genre_id=None, is_opened=None, is_show_plaza=None, passwd=None, store_link_visible=None): ''' :param id: 应用id,可在"应用管理"->"基本信息"查看 :param name: app 名称 :param desc: app 详细描述 :param short: app 短链接 :param genre_id: 类别 id :param is_opened: 是否公开 :param is_show_plaza: 是否展示在广场页 :param passwd: 密码访问(不能和is_opened is_show_plaza联合使用) :param store_link_visible: 应用市场链接是否显示 :return: ''' url = "%s/%s" % (self.app_url, id) data = { "api_token": self.api_token, } if name: data["name"] = name if desc: data["desc"] = desc if short: data["short"] = short if genre_id: data["genre_id"] = genre_id if is_opened: data["is_opened"] = is_opened if is_show_plaza and not passwd: data["is_show_plaza"] = is_show_plaza if passwd and not is_show_plaza: data["passwd"] = passwd if is_show_plaza and passwd: raise Exception("密码访问不能和is_opened is_show_plaza联合使用") if store_link_visible: data["name"] = store_link_visible res = self.__request("put", url, data) if res.status_code == 200: return res.json() else: raise Exception(res.content) class QrCode(object): @staticmethod def make_logo_qr(str, logo_path, save_path=None): qr = qrcode.QRCode( version=4, error_correction=qrcode.constants.ERROR_CORRECT_Q, box_size=8, border=2 ) # 添加转换内容 qr.add_data(str) qr.make(fit=True) # 生成二维码 img = qr.make_image() # img = img.convert("RGBA") # 添加logo if logo_path and os.path.exists(logo_path): icon = Image.open(logo_path) # 获取二维码图片的大小 img_w, img_h = img.size factor = 4 size_w = int(img_w / factor) size_h = int(img_h / factor) # logo图片的大小不能超过二维码图片的1/4 icon_w, icon_h = icon.size if icon_w > size_w: icon_w = size_w if icon_h > size_h: icon_h = size_h icon = icon.resize((icon_w, icon_h), Image.ANTIALIAS) # 详见:http://pillow.readthedocs.org/handbook/tutorial.html # 计算logo在二维码图中的位置 w = int((img_w - icon_w) / 2) h = int((img_h - icon_h) / 2) icon = icon.convert("RGBA") img.paste(icon, (w, h), icon) # 详见:http://pillow.readthedocs.org/reference/Image.html#PIL.Image.Image.paste # 保存处理后图片 if save_path: if not os.path.isdir(os.path.dirname(save_path)): os.makedirs(os.path.dirname(save_path)) img.save(save_path) else: imgByteArr = io.BytesIO() img.save(imgByteArr, format="PNG") return imgByteArr.getvalue() if __name__ == '__main__': apk_path = "/root/hehemajiang.apk" API_TOKEN = "3d2a41159a7d8c396ddf0e88799de14a" fir = FirApi(API_TOKEN) # 上传 fir.upload_app(apk_path) # 获取版本信息 # print(fir.get_version_by_id("5dfb54d3b2eb4612589c3040")) # 获取app详细信息 # print(fir.get_app_infos_by_id("5dfb54d3b2eb4612589c3040")) # 获取列表 # print(fir.get_app_list())