Browse Source

feat(sessionimport): convert Pyrogram SQLite session to gotd session.Data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dot 2 tuần trước cách đây
mục cha
commit
d6b22c911b

+ 62 - 0
internal/telegram/sessionimport/pyrogram.go

@@ -0,0 +1,62 @@
+package sessionimport
+
+import (
+	"crypto/sha1"
+	"database/sql"
+	"fmt"
+
+	"github.com/gotd/td/session"
+	_ "modernc.org/sqlite"
+)
+
+// ConvertPyrogramSession reads a Pyrogram/Telethon SQLite session file and
+// builds a gotd/td session.Data representing the same authorization.
+//
+// Pyrogram schema (single row in `sessions` table):
+//
+//	CREATE TABLE sessions (
+//	  dc_id     INTEGER PRIMARY KEY,
+//	  api_id    INTEGER,
+//	  test_mode INTEGER,
+//	  auth_key  BLOB,         -- 256 bytes
+//	  date      INTEGER NOT NULL,
+//	  user_id   INTEGER,
+//	  is_bot    INTEGER);
+func ConvertPyrogramSession(sqlitePath string) (*session.Data, error) {
+	// Open READ-ONLY + IMMUTABLE. Pyrogram .session files may be in-use by a
+	// running Pyrogram client; a write-capable handle could corrupt them via
+	// journal rollback on crash.
+	dsn := fmt.Sprintf("file:%s?mode=ro&immutable=1", sqlitePath)
+	db, err := sql.Open("sqlite", dsn)
+	if err != nil {
+		return nil, fmt.Errorf("open pyrogram sqlite: %w", err)
+	}
+	defer db.Close()
+
+	var dcID int
+	var authKey []byte
+	err = db.QueryRow(`SELECT dc_id, auth_key FROM sessions LIMIT 1`).Scan(&dcID, &authKey)
+	if err != nil {
+		return nil, fmt.Errorf("read pyrogram sessions row: %w", err)
+	}
+	if len(authKey) != 256 {
+		return nil, fmt.Errorf("invalid auth_key length: %d (want 256)", len(authKey))
+	}
+
+	addr, err := TGDCAddr(dcID)
+	if err != nil {
+		return nil, err
+	}
+
+	// auth_key_id = SHA-1(auth_key)[12:20] — lower 64 bits of the SHA-1 digest.
+	sum := sha1.Sum(authKey)
+	keyID := make([]byte, 8)
+	copy(keyID, sum[12:20])
+
+	return &session.Data{
+		DC:        dcID,
+		Addr:      addr,
+		AuthKey:   authKey,
+		AuthKeyID: keyID,
+	}, nil
+}

+ 40 - 0
internal/telegram/sessionimport/pyrogram_test.go

@@ -0,0 +1,40 @@
+package sessionimport
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+// Real protocol-number session file lives at D:\spider\tgs\13252753163\13252753163.session
+// Test skips when that file is missing so CI environments don't break.
+func TestConvertPyrogramSession_RealFile(t *testing.T) {
+	path := `D:\spider\tgs\13252753163\13252753163.session`
+	if _, err := os.Stat(path); err != nil {
+		t.Skipf("real session file absent: %v", err)
+	}
+
+	data, err := ConvertPyrogramSession(path)
+	if err != nil {
+		t.Fatalf("convert failed: %v", err)
+	}
+	if data.DC < 1 || data.DC > 5 {
+		t.Errorf("unexpected dc: %d", data.DC)
+	}
+	if len(data.AuthKey) != 256 {
+		t.Errorf("auth_key length = %d, want 256", len(data.AuthKey))
+	}
+	if len(data.AuthKeyID) != 8 {
+		t.Errorf("auth_key_id length = %d, want 8", len(data.AuthKeyID))
+	}
+	if data.Addr == "" {
+		t.Error("addr empty")
+	}
+}
+
+func TestConvertPyrogramSession_MissingFile(t *testing.T) {
+	missing := filepath.Join(t.TempDir(), "does-not-exist.session")
+	if _, err := ConvertPyrogramSession(missing); err == nil {
+		t.Error("expected error for missing file")
+	}
+}