400 lines
11 KiB
Go
400 lines
11 KiB
Go
// Command appearancefetch 从官方 Telegram 拉取墙纸 + 聊天主题,下载文档/缩略图,
|
|
// 生成 telesrv 外观 seed(Default_appearance_seed.json + Default_wallpapers/{documents,thumbs/m}/*.dat)。
|
|
// 复用 internal/seed/appearance 的结构体保证 schema 完全一致。peer_colors 从现有 JSON 沿用。
|
|
//
|
|
// 需登录(墙纸/主题接口非免登)。api 凭据用 TDesktop 开源公开的 id/hash。
|
|
// 子命令:
|
|
//
|
|
// appearancefetch sendcode env: PHONE 发送登录码,打印 CODE_HASH
|
|
// appearancefetch fetch <out> <carry> env: PHONE CODE CODE_HASH PASSWORD 登录+拉取+生成
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/iamxvbaba/td/telegram"
|
|
"github.com/iamxvbaba/td/telegram/auth"
|
|
"github.com/iamxvbaba/td/telegram/downloader"
|
|
"github.com/iamxvbaba/td/tg"
|
|
|
|
"telesrv/internal/seed/appearance"
|
|
)
|
|
|
|
const (
|
|
apiID = 17349
|
|
apiHash = "344583e45741c457fe1862106095a5eb"
|
|
)
|
|
|
|
func sessionPath() string {
|
|
if p := os.Getenv("SESSION"); p != "" {
|
|
return p
|
|
}
|
|
return "/tmp/appearance.session"
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: appearancefetch sendcode | appearancefetch fetch <out_dir> <carry_json>")
|
|
os.Exit(2)
|
|
}
|
|
cmd := os.Args[1]
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
|
defer cancel()
|
|
|
|
client := telegram.NewClient(apiID, apiHash, telegram.Options{
|
|
SessionStorage: &telegram.FileSessionStorage{Path: sessionPath()},
|
|
})
|
|
err := client.Run(ctx, func(ctx context.Context) error {
|
|
switch cmd {
|
|
case "sendcode":
|
|
return doSendCode(ctx, client)
|
|
case "fetch":
|
|
if len(os.Args) < 3 {
|
|
return errors.New("fetch needs <out_dir>")
|
|
}
|
|
return doFetch(ctx, client, os.Args[2])
|
|
default:
|
|
return fmt.Errorf("unknown cmd %q", cmd)
|
|
}
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "ERROR:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func doSendCode(ctx context.Context, client *telegram.Client) error {
|
|
phone := os.Getenv("PHONE")
|
|
if phone == "" {
|
|
return errors.New("PHONE env required")
|
|
}
|
|
sent, err := client.Auth().SendCode(ctx, phone, auth.SendCodeOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("sendCode: %w", err)
|
|
}
|
|
code, ok := sent.(*tg.AuthSentCode)
|
|
if !ok {
|
|
return fmt.Errorf("sentCode = %T", sent)
|
|
}
|
|
fmt.Println("CODE_HASH=" + code.PhoneCodeHash)
|
|
return nil
|
|
}
|
|
|
|
func doFetch(ctx context.Context, client *telegram.Client, outDir string) error {
|
|
phone := os.Getenv("PHONE")
|
|
code := os.Getenv("CODE")
|
|
codeHash := os.Getenv("CODE_HASH")
|
|
password := os.Getenv("PASSWORD")
|
|
|
|
status, err := client.Auth().Status(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("auth status: %w", err)
|
|
}
|
|
if !status.Authorized {
|
|
_, err := client.Auth().SignIn(ctx, phone, code, codeHash)
|
|
if errors.Is(err, auth.ErrPasswordAuthNeeded) {
|
|
if _, err := client.Auth().Password(ctx, password); err != nil {
|
|
return fmt.Errorf("2FA: %w", err)
|
|
}
|
|
} else if err != nil {
|
|
return fmt.Errorf("signIn: %w", err)
|
|
}
|
|
fmt.Println("[authorized]")
|
|
} else {
|
|
fmt.Println("[already authorized]")
|
|
}
|
|
|
|
api := client.API()
|
|
docsDir := filepath.Join(outDir, "Default_wallpapers", "documents")
|
|
thumbsDir := filepath.Join(outDir, "Default_wallpapers", "thumbs", "m")
|
|
if err := os.MkdirAll(docsDir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(thumbsDir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
dl := downloader.NewDownloader()
|
|
download := func(doc *tg.Document, thumb string) ([]byte, error) {
|
|
loc := &tg.InputDocumentFileLocation{
|
|
ID: doc.ID,
|
|
AccessHash: doc.AccessHash,
|
|
FileReference: doc.FileReference,
|
|
ThumbSize: thumb,
|
|
}
|
|
var buf bytes.Buffer
|
|
if _, err := dl.Download(api, loc).Stream(ctx, &buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
// 转一个 tg.Document → appearance.Document 并下载正文 + "m" 缩略图。
|
|
convDoc := func(dc tg.DocumentClass) (appearance.Document, error) {
|
|
doc, ok := dc.(*tg.Document)
|
|
if !ok || doc.ID == 0 {
|
|
return appearance.Document{}, nil
|
|
}
|
|
out := appearance.Document{
|
|
ID: doc.ID,
|
|
AccessHash: doc.AccessHash,
|
|
Date: doc.Date,
|
|
MimeType: doc.MimeType,
|
|
Size: doc.Size,
|
|
DCID: doc.DCID,
|
|
}
|
|
for _, a := range doc.Attributes {
|
|
switch attr := a.(type) {
|
|
case *tg.DocumentAttributeImageSize:
|
|
out.Attributes = append(out.Attributes, appearance.DocumentAttribute{Kind: "image_size", W: attr.W, H: attr.H})
|
|
case *tg.DocumentAttributeFilename:
|
|
out.Attributes = append(out.Attributes, appearance.DocumentAttribute{Kind: "filename", FileName: attr.FileName})
|
|
}
|
|
}
|
|
// 正文
|
|
data, err := download(doc, "")
|
|
if err != nil {
|
|
return appearance.Document{}, fmt.Errorf("download doc %d: %w", doc.ID, err)
|
|
}
|
|
name := fmt.Sprintf("%d.dat", doc.ID)
|
|
if err := os.WriteFile(filepath.Join(docsDir, name), data, 0o644); err != nil {
|
|
return appearance.Document{}, err
|
|
}
|
|
sum := sha256.Sum256(data)
|
|
out.Path = "Default_wallpapers/documents/" + name
|
|
out.SHA256 = hex.EncodeToString(sum[:])
|
|
// "m" 缩略图
|
|
for _, t := range doc.Thumbs {
|
|
ps, ok := t.(*tg.PhotoSize)
|
|
if !ok || ps.Type != "m" {
|
|
continue
|
|
}
|
|
tdata, err := download(doc, "m")
|
|
if err != nil {
|
|
return appearance.Document{}, fmt.Errorf("download thumb %d: %w", doc.ID, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(thumbsDir, name), tdata, 0o644); err != nil {
|
|
return appearance.Document{}, err
|
|
}
|
|
tsum := sha256.Sum256(tdata)
|
|
out.Thumbs = append(out.Thumbs, appearance.PhotoSize{
|
|
Kind: "size", Type: "m", W: ps.W, H: ps.H, Size: ps.Size,
|
|
Path: "Default_wallpapers/thumbs/m/" + name, SHA256: hex.EncodeToString(tsum[:]),
|
|
})
|
|
break
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
convWallpaperSettings := func(s tg.WallPaperSettings) appearance.WallpaperSettings {
|
|
out := appearance.WallpaperSettings{}
|
|
if v, ok := s.GetBackgroundColor(); ok {
|
|
out.BackgroundColor = v
|
|
}
|
|
if v, ok := s.GetSecondBackgroundColor(); ok {
|
|
out.SecondBackgroundColor = v
|
|
}
|
|
if v, ok := s.GetThirdBackgroundColor(); ok {
|
|
out.ThirdBackgroundColor = v
|
|
}
|
|
if v, ok := s.GetFourthBackgroundColor(); ok {
|
|
out.FourthBackgroundColor = v
|
|
}
|
|
if v, ok := s.GetIntensity(); ok {
|
|
out.Intensity = v
|
|
}
|
|
if v, ok := s.GetRotation(); ok {
|
|
out.Rotation = v
|
|
}
|
|
out.Blur = s.Blur
|
|
out.Motion = s.Motion
|
|
return out
|
|
}
|
|
|
|
convWallpaper := func(wc tg.WallPaperClass) (appearance.Wallpaper, error) {
|
|
switch wp := wc.(type) {
|
|
case *tg.WallPaper:
|
|
doc, err := convDoc(wp.Document)
|
|
if err != nil {
|
|
return appearance.Wallpaper{}, err
|
|
}
|
|
out := appearance.Wallpaper{
|
|
ID: wp.ID, AccessHash: wp.AccessHash, Type: 0,
|
|
Default: wp.Default, Pattern: wp.Pattern, Dark: wp.Dark,
|
|
Slug: wp.Slug, Document: doc,
|
|
}
|
|
if s, ok := wp.GetSettings(); ok {
|
|
out.Settings = convWallpaperSettings(s)
|
|
}
|
|
return out, nil
|
|
case *tg.WallPaperNoFile:
|
|
out := appearance.Wallpaper{ID: wp.ID, Type: 1, Default: wp.Default, Dark: wp.Dark}
|
|
if s, ok := wp.GetSettings(); ok {
|
|
out.Settings = convWallpaperSettings(s)
|
|
}
|
|
return out, nil
|
|
}
|
|
return appearance.Wallpaper{}, nil
|
|
}
|
|
|
|
// ---- 墙纸 ----
|
|
wpRes, err := api.AccountGetWallPapers(ctx, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("getWallPapers: %w", err)
|
|
}
|
|
var wallpapers []appearance.Wallpaper
|
|
if w, ok := wpRes.(*tg.AccountWallPapers); ok {
|
|
for _, wc := range w.Wallpapers {
|
|
conv, err := convWallpaper(wc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if conv.ID != 0 {
|
|
wallpapers = append(wallpapers, conv)
|
|
}
|
|
}
|
|
}
|
|
fmt.Printf("[wallpapers] %d\n", len(wallpapers))
|
|
|
|
// ---- 聊天主题 ----
|
|
baseThemeName := func(b tg.BaseThemeClass) string {
|
|
switch b.(type) {
|
|
case *tg.BaseThemeClassic:
|
|
return "classic"
|
|
case *tg.BaseThemeDay:
|
|
return "day"
|
|
case *tg.BaseThemeNight:
|
|
return "night"
|
|
case *tg.BaseThemeTinted:
|
|
return "tinted"
|
|
case *tg.BaseThemeArctic:
|
|
return "arctic"
|
|
}
|
|
return ""
|
|
}
|
|
var chatThemes []appearance.ChatTheme
|
|
thRes, err := api.AccountGetChatThemes(ctx, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("getChatThemes: %w", err)
|
|
}
|
|
if t, ok := thRes.(*tg.AccountThemes); ok {
|
|
for _, theme := range t.Themes {
|
|
ct := appearance.ChatTheme{
|
|
ID: theme.ID, AccessHash: theme.AccessHash, Slug: theme.Slug,
|
|
Title: theme.Title, Emoticon: theme.Emoticon, Default: theme.Default,
|
|
}
|
|
for _, s := range theme.Settings {
|
|
ts := appearance.ThemeSettings{
|
|
BaseTheme: baseThemeName(s.BaseTheme),
|
|
AccentColor: s.AccentColor,
|
|
}
|
|
if v, ok := s.GetOutboxAccentColor(); ok {
|
|
ts.OutboxAccentColor = v
|
|
}
|
|
if v, ok := s.GetMessageColors(); ok {
|
|
ts.MessageColors = v
|
|
}
|
|
if w, ok := s.GetWallpaper(); ok {
|
|
wp, err := convWallpaper(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ts.Wallpaper = wp
|
|
}
|
|
ct.Settings = append(ct.Settings, ts)
|
|
}
|
|
chatThemes = append(chatThemes, ct)
|
|
}
|
|
}
|
|
fmt.Printf("[chat_themes] %d\n", len(chatThemes))
|
|
|
|
// ---- peer colors / profile colors 从官方拉 ----
|
|
mapColorSet := func(c tg.HelpPeerColorSetClass) *appearance.ColorSet {
|
|
switch s := c.(type) {
|
|
case *tg.HelpPeerColorSet:
|
|
return &appearance.ColorSet{PredicateName: "help.peerColorSet", Colors: append([]int(nil), s.Colors...)}
|
|
case *tg.HelpPeerColorProfileSet:
|
|
return &appearance.ColorSet{
|
|
PredicateName: "help.peerColorProfileSet",
|
|
PaletteColors: append([]int(nil), s.PaletteColors...),
|
|
BgColors: append([]int(nil), s.BgColors...),
|
|
StoryColors: append([]int(nil), s.StoryColors...),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
fetchColors := func(profile bool) ([]appearance.ColorOption, error) {
|
|
var res tg.HelpPeerColorsClass
|
|
var err error
|
|
if profile {
|
|
res, err = api.HelpGetPeerProfileColors(ctx, 0)
|
|
} else {
|
|
res, err = api.HelpGetPeerColors(ctx, 0)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pc, ok := res.(*tg.HelpPeerColors)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
out := make([]appearance.ColorOption, 0, len(pc.Colors))
|
|
for _, o := range pc.Colors {
|
|
co := appearance.ColorOption{ID: o.ColorID, Hidden: o.GetHidden()}
|
|
if v, ok := o.GetChannelMinLevel(); ok {
|
|
co.ChannelMinLevel = v
|
|
}
|
|
if c, ok := o.GetColors(); ok {
|
|
co.Colors = mapColorSet(c)
|
|
}
|
|
if c, ok := o.GetDarkColors(); ok {
|
|
co.DarkColors = mapColorSet(c)
|
|
}
|
|
out = append(out, co)
|
|
}
|
|
return out, nil
|
|
}
|
|
peerColors, err := fetchColors(false)
|
|
if err != nil {
|
|
return fmt.Errorf("getPeerColors: %w", err)
|
|
}
|
|
peerProfileColors, err := fetchColors(true)
|
|
if err != nil {
|
|
return fmt.Errorf("getPeerProfileColors: %w", err)
|
|
}
|
|
fmt.Printf("[peer_colors] %d / [peer_profile_colors] %d\n", len(peerColors), len(peerProfileColors))
|
|
|
|
catalog := appearance.Catalog{
|
|
Source: "official telegram (appearancefetch)",
|
|
ExportedAt: time.Now().UTC().Format(time.RFC3339),
|
|
Notes: appearance.Notes{
|
|
Server: "official",
|
|
ChatThemesTotal: len(chatThemes),
|
|
WallpaperDocumentsBucket: "documents",
|
|
WallpaperThumbsBucket: "thumbs",
|
|
},
|
|
ChatThemes: chatThemes,
|
|
Wallpapers: wallpapers,
|
|
PeerColors: peerColors,
|
|
PeerProfileColors: peerProfileColors,
|
|
}
|
|
out, err := json.MarshalIndent(catalog, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
jsonPath := filepath.Join(outDir, "Default_appearance_seed.json")
|
|
if err := os.WriteFile(jsonPath, out, 0o644); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("wrote %s (wallpapers=%d chat_themes=%d peer_colors=%d profile=%d)\n",
|
|
jsonPath, len(wallpapers), len(chatThemes), len(catalog.PeerColors), len(catalog.PeerProfileColors))
|
|
return nil
|
|
}
|