chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00

387 lines
11 KiB
Go

// File-server-Loop für MobileBackup2 Restore-Mode.
//
// Während Restore drives das iPhone das gespräch:
// - Es schickt DLMessageDownloadFiles mit Filenamen
// - Es erwartet Datei-Content via custom binary chunks (NICHT DLMessage!)
//
// Wire-Format für File-Transfer (auf demselben Socket wie DLMessage):
//
// → [4-byte filename-length big-endian][filename UTF-8]
// → [4-byte code=12 DATA][4-byte 0 length] (start)
// → [4-byte code=12 DATA][4-byte chunk-length][chunk-data] (repeat)
// → [4-byte code=0 DONE][4-byte 0]
//
// Nach allen Files:
// → [4-byte 0 — no more files]
// → DLMessageStatusResponse [0, "___EmptyParameterString___", {}]
//
// Codes:
//
// CODE_FILE_DONE = 0 (end-of-file marker)
// CODE_FILE_DATA = 12 (data chunk)
// CODE_FILE_ERROR = 6 (error)
package mobilebackup2
import (
"encoding/binary"
"errors"
"fmt"
"io"
"strconv"
"github.com/raynis/rebreak-supervise-magic/internal/dlmessage"
)
// 1-byte chunk-codes (matched libimobiledevice's CODE_FILE_DATA etc).
const (
fileCodeDone byte = 0x00 // CODE_SUCCESS — end of file
fileCodeData byte = 0x0c // CODE_FILE_DATA — chunk
fileCodeError byte = 0x06 // CODE_ERROR_LOCAL — file not found / error
// Apple sentinel: status-response "no error".
emptyParameterString = "___EmptyParameterString___"
)
// FileProvider liefert Datei-Content für einen relativen Pfad.
// Returnt (content, true) wenn vorhanden, ([], false) sonst.
type FileProvider func(relpath string) ([]byte, bool)
// ServeFiles fängt das iPhone-driven file-serve-loop ab.
// Loopt bis DLMessageDisconnect oder Error.
//
// Files werden via `provider` geliefert. Wenn provider eine angefragte
// Datei nicht hat: Server returnt CODE_FILE_ERROR.
//
// onProgress wird bei jedem Mess-event aufgerufen (für UI/Log).
func (c *Client) ServeFiles(provider FileProvider, onProgress func(event string, info string)) error {
if onProgress == nil {
onProgress = func(string, string) {}
}
// Wenn iPhone uns ein ProcessMessage:Response mit ErrorCode=0 schickt,
// signalisiert das: Restore-Operation completed successfully. Danach
// trennt das iPhone die USB-Verbindung um in den Restore-Mode zu booten →
// wir bekommen ein EOF. Das ist KEIN Fehler — es ist erwartetes Verhalten.
successConfirmed := false
for {
t, args, err := c.dl.Receive()
if err != nil {
if successConfirmed {
// iPhone hat success-Response geschickt + disconnected um zu rebooten.
onProgress("ServeFiles", "device disconnected after successful Response — reboot expected")
return nil
}
return fmt.Errorf("serve: receive: %w", err)
}
switch t {
case dlmessage.TypeProcessMessage:
if len(args) == 0 {
continue
}
dict, ok := args[0].(map[string]interface{})
if !ok {
continue
}
msgName, _ := dict["MessageName"].(string)
// 2026-05-28 DEBUG: log full dict to understand what iPhone sends
onProgress("ProcessMessage", fmt.Sprintf("%s | full=%v", msgName, dict))
if msgName == "DLMessageDisconnect" || msgName == "Disconnect" {
return nil
}
// "Response" = iPhone signals operation completed — NICHT antworten,
// nur weiter loopen + auf nächste Message warten.
// Wenn ErrorCode=0 → success → nachfolgender EOF ist erwartet.
if msgName == "Response" {
ed, _ := dict["ErrorDescription"].(string)
if ec, ok := dict["ErrorCode"]; ok {
code, parsed := parseErrorCode(ec)
if !parsed {
return fmt.Errorf("serve: response error (unknown code type %T): %s", ec, ed)
}
if code == 0 {
successConfirmed = true
} else {
return fmt.Errorf("serve: response error code=%d: %s", code, ed)
}
} else {
if ed != "" {
return fmt.Errorf("serve: response without error code: %s", ed)
}
}
continue
}
// Andere Sub-Messages — Status OK respond.
if err := c.sendStatusOK(); err != nil {
return err
}
case dlmessage.TypeStatusResponse:
// Device-Status — wir loggen + machen weiter
onProgress("StatusResponse", fmt.Sprintf("%v", args))
// Falls error: abbrechen
if len(args) > 0 {
if code, ok := args[0].(uint64); ok && code != 0 {
return fmt.Errorf("serve: device error code=%d: %v", code, args)
}
}
case dlmessage.TypeGetFreeDiskSpace:
// Device fragt nach freiem Speicher — wir behaupten Schubladen voll Platz
onProgress("GetFreeDiskSpace", "")
// Antwort: [0, errStr, freeSpaceUint64]
if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, uint64(1<<40)); err != nil {
return err
}
case dlmessage.TypeContentsOfDirectory:
// Device möchte Verzeichnis listen — wir antworten mit empty dict
dirname := ""
if len(args) > 0 {
dirname, _ = args[0].(string)
}
onProgress("ContentsOfDirectory", dirname)
// Antwort: status [0, "", {}]
if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{}); err != nil {
return err
}
case dlmessage.TypeCreateDirectory, dlmessage.TypeMoveItems, dlmessage.TypeRemoveItems, dlmessage.TypeCopyItem:
onProgress(t, "")
if err := c.sendStatusOK(); err != nil {
return err
}
case dlmessage.TypeDownloadFiles:
// Device fragt files an — args[0] ist []string
var fileList []string
if len(args) > 0 {
if arr, ok := args[0].([]interface{}); ok {
for _, item := range arr {
if s, ok := item.(string); ok {
fileList = append(fileList, s)
}
}
}
}
onProgress("DownloadFiles", fmt.Sprintf("count=%d", len(fileList)))
if err := c.sendFiles(fileList, provider, onProgress); err != nil {
return fmt.Errorf("serve: send files: %w", err)
}
case dlmessage.TypeUploadFiles:
// iPhone will UNS files uploaden (sein current state).
// Wir ACK + receive raw upload-stream.
onProgress("UploadFiles", "device uploading")
if err := c.sendStatusOK(); err != nil {
return err
}
if err := c.receiveFiles(onProgress); err != nil {
return fmt.Errorf("serve: receive files: %w", err)
}
case dlmessage.TypeDisconnect:
onProgress("Disconnect", "explicit")
return nil
default:
// unbekannter Typ — log + status OK
onProgress("Unknown", t)
if err := c.sendStatusOK(); err != nil {
return err
}
}
}
}
func (c *Client) sendStatusOK() error {
return c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{})
}
func parseErrorCode(v interface{}) (int64, bool) {
switch n := v.(type) {
case uint64:
return int64(n), true
case int64:
return n, true
case int:
return int64(n), true
case uint32:
return int64(n), true
case int32:
return int64(n), true
case float64:
return int64(n), true
case string:
parsed, err := strconv.ParseInt(n, 10, 64)
if err != nil {
return 0, false
}
return parsed, true
default:
return 0, false
}
}
// sendFiles antwortet auf DLMessageDownloadFiles. Wire-format aus libimobiledevice
// mb2_handle_send_file verifiziert:
//
// per file:
// [4-byte BE filename-length][filename UTF-8]
// if file exists:
// per chunk: [4-byte BE (1+chunk-size)][1-byte 0x0c CODE_DATA][chunk-data]
// end-of-file: [4-byte BE 1][1-byte 0x00 CODE_SUCCESS]
// else:
// [4-byte BE 1][1-byte 0x06 CODE_ERROR_LOCAL]
//
// after all files:
// [4-byte BE 0] (terminator = "no more files")
// DLMessageStatusResponse [0, "___EmptyParameterString___", {<errors>}]
func (c *Client) sendFiles(fileList []string, provider FileProvider, onProgress func(string, string)) error {
for _, fname := range fileList {
content, ok := provider(fname)
onProgress("send-file", fmt.Sprintf("%s (%d bytes, found=%v)", fname, len(content), ok))
// filename header
if err := c.writeRawU32(uint32(len(fname))); err != nil {
return err
}
if err := c.deviceConn.Send([]byte(fname)); err != nil {
return fmt.Errorf("write filename: %w", err)
}
if !ok {
// ERROR: [length=1][CODE_ERROR_LOCAL]
if err := c.writeRawU32(1); err != nil {
return err
}
if err := c.deviceConn.Send([]byte{fileCodeError}); err != nil {
return err
}
continue
}
// DATA chunks
const maxChunk = 32 * 1024
for offset := 0; offset < len(content); offset += maxChunk {
end := offset + maxChunk
if end > len(content) {
end = len(content)
}
chunk := content[offset:end]
// [length=1+chunk][CODE_DATA][chunk]
if err := c.writeRawU32(uint32(1 + len(chunk))); err != nil {
return err
}
if err := c.deviceConn.Send([]byte{fileCodeData}); err != nil {
return err
}
if err := c.deviceConn.Send(chunk); err != nil {
return err
}
}
// END-OF-FILE: [length=1][CODE_SUCCESS]
if err := c.writeRawU32(1); err != nil {
return err
}
if err := c.deviceConn.Send([]byte{fileCodeDone}); err != nil {
return err
}
}
// Terminator: [length=0]
if err := c.writeRawU32(0); err != nil {
return err
}
// Final status
return c.sendStatusOK()
}
func (c *Client) writeRawU32(v uint32) error {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, v)
return c.deviceConn.Send(buf)
}
// receiveFiles empfängt iPhone's file-upload-stream nach DLMessageUploadFiles.
// Wire-format mirror von sendFiles (gleiche frames, andere Richtung):
//
// per file:
// [4-byte BE path-len][path]
// per chunk: [4-byte BE (1+chunk-size)][1-byte code][data]
// end-of-file: [4-byte BE 1][1-byte 0x00 SUCCESS]
// terminator: [4-byte BE 0]
//
// Wir discardieren den Inhalt — iPhone's "backup-of-backup" interessiert uns nicht,
// nur dass wir den Stream sauber durchlesen damit das Protokoll weitergeht.
func (c *Client) receiveFiles(onProgress func(string, string)) error {
reader := c.deviceConn.Reader()
totalFiles := 0
totalBytes := uint64(0)
for {
// path-length-prefix
var pathLenBuf [4]byte
if _, err := io.ReadFull(reader, pathLenBuf[:]); err != nil {
return fmt.Errorf("read path-len: %w", err)
}
pathLen := binary.BigEndian.Uint32(pathLenBuf[:])
if pathLen == 0 {
// terminator — no more files
onProgress("upload-done", fmt.Sprintf("%d files, %d bytes total", totalFiles, totalBytes))
return nil
}
if pathLen > 65536 {
return fmt.Errorf("upload-path too large: %d", pathLen)
}
// path-bytes
pathBuf := make([]byte, pathLen)
if _, err := io.ReadFull(reader, pathBuf); err != nil {
return fmt.Errorf("read path: %w", err)
}
path := string(pathBuf)
totalFiles++
// chunks
fileBytes := uint64(0)
for {
var chunkLenBuf [4]byte
if _, err := io.ReadFull(reader, chunkLenBuf[:]); err != nil {
return fmt.Errorf("read chunk-len for %s: %w", path, err)
}
chunkLen := binary.BigEndian.Uint32(chunkLenBuf[:])
if chunkLen == 0 {
// some implementations send zero-length as terminator
break
}
// 1-byte code
var code [1]byte
if _, err := io.ReadFull(reader, code[:]); err != nil {
return fmt.Errorf("read chunk-code: %w", err)
}
dataLen := chunkLen - 1
if dataLen > 0 {
discard := make([]byte, dataLen)
if _, err := io.ReadFull(reader, discard); err != nil {
return fmt.Errorf("read chunk-data: %w", err)
}
fileBytes += uint64(dataLen)
}
// code 0x00 (CODE_SUCCESS) = end-of-file
if code[0] == fileCodeDone {
break
}
}
totalBytes += fileBytes
onProgress("upload-file", fmt.Sprintf("%s (%d bytes)", path, fileBytes))
}
}
// ErrServeAborted ist ein Sentinel den FileProvider zurückgeben kann um Loop
// zu beenden (z.B. bei Disconnect-Trigger).
var ErrServeAborted = errors.New("serve aborted")