├── requirements.txt ├── src ├── enums.py ├── sendTelegram.py ├── git.py ├── basics.py ├── processX.py ├── tele.py ├── main.py ├── constants.py ├── appData.py ├── common.py ├── model.py └── compare.py ├── .github └── workflows │ ├── auto.yml │ └── manual-tele.yml ├── .gitignore └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | bs4 3 | requests 4 | tqdm 5 | urllib3 6 | user_agent 7 | python-dotenv 8 | apkutils 9 | gdown -------------------------------------------------------------------------------- /src/enums.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | class Application(Enum): 5 | X = "x" 6 | GROK = "grok" 7 | 8 | class Platform(Enum): 9 | ANDROID = "android" 10 | IOS = "ios" 11 | WEB = "web" 12 | 13 | class ReleaseType(Enum): 14 | ALPHA = "alpha" 15 | BETA = "beta" 16 | STABLE = "stable" 17 | WEB = "web" 18 | 19 | class Source(Enum): 20 | MAN = "manual" 21 | WEB = "web" 22 | IOS = "stable_ios" 23 | APT = "aptoide" 24 | APKC = "apkcombo" 25 | APKM = "apkmirror" 26 | APKP = "apkpure" 27 | UPTO = "uptodown" 28 | DK= "DontKnow" 29 | 30 | @classmethod 31 | def _missing_(Source, value): 32 | return Source.DK -------------------------------------------------------------------------------- /src/sendTelegram.py: -------------------------------------------------------------------------------- 1 | from compare import flagMessage 2 | from constants import MANIFEST_FILE_NAME,getEnv,isDebug 3 | from basics import readJson,printSubCmd 4 | from tele import editMsg,sendMsg 5 | from model import DATA 6 | 7 | def sendMessage(manifest): 8 | data:DATA = DATA.fromJSON(manifest) 9 | 10 | flagMsg = flagMessage(data) 11 | teleMsg = data.teleMsg(flagMsg) 12 | editMsg(data.msg_id,teleMsg) 13 | 14 | if not isDebug(): 15 | manifest = readJson(MANIFEST_FILE_NAME) 16 | if manifest['sts']: 17 | if int(getEnv("SHARE_TO_TELE")): 18 | sendMessage(manifest) 19 | else: 20 | printSubCmd("share to tele is false, so skipping reporting telegram","!") 21 | else: 22 | printSubCmd("Status is false, so skipping reporting telegram","!") -------------------------------------------------------------------------------- /src/git.py: -------------------------------------------------------------------------------- 1 | import os 2 | from basics import readJson 3 | from constants import MANIFEST_FILE_NAME 4 | from enums import Platform 5 | from model import DATA 6 | 7 | def run(cmd): 8 | os.system(cmd) 9 | 10 | manifest = readJson(MANIFEST_FILE_NAME) 11 | s = manifest['sts'] 12 | if s: 13 | data:DATA = DATA.fromJSON(manifest) 14 | vername = data.vername 15 | vername = "web: "+vername[:5] if data.platform == Platform.WEB else vername 16 | commitMsg = f"🤖: {vername}" 17 | 18 | MAIL_ID = "41898282+github-actions[bot]@users.noreply.github.com" 19 | NAME = "github-actions[bot]" 20 | run(f'git config --global user.email "{MAIL_ID}"') 21 | run(f'git config --global user.name "{NAME}"') 22 | run(f'git pull') 23 | run(f'git add .') 24 | run(f'git commit -m "{commitMsg}"') 25 | run(f'git push') 26 | else: 27 | print(f'Status : {s}') -------------------------------------------------------------------------------- /src/basics.py: -------------------------------------------------------------------------------- 1 | import os,json 2 | from sys import exc_info 3 | from traceback import format_exception 4 | from dotenv import load_dotenv 5 | 6 | def get_exception(): 7 | etype, value, tb = exc_info() 8 | info, error = format_exception(etype, value, tb)[-2:] 9 | return f'Exception in: {info}: {error}' 10 | 11 | def printCmd(msg): 12 | print(f"*************** {msg.upper()} *************") 13 | 14 | def printSubCmd(msg,sym="-"): 15 | print(f"{sym} {msg.capitalize()}") 16 | 17 | def printJson(data): 18 | print(json.dumps(data,indent=4)) 19 | 20 | def readFile(filename): 21 | printSubCmd(f"Reading = {filename}") 22 | f = open(filename,'r',encoding='utf-8') 23 | d = f.read() 24 | f.close() 25 | return d 26 | 27 | def writeFile(fileName,data): 28 | printSubCmd(f"Writing = {fileName}") 29 | f = open(fileName, 'w') 30 | f.write(data) 31 | f.close() 32 | 33 | def writeJson(fileName,data): 34 | printSubCmd(f"Writing = {fileName}") 35 | f = open(fileName, 'w') 36 | json.dump(data,f,indent=4) 37 | f.close() 38 | 39 | def readJson(filename): 40 | printSubCmd(f"Reading = {filename}") 41 | f = open(filename,'r') 42 | d = json.load(f) 43 | f.close() 44 | return d 45 | 46 | def printLine(): 47 | return "*--------------*" -------------------------------------------------------------------------------- /src/processX.py: -------------------------------------------------------------------------------- 1 | import requests, json, os, shutil, sys 2 | from tqdm import tqdm 3 | from pprint import pprint 4 | from constants import ( 5 | DUMMY_FOLDER, 6 | MAIN_FOLDER, 7 | NEW_FILE_NAME, 8 | OLD_FILE_NAME, 9 | MANIFEST_FILE_NAME, getEnv,getRootDir 10 | ) 11 | from basics import writeJson, readJson, get_exception 12 | from enums import Source, Platform, ReleaseType, Application 13 | from common import downloader,downloadAndroid,pairipDetector,unzipper 14 | from model import DATA 15 | from appData import webfeatureSwitches 16 | 17 | def process(data:DATA,flagFileName:str): 18 | sts = False 19 | try: 20 | isPairip = data.pairip 21 | app = data.app 22 | typ = data.typ 23 | platform = data.platform 24 | source = data.src 25 | down_link = data.link 26 | 27 | 28 | if platform == Platform.WEB: 29 | vername = data.vername 30 | sha, hash = vername.split("::") 31 | fs = webfeatureSwitches(hash) 32 | # create the flags in existing flags 33 | writeJson(flagFileName, fs) 34 | writeJson(NEW_FILE_NAME,fs) 35 | sts = True 36 | 37 | elif platform == Platform.IOS: 38 | downloader(down_link) 39 | s = unzipper(platform) 40 | 41 | if not s: 42 | raise Exception("Error unzipping") 43 | sts = True 44 | 45 | elif platform == Platform.ANDROID: 46 | downloadAndroid(down_link,source) 47 | 48 | s = unzipper(platform) 49 | if not s: 50 | raise Exception("Error unzipping") 51 | 52 | isPairip = pairipDetector() 53 | sts = True 54 | 55 | data.pairip = isPairip 56 | 57 | except Exception as e: 58 | print(get_exception()) 59 | 60 | d = data.toJSON() 61 | d["sts"]=sts 62 | writeJson(MANIFEST_FILE_NAME, d) 63 | return sts -------------------------------------------------------------------------------- /.github/workflows/auto.yml: -------------------------------------------------------------------------------- 1 | name: Features - Auto 2 | 3 | on: 4 | repository_dispatch: 5 | types: [update-event] 6 | 7 | jobs: 8 | download-and-extract: 9 | runs-on: ubuntu-latest 10 | env: 11 | BOT_TOKEN: ${{ secrets.BOT_TOKEN }} 12 | CHANNEL_ID: ${{ secrets.CHANNEL_ID }} 13 | CHANNEL_NAME: ${{ secrets.CHANNEL_NAME }} 14 | PIN_MSG: ${{ secrets.X_PIN_MSG }} 15 | DEBUG: ${{ secrets.DEBUG }} 16 | SHARE_TO_TELE: ${{ secrets.SHARE_TO_TELE }} 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: '3.x' 27 | 28 | 29 | - name: Install modules 30 | run: | 31 | pip install -r requirements.txt 32 | 33 | 34 | - name: Download & Extract 35 | run: | 36 | python src/main.py ${{ github.event.client_payload.app }} ${{ github.event.client_payload.platform }} ${{ github.event.client_payload.source }} ${{ github.event.client_payload.type }} ${{ github.event.client_payload.msgId }} ${{ github.event.client_payload.vername }} ${{ github.event.client_payload.vercode }} ${{ github.event.client_payload.downLink }} ${{ env.DEBUG }} 37 | 38 | - name: Add extracted file to Git 39 | id: vars 40 | run: | 41 | git fetch 42 | CHANGES=$(git diff --name-only origin/main) 43 | if [ -n "$CHANGES" ]; then 44 | echo "Changes found." 45 | python src/git.py 46 | echo "PUSH=1" >> $GITHUB_OUTPUT 47 | else 48 | echo "No changes found." 49 | echo "PUSH=0" >> $GITHUB_OUTPUT 50 | fi 51 | 52 | - name: Commit data 53 | uses: rlespinasse/git-commit-data-action@v1 54 | 55 | - name: Changes to Telegram 56 | run: | 57 | python src/sendTelegram.py ${{ env.BOT_TOKEN }} ${{ env.CHANNEL_ID }} ${{ env.CHANNEL_NAME }} ${{ env.X_PIN_MSG }} ${{ env.SHARE_TO_TELE }} ${{ env.DEBUG }} -------------------------------------------------------------------------------- /src/tele.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from basics import get_exception,printJson,printSubCmd 3 | from common import getEnv 4 | from constants import getChannelId 5 | 6 | spl_ch = ['**', '``', '[[', ']]', '((', '))', '~', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' ] 7 | def santizeText(txt): 8 | for ch in spl_ch: 9 | txt = txt.replace(ch,f'\\{ch}') 10 | return txt 11 | 12 | def sendMsg(text,tag="untitled"): 13 | printSubCmd("Telegram sending message") 14 | BOT_TOKEN = getEnv("BOT_TOKEN") 15 | channel_id = getChannelId() 16 | tele_api_send_msg = 'https://api.telegram.org/bot'+BOT_TOKEN+'/sendMessage' 17 | text = santizeText(text) 18 | cont = { 19 | "chat_id":channel_id, 20 | "text":text, 21 | "disable_web_page_preview":1, 22 | "parse_mode" : "MarkdownV2" 23 | } 24 | try: 25 | req = requests.post(tele_api_send_msg,data=cont) 26 | pkjson = req.json() 27 | if req.status_code==200: 28 | new_msg_id = pkjson['result']['message_id'] 29 | rd = new_msg_id 30 | printSubCmd(f"Telegram Uploaded: {tag}") 31 | # pinMsg(chat_id,new_msg_id,tag) 32 | else: 33 | printSubCmd("Telegram action failed","x") 34 | printJson(pkjson) 35 | rd = False 36 | return rd 37 | except Exception as e: 38 | print(get_exception()) 39 | return False 40 | 41 | def editMsg(msgId,txt): 42 | printSubCmd("Telegram editting message") 43 | BOT_TOKEN = getEnv("BOT_TOKEN") 44 | channel_id = getChannelId() 45 | tele_api_edit_msg = 'https://api.telegram.org/bot'+BOT_TOKEN+'/editMessageText' 46 | txt = santizeText(txt) 47 | cont = { 48 | "chat_id":channel_id, 49 | "message_id":msgId, 50 | "text":txt, 51 | "disable_web_page_preview":1, 52 | "parse_mode" : "MarkdownV2", 53 | } 54 | try: 55 | req = requests.post(tele_api_edit_msg,data=cont) 56 | 57 | pkjson = req.json() 58 | if req.status_code==200: 59 | new_msg_id = pkjson['result']['message_id'] 60 | printSubCmd("Telegram message editted") 61 | return new_msg_id 62 | else: 63 | printSubCmd("Telegram action failed","x") 64 | printJson(pkjson) 65 | new_msg_id = False 66 | return new_msg_id 67 | except Exception as e: 68 | print(get_exception()) 69 | return False -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os, shutil, sys 2 | from constants import isDebug, getRootDir, DUMMY_FOLDER, MAIN_FOLDER,OLD_FILE_NAME,NEW_FILE_NAME 3 | from enums import Application, ReleaseType, Platform, Source 4 | from common import writeJson, readJson, get_exception 5 | from processX import process as xProcess 6 | from model import DATA 7 | from basics import printCmd 8 | from compare import compareFlags 9 | import argparse 10 | 11 | VER = "v21.1 : rearrange arguments" 12 | 13 | def flagName(data:DATA): 14 | os.makedirs(MAIN_FOLDER,exist_ok=True) 15 | 16 | app = data.app.value 17 | platform = data.platform.value 18 | typ = data.typ.value 19 | 20 | flagFileName = f"{app}_flags_{platform}" 21 | flagFileName = flagFileName if platform == Platform.WEB.value else f"{flagFileName}_{typ}" 22 | flagFileName +=".json" 23 | return getRootDir()+"/"+flagFileName 24 | 25 | def main(data:DATA): 26 | flagFileName = flagName(data) 27 | 28 | # move existing/default flags to old flags 29 | shutil.move(flagFileName, OLD_FILE_NAME) 30 | 31 | sts = False 32 | app:Application = data.app 33 | plt:Platform = data.platform 34 | 35 | printCmd(f"processing {app.value}") 36 | if app == Application.X: 37 | if plt == Platform.IOS: 38 | # move existing/default ipad flags to old flags 39 | shutil.move(flagFileName.replace("ios","ipad"), OLD_FILE_NAME+"_2") 40 | 41 | sts = xProcess(data,flagFileName) 42 | 43 | if sts: 44 | # new flags as default flags 45 | shutil.copy(NEW_FILE_NAME, flagFileName) 46 | if plt == Platform.IOS and app == Application.X: 47 | shutil.copy(NEW_FILE_NAME+"_2",flagFileName.replace("ios","ipad")) 48 | 49 | printCmd(f"comparing flags") 50 | compareFlags() 51 | else: 52 | print("Status: False") 53 | 54 | if not isDebug(): 55 | if os.path.exists(DUMMY_FOLDER): 56 | shutil.rmtree(DUMMY_FOLDER) 57 | args = sys.argv 58 | 59 | app = args[1].lower() 60 | plt = args[2].lower() 61 | src = args[3].lower() 62 | typ = args[4].lower() 63 | msg_id = args[5] 64 | vername = args[6] 65 | vercode = args[7] 66 | down_link = args[8] 67 | 68 | app = Application(app) 69 | typ = ReleaseType(typ) 70 | platform = Platform(plt) 71 | source = Source(src.strip()) 72 | data:DATA = DATA(vername, down_link, msg_id, source, platform, typ, app,vercode) 73 | 74 | main(data) 75 | 76 | -------------------------------------------------------------------------------- /.github/workflows/manual-tele.yml: -------------------------------------------------------------------------------- 1 | name: Features - Manual (Tele) 2 | 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | appname_input: 8 | description: 'App name - X/Grok' 9 | required: true 10 | type_input: 11 | description: 'Release type - Stable/Beta/Alpha' 12 | required: true 13 | platform_input: 14 | description: 'Platform - Android/iOS/Web' 15 | required: true 16 | source_input: 17 | description: 'Source [apt(Aptoide) / apkc(ApkCombo) / web(Web)]' 18 | required: true 19 | vername_input: 20 | description: 'Enter the vername' 21 | required: true 22 | vercode_input: 23 | description: 'Enter the vercode' 24 | required: true 25 | downLink_input: 26 | description: 'Enter Download Link' 27 | required: true 28 | msg_id_input: 29 | description: 'Message ID' 30 | required: true 31 | 32 | 33 | jobs: 34 | download-and-extract: 35 | runs-on: ubuntu-latest 36 | env: 37 | BOT_TOKEN: ${{ secrets.BOT_TOKEN }} 38 | CHANNEL_ID: ${{ secrets.CHANNEL_ID }} 39 | CHANNEL_NAME: ${{ secrets.CHANNEL_NAME }} 40 | PIN_MSG: ${{ secrets.X_PIN_MSG }} 41 | DEBUG: ${{ secrets.DEBUG }} 42 | SHARE_TO_TELE: ${{ secrets.SHARE_TO_TELE }} 43 | 44 | steps: 45 | - name: Checkout code 46 | uses: actions/checkout@v2 47 | 48 | 49 | - name: Install modules 50 | run: | 51 | pip install -r requirements.txt 52 | 53 | 54 | - name: Download & Extract 55 | run: | 56 | python src/main.py ${{ github.event.inputs.appname_input }} ${{ github.event.inputs.platform_input }} ${{ github.event.inputs.source_input }} ${{ github.event.inputs.type_input }} ${{ github.event.inputs.msg_id_input }} ${{ github.event.inputs.vername_input }} ${{ github.event.inputs.vercode_input }} ${{ github.event.inputs.downLink_input }} ${{ env.DEBUG }} 57 | 58 | - name: Add extracted file to Git 59 | id: vars 60 | run: | 61 | git fetch 62 | CHANGES=$(git diff --name-only origin/main) 63 | if [ -n "$CHANGES" ]; then 64 | echo "Changes found." 65 | python src/git.py 66 | echo "PUSH=1" >> $GITHUB_OUTPUT 67 | else 68 | echo "No changes found." 69 | echo "PUSH=0" >> $GITHUB_OUTPUT 70 | fi 71 | 72 | - name: Commit data 73 | uses: rlespinasse/git-commit-data-action@v1 74 | 75 | - name: Changes to Telegram 76 | run: | 77 | python src/sendTelegram.py ${{ env.BOT_TOKEN }} ${{ env.CHANNEL_ID }} ${{ env.CHANNEL_NAME }} ${{ env.X_PIN_MSG }} ${{ env.SHARE_TO_TELE }} ${{ env.DEBUG }} -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | test.py 131 | test/ 132 | dummy/ 133 | temp/ 134 | extracted/ 135 | app.xapk 136 | tele test 2.py 137 | feature_data.json 138 | version_data.json 139 | main_old.py 140 | manifest.json 141 | ./apkcombo.py 142 | TODO.txt 143 | *.ipa 144 | x.ipa 145 | deploy.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # X (Twitter) Flags 3 | 4 | A repo that keeps track of feature flags from different builds of 𝕏 (Twitter) and update it to my [Telegram channel](https://telegram.me/twitter_app_flags) 5 | 6 | 7 | ## Latest Feature Flags 8 | 9 | * **Android** 10 | - [Alpha Flags](https://github.com/Swakshan/X-Flags/blob/main/x_flags_android_alpha.json) 11 | - [Beta Flags](https://github.com/Swakshan/X-Flags/blob/main/x_flags_android_beta.json) 12 | - [Stable Flags](https://github.com/Swakshan/X-Flags/blob/main/x_flags_android_stable.json) 13 | 14 | * **iOS** 15 | - [Stable Flags: iPhone](https://github.com/Swakshan/X-Flags/blob/main/x_flags_ios_stable.json) 16 | - [Stable Flags: iPad](https://github.com/Swakshan/X-Flags/blob/main/x_flags_ipad_stable.json) 17 | 18 | * **Web** 19 | - [Web Flags](https://github.com/Swakshan/X-Flags/blob/main/x_flags_web.json) 20 | 21 | 22 | ## FAQ 23 | 24 | #### How to enable the flags ? 25 | 26 | The flags are present in **"feature_switch_manifest"** file, located at **"res/raw/"** inside the apk/apks file. We can modify the file and re-build the apk. 27 | 28 | #### Is there a simplier way ? 29 | 30 | We can use xposed/lsposed module like [TwiFucker](https://github.com/Dr-TSNG/TwiFucker). 31 | 32 | #### How to get notified when there is a build released ? 33 | 34 | Check out my [Telegram Channel](https://telegram.me/twitter_app_flags) to keep track of new releases. 35 | 36 | #### Is it safe to modify the flags ? 37 | 38 | Feature flags are used by twitter employees to test new features before they are released to public. **Use it at your own risk.** 39 | 40 | #### Is there a way to modify flags in web version ? 41 | 42 | Yes. Check out [x-feature-flags](https://chrome.google.com/webstore/detail/secret-twitter-features/phioeneleonlckednejcmajbkmhhiepm) by [Yaroslav](https://x.com/512x512/) & [Lohansimpson](https://x.com/lohansimpson/) 43 | 44 | #### Is there way for iOS users? 45 | 46 | Flags are present inside the ipa file in json format. Extract the ipa file Modify the flags, rezip the ipa file and install it. 47 | In case of non-jailbroken devices you can use [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/) etc 48 | 49 | ## My Other Works 50 | 51 | [![](https://img.shields.io/badge/Telegram-Instagram%20Update%20Tracker-111?label=&logo=telegram&logoWidth=30)](https://telegram.me/instabuilds) 52 | 53 | [![](https://img.shields.io/badge/Telegram-WhatsApp%20Update%20Tracker-111?label=&logo=telegram&logoWidth=30)](https://telegram.me/whatsappbuilds) 54 | 55 | 56 | ## My Socials 57 | 58 | [![Instagram](https://img.shields.io/badge/Instagram-%23E4405F.svg?style=for-the-badge&logo=Instagram&logoColor=white)](https://instagram.com/therealswak/) 59 | 60 | [![𝕏 (twitter)](https://img.shields.io/badge/%20X%20(Twitter)-000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/swak_12) 61 | 62 | -------------------------------------------------------------------------------- /src/constants.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | 3 | from pytz import timezone 4 | import os 5 | from enums import Application 6 | from user_agent import generate_user_agent 7 | 8 | load_dotenv() 9 | 10 | 11 | def getRootDir(): 12 | current_dir = os.path.dirname(os.path.abspath(__file__)) 13 | return os.path.dirname(current_dir) 14 | 15 | APP_NAME = "Twitter" 16 | PKG_NAME = 'com.twitter.android' 17 | DUMMY_FOLDER = getRootDir() + '/dummy/' 18 | ZIP_FILE = DUMMY_FOLDER + 'app.zip' 19 | EXTRACT_FOLDER = DUMMY_FOLDER + 'Extracted/' 20 | MAIN_FOLDER = DUMMY_FOLDER + 'main/' 21 | OLD_FILE_NAME = MAIN_FOLDER + 'old_feature_data.json' 22 | NEW_FILE_NAME = MAIN_FOLDER + 'new_feature_data.json' 23 | CHANGES_FILE_NAME = MAIN_FOLDER + 'changes.json' 24 | MANIFEST_FILE_NAME = "manifest.json" 25 | NEW_FLAG_LIMIT = 10 26 | USERNAME = "Swakshan" 27 | REPO_NAME = "X-Flags" 28 | 29 | 30 | def getEnv(data): 31 | try: 32 | return os.environ.get(data) 33 | except Exception as e: 34 | print(str(e)) 35 | return "0" 36 | 37 | 38 | def isDebug(): 39 | return int(getEnv("DEBUG")) 40 | 41 | 42 | def getChannelId(): 43 | return getEnv("CHANNEL_ID_TEST") if isDebug() else getEnv("CHANNEL_ID") 44 | 45 | 46 | def getChannelLink(): 47 | CHANNEL_NAME = getEnv("CHANNEL_NAME") 48 | return f"t.me/{CHANNEL_NAME}" 49 | 50 | 51 | def getPinMsgID(app: Application): 52 | if app == Application.X: 53 | return getEnv("X_PIN_MSG") 54 | 55 | if app == Application.GROK: 56 | return getEnv("GROK_PIN_MSG") 57 | 58 | 59 | def getPackageName(app: Application): 60 | if app == Application.X: 61 | return "com.twitter.android" 62 | 63 | if app == Application.GROK: 64 | return "ai.x.grok" 65 | 66 | 67 | def getAPKMCode(app: Application): 68 | if app == Application.X: 69 | return "x-corp/twitter" 70 | 71 | if app == Application.GROK: 72 | return "xai/grok" 73 | 74 | def getAPKMSlug(app: Application): 75 | if app == Application.X: 76 | return "x" 77 | 78 | if app == Application.GROK: 79 | return "grok-ai-assistant" 80 | 81 | 82 | def getUptoCode(app: Application): 83 | if app == Application.X: 84 | return "16792" 85 | 86 | if app == Application.GROK: 87 | return "1000312412" 88 | 89 | 90 | def getAppleStoreCode(app: Application): 91 | if app == Application.X: 92 | return "id333903271" 93 | 94 | if app == Application.GROK: 95 | return "id6670324846" 96 | 97 | 98 | def headers(): 99 | return { 100 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 101 | "accept-language": "en-GB,en;q=0.9", 102 | "cache-control": "no-cache", 103 | "pragma": "no-cache", 104 | "priority": "u=0, i", 105 | "sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"', 106 | "sec-ch-ua-mobile": "?0", 107 | "sec-ch-ua-platform": '"Windows"', 108 | "sec-fetch-dest": "document", 109 | "sec-fetch-mode": "navigate", 110 | "sec-fetch-site": "none", 111 | "sec-fetch-user": "?1", 112 | "upgrade-insecure-requests": "1", 113 | "user-agent": generate_user_agent() 114 | } 115 | -------------------------------------------------------------------------------- /src/appData.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from bs4 import BeautifulSoup as bs 4 | from pprint import pprint 5 | from urllib.parse import unquote 6 | from datetime import datetime 7 | from constants import headers 8 | 9 | hdr = headers() 10 | proxyUrl = "https://translate.google.com/website?sl=ta&tl=en&hl=en&client=webapp&u=" 11 | 12 | def beautifulSoup(url,proxy=1): 13 | if proxy: 14 | url = proxyUrl+url 15 | #print(url) 16 | req = requests.get(url, headers=hdr) 17 | if req.status_code != 200: 18 | raise Exception("page not found:\nURL: "+url) 19 | 20 | txt = req.text 21 | return bs(txt, 'html.parser') 22 | 23 | def apkCombo(url): 24 | pS:bs = beautifulSoup(url) 25 | 26 | file_list = pS.find('ul', attrs={'class': 'file-list'}) 27 | details = file_list.find('li') 28 | link = details.find('a')['href'] 29 | if "r2?u=" in link: 30 | s = link.find("r2?u=")+len("r2?u=") 31 | link = link[s:].replace("&_x_tr_sl=ta&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp","") 32 | else: 33 | link+='&fp=howareyou123&ip=0.0.0.0.0' 34 | link = link.replace(proxyUrl, '') 35 | return unquote(link) 36 | 37 | 38 | def apkM(url): 39 | pS:bs = beautifulSoup(url) 40 | downloadBtn = pS.find("a",{"class":"downloadButton"}) 41 | downloadPage = downloadBtn['href'] 42 | # downloadBtnText = downloadBtn.text.strip().lower() 43 | # is_bundle = True if "apk bundle" in downloadBtnText else False 44 | pS:bs = beautifulSoup(downloadPage,0) 45 | 46 | downloadLink = pS.find("a",{'id':'download-link'})['href'] 47 | downloadLink = downloadLink.replace("www-apkmirror-com.translate.goog","www.apkmirror.com").replace("&_x_tr_sl=ta&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp","") 48 | return downloadLink 49 | 50 | 51 | def xManifestSwitches(hash): 52 | FS_URL = f"https://abs.twimg.com/responsive-web/client-web/feature-switch-manifest.{hash}.js" 53 | req = requests.get(FS_URL,headers=hdr) 54 | res = req.text 55 | res = res[res.find("{"):res.find("}}};")] 56 | 57 | res = res.replace("!0","true\n").replace("!1","false\n") 58 | res = res.replace("},",'},"').replace(':{value:','":{"value":').replace(':{name:','":{"name":').replace(',type:',',"type":').replace(',defaultValue:',',"defaultValue":') 59 | res = res.replace("feature_set_token:",'"feature_set_token":').replace(',config:',',"config":').replace(',"debug:',',"debug":').replace(',enumeration_values',',"enumeration_values"') 60 | res = res.replace('"":','":').replace(":.",":0.") 61 | res = res+"}}}" 62 | 63 | return json.loads(res) 64 | 65 | def xWebFlags(): 66 | link = "https://x.com" 67 | url = f"{proxyUrl}{link}" 68 | 69 | req = requests.get(url, headers=hdr) 70 | res = req.text 71 | 72 | sHint = "window.__INITIAL_STATE__=" 73 | eHint = "window.__META_DATA__=" 74 | s = res.find(sHint) + len(sHint) 75 | e = res[s:].find(eHint) + s - 1 76 | fd = res[s:e] 77 | flagData = json.loads(fd) 78 | featureSwitch = flagData['featureSwitch'] 79 | token = featureSwitch["featureSetToken"] 80 | 81 | defaultConfig = featureSwitch['defaultConfig'] 82 | userFlag = featureSwitch['user']['config'] 83 | 84 | flags = {**defaultConfig, **userFlag} 85 | return {"feature_set_token":token,"config":flags} 86 | 87 | def webfeatureSwitches(hash): 88 | xFlags = xWebFlags() 89 | manifestFlags = xManifestSwitches(hash) 90 | flags = {**xFlags['config'],**manifestFlags['config']} 91 | token = xFlags['feature_set_token'] 92 | 93 | return {"feature_set_token":token,"config":flags,"debug":manifestFlags['debug']} 94 | -------------------------------------------------------------------------------- /src/common.py: -------------------------------------------------------------------------------- 1 | from enums import Source, Platform, ReleaseType 2 | 3 | from constants import (ZIP_FILE, EXTRACT_FOLDER, PKG_NAME, NEW_FILE_NAME, 4 | headers, getEnv) 5 | from basics import get_exception, readJson, writeJson,printSubCmd 6 | import requests 7 | import zipfile 8 | import os 9 | import shutil 10 | from apkutils import APK 11 | from tqdm import tqdm 12 | from appData import apkM,apkCombo 13 | import gdown 14 | 15 | def downloader(url, filePath=ZIP_FILE, isJson=False): 16 | if os.path.exists(filePath): 17 | printSubCmd(f"{filePath} exists" , "!") 18 | return True 19 | 20 | if "drive.google.com" in url: 21 | gdown.download(url, ZIP_FILE, quiet=False,fuzzy=True) 22 | return True 23 | 24 | response = requests.get(url, stream=True, headers=headers()) 25 | if not isJson: 26 | total_size_in_bytes = int(response.headers.get('content-length', 0)) 27 | block_size = 1024 # 1 Kibibyte 28 | progress_bar = tqdm(total=total_size_in_bytes, 29 | unit='iB', 30 | unit_scale=True) 31 | 32 | with open(filePath, 'wb') as file: 33 | for data in response.iter_content(block_size): 34 | progress_bar.update(len(data)) 35 | file.write(data) 36 | progress_bar.close() 37 | else: 38 | js = response.json() 39 | writeJson(filePath, js) 40 | 41 | def downloadAndroid(url, source): 42 | down_link = None 43 | if source == Source.APKM: 44 | down_link = apkM(url) 45 | 46 | elif source == Source.APKC: 47 | down_link = apkCombo(url) 48 | 49 | elif source == Source.APT or source == Source.APKP or Source.MAN: 50 | down_link = url 51 | 52 | else: 53 | raise Exception(f"Error: Source not found + {source.value}") 54 | printSubCmd("Downloading " + down_link + " from: " + source.value) 55 | downloader(url=down_link) 56 | 57 | def unzipper(platform): 58 | def extract(src, new_name): 59 | zip_obj.extract(src, path=EXTRACT_FOLDER) 60 | fg = readJson(EXTRACT_FOLDER + src) 61 | writeJson(new_name, fg) 62 | return True 63 | feature_file = False 64 | zip_obj = zipfile.ZipFile(ZIP_FILE, "r") 65 | file_list = zip_obj.namelist() 66 | 67 | if platform == Platform.ANDROID: 68 | FLAG_FOLDER = "res/raw" 69 | FLAG_FILE = f"{FLAG_FOLDER}/feature_switch_manifest" 70 | apk_name_1 = f"{PKG_NAME}.apk" 71 | apk_name_2 = f"base.apk" 72 | 73 | if apk_name_1 in file_list or apk_name_2 in file_list: 74 | apk_name = apk_name_2 if apk_name_2 in file_list else apk_name_1 75 | # Needed to change the name of the apk for pairip detection 76 | temp_path = zip_obj.extract(apk_name, path=EXTRACT_FOLDER) 77 | new_path = f"{EXTRACT_FOLDER}app.apk" 78 | shutil.move(temp_path, new_path) 79 | zip_obj = zipfile.ZipFile(new_path, "r") 80 | feature_file = True 81 | 82 | elif FLAG_FILE in file_list: 83 | feature_file = True 84 | 85 | if feature_file: 86 | return extract(FLAG_FILE, NEW_FILE_NAME) 87 | 88 | elif platform == Platform.IOS: 89 | FLAG_FOLDER = "Payload/Twitter.app" 90 | f1 = False 91 | f2 = False 92 | 93 | FLAG_FILE = f"{FLAG_FOLDER}/fs_embedded_defaults_production.json" 94 | if FLAG_FILE in file_list: 95 | f1 = extract(FLAG_FILE, NEW_FILE_NAME) 96 | 97 | FLAG_FILE = f"{FLAG_FOLDER}/fs_embedded_defaults_ipad_production.json" 98 | if FLAG_FILE in file_list: 99 | f2 = extract(FLAG_FILE, NEW_FILE_NAME+"_2") 100 | 101 | return f1 & f2 102 | 103 | return False 104 | 105 | def pairipDetector(): 106 | printSubCmd("Detecting Paririp") 107 | apkPath = ZIP_FILE 108 | xapkPath = f"{EXTRACT_FOLDER}app.apk" 109 | if os.path.exists(xapkPath): 110 | apkPath = xapkPath 111 | 112 | apk = APK.from_file(apkPath) 113 | manifest = apk.get_manifest() 114 | 115 | if "com.pairip.application.Application" in manifest: 116 | return True 117 | return False -------------------------------------------------------------------------------- /src/model.py: -------------------------------------------------------------------------------- 1 | from constants import ( 2 | getChannelLink, 3 | getAPKMCode, 4 | getAPKMSlug, 5 | getPackageName, 6 | getPinMsgID, 7 | MANIFEST_FILE_NAME, 8 | ) 9 | from enums import * 10 | class DATA: 11 | def __init__( 12 | self, vername, link, msg_id, src, platform, typ, app, vercode,isPairip=False, 13 | ) -> None: 14 | self.vername = vername 15 | self.link = link 16 | self.msg_id = int(msg_id) 17 | self.src = src 18 | self.platform = platform 19 | self.pairip = False 20 | self.typ = typ 21 | self.app = app 22 | self.pairip = isPairip 23 | self.emoji = "⚠️" 24 | self.changeLogs = "" # grok and IOS has 25 | if app == Application.X: 26 | self.emoji = "𝕏" 27 | elif app == Application.GROK: 28 | self.emoji = "🤖" 29 | else: 30 | print("WTF: app name") 31 | self.vercode = vercode 32 | 33 | @classmethod 34 | def fromJSON(self, json_map: dict): 35 | src = Source(json_map["src"]) 36 | app = Application(json_map["app"]) 37 | typ = ReleaseType(json_map["type"]) 38 | plt = Platform(json_map["platform"]) 39 | return self( 40 | json_map["vername"], 41 | json_map["link"], 42 | json_map["msg_id"], 43 | src, 44 | plt, 45 | typ, 46 | app, 47 | json_map["vercode"], 48 | json_map["pairip"], 49 | ) 50 | def toJSON(self): 51 | rd = {} 52 | rd["msg_id"] = self.msg_id 53 | rd["src"] = self.src.value 54 | rd["vername"] = self.vername 55 | rd["vercode"] = self.vercode 56 | rd["type"] = self.typ.value 57 | rd["platform"] = self.platform.value 58 | rd["app"] = self.app.value 59 | rd["link"] = self.link 60 | rd["pairip"] = self.pairip 61 | return rd 62 | 63 | def teleMsg(self,flagData): 64 | global linkRow, linkCount 65 | linkRow = "" 66 | linkCount = 0 67 | def linkRowFormer(name, link): 68 | global linkRow, linkCount 69 | tempLink = f"[{name}]({link})" 70 | linkRow += tempLink 71 | if linkCount % 2 == 0: 72 | linkRow += " | " 73 | else: 74 | linkRow += "\n" 75 | linkCount += 1 76 | pin_link = f"{getChannelLink()}/{getPinMsgID(self.app)}" 77 | appName = self.app.value 78 | platform = self.platform.value 79 | typ = self.typ.value 80 | emoji = self.emoji 81 | link = self.link 82 | vername = self.vername 83 | rd = f"{emoji} *{appName.upper()} Update* {emoji}\n" 84 | platformText = platform.title() 85 | if platform == Platform.IOS.value: 86 | platformText = "iOS" 87 | rd = f"{rd}\n_Platform:_ *{platformText}*" 88 | if platform == Platform.WEB.value: 89 | rd = f"{rd}\n_Hash_:\n`{vername.split("::")[0]}`" 90 | linkRowFormer("Web Link", link) 91 | linkRow = linkRow[:-3] + "\n" 92 | else: 93 | rd = f"{rd}\n_Type:_ *{typ.upper()}*" 94 | rd = f"{rd}\n_Version:_ `{vername}`" 95 | rd = f"{rd}\n_Vercode:_ `{self.vercode}`" 96 | 97 | if platform == Platform.ANDROID.value: 98 | if self.app == Application.X: 99 | prStr = ( 100 | "🚫App contains PairipLib🚫" 101 | if self.pairip 102 | else "❇️App does not contain PairipLib❇️" 103 | ) 104 | rd = f"{rd}\n\n{prStr}" 105 | 106 | pkgName = getPackageName(self.app) 107 | apkmCode = getAPKMCode(self.app) 108 | apkmSlug = getAPKMSlug(self.app) 109 | 110 | ps_link = "https://play.google.com/store/apps/details?id=" + pkgName 111 | apkp_link = f"https://d.apkpure.com/b/XAPK/{pkgName}?versionCode={self.vercode}" 112 | apkm_vername = vername.replace(".", "-") 113 | apkm_verSlug = f"{apkmSlug}-{apkm_vername}" 114 | apkm_link = f"https://www.apkmirror.com/apk/{apkmCode}/{apkm_verSlug}-release/{apkm_verSlug}-android-apk-download/" 115 | 116 | if "aptoide" in link: 117 | linkRowFormer("Aptoide", link) 118 | linkRowFormer("Play Store", ps_link) 119 | linkRowFormer("ApkMirror", apkm_link) 120 | linkRowFormer("APKPure", apkp_link) 121 | elif platform == Platform.IOS.value: 122 | linkRowFormer("Apple Store", link) 123 | linkRow = linkRow[:-3] + "\n" 124 | linkRow = linkRow + "\n" if linkRow[-2] == "|" else linkRow 125 | if len(self.changeLogs): 126 | rd = f"{rd}\n\n__Changelogs:__\n{self.changeLogs}" 127 | rd = f"{rd}\n\n{linkRow}----------------------------\n[Other {appName.title()} Versions]({pin_link})" 128 | rd = f"{rd}\n----------------------------\n{flagData}" 129 | rd = f"{rd}\n\n#{appName.capitalize()} #{typ} #{platform}" 130 | return rd 131 | def __repr__(self): 132 | return str(self.toJSON()) -------------------------------------------------------------------------------- /src/compare.py: -------------------------------------------------------------------------------- 1 | from constants import OLD_FILE_NAME,NEW_FILE_NAME,CHANGES_FILE_NAME,MANIFEST_FILE_NAME,USERNAME,REPO_NAME,NEW_FLAG_LIMIT,getEnv 2 | from enums import Platform,Application 3 | from model import DATA 4 | from basics import writeJson,readJson,printSubCmd 5 | import os 6 | 7 | def compareFlags(): 8 | def getValue(config:dict): 9 | if "type" in config and config['type'] == "experiment": 10 | return "experiment" 11 | if "value" in config: 12 | return config['value'] 13 | if "defaultValue" in config: 14 | return config['defaultValue'] 15 | return "" 16 | 17 | def createChangesFile(data:DATA,new_file_name,old_file_name,changes_file_name): 18 | old_file = readJson(old_file_name) 19 | new_file = readJson(new_file_name) 20 | 21 | new_flags = {} 22 | old_flags = {} 23 | new_debug_flags = {} 24 | old_debug_flags = {} 25 | 26 | if data.app == Application.X: 27 | if data.platform == Platform.WEB: 28 | new_flags = new_file['config'] 29 | old_flags = old_file['config'] 30 | 31 | new_debug_flags = list(new_file['debug'].keys()) 32 | old_debug_flags = list(old_file['debug'].keys()) 33 | else: 34 | new_flags = new_file['default']['config'] 35 | old_flags = old_file['default']['config'] 36 | if data.platform == Platform.ANDROID: 37 | new_debug_flags = new_file['experiment_names'] 38 | old_debug_flags = old_file['experiment_names'] 39 | 40 | 41 | old_flags_copy = old_flags.copy() 42 | old_debug_flags_copy = old_debug_flags.copy() 43 | 44 | FLAGS = {'added':{},'updated':0,'removed':0} 45 | for flag in new_flags: 46 | flag_value = getValue(new_flags[flag]) 47 | 48 | if flag not in old_flags: # If the flag is present in new but not in old. 49 | FLAGS['added'][flag] = type(flag_value).__name__ 50 | 51 | else: # If the flag is present in old check if removed or updated. 52 | if flag_value != getValue(old_flags[flag]): # If the flag value is updated. 53 | FLAGS['updated']+=1 54 | old_flags_copy.pop(flag) # Remove that flag from old flags to calculate removed flags 55 | 56 | FLAGS['removed'] = len(old_flags_copy) 57 | 58 | DEBUG_FLAGS = {'added':[],'removed':0} 59 | for flag in new_debug_flags: 60 | if flag not in old_debug_flags: 61 | DEBUG_FLAGS['added'].append(flag) 62 | else: 63 | old_debug_flags_copy.remove(flag) 64 | DEBUG_FLAGS['removed'] = len(old_debug_flags_copy) 65 | 66 | CHANGES = {'flags':FLAGS,'debug_flags':DEBUG_FLAGS} 67 | 68 | writeJson(changes_file_name,CHANGES) 69 | 70 | manifest = readJson(MANIFEST_FILE_NAME) 71 | if not manifest['sts']: 72 | raise Exception("Status is false so skipping change compare") 73 | 74 | data:DATA = DATA.fromJSON(manifest) 75 | 76 | createChangesFile(data,NEW_FILE_NAME,OLD_FILE_NAME,CHANGES_FILE_NAME) 77 | 78 | ipadFileNameExt = "_2" 79 | if os.path.exists(OLD_FILE_NAME+ipadFileNameExt): 80 | createChangesFile(data,NEW_FILE_NAME+ipadFileNameExt,OLD_FILE_NAME+ipadFileNameExt,CHANGES_FILE_NAME+ipadFileNameExt) 81 | 82 | 83 | 84 | def commitLinkFormat(flag_data,include_added=False): 85 | def countFormat(count, ns="Flags"): 86 | if not count: 87 | return False 88 | 89 | f = ns if count > 1 else ns[:-1] 90 | return f"{count} {f}" 91 | 92 | # include added flags for X ipad 93 | new_flag_limit = -1 if include_added else NEW_FLAG_LIMIT 94 | 95 | msg = "" 96 | for key in flag_data: 97 | flag_det = flag_data[key] 98 | ns = key.title().replace("_", " ") 99 | for func in flag_det: 100 | c = 0 101 | if func == "added": 102 | c = len(flag_det['added']) 103 | if c < new_flag_limit : 104 | continue 105 | else: 106 | c = flag_det[func] 107 | 108 | fStr = countFormat(c, ns) 109 | if fStr: 110 | msg = f"{msg} and {fStr} {func.title()}" 111 | msg = msg[5:] if len(msg) else "Repo Link" 112 | return msg 113 | 114 | 115 | def flagMessage(data:DATA): 116 | printSubCmd("forming Telegram flag message") 117 | SHA = getEnv("GIT_COMMIT_SHA") 118 | flag_details = readJson(CHANGES_FILE_NAME) 119 | 120 | l = "--------------" 121 | nf = "" 122 | df = "" 123 | flag_data = flag_details["flags"] 124 | added_flags = flag_data["added"].items() 125 | new_flags = dict(list(added_flags)[:NEW_FLAG_LIMIT]) 126 | for f in new_flags: 127 | ty = new_flags[f] 128 | nf = f"• `{f}` :{ty}\n{nf}" 129 | nfC = len(new_flags) 130 | 131 | debug_flag_data = flag_details["debug_flags"] 132 | debug_added_flags = debug_flag_data["added"] 133 | debug_flags = debug_added_flags[:NEW_FLAG_LIMIT] 134 | for f in debug_flags: 135 | df = f"• `{f}`\n{df}" 136 | dfC = len(debug_flags) 137 | 138 | commit_link = f"https://github.com/{USERNAME}/{REPO_NAME}/commit/{SHA}?diff=split" 139 | commit_link_str = commitLinkFormat(flag_details) 140 | rd = "" 141 | if nfC: 142 | rd = f"{rd}\n__New Flags__" 143 | rd = f"{rd}\n{nf}" 144 | rd = f"{rd}\nAnd more..." if len(added_flags) > NEW_FLAG_LIMIT else rd 145 | rd = f"{rd}\n{l}" 146 | if dfC: 147 | rd = f"{rd}\n__New Debug Flags__" 148 | rd = f"{rd}\n{df}" 149 | rd = f"{rd}\nAnd more..." if len(debug_added_flags) > NEW_FLAG_LIMIT else rd 150 | rd = f"{rd}\n{l}" 151 | 152 | elif not nfC and not dfC: 153 | rd = f"{rd}\nNo New Flags\n{l}" 154 | 155 | if data.platform == Platform.IOS and data.app == Application.X: 156 | rd = f"{rd}\n__iPhone__:\n[{commit_link_str}]({commit_link})" 157 | 158 | flag_details = readJson(CHANGES_FILE_NAME+"_2") 159 | commit_link_str = commitLinkFormat(flag_details,True) 160 | rd = f"{rd}\n__iPad__:\n[{commit_link_str}]({commit_link})" 161 | else: 162 | rd = f"{rd}\n[{commit_link_str}]({commit_link})" 163 | return rd --------------------------------------------------------------------------------