Repository: lluni/twitter-apk Branch: master Commit: 4d468482adcd Files: 16 Total size: 20.8 KB Directory structure: gitextract_pj8qm7k7/ ├── .envrc ├── .github/ │ └── workflows/ │ ├── build.yaml │ ├── daily.yaml │ └── manual.yaml ├── .gitignore ├── README.md ├── apkmirror.py ├── build_variants.py ├── constants.py ├── download_bins.py ├── flake.nix ├── github.py ├── ks.keystore ├── main.py ├── pyproject.toml └── utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .envrc ================================================ use flake ================================================ FILE: .github/workflows/build.yaml ================================================ name: Release on: workflow_dispatch: permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up java uses: actions/setup-java@v4 with: distribution: 'zulu' # See 'Supported distributions' for available options java-version: '17' - name: Install uv uses: astral-sh/setup-uv@v7 with: python-version: "3.13" enable-cache: true - name: Install dependencies run: uv sync --frozen - name: Try building env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TG_TOKEN: ${{ secrets.TG_TOKEN }} TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} TG_THREAD_ID: ${{ secrets.TG_THREAD_ID }} run: | mkdir -p bins uv run main.py ================================================ FILE: .github/workflows/daily.yaml ================================================ name: Release - Daily on: schedule: - cron: '0 0 * * *' # daily at 00:00 UTC workflow_dispatch: permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up java uses: actions/setup-java@v4 with: distribution: 'zulu' # See 'Supported distributions' for available options java-version: '17' - name: Install uv uses: astral-sh/setup-uv@v7 with: python-version: "3.13" enable-cache: true - name: Install dependencies run: uv sync --frozen - name: Try building env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TG_TOKEN: ${{ secrets.TG_TOKEN }} TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} TG_THREAD_ID: ${{ secrets.TG_THREAD_ID }} run: | mkdir -p bins uv run main.py ================================================ FILE: .github/workflows/manual.yaml ================================================ name: Release - manual on: workflow_dispatch: inputs: version_input: description: 'Enter the version' required: true permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up java uses: actions/setup-java@v4 with: distribution: 'zulu' # See 'Supported distributions' for available options java-version: '17' - name: Install uv uses: astral-sh/setup-uv@v7 with: python-version: "3.13" enable-cache: true - name: Install dependencies run: uv sync --frozen - name: Try building env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TG_TOKEN: ${{ secrets.TG_TOKEN }} TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} TG_THREAD_ID: ${{ secrets.TG_THREAD_ID }} run: | mkdir -p bins uv run main.py --m 1 --v ${{ github.event.inputs.version_input }} ================================================ FILE: .gitignore ================================================ .venv __pycache__ test.py .direnv ================================================ FILE: README.md ================================================ Apk builds of [piko](https://github.com/crimera/piko) patches # Credits - [revanced](https://github.com/ReVanced) - [@REAndroid's APKEditor](https://github.com/REAndroid/APKEditor) - Used in merging split apks - [j-hc](https://github.com/j-hc) - Project is inspired by j-hc's revanced builder template. ================================================ FILE: apkmirror.py ================================================ from dataclasses import dataclass from typing import cast from bs4 import BeautifulSoup, Tag from utils import download, get_scraper @dataclass class Version: version: str link: str @dataclass class Variant: is_bundle: bool link: str architecture: str @dataclass class App: name: str link: str class FailedToFindElement(Exception): def __init__(self, message=None) -> None: self.message = ( f"Failed to find element{' ' + message if message is not None else ''}" # noqa: E501 ) super().__init__(self.message) class FailedToFetch(Exception): def __init__(self, url=None) -> None: self.message = f"Failed to fetch{' ' + url if url is not None else ''}" # noqa: E501 super().__init__(self.message) def get_versions(url: str) -> list[Version]: """Get the latest version of the app from the given apkmirror url""" response = get_scraper().get(url) if response.status_code != 200: raise FailedToFetch(f"{url}: {response.status_code}") bs4 = BeautifulSoup(response.text, "html.parser") versions = bs4.find("div", attrs={"class": "listWidget"}) out: list[Version] = [] if versions is not None: for versionRow in cast(Tag, versions).findChildren("div", recursive=False)[1:]: if versionRow is None: print(f"{versionRow} is None") continue version = versionRow.find("span", {"class": "infoSlide-value"}) if version is None: continue version = version.string.strip() link = f"https://www.apkmirror.com/{versionRow.find('a')['href']}" out.append(Version(version=version, link=link)) return out def download_apk(variant: Variant, path: str = "big_file.apkm"): """Download apk from the variant link""" url = variant.link response = get_scraper().get(url) if response.status_code != 200: raise FailedToFetch(url) response_body = BeautifulSoup(response.content, "html.parser") downloadButton = response_body.find("a", {"class": "downloadButton"}) if downloadButton is None: raise FailedToFindElement("Download button") download_page_link = ( f"https://www.apkmirror.com/{cast(Tag, downloadButton).attrs['href']}" ) download_page = get_scraper().get(download_page_link) if response.status_code != 200: raise FailedToFetch(download_page_link) download_page_body = BeautifulSoup(download_page.content, "html.parser") direct_link = download_page_body.find("a", {"rel": "nofollow"}) if direct_link is None: raise FailedToFindElement("download link") direct_link_href = cast(Tag, direct_link).attrs["href"] direct_link_url = f"https://www.apkmirror.com/{direct_link_href}" print(f"Direct link: {direct_link_url}") download( direct_link_url, path, use_scraper=True, headers={"Referer": download_page_link}, ) def get_variants(version: Version) -> list[Variant]: url = version.link variants_page = get_scraper().get(url) if variants_page is None: raise FailedToFetch(url) variants_page_body = BeautifulSoup(variants_page.content, "html.parser") variants_table = variants_page_body.find("div", {"class": "table"}) if variants_table is None: raise FailedToFindElement("variants table") variants_table_rows = cast(Tag, variants_table).findChildren( "div", recursive=False )[1:] variants: list[Variant] = [] for variant_row in variants_table_rows: cells = variant_row.findChildren( "div", {"class": "table-cell"}, recursive=False ) if len(cells) == 0: print("Could not find cells") is_bundle_tag = variant_row.find("span", {"class": "apkm-badge"}) is_bundle = False if is_bundle_tag is None: print("Failed to find apk-badge") else: is_bundle = is_bundle_tag.string.strip() == "BUNDLE" architecture: str = cells[1].string link_element = variant_row.find("a", {"class": "accent_color"}) if link_element is None: print("Failed to find the link element") link: str = f"https://www.apkmirror.com{link_element.attrs['href']}" variants.append( Variant(is_bundle=is_bundle, link=link, architecture=architecture) ) print(variants) return variants ================================================ FILE: build_variants.py ================================================ from apkmirror import Version from utils import patch_apk def build_apks(latest_version: Version): # patch apk = "big_file_merged.apk" patches = "bins/patches.mpp" cli = "bins/morphe-cli.jar" common_includes = [ "Enable app downgrading", "Hide FAB", "Disable chirp font", "Add ability to copy media link", "Hide Banner", "Hide promote button", "Hide Community Notes", "Delete from database", "Customize Navigation Bar items", "Remove premium upsell", "Control video auto scroll", "Force enable translate", ] common_excludes = [] patch_apk( cli, patches, apk, includes=["Dynamic color"] + common_includes, excludes=common_excludes, out=f"x-piko-material-you-v{latest_version.version}.apk", ) patch_apk( cli, patches, apk, includes=common_includes, excludes=["Dynamic color"] + common_excludes, out=f"x-piko-v{latest_version.version}.apk", ) patch_apk( cli, patches, apk, includes=["Bring back twitter", "Dynamic color"] + common_includes, excludes=common_excludes, out=f"twitter-piko-material-you-v{latest_version.version}.apk", ) patch_apk( cli, patches, apk, includes=["Bring back twitter"] + common_includes, excludes=["Dynamic color"] + common_excludes, out=f"twitter-piko-v{latest_version.version}.apk", ) ================================================ FILE: constants.py ================================================ HEADERS = { "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", "accept-language": "en-GB,en;q=0.9", "cache-control": "no-cache", "pragma": "no-cache", "priority": "u=0, i", "sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "none", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", } ================================================ FILE: download_bins.py ================================================ import re import requests from utils import download def download_release_asset( repo: str, regex: str, out_dir: str, filename = None, include_prereleases: bool = False, version = None, ): url = f"https://api.github.com/repos/{repo}/releases" response = requests.get(url) if response.status_code != 200: raise Exception("Failed to fetch github") releases = [r for r in response.json() if include_prereleases or not r["prerelease"]] if not releases: raise Exception(f"No releases found for {repo}") if version is not None: releases = [r for r in releases if r["tag_name"] == version] if not releases: raise Exception(f"No release found for version {version}") latest_release = releases[0] link = None for asset in latest_release["assets"]: name = asset["name"] if re.search(regex, name): link = asset["browser_download_url"] if filename is None: filename = name break if link is None: raise Exception(f"Failed to find asset matching {regex} on release {latest_release['tag_name']}") download(link, f"{out_dir.lstrip('/')}/{filename}") return latest_release def download_apkeditor(): print("Downloading APKEditor") download_release_asset("REAndroid/APKEditor", "APKEditor", "bins", "apkeditor.jar") def download_morphe_cli(include_prereleases: bool = False): print("Downloading morphe cli") download_release_asset( "MorpheApp/morphe-cli", r"^morphe-cli.*-all\.jar$", "bins", "morphe-cli.jar", include_prereleases=include_prereleases, ) ================================================ FILE: flake.nix ================================================ { description = "A development environment for twitter-apk with Java"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { devShells.default = pkgs.mkShell { packages = with pkgs; [ jre # Java Runtime Environment (OpenJDK) ]; shellHook = '' echo "Environment loaded!" echo "Java: $(java --version | head -n1)" ''; }; } ); } ================================================ FILE: github.py ================================================ import requests from constants import HEADERS from dataclasses import dataclass @dataclass class Asset: browser_download_url: str name: str @dataclass class GithubRelease: tag_name: str html_url: str assets: list[Asset] def get_last_build_version(repo_url: str) -> GithubRelease | None: url = f"https://api.github.com/repos/{repo_url}/releases/latest" response = requests.get(url, headers=HEADERS) print(response.status_code) if response.status_code == 200: release = response.json() assets = [ Asset( browser_download_url=asset["browser_download_url"], name=asset["name"] ) for asset in release["assets"] ] return GithubRelease( tag_name=release["tag_name"], html_url=release["html_url"], assets=assets ) elif response.status_code == 404: return ================================================ FILE: main.py ================================================ from apkmirror import Version, Variant from build_variants import build_apks from download_bins import download_apkeditor, download_morphe_cli, download_release_asset import github from utils import panic, merge_apk, publish_release import apkmirror import os import argparse def get_latest_release(versions: list[Version]) -> Version | None: for i in versions: if i.version.find("release") >= 0: return i def process(latest_version: Version): variants: list[Variant] = apkmirror.get_variants(latest_version) download_link: Variant | None = None for variant in variants: if variant.is_bundle and ("universal" in variant.architecture or "arm64-v8a" in variant.architecture): download_link = variant break if download_link is None: raise Exception("Bundle not Found") apkmirror.download_apk(download_link) if not os.path.exists("big_file.apkm"): panic("Failed to download apk") download_apkeditor() if not os.path.exists("big_file_merged.apk"): merge_apk("big_file.apkm") else: print("apkm is already merged") download_morphe_cli(include_prereleases=True) print("Downloading patches") pikoRelease = download_release_asset( "crimera/piko", "^patches.*mpp$", "bins", "patches.mpp", include_prereleases=True ) message: str = f""" Changelogs: [piko-{pikoRelease["tag_name"]}]({pikoRelease["html_url"]}) """ build_apks(latest_version) publish_release( latest_version.version, [ f"x-piko-v{latest_version.version}.apk", f"x-piko-material-you-v{latest_version.version}.apk", f"twitter-piko-v{latest_version.version}.apk", f"twitter-piko-material-you-v{latest_version.version}.apk", ], message, latest_version.version ) def main(): # get latest version url: str = "https://www.apkmirror.com/apk/x-corp/twitter/" repo_url: str = "lluni/twitter-apk" versions = apkmirror.get_versions(url) latest_version = get_latest_release(versions) if latest_version is None: raise Exception("Could not find the latest version") # only continue if it's a release if latest_version.version.find("release") < 0: panic("Latest version is not a release version") last_build_version: github.GithubRelease | None = github.get_last_build_version( repo_url ) if last_build_version is None: panic("Failed to fetch the latest build version") return # Begin stuff if last_build_version.tag_name != latest_version.version: print(f"New version found: {latest_version.version}") else: print("No new version found") return process(latest_version) def manual(version:str): link = f'https://www.apkmirror.com/apk/x-corp/twitter/x-{version.replace(".","-")}-release' latest_version = Version(link=link,version=version) process(latest_version) if __name__ == "__main__": parser = argparse.ArgumentParser(description='Piko APK') # 0 = auto; 1 = manual; parser.add_argument('--m', action="store", dest='mode', default=0) parser.add_argument('--v', action="store", dest='version', default=0) args = parser.parse_args() mode = args.mode if not mode: # auto main() else: # manual version = args.version if not version: raise Exception("Version is required.") manual(version) ================================================ FILE: pyproject.toml ================================================ [project] name = "twitter-apk" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ "beautifulsoup4>=4.13.4", "requests>=2.32.3", "cloudscraper>=1.2.71", ] ================================================ FILE: utils.py ================================================ import os import requests import subprocess import sys from typing import Optional, List from github import get_last_build_version _scraper = None def get_scraper(): global _scraper if _scraper is None: import cloudscraper _scraper = cloudscraper.create_scraper() _scraper.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" }) return _scraper def panic(message: str): print(message, file=sys.stderr) exit(1) def send_message(message: str, token: str, chat_id: str, thread_id: str): endpoint = f"https://api.telegram.org/bot{token}/sendMessage" data = { "parse_mode": "Markdown", "disable_web_page_preview": "true", "text": message, "message_thread_id": thread_id, "chat_id": chat_id, } requests.post(endpoint, data=data) def report_to_telegram(): tg_token = os.environ["TG_TOKEN"] tg_chat_id = os.environ["TG_CHAT_ID"] tg_thread_id = os.environ["TG_THREAD_ID"] release = get_last_build_version("crimera/twitter-apk") if release is None: raise Exception("Could not fetch release") downloads = [ f"[{asset.name}]({asset.browser_download_url})" for asset in release.assets ] message = f""" [New Update Released !]({release.html_url}) ▼ Downloads ▼ {"\n\n".join(downloads)} """ print(message) send_message(message, tg_token, tg_chat_id, tg_thread_id) def download(link, out, headers=None, use_scraper=False): dir_name = os.path.dirname(out) if dir_name: os.makedirs(dir_name, exist_ok=True) if os.path.exists(out): print(f"{out} already exists skipping download") return if use_scraper: print(f"Downloading with scraper: {link}") session = get_scraper() if use_scraper else requests # https://www.slingacademy.com/article/python-requests-module-how-to-download-files-from-urls/#Streaming_Large_Files with session.get(link, stream=True, headers=headers) as r: r.raise_for_status() with open(out, "wb") as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) def run_command(command: list[str]): cmd = subprocess.run(command, capture_output=True, shell=True) try: cmd.check_returncode() except subprocess.CalledProcessError: print(cmd.stdout) print(cmd.stderr) exit(1) def merge_apk(path: str): subprocess.run( ["java", "-jar", "./bins/apkeditor.jar", "m", "-extractNativeLibs", "true", "-i", path] ).check_returncode() def patch_apk( cli: str, patches: str, apk: str, includes: list[str] | None = None, excludes: list[str] | None = None, out: str | None = None, ): command = [ "java", "-jar", cli, "patch", "-p", patches, # use j-hc's keystore so we wouldn't need to reinstall "--keystore", "ks.keystore", "--keystore-entry-password", "123456789", "--keystore-password", "123456789", "--signer", "jhc", "--keystore-entry-alias", "jhc", ] if includes is not None: for i in includes: command.append("-e") command.append(i) if excludes is not None: for e in excludes: command.append("-d") command.append(e) if out is not None: command.append("--out") command.append(out) command.append(apk) subprocess.run(command).check_returncode() def publish_release(tag: str, files: list[str], message: str, title = ""): key = os.environ.get("GITHUB_TOKEN") if key is None: raise Exception("GITHUB_TOKEN is not set") command = ["gh", "release", "create", "--latest", tag, "--notes", message, "--title", title] if len(files) == 0: raise Exception("Files should have atleast one item") for file in files: command.append(file) subprocess.run(command, env=os.environ.copy()).check_returncode()