Compare commits

..

17 Commits

Author SHA1 Message Date
Sunny Young
ebdcb7bbd2 config: add version 32288 support 2025-12-11 15:10:03 +08:00
Sunny Young
2a9978b046 config: add version 31960 support 2025-12-10 00:10:09 +08:00
Sunny Young
ac9b1575de command: optimize with option group 2025-12-10 00:08:47 +08:00
Sunny Young
0682644327 reorder code blocks 2025-12-08 14:52:08 +08:00
Sunny Young
3cc0f2c5c6 update README.md 2025-12-07 15:46:34 +08:00
Sunny Young
f8226536dc update README.md 2025-12-07 15:13:48 +08:00
Sunny Young
87d4854174 update README.md 2025-12-07 15:05:38 +08:00
Sunny Young
4f801b35f2 update README.md 2025-12-07 13:59:22 +08:00
Sunny Young
6d3c75faec config: replace with master url 2025-12-07 13:52:41 +08:00
Sunny Young
6922703e57 update README.md 2025-12-07 13:49:14 +08:00
Sunny Young
171c352dce command: fix default command flow to show help instead of error 2025-12-07 13:49:14 +08:00
Sunny Young
a58f04b1d4 subcommand: add versions 2025-12-07 13:49:14 +08:00
Sunny Young
003643d063 config: add version 32281 support 2025-12-07 13:49:14 +08:00
Sunny Young
4b1bd7c2ab refactor: use async/await instead of promises
support both local and network config
2025-12-07 13:49:14 +08:00
Sunny Young
540f13b35b config: add support for multiple instances 2025-12-07 13:49:14 +08:00
Sunny Young
723e982bf3 fix a config.json reading issue 2025-12-07 13:49:14 +08:00
Sunny Young
8e13e947d5 refactor: support WeChat 4.x 2025-12-07 13:49:14 +08:00
9 changed files with 382 additions and 137 deletions

View File

@ -1 +0,0 @@
2.7.8

View File

@ -1,5 +0,0 @@
# Contributing
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
[![Contributors](https://opencollective.com/WeChatTweak-macOS/contributors.svg?width=890&button=false)](https://github.com/Sunnyyoung/WeChatTweak-macOS/graphs/contributors)

View File

@ -1,25 +1,15 @@
{ {
"object": { "originHash" : "fc7f739a75c4c771c736e9ff1f765bbd53fb1f3c5beaa621fbc867f0d71964c4",
"pins": [ "pins" : [
{ {
"package": "PromiseKit", "identity" : "swift-argument-parser",
"repositoryURL": "https://github.com/mxcl/PromiseKit", "kind" : "remoteSourceControl",
"state": { "location" : "https://github.com/apple/swift-argument-parser",
"branch": null, "state" : {
"revision": "2bc44395edb4f8391902a9ff7c220471882a4d07", "revision" : "cdd0ef3755280949551dc26dee5de9ddeda89f54",
"version": "8.2.0" "version" : "1.6.2"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "cdd0ef3755280949551dc26dee5de9ddeda89f54",
"version": "1.6.2"
} }
} }
] ],
}, "version" : 3
"version": 1
} }

View File

@ -16,14 +16,12 @@ let package = Package(
) )
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/mxcl/PromiseKit", from: "8.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.6.0")
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")
], ],
targets: [ targets: [
.executableTarget( .executableTarget(
name: "WeChatTweak", name: "WeChatTweak",
dependencies: [ dependencies: [
"PromiseKit",
.product(name: "ArgumentParser", package: "swift-argument-parser") .product(name: "ArgumentParser", package: "swift-argument-parser")
] ]
) )

View File

@ -1,17 +1,31 @@
# WeChatTweak # WeChatTweak
[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](LICENSE) [![README](https://img.shields.io/badge/GitHub-black?logo=github&logoColor=white)](https://github.com/sunnyyoung/WeChatTweak)
[![README](https://img.shields.io/badge/Telegram-WeChatTweak-brightgreen.svg)](https://t.me/wechattweak) [![README](https://img.shields.io/badge/Telegram-black?logo=telegram&logoColor=white)](https://t.me/wechattweak)
[![README](https://img.shields.io/badge/FAQ-black?logo=googledocs&logoColor=white)](https://github.com/sunnyyoung/WeChatTweak/wiki/FAQ)
A command-line tool for tweaking WeChat.
## 功能 ## 功能
- [x] 阻止消息撤回 - 阻止消息撤回
- [ ] 客户端无限多开 - 阻止自动更新
- 客户端多开
## 使用 ## 安装&使用
```bash ```bash
# 安装
brew install sunnyyoung/tap/wechattweak
# 更新
brew upgrade wechattweak
# 执行 Patch
wechattweak patch wechattweak patch
# 查看所有支持的 WeChat 版本
wechattweak versions
``` ```
## 参考 ## 参考
@ -22,9 +36,9 @@ wechattweak patch
## 贡献者 ## 贡献者
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. This project exists thanks to all the people who contribute.
[![Contributors](https://opencollective.com/WeChatTweak-macOS/contributors.svg?width=890&button=false)](https://github.com/Sunnyyoung/WeChatTweak-macOS/graphs/contributors) [![Contributors](https://contrib.rocks/image?repo=sunnyyoung/WeChatTweak)](https://github.com/sunnyyoung/WeChatTweak/graphs/contributors)
## License ## License

View File

@ -5,7 +5,6 @@
// //
import Foundation import Foundation
import PromiseKit
import ArgumentParser import ArgumentParser
struct Command { struct Command {
@ -15,46 +14,44 @@ struct Command {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case let .executing(command, error): case let .executing(command, error):
return "Execute command: \(command) failed: \(error)" return "executing: \(command) error: \(error)"
} }
} }
} }
static func patch(app: URL, config: Config) -> Promise<Void> { static func version(app: URL) async throws -> String? {
print("------ Path ------") try await Command.execute(command: "defaults read \(app.appendingPathComponent("Contents/Info.plist").path) CFBundleVersion")
return Promise { seal in
do {
seal.fulfill(try Patcher.patch(binary: app.appendingPathComponent("Contents/MacOS/WeChat"), config: config))
} catch {
seal.reject(error)
}
}
} }
static func resign(app: URL) -> Promise<Void> { static func patch(app: URL, config: Config) async throws {
print("------ Resign ------") try Patcher.patch(binary: app.appendingPathComponent("Contents/MacOS/WeChat"), config: config)
return firstly {
Command.execute(command: "codesign --remove-sign \(app.path)")
}.then {
Command.execute(command: "codesign --force --deep --sign - \(app.path)")
}.then {
Command.execute(command: "xattr -cr \(app.path)")
}
} }
private static func execute(command: String) -> Promise<Void> { static func resign(app: URL) async throws {
return Promise { seal in try await Command.execute(command: "codesign --remove-sign \(app.path)")
print("Execute command: \(command)") try await Command.execute(command: "codesign --force --deep --sign - \(app.path)")
var error: NSDictionary? try await Command.execute(command: "xattr -cr \(app.path)")
}
@discardableResult
private static func execute(command: String) async throws -> String? {
guard let script = NSAppleScript(source: "do shell script \"\(command)\"") else { guard let script = NSAppleScript(source: "do shell script \"\(command)\"") else {
return seal.reject(Error.executing(command: command, error: ["error": "Create script failed."])) throw Error.executing(
command: command,
error: ["error": "Create script failed."]
)
} }
script.executeAndReturnError(&error)
var error: NSDictionary?
let descriptor = script.executeAndReturnError(&error)
if let error = error { if let error = error {
seal.reject(Error.executing(command: command, error: error)) throw Error.executing(
command: command,
error: error
)
} else { } else {
seal.fulfill(()) return descriptor.stringValue
}
} }
} }
} }

View File

@ -80,6 +80,20 @@ struct Config: Decodable {
let version: String let version: String
let targets: [Target] let targets: [Target]
static func load(url: URL) async throws -> [Config] {
if url.isFileURL {
return try JSONDecoder().decode(
[Config].self,
from: Data(contentsOf: url)
)
} else {
return try JSONDecoder().decode(
[Config].self,
from: try await URLSession.shared.data(from: url).0
)
}
}
} }
private extension Data { private extension Data {

View File

@ -5,13 +5,71 @@
// //
import Foundation import Foundation
import PromiseKit import Dispatch
import ArgumentParser import ArgumentParser
struct Patch: ParsableCommand { // MARK: Versions
extension Tweak {
struct Versions: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "List all supported WeChat versions")
@OptionGroup
var options: Tweak.Options
mutating func run() async throws {
print("------ Current version ------")
print(try await Command.version(app: options.app) ?? "unknown")
print("------ Supported versions ------")
try await Config.load(url: options.config).forEach({ print($0.version) })
Darwin.exit(EXIT_SUCCESS)
}
}
}
// MARK: Patch
extension Tweak {
struct Patch: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Patch WeChat.app")
@OptionGroup
var options: Tweak.Options
mutating func run() async throws {
print("------ Version ------")
let version = try await Command.version(app: options.app)
print("WeChat version: \(version ?? "unknown")")
print("------ Config ------")
guard let config = (try await Config.load(url: options.config)).first(where: { $0.version == version }) else {
throw Error.unsupportedVersion
}
print("Matched config: \(config)")
print("------ Patch ------")
try await Command.patch(
app: options.app,
config: config
)
print("Done!")
print("------ Resign ------")
try await Command.resign(
app: options.app
)
print("Done!")
Darwin.exit(EXIT_SUCCESS)
}
}
}
// MARK: Tweak
struct Tweak: AsyncParsableCommand {
enum Error: LocalizedError { enum Error: LocalizedError {
case invalidApp case invalidApp
case invalidConfig case invalidConfig
case invalidVersion
case unsupportedVersion case unsupportedVersion
var errorDescription: String? { var errorDescription: String? {
@ -20,17 +78,18 @@ struct Patch: ParsableCommand {
return "Invalid app path" return "Invalid app path"
case .invalidConfig: case .invalidConfig:
return "Invalid patch config" return "Invalid patch config"
case .invalidVersion:
return "Invalid app version"
case .unsupportedVersion: case .unsupportedVersion:
return "Unsupported WeChat version" return "Unsupported WeChat version"
} }
} }
} }
static let configuration = CommandConfiguration(abstract: "Patch WeChat.app") struct Options: ParsableArguments {
@Option( @Option(
name: .shortAndLong, name: .shortAndLong,
help: "Default: /Applications/WeChat.app", help: "Path of WeChat.app",
transform: { transform: {
guard FileManager.default.fileExists(atPath: $0) else { guard FileManager.default.fileExists(atPath: $0) else {
throw Error.invalidApp throw Error.invalidApp
@ -42,64 +101,38 @@ struct Patch: ParsableCommand {
@Option( @Option(
name: .shortAndLong, name: .shortAndLong,
help: "Default: ./config.json", help: "Local path or Remote URL of config.json",
transform: { transform: {
guard FileManager.default.fileExists(atPath: $0) else { if FileManager.default.fileExists(atPath: $0) {
return URL(fileURLWithPath: $0)
} else {
guard let url = URL(string: $0) else {
throw Error.invalidConfig throw Error.invalidConfig
} }
return URL(fileURLWithPath: $0) return url
}
} }
) )
var config: URL = { var config: URL = URL(string:"https://raw.githubusercontent.com/sunnyyoung/WeChatTweak/refs/heads/master/config.json")!
var size: UInt32 = 0
_NSGetExecutablePath(nil, &size)
var buffer = [CChar](repeating: 0, count: Int(size))
guard _NSGetExecutablePath(&buffer, &size) == 0, let path = String(utf8String: buffer) else {
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}
return URL(fileURLWithPath: path).resolvingSymlinksInPath().deletingLastPathComponent().appendingPathComponent("config.json")
}()
func run() throws {
let configs = try JSONDecoder().decode([Config].self, from: Data(contentsOf: self.config))
guard
let info = NSDictionary(contentsOf: self.app.appendingPathComponent("Contents/Info.plist")),
let version = info["CFBundleVersion"] as? String,
let config = configs.first(where: { $0.version == version })
else {
throw Error.unsupportedVersion
} }
firstly {
Command.patch(
app: self.app,
config: config
)
}.then {
Command.resign(app: self.app)
}.ensure {
print("")
}.done {
print("🎉 Done!")
Darwin.exit(EXIT_SUCCESS)
}.catch { error in
print("🚨 \(error.localizedDescription)", stderr)
Darwin.exit(EXIT_FAILURE)
}
}
}
struct Tweak: ParsableCommand {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "wechattweak", commandName: "wechattweak",
abstract: "A command-line tool for tweaking WeChat.", abstract: "A command-line tool for tweaking WeChat.",
subcommands: [ subcommands: [
Versions.self,
Patch.self Patch.self
], ]
defaultSubcommand: Self.self
) )
mutating func run() async throws {
print(Tweak.helpMessage())
Darwin.exit(EXIT_SUCCESS)
}
} }
Tweak.main() Task {
CFRunLoopRun() await Tweak.main()
}
Dispatch.dispatchMain()

View File

@ -71,6 +71,211 @@
"asm": "00008052C0035FD6" "asm": "00008052C0035FD6"
} }
] ]
},
{
"identifier": "multiInstance",
"entries": [
{
"arch": "arm64",
"addr": "1001e1c10",
"asm": "20008052C0035FD6"
}
]
}
]
},
{
"version": "32281",
"targets": [
{
"identifier": "revoke",
"entries": [
{
"arch": "arm64",
"addr": "103db33e0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "startUpdater",
"entries": [
{
"arch": "arm64",
"addr": "1001e9ed0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "startBackgroundUpdatesCheck",
"entries": [
{
"arch": "arm64",
"addr": "1001ecb10",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "checkForUpdates",
"entries": [
{
"arch": "arm64",
"addr": "1001ec73c",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "enableAutoUpdate",
"entries": [
{
"arch": "arm64",
"addr": "1001ecfc0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "automaticallyDownloadsUpdates",
"entries": [
{
"arch": "arm64",
"addr": "1001f59e0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "canCheckForUpdate",
"entries": [
{
"arch": "arm64",
"addr": "1001f59e0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "multiInstance",
"entries": [
{
"arch": "arm64",
"addr": "1001e1a74",
"asm": "20008052C0035FD6"
}
]
}
]
},
{
"version": "32288",
"targets": [
{
"identifier": "revoke",
"entries": [
{
"arch": "arm64",
"addr": "103db34c0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "startUpdater",
"entries": [
{
"arch": "arm64",
"addr": "1001e9ed0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "startBackgroundUpdatesCheck",
"entries": [
{
"arch": "arm64",
"addr": "1001ecb10",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "checkForUpdates",
"entries": [
{
"arch": "arm64",
"addr": "1001ec73c",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "enableAutoUpdate",
"entries": [
{
"arch": "arm64",
"addr": "1001ecfc0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "automaticallyDownloadsUpdates",
"entries": [
{
"arch": "arm64",
"addr": "1001f59e0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "canCheckForUpdate",
"entries": [
{
"arch": "arm64",
"addr": "1001f59e0",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "multiInstance",
"entries": [
{
"arch": "arm64",
"addr": "1001e1a74",
"asm": "20008052C0035FD6"
}
]
}
]
},
{
"version": "31960",
"targets": [
{
"identifier": "revoke",
"entries": [
{
"arch": "arm64",
"addr": "10408a408",
"asm": "00008052C0035FD6"
}
]
},
{
"identifier": "multiInstance",
"entries": [
{
"arch": "arm64",
"addr": "1001e4a38",
"asm": "20008052C0035FD6"
}
]
} }
] ]
} }