23 Commits
Author SHA1 Message Date
semantic-release-bot 9540e6443a chore: Release v1.2.0 [skip ci]
## [1.2.0](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.1.7...v1.2.0) (2026-09-01)

### ✨ New Features

* age verification ([7472659](http://gitea:3000/StanPonomarev/twitter-patch/commit/74726598be4ceb147817e34ccb6cf435ece8f0bc))
2026-09-01 20:43:46 +00:00
StanPonomarev 74726598be feat: age verification
Release / Release (push) Successful in 59s
2026-09-01 22:42:42 +02:00
semantic-release-bot 96a1e1f6b9 chore: Release v1.1.7 [skip ci]
## [1.1.7](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.1.6...v1.1.7) (2026-08-24)

### 🐛 Bug Fixes

* version check again ([104ca41](http://gitea:3000/StanPonomarev/twitter-patch/commit/104ca41fda3ee3ee9215519e6266c37f87e86ffc))
2026-08-24 11:34:09 +00:00
StanPonomarev 104ca41fda fix: version check again
Release / Release (push) Successful in 54s
2026-08-24 13:33:12 +02:00
semantic-release-bot 5256b47f90 chore: Release v1.1.6 [skip ci]
## [1.1.6](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.1.5...v1.1.6) (2026-08-23)

### 🐛 Bug Fixes

* google update screen ([5d932b0](http://gitea:3000/StanPonomarev/twitter-patch/commit/5d932b06a9e63659a98cbc9b36c4fe17807acc39))
2026-08-23 10:17:13 +00:00
StanPonomarev 5d932b06a9 fix: google update screen
Release / Release (push) Successful in 57s
2026-08-23 12:16:13 +02:00
semantic-release-bot 076f1c2479 chore: Release v1.1.5 [skip ci]
## [1.1.5](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.1.4...v1.1.5) (2026-08-23)

### 🐛 Bug Fixes

* vending update screen ([48f0cec](http://gitea:3000/StanPonomarev/twitter-patch/commit/48f0cecbe47d73ee06c15915d5c57694856b904e))
2026-08-23 05:14:55 +00:00
StanPonomarev 48f0cecbe4 fix: vending update screen
Release / Release (push) Successful in 56s
2026-08-23 07:14:00 +02:00
semantic-release-bot 015ce16efc chore: Release v1.1.4 [skip ci]
## [1.1.4](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.1.3...v1.1.4) (2026-08-22)

### 🐛 Bug Fixes

* more update required fixes ([97c30af](http://gitea:3000/StanPonomarev/twitter-patch/commit/97c30af97d00e7f92b5c2801e9e744f00704758a))
2026-08-22 19:19:36 +00:00
StanPonomarev 97c30af97d fix: more update required fixes
Release / Release (push) Successful in 58s
2026-08-22 21:18:38 +02:00
semantic-release-bot 7727f37a18 chore: Release v1.1.3 [skip ci]
## [1.1.3](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.1.2...v1.1.3) (2026-08-22)

### 🐛 Bug Fixes

* xchat shutting down the app ([1985bf3](http://gitea:3000/StanPonomarev/twitter-patch/commit/1985bf307b4552655216079082757a53af00140b))
2026-08-22 15:34:34 +00:00
StanPonomarev 1985bf307b fix: xchat shutting down the app
Release / Release (push) Successful in 54s
2026-08-22 17:33:41 +02:00
semantic-release-bot 351917cbbd chore: Release v1.1.2 [skip ci]
## [1.1.2](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.1.1...v1.1.2) (2026-08-22)

### 🐛 Bug Fixes

* manifest ([7062792](http://gitea:3000/StanPonomarev/twitter-patch/commit/7062792215590cce79caa6e78354ee73375eaa79))
2026-08-22 08:27:54 +00:00
StanPonomarev 7062792215 fix: manifest
Release / Release (push) Successful in 58s
2026-08-22 10:26:52 +02:00
semantic-release-bot 1b47dcf787 chore: Release v1.1.1 [skip ci]
## [1.1.1](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.1.0...v1.1.1) (2026-08-21)

### 🐛 Bug Fixes

* update screen ([cb1230a](http://gitea:3000/StanPonomarev/twitter-patch/commit/cb1230a7abf46edb11f2b8dad100b091dd672aee))
2026-08-21 23:30:10 +00:00
StanPonomarev cb1230a7ab fix: update screen
Release / Release (push) Successful in 56s
2026-08-22 01:29:12 +02:00
semantic-release-bot 29816c940a chore: Release v1.1.0 [skip ci]
## [1.1.0](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.0.3...v1.1.0) (2026-08-21)

### ✨ New Features

* remove upgrade button ([723964b](http://gitea:3000/StanPonomarev/twitter-patch/commit/723964bc29d0a9edc7ad1915c9cf5669fe9e6f95))
2026-08-21 18:50:40 +00:00
StanPonomarev 723964bc29 feat: remove upgrade button
Release / Release (push) Successful in 56s
2026-08-21 20:49:42 +02:00
semantic-release-bot 6ec6b0032b chore: Release v1.0.3 [skip ci]
## [1.0.3](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.0.2...v1.0.3) (2026-08-21)

### 🐛 Bug Fixes

* artifact ([0a2b472](http://gitea:3000/StanPonomarev/twitter-patch/commit/0a2b4725fb53bf0fa741dc4be1a9914ad1e5be8b))
2026-08-21 16:15:17 +00:00
StanPonomarev 0a2b4725fb fix: artifact
Release / Release (push) Successful in 54s
2026-08-21 18:14:25 +02:00
StanPonomarev 5dc2c501b1 add AGENTS.md
Release / Release (push) Successful in 54s
2026-08-21 17:49:25 +02:00
semantic-release-bot b7c143788b chore: Release v1.0.2 [skip ci]
## [1.0.2](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.0.1...v1.0.2) (2026-08-21)

### 🐛 Bug Fixes

* release again ([ef64be8](http://gitea:3000/StanPonomarev/twitter-patch/commit/ef64be8da665680ebf9fadcf60ae79046594292d))
2026-08-21 15:48:27 +00:00
StanPonomarev ef64be8da6 fix: release again
Release / Release (push) Successful in 1m5s
2026-08-21 17:45:56 +02:00
14 changed files with 614 additions and 24 deletions
+20 -9
View File
@@ -4,20 +4,30 @@ const path = require('node:path');
function releaseEnvironment(context) { function releaseEnvironment(context) {
const env = context.env ?? process.env; const env = context.env ?? process.env;
const serverUrl = env.GITEA_URL?.replace(/\/$/, ''); const serverUrl = env.GITEA_URL?.replace(/\/$/, '');
const publicUrl = env.GITEA_PUBLIC_URL?.replace(/\/$/, '');
const repository = env.GITEA_REPOSITORY; const repository = env.GITEA_REPOSITORY;
const token = env.GITEA_TOKEN; const token = env.GITEA_TOKEN;
if (!serverUrl || !repository || !token) { if (!serverUrl || !publicUrl || !repository || !token) {
throw new Error('GITEA_URL, GITEA_REPOSITORY, and GITEA_TOKEN are required'); throw new Error(
'GITEA_URL, GITEA_PUBLIC_URL, GITEA_REPOSITORY, and GITEA_TOKEN are required',
);
} }
if (!/^https?:\/\//.test(serverUrl)) { if (!/^https?:\/\//.test(serverUrl)) {
throw new Error('GITEA_URL must be an HTTP(S) URL'); throw new Error('GITEA_URL must be an HTTP(S) URL');
} }
if (!/^https?:\/\//.test(publicUrl)) {
throw new Error('GITEA_PUBLIC_URL must be an HTTP(S) URL');
}
if (!/^[^/]+\/[^/]+$/.test(repository)) { if (!/^[^/]+\/[^/]+$/.test(repository)) {
throw new Error('GITEA_REPOSITORY must use the owner/repository format'); throw new Error('GITEA_REPOSITORY must use the owner/repository format');
} }
return { serverUrl, repository, token }; return { serverUrl, publicUrl, repository, token };
}
function makeUrlsPublic(value, serverUrl, publicUrl) {
return value?.split(serverUrl).join(publicUrl);
} }
async function giteaRequest(url, token, options = {}) { async function giteaRequest(url, token, options = {}) {
@@ -43,10 +53,11 @@ module.exports.verifyConditions = async (_pluginConfig, context) => {
module.exports.prepare = async (pluginConfig, context) => { module.exports.prepare = async (pluginConfig, context) => {
const { nextRelease, cwd } = context; const { nextRelease, cwd } = context;
const notes = nextRelease.notes?.trim(); const rawNotes = nextRelease.notes?.trim();
if (!notes) return; if (!rawNotes) return;
const { serverUrl, repository } = releaseEnvironment(context); const { serverUrl, publicUrl, repository } = releaseEnvironment(context);
const notes = makeUrlsPublic(rawNotes, serverUrl, publicUrl);
const changelogPath = path.resolve(cwd, pluginConfig.changelogFile ?? 'CHANGELOG.md'); const changelogPath = path.resolve(cwd, pluginConfig.changelogFile ?? 'CHANGELOG.md');
const releaseJsonPath = path.resolve(cwd, pluginConfig.releaseJsonPath ?? 'patches-bundle.json'); const releaseJsonPath = path.resolve(cwd, pluginConfig.releaseJsonPath ?? 'patches-bundle.json');
@@ -67,7 +78,7 @@ module.exports.prepare = async (pluginConfig, context) => {
const releaseData = { const releaseData = {
created_at: new Date().toISOString().replace(/\.\d{3}Z$/, ''), created_at: new Date().toISOString().replace(/\.\d{3}Z$/, ''),
description: notes, description: notes,
download_url: `${serverUrl}/${repository}/releases/download/${nextRelease.gitTag}/${assetName}`, download_url: `${publicUrl}/${repository}/releases/download/${nextRelease.gitTag}/${assetName}`,
signature_download_url: '', signature_download_url: '',
version, version,
}; };
@@ -75,7 +86,7 @@ module.exports.prepare = async (pluginConfig, context) => {
}; };
module.exports.publish = async (pluginConfig, context) => { module.exports.publish = async (pluginConfig, context) => {
const { serverUrl, repository, token } = releaseEnvironment(context); const { serverUrl, publicUrl, repository, token } = releaseEnvironment(context);
const { branch, cwd, nextRelease } = context; const { branch, cwd, nextRelease } = context;
const repositoryPath = repository.split('/').map(encodeURIComponent).join('/'); const repositoryPath = repository.split('/').map(encodeURIComponent).join('/');
const apiUrl = `${serverUrl}/api/v1/repos/${repositoryPath}`; const apiUrl = `${serverUrl}/api/v1/repos/${repositoryPath}`;
@@ -96,7 +107,7 @@ module.exports.publish = async (pluginConfig, context) => {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
body: nextRelease.notes, body: makeUrlsPublic(nextRelease.notes, serverUrl, publicUrl),
draft: false, draft: false,
name: nextRelease.name, name: nextRelease.name,
prerelease: false, prerelease: false,
+1
View File
@@ -52,6 +52,7 @@ jobs:
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_URL: ${{ gitea.server_url }} GITEA_URL: ${{ gitea.server_url }}
GITEA_PUBLIC_URL: https://git.krtss.de
GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_REF_NAME: ${{ gitea.ref_name }} GITEA_REF_NAME: ${{ gitea.ref_name }}
run: npx semantic-release run: npx semantic-release
+2 -2
View File
@@ -28,6 +28,7 @@
} }
} }
], ],
"gradle-semantic-release-plugin",
[ [
"./.gitea/scripts/morphe-release.cjs", "./.gitea/scripts/morphe-release.cjs",
{ {
@@ -36,11 +37,10 @@
"assetDirectory": "patches/build/libs" "assetDirectory": "patches/build/libs"
} }
], ],
"gradle-semantic-release-plugin",
[ [
"@semantic-release/exec", "@semantic-release/exec",
{ {
"prepareCmd": "./gradlew generatePatchesList && jq '.version=\"${nextRelease.version}\"' patches-list.json > patches-list.json.tmp && mv patches-list.json.tmp patches-list.json && python3 .gitea/scripts/generate_patches_readme.py \"$GITEA_URL\" \"$GITEA_REPOSITORY\" \"$GITEA_REF_NAME\" patches-list.json README.md" "prepareCmd": "./gradlew generatePatchesList && jq '.version=\"${nextRelease.version}\"' patches-list.json > patches-list.json.tmp && mv patches-list.json.tmp patches-list.json && python3 .gitea/scripts/generate_patches_readme.py \"$GITEA_PUBLIC_URL\" \"$GITEA_REPOSITORY\" \"$GITEA_REF_NAME\" patches-list.json README.md"
} }
], ],
[ [
+21
View File
@@ -0,0 +1,21 @@
# Agent Instructions
## Release-managed files
Agents must never create, edit, delete, restore, regenerate, format, resolve conflicts in, or stage files managed by the release pipeline.
Protected files:
- `CHANGELOG.md`
- `gradle.properties`
- `patches-bundle.json`
- `patches-list.json`
- `README.md`
These files are generated, updated, and committed exclusively by `semantic-release`. Manual changes or conflict resolutions cause divergence from the release commit and create pull or rebase conflicts.
When a task requires different generated content, change only the source, generator, or release configuration responsible for producing it. Leave every protected file untouched and tell the user that the next release must regenerate it.
If a protected file is conflicted, do not resolve, restore, or stage it. Stop and report the conflict to the user.
Do not run release or generation commands that modify protected files, including `npx semantic-release`, `./gradlew generatePatchesList`, or `.gitea/scripts/generate_patches_readme.py`. A dry run is allowed only when it is proven not to write any protected file.
+66
View File
@@ -1,3 +1,69 @@
## [1.2.0](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.7...v1.2.0) (2026-09-01)
### ✨ New Features
* age verification ([7472659](https://git.krtss.de/StanPonomarev/twitter-patch/commit/74726598be4ceb147817e34ccb6cf435ece8f0bc))
## [1.1.7](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.6...v1.1.7) (2026-08-24)
### 🐛 Bug Fixes
* version check again ([104ca41](https://git.krtss.de/StanPonomarev/twitter-patch/commit/104ca41fda3ee3ee9215519e6266c37f87e86ffc))
## [1.1.6](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.5...v1.1.6) (2026-08-23)
### 🐛 Bug Fixes
* google update screen ([5d932b0](https://git.krtss.de/StanPonomarev/twitter-patch/commit/5d932b06a9e63659a98cbc9b36c4fe17807acc39))
## [1.1.5](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.4...v1.1.5) (2026-08-23)
### 🐛 Bug Fixes
* vending update screen ([48f0cec](https://git.krtss.de/StanPonomarev/twitter-patch/commit/48f0cecbe47d73ee06c15915d5c57694856b904e))
## [1.1.4](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.3...v1.1.4) (2026-08-22)
### 🐛 Bug Fixes
* more update required fixes ([97c30af](https://git.krtss.de/StanPonomarev/twitter-patch/commit/97c30af97d00e7f92b5c2801e9e744f00704758a))
## [1.1.3](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.2...v1.1.3) (2026-08-22)
### 🐛 Bug Fixes
* xchat shutting down the app ([1985bf3](https://git.krtss.de/StanPonomarev/twitter-patch/commit/1985bf307b4552655216079082757a53af00140b))
## [1.1.2](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.1...v1.1.2) (2026-08-22)
### 🐛 Bug Fixes
* manifest ([7062792](https://git.krtss.de/StanPonomarev/twitter-patch/commit/7062792215590cce79caa6e78354ee73375eaa79))
## [1.1.1](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.0...v1.1.1) (2026-08-21)
### 🐛 Bug Fixes
* update screen ([cb1230a](https://git.krtss.de/StanPonomarev/twitter-patch/commit/cb1230a7abf46edb11f2b8dad100b091dd672aee))
## [1.1.0](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.0.3...v1.1.0) (2026-08-21)
### ✨ New Features
* remove upgrade button ([723964b](https://git.krtss.de/StanPonomarev/twitter-patch/commit/723964bc29d0a9edc7ad1915c9cf5669fe9e6f95))
## [1.0.3](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.0.2...v1.0.3) (2026-08-21)
### 🐛 Bug Fixes
* artifact ([0a2b472](https://git.krtss.de/StanPonomarev/twitter-patch/commit/0a2b4725fb53bf0fa741dc4be1a9914ad1e5be8b))
## [1.0.2](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.0.1...v1.0.2) (2026-08-21)
### 🐛 Bug Fixes
* release again ([ef64be8](https://git.krtss.de/StanPonomarev/twitter-patch/commit/ef64be8da665680ebf9fadcf60ae79046594292d))
## [1.0.1](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.0.0...v1.0.1) (2026-08-21) ## [1.0.1](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.0.0...v1.0.1) (2026-08-21)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
+4 -2
View File
@@ -4,9 +4,9 @@ Patch bundle for X (Twitter), maintained by
[Stan Ponomarev](https://git.krtss.de/StanPonomarev). [Stan Ponomarev](https://git.krtss.de/StanPonomarev).
<!-- PATCHES_START --> <!-- PATCHES_START -->
> **[v1.0.1](http://gitea:3000/StanPonomarev/twitter-patch/releases/tag/v1.0.1)**&nbsp;&nbsp;•&nbsp;&nbsp;`main`&nbsp;&nbsp;•&nbsp;&nbsp;1 patches total > **[v1.2.0](https://git.krtss.de/StanPonomarev/twitter-patch/releases/tag/v1.2.0)**&nbsp;&nbsp;•&nbsp;&nbsp;`main`&nbsp;&nbsp;•&nbsp;&nbsp;3 patches total
<details open> <details open>
<summary>📦 X&nbsp;&nbsp;•&nbsp;&nbsp;1 patch</summary> <summary>📦 X&nbsp;&nbsp;•&nbsp;&nbsp;3 patches</summary>
<br> <br>
**🎯 Supported versions:** **🎯 Supported versions:**
@@ -17,6 +17,8 @@ Patch bundle for X (Twitter), maintained by
| 💊&nbsp;Patch | 📜&nbsp;Description | ⚙️&nbsp;Options | | 💊&nbsp;Patch | 📜&nbsp;Description | ⚙️&nbsp;Options |
|----------|----------------|-----------| |----------|----------------|-----------|
| [Disable outdated version screen](#disable-outdated-version-screen) | Prevents X from blocking app access when it marks the client version as outdated. | | | [Disable outdated version screen](#disable-outdated-version-screen) | Prevents X from blocking app access when it marks the client version as outdated. | |
| [Remove post age verification](#remove-post-age-verification) | Allows age-gated posts to be viewed without completing age verification. | |
| [Remove timeline Upgrade button](#remove-timeline-upgrade-button) | Removes the Premium Upgrade button from the timeline's top bar. | |
</details> </details>
+1 -1
View File
@@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official kotlin.code.style = official
version = 1.0.1 version = 1.2.0
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"created_at": "2026-08-21T15:07:17", "created_at": "2026-09-01T20:43:32",
"description": "## [1.0.1](http://gitea:3000/StanPonomarev/twitter-patch/compare/v1.0.0...v1.0.1) (2026-08-21)\n\n### 🐛 Bug Fixes\n\n* update release strat ([818e291](http://gitea:3000/StanPonomarev/twitter-patch/commit/818e291ffdfa17eff5f57e9a47e3f73a00b7c050))", "description": "## [1.2.0](https://git.krtss.de/StanPonomarev/twitter-patch/compare/v1.1.7...v1.2.0) (2026-09-01)\n\n### ✨ New Features\n\n* age verification ([7472659](https://git.krtss.de/StanPonomarev/twitter-patch/commit/74726598be4ceb147817e34ccb6cf435ece8f0bc))",
"download_url": "http://gitea:3000/StanPonomarev/twitter-patch/releases/download/v1.0.1/patches-1.0.1.mpp", "download_url": "https://git.krtss.de/StanPonomarev/twitter-patch/releases/download/v1.2.0/patches-1.2.0.mpp",
"signature_download_url": "", "signature_download_url": "",
"version": "1.0.1" "version": "1.2.0"
} }
+55 -1
View File
@@ -1,11 +1,65 @@
{ {
"NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.",
"version": "1.0.1", "version": "1.2.0",
"patches": [ "patches": [
{ {
"name": "Disable outdated version screen", "name": "Disable outdated version screen",
"description": "Prevents X from blocking app access when it marks the client version as outdated.", "description": "Prevents X from blocking app access when it marks the client version as outdated.",
"default": true, "default": true,
"dependencies": [
"ResourcePatch"
],
"compatiblePackages": [
{
"packageName": "com.twitter.android",
"name": "X",
"description": null,
"apkFileType": "APKM",
"appIconColor": "#000000",
"signatures": null,
"targets": [
{
"version": "12.7.1-release.0",
"versionCodes": null,
"isExperimental": false,
"minSdk": 28,
"description": null
}
]
}
],
"options": []
},
{
"name": "Remove post age verification",
"description": "Allows age-gated posts to be viewed without completing age verification.",
"default": true,
"dependencies": [],
"compatiblePackages": [
{
"packageName": "com.twitter.android",
"name": "X",
"description": null,
"apkFileType": "APKM",
"appIconColor": "#000000",
"signatures": null,
"targets": [
{
"version": "12.7.1-release.0",
"versionCodes": null,
"isExperimental": false,
"minSdk": 28,
"description": null
}
]
}
],
"options": []
},
{
"name": "Remove timeline Upgrade button",
"description": "Removes the Premium Upgrade button from the timeline's top bar.",
"default": true,
"dependencies": [], "dependencies": [],
"compatiblePackages": [ "compatiblePackages": [
{ {
+1 -1
View File
@@ -43,7 +43,7 @@ tasks {
register<JavaExec>("generatePatchesList") { register<JavaExec>("generatePatchesList") {
description = "Build patch with patch list" description = "Build patch with patch list"
dependsOn(build) dependsOn(buildAndroid)
classpath = sourceSets["main"].runtimeClasspath + patchListGeneratorClasspath classpath = sourceSets["main"].runtimeClasspath + patchListGeneratorClasspath
mainClass.set("util.PatchListGeneratorKt") mainClass.set("util.PatchListGeneratorKt")
@@ -23,7 +23,7 @@ import kotlin.test.assertTrue
class MorphePatchIntegrationTest { class MorphePatchIntegrationTest {
@Test @Test
fun `applies outdated version patch to target APKM`() { fun `applies patches to target APKM`() {
val inputApkm = requiredPathProperty("integrationTest.inputApkm") val inputApkm = requiredPathProperty("integrationTest.inputApkm")
val patchBundle = requiredPathProperty("integrationTest.patchBundle") val patchBundle = requiredPathProperty("integrationTest.patchBundle")
val morpheCache = requiredPathProperty("integrationTest.morpheCache") val morpheCache = requiredPathProperty("integrationTest.morpheCache")
@@ -47,7 +47,9 @@ class MorphePatchIntegrationTest {
"patch", "patch",
"--patches=${patchBundle.absolutePathString()}", "--patches=${patchBundle.absolutePathString()}",
"--exclusive", "--exclusive",
"--enable=$PATCH_NAME", "--enable=$OUTDATED_PATCH_NAME",
"--enable=$UPGRADE_PATCH_NAME",
"--enable=$POST_AGE_VERIFICATION_PATCH_NAME",
"--unsigned", "--unsigned",
"--out=${patchedApk.absolutePathString()}", "--out=${patchedApk.absolutePathString()}",
"--result-file=${resultFile.absolutePathString()}", "--result-file=${resultFile.absolutePathString()}",
@@ -91,7 +93,12 @@ class MorphePatchIntegrationTest {
val appliedPatchNames = result.getAsJsonArray("appliedPatches") val appliedPatchNames = result.getAsJsonArray("appliedPatches")
.map { it.asJsonObject.get("name").asString } .map { it.asJsonObject.get("name").asString }
assertTrue(PATCH_NAME in appliedPatchNames, "Expected patch was not applied: $appliedPatchNames") assertTrue(
appliedPatchNames.containsAll(
listOf(OUTDATED_PATCH_NAME, UPGRADE_PATCH_NAME, POST_AGE_VERIFICATION_PATCH_NAME)
),
"Expected patches were not applied: $appliedPatchNames"
)
assertTrue(result.getAsJsonArray("failedPatches").isEmpty, "Morphe reported failed patches") assertTrue(result.getAsJsonArray("failedPatches").isEmpty, "Morphe reported failed patches")
} finally { } finally {
workDirectory.toFile().deleteRecursively() workDirectory.toFile().deleteRecursively()
@@ -142,7 +149,9 @@ class MorphePatchIntegrationTest {
} }
private companion object { private companion object {
const val PATCH_NAME = "Disable outdated version screen" const val OUTDATED_PATCH_NAME = "Disable outdated version screen"
const val UPGRADE_PATCH_NAME = "Remove timeline Upgrade button"
const val POST_AGE_VERIFICATION_PATCH_NAME = "Remove post age verification"
const val MORPHE_VERSION = "1.13.1" const val MORPHE_VERSION = "1.13.1"
const val MORPHE_SHA256 = "a212531f68d3ada4b504bf9dac3e02460589f3bf8868ee66e1b4780152d3466b" const val MORPHE_SHA256 = "a212531f68d3ada4b504bf9dac3e02460589f3bf8868ee66e1b4780152d3466b"
const val MORPHE_URL = const val MORPHE_URL =
@@ -3,7 +3,9 @@ package app.twitter.patches
import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
import app.morphe.patcher.Fingerprint import app.morphe.patcher.Fingerprint
import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.bytecodePatch
import app.morphe.patcher.patch.resourcePatch
import app.twitter.patches.shared.Constants.COMPATIBILITY_X import app.twitter.patches.shared.Constants.COMPATIBILITY_X
import org.w3c.dom.Element
private val outdatedVersionEnforcementFingerprint = Fingerprint( private val outdatedVersionEnforcementFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/o;", definingClass = "Lcom/twitter/subsystem/clientshutdown/o;",
@@ -13,6 +15,41 @@ private val outdatedVersionEnforcementFingerprint = Fingerprint(
strings = listOf("Target version is ", "Detected shutdown") strings = listOf("Target version is ", "Detected shutdown")
) )
private val forceUpdateNavigationFingerprint = Fingerprint(
definingClass = "Lcom/x/android/main/helpers/o8;",
name = "a",
returnType = "Lcom/x/compose/core/o2;",
parameters = listOf(
"Lcom/x/navigation/RootNavigationArgs;",
"Lcom/arkivanov/decompose/c;",
"Lcom/x/navigation/f1;"
)
)
private val shutdownForegroundHandlerFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/m;",
name = "invoke",
returnType = "Ljava/lang/Object;",
parameters = listOf("Ljava/lang/Object;"),
strings = listOf("Entered foreground, shutdown is ", "Launching app update")
)
private val appUpdateLauncherFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/i;",
name = "a",
returnType = "V",
parameters = listOf("Ljava/lang/String;"),
strings = listOf("Launching app update")
)
private val shutdownEntryPointDisablerFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/i;",
name = "b",
returnType = "V",
parameters = emptyList(),
strings = listOf("com.twitter.clientshutdown.update.AppUpdateActivityAlias")
)
private val shutdownStateFingerprint = Fingerprint( private val shutdownStateFingerprint = Fingerprint(
definingClass = "Lcom/twitter/subsystem/clientshutdown/q;", definingClass = "Lcom/twitter/subsystem/clientshutdown/q;",
name = "isShutdown", name = "isShutdown",
@@ -20,6 +57,93 @@ private val shutdownStateFingerprint = Fingerprint(
parameters = emptyList() parameters = emptyList()
) )
private val appUpdateScreenRendererFingerprint = Fingerprint(
definingClass = "Lcom/twitter/clientshutdown/update/j;",
name = "<init>",
returnType = "V",
parameters = listOf(
"Landroid/view/View;",
"Landroid/app/Activity;",
"Lcom/twitter/app/common/activity/b;",
"Lcom/twitter/subsystem/clientshutdown/api/c;"
),
strings = listOf("rootView", "activityFinisher", "eventLogHelper")
)
private val forceUpdateScreenRendererFingerprint = Fingerprint(
definingClass = "Lcom/x/forceupdate/g;",
name = "a",
returnType = "V",
parameters = listOf(
"Lkotlin/jvm/functions/Function0;",
"Lkotlin/jvm/functions/Function1;",
"Landroidx/compose/ui/Modifier;",
"Landroidx/compose/runtime/Composer;",
"I"
),
strings = listOf("onUpdateClick", "onLearnMoreClick")
)
private val playCoreUpdateManagerFingerprint = Fingerprint(
definingClass = "Lcom/x/android/appupdate/b${'$'}a;",
name = "invokeSuspend",
returnType = "Ljava/lang/Object;",
parameters = listOf("Ljava/lang/Object;"),
strings = listOf("x_lite_in_app_update_enabled")
)
private val serverDrivenCoverRendererFingerprint = Fingerprint(
definingClass = "Lcom/x/urt/cover/ui/j;",
name = "a",
returnType = "V",
parameters = listOf(
"Lcom/x/urt/cover/component/e;",
"Landroidx/compose/ui/Modifier;",
"Landroidx/compose/runtime/Composer;",
"I"
),
strings = listOf("component")
)
private val serverDrivenCoverLifecycleFingerprint = Fingerprint(
definingClass = "Lcom/x/urt/cover/component/a;",
name = "h",
returnType = "V",
parameters = emptyList(),
strings = listOf("impression")
)
private val brazeInAppMessageDisplayFingerprint = Fingerprint(
definingClass = "Lcom/braze/ui/inappmessage/BrazeInAppMessageManager;",
name = "displayInAppMessage${'$'}suspendImpl",
returnType = "Ljava/lang/Object;",
parameters = listOf(
"Lcom/braze/ui/inappmessage/BrazeInAppMessageManager;",
"Lcom/braze/models/inappmessage/IInAppMessage;",
"Z",
"Lkotlin/coroutines/Continuation;"
),
strings = listOf("In-app message is expired. Doing nothing. Expiration: ")
)
private const val APP_UPDATE_ACTIVITY = "com.twitter.clientshutdown.update.AppUpdateActivity"
private val disableOutdatedVersionActivityPatch = resourcePatch {
execute {
document("AndroidManifest.xml").use { document ->
val appUpdateActivity = document.getElementsByTagName("activity")
.let { activities ->
(0 until activities.length)
.asSequence()
.map { activities.item(it) }
.filterIsInstance<Element>()
.single { it.getAttribute("android:name") == APP_UPDATE_ACTIVITY }
}
appUpdateActivity.setAttribute("android:enabled", "false")
}
}
}
@Suppress("unused") @Suppress("unused")
val disableOutdatedVersionScreenPatch = bytecodePatch( val disableOutdatedVersionScreenPatch = bytecodePatch(
name = "Disable outdated version screen", name = "Disable outdated version screen",
@@ -27,8 +151,44 @@ val disableOutdatedVersionScreenPatch = bytecodePatch(
default = true default = true
) { ) {
compatibleWith(COMPATIBILITY_X) compatibleWith(COMPATIBILITY_X)
dependsOn(disableOutdatedVersionActivityPatch)
execute { execute {
forceUpdateNavigationFingerprint.method.addInstructions(
0,
"""
move-object/from16 v2, p1
instance-of v0, v2, Lcom/x/navigation/ForceUpdateArgs;
if-eqz v0, :force_update_allowed
sget-object v0, Lcom/x/navigation/MainLandingArgs${'$'}TabType;->HOME:Lcom/x/navigation/MainLandingArgs${'$'}TabType;
const/4 v1, 0x0
new-instance v2, Lcom/x/navigation/MainLandingArgs;
invoke-direct {v2, v0, v1, v1, v1}, Lcom/x/navigation/MainLandingArgs;-><init>(Lcom/x/navigation/MainLandingArgs${'$'}TabType;Lcom/x/navigation/AcceptGroupInviteArgs;Lcom/x/navigation/RootNavigationArgs;Lcom/x/models/PostIdentifier;)V
move-object/16 p1, v2
:force_update_allowed
"""
)
shutdownForegroundHandlerFingerprint.method.addInstructions(
0,
"""
sget-object v0, Lkotlin/Unit;->a:Lkotlin/Unit;
return-object v0
"""
)
appUpdateLauncherFingerprint.method.addInstructions(
0,
"return-void"
)
shutdownEntryPointDisablerFingerprint.method.addInstructions(
0,
"""
invoke-virtual {p0}, Lcom/twitter/subsystem/clientshutdown/i;->c()V
return-void
"""
)
outdatedVersionEnforcementFingerprint.method.addInstructions( outdatedVersionEnforcementFingerprint.method.addInstructions(
0, 0,
""" """
@@ -44,5 +204,207 @@ val disableOutdatedVersionScreenPatch = bytecodePatch(
return v0 return v0
""" """
) )
appUpdateScreenRendererFingerprint.method.addInstructions(
0,
"""
new-instance v0, Lcom/twitter/subsystem/clientshutdown/a;
invoke-direct {v0, p2}, Lcom/twitter/subsystem/clientshutdown/a;-><init>(Landroid/content/Context;)V
const/4 v1, 0x2
const-string v2, "com.twitter.clientshutdown.update.AppUpdateActivityAlias"
invoke-virtual {v0, v1, v2}, Lcom/twitter/subsystem/clientshutdown/a;->b(ILjava/lang/String;)V
invoke-virtual {p2}, Landroid/app/Activity;->finish()V
"""
)
forceUpdateScreenRendererFingerprint.method.addInstructions(
0,
"return-void"
)
playCoreUpdateManagerFingerprint.method.addInstructions(
0,
"""
sget-object v0, Lkotlin/Unit;->a:Lkotlin/Unit;
return-object v0
"""
)
serverDrivenCoverRendererFingerprint.method.addInstructions(
0,
"""
move-object/from16 v0, p0
invoke-interface {v0}, Lcom/x/urt/cover/component/e;->a()Lcom/x/models/timelines/instructions/cover/URTCover;
move-result-object v0
instance-of v1, v0, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}FullCover;
if-eqz v1, :check_outdated_half_cover
check-cast v0, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}FullCover;
invoke-virtual {v0}, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}FullCover;->getData()Lcom/x/models/timelines/instructions/cover/URTFullCover;
move-result-object v0
invoke-virtual {v0}, Lcom/x/models/timelines/instructions/cover/URTFullCover;->getPrimaryText()Lcom/x/models/text/RichText;
move-result-object v1
invoke-virtual {v1}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v1
const-string v2, "This app is out of date."
invoke-virtual {v1, v2}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v1
if-nez v1, :block_outdated_server_cover
invoke-virtual {v0}, Lcom/x/models/timelines/instructions/cover/URTFullCover;->getSecondaryText()Lcom/x/models/text/RichText;
move-result-object v1
if-eqz v1, :check_outdated_full_cover_detail
invoke-virtual {v1}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v1
const-string v2, "This app is out of date."
invoke-virtual {v1, v2}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v1
if-nez v1, :block_outdated_server_cover
:check_outdated_full_cover_detail
invoke-virtual {v0}, Lcom/x/models/timelines/instructions/cover/URTFullCover;->getDetailText()Lcom/x/models/text/RichText;
move-result-object v1
if-eqz v1, :outdated_server_cover_allowed
invoke-virtual {v1}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v1
const-string v2, "This app is out of date."
invoke-virtual {v1, v2}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v1
if-nez v1, :block_outdated_server_cover
goto :outdated_server_cover_allowed
:check_outdated_half_cover
instance-of v1, v0, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}HalfCover;
if-eqz v1, :outdated_server_cover_allowed
check-cast v0, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}HalfCover;
invoke-virtual {v0}, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}HalfCover;->getData()Lcom/x/models/timelines/instructions/cover/URTHalfCover;
move-result-object v0
invoke-virtual {v0}, Lcom/x/models/timelines/instructions/cover/URTHalfCover;->getPrimaryText()Lcom/x/models/text/RichText;
move-result-object v1
invoke-virtual {v1}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v1
const-string v2, "This app is out of date."
invoke-virtual {v1, v2}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v1
if-nez v1, :block_outdated_server_cover
invoke-virtual {v0}, Lcom/x/models/timelines/instructions/cover/URTHalfCover;->getSecondaryText()Lcom/x/models/text/RichText;
move-result-object v1
if-eqz v1, :outdated_server_cover_allowed
invoke-virtual {v1}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v1
const-string v2, "This app is out of date."
invoke-virtual {v1, v2}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v1
if-eqz v1, :outdated_server_cover_allowed
:block_outdated_server_cover
return-void
:outdated_server_cover_allowed
"""
)
serverDrivenCoverLifecycleFingerprint.method.addInstructions(
0,
"""
iget-object v0, p0, Lcom/x/urt/cover/component/a;->a:Lcom/x/urt/cover/component/b;
invoke-virtual {v0}, Lcom/x/urt/cover/component/b;->a()Lcom/x/models/timelines/instructions/cover/URTCover;
move-result-object v1
instance-of v2, v1, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}FullCover;
if-eqz v2, :check_retained_outdated_half_cover
check-cast v1, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}FullCover;
invoke-virtual {v1}, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}FullCover;->getData()Lcom/x/models/timelines/instructions/cover/URTFullCover;
move-result-object v1
invoke-virtual {v1}, Lcom/x/models/timelines/instructions/cover/URTFullCover;->getPrimaryText()Lcom/x/models/text/RichText;
move-result-object v2
invoke-virtual {v2}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v2
const-string v3, "This app is out of date."
invoke-virtual {v2, v3}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v2
if-nez v2, :clear_retained_outdated_cover
invoke-virtual {v1}, Lcom/x/models/timelines/instructions/cover/URTFullCover;->getSecondaryText()Lcom/x/models/text/RichText;
move-result-object v2
if-eqz v2, :check_retained_outdated_full_cover_detail
invoke-virtual {v2}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v2
const-string v3, "This app is out of date."
invoke-virtual {v2, v3}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v2
if-nez v2, :clear_retained_outdated_cover
:check_retained_outdated_full_cover_detail
invoke-virtual {v1}, Lcom/x/models/timelines/instructions/cover/URTFullCover;->getDetailText()Lcom/x/models/text/RichText;
move-result-object v2
if-eqz v2, :retained_server_cover_allowed
invoke-virtual {v2}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v2
const-string v3, "This app is out of date."
invoke-virtual {v2, v3}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v2
if-nez v2, :clear_retained_outdated_cover
goto :retained_server_cover_allowed
:check_retained_outdated_half_cover
instance-of v2, v1, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}HalfCover;
if-eqz v2, :retained_server_cover_allowed
check-cast v1, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}HalfCover;
invoke-virtual {v1}, Lcom/x/models/timelines/instructions/cover/URTCover${'$'}HalfCover;->getData()Lcom/x/models/timelines/instructions/cover/URTHalfCover;
move-result-object v1
invoke-virtual {v1}, Lcom/x/models/timelines/instructions/cover/URTHalfCover;->getPrimaryText()Lcom/x/models/text/RichText;
move-result-object v2
invoke-virtual {v2}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v2
const-string v3, "This app is out of date."
invoke-virtual {v2, v3}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v2
if-nez v2, :clear_retained_outdated_cover
invoke-virtual {v1}, Lcom/x/models/timelines/instructions/cover/URTHalfCover;->getSecondaryText()Lcom/x/models/text/RichText;
move-result-object v2
if-eqz v2, :retained_server_cover_allowed
invoke-virtual {v2}, Lcom/x/models/text/RichText;->getText()Ljava/lang/String;
move-result-object v2
const-string v3, "This app is out of date."
invoke-virtual {v2, v3}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v2
if-eqz v2, :retained_server_cover_allowed
:clear_retained_outdated_cover
iget-object v1, v0, Lcom/x/urt/cover/component/b;->c:Lcom/x/urt/cover/component/e${'$'}a;
iget-object v1, v1, Lcom/x/urt/cover/component/e${'$'}a;->a:Lcom/twitter/tipjar/ui/screen/c;
invoke-virtual {v1}, Lcom/twitter/tipjar/ui/screen/c;->invoke()Ljava/lang/Object;
return-void
:retained_server_cover_allowed
"""
)
brazeInAppMessageDisplayFingerprint.method.addInstructions(
0,
"""
move-object/from16 v0, p1
instance-of v1, v0, Lcom/braze/models/inappmessage/InAppMessageImmersiveBase;
if-eqz v1, :check_braze_outdated_message_body
move-object v1, v0
check-cast v1, Lcom/braze/models/inappmessage/InAppMessageImmersiveBase;
invoke-virtual {v1}, Lcom/braze/models/inappmessage/InAppMessageImmersiveBase;->getHeader()Ljava/lang/String;
move-result-object v1
if-eqz v1, :check_braze_outdated_message_body
const-string v2, "This app is out of date."
invoke-virtual {v1, v2}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v1
if-nez v1, :block_braze_outdated_message
:check_braze_outdated_message_body
instance-of v1, v0, Lcom/braze/models/inappmessage/InAppMessageBase;
if-eqz v1, :braze_in_app_message_allowed
check-cast v0, Lcom/braze/models/inappmessage/InAppMessageBase;
invoke-virtual {v0}, Lcom/braze/models/inappmessage/InAppMessageBase;->getMessage()Ljava/lang/String;
move-result-object v0
if-eqz v0, :braze_in_app_message_allowed
const-string v1, "This app is out of date."
invoke-virtual {v0, v1}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v1
if-nez v1, :block_braze_outdated_message
const-string v1, "current app version is unsupported"
invoke-virtual {v0, v1}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v0
if-eqz v0, :braze_in_app_message_allowed
:block_braze_outdated_message
sget-object v0, Lkotlin/Unit;->a:Lkotlin/Unit;
return-object v0
:braze_in_app_message_allowed
"""
)
} }
} }
@@ -0,0 +1,32 @@
package app.twitter.patches
import app.morphe.patcher.Fingerprint
import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
import app.morphe.patcher.patch.bytecodePatch
import app.twitter.patches.shared.Constants.COMPATIBILITY_X
private val postAgeVerificationActionFingerprint = Fingerprint(
definingClass = "Lcom/twitter/model/mediavisibility/c${'$'}b;",
name = "a",
returnType = "Lcom/twitter/model/mediavisibility/c;",
parameters = listOf("Ljava/lang/String;")
)
@Suppress("unused")
val removePostAgeVerificationPatch = bytecodePatch(
name = "Remove post age verification",
description = "Allows age-gated posts to be viewed without completing age verification.",
default = true
) {
compatibleWith(COMPATIBILITY_X)
execute {
postAgeVerificationActionFingerprint.method.addInstructions(
0,
"""
const/4 v0, 0x0
return-object v0
"""
)
}
}
@@ -0,0 +1,32 @@
package app.twitter.patches
import app.morphe.patcher.patch.resourcePatch
import app.twitter.patches.shared.Constants.COMPATIBILITY_X
import org.w3c.dom.Element
private const val TOOLBAR_ITEMS_PATH = "res/menu/toolbar_items.xml"
private const val UPGRADE_BUTTON_ID = "premium_upsell_upgrade"
@Suppress("unused")
val removeTimelineUpgradeButtonPatch = resourcePatch(
name = "Remove timeline Upgrade button",
description = "Removes the Premium Upgrade button from the timeline's top bar.",
default = true
) {
compatibleWith(COMPATIBILITY_X)
execute {
document(TOOLBAR_ITEMS_PATH).use { document ->
val menu = document.documentElement
val upgradeButton = (0 until menu.childNodes.length)
.asSequence()
.map { menu.childNodes.item(it) }
.filterIsInstance<Element>()
.single {
it.getAttribute("android:id").substringAfterLast('/') == UPGRADE_BUTTON_ID
}
menu.removeChild(upgradeButton)
}
}
}