├── .deepsource.toml ├── .github ├── FUNDING.yml └── workflows │ └── tlparser.yml ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── errors.go ├── examples ├── auth │ └── userauth.go ├── bot │ └── echobot.go ├── broadcast │ └── broadcast.go ├── buttons │ └── replymarkup.go ├── cache │ └── main.go ├── conversation │ └── conversation.go ├── fileid │ └── fileid.go ├── getchats │ └── getchats.go ├── gifts │ └── main.go ├── inline │ └── inlinebot.go ├── multiple │ └── main.go ├── progress │ └── upload.go ├── proxy │ └── proxy.go ├── react │ └── react.go ├── sessions │ ├── pyrogram │ │ └── main.go │ ├── string │ │ └── main.go │ ├── tdata │ │ └── main.go │ └── telethon │ │ └── main.go ├── simple │ └── simpleauth.go └── stars │ └── stars.go ├── go.mod ├── go.sum ├── handshake.go ├── internal ├── aes_ige │ ├── aes.go │ ├── aes_common.go │ ├── errors.go │ ├── ige_cipher.go │ └── password.go ├── cmd │ └── tlgen │ │ ├── gen │ │ ├── common.go │ │ ├── gen.go │ │ ├── schema.go │ │ ├── tl_gen_enums.go │ │ ├── tl_gen_init.go │ │ ├── tl_gen_interfaces.go │ │ ├── tl_gen_methods.go │ │ ├── tl_gen_structs.go │ │ └── utils.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ └── tlparser │ │ ├── cursor.go │ │ ├── excluded.go │ │ ├── extra.go │ │ ├── parser.go │ │ └── schema.go ├── encoding │ └── tl │ │ ├── common_types.go │ │ ├── const.go │ │ ├── cursor_r.go │ │ ├── cursor_w.go │ │ ├── decoder.go │ │ ├── encoder.go │ │ ├── errors.go │ │ ├── object.go │ │ ├── register.go │ │ ├── tag.go │ │ └── utils.go ├── keys │ └── keys.go ├── math │ └── math.go ├── mode │ ├── arbiged.go │ ├── errors.go │ ├── full.go │ ├── intermediate.go │ └── mode.go ├── mtproto │ ├── messages │ │ └── messages.go │ └── objects │ │ ├── const.go │ │ ├── init.go │ │ ├── methods.go │ │ └── types.go ├── session │ ├── file.go │ ├── interface.go │ └── string.go ├── transport │ ├── connection.go │ ├── interfaces.go │ ├── proxy.go │ ├── transport.go │ ├── utils.go │ └── writer.go └── utils │ ├── logging.go │ ├── media.go │ ├── set.go │ ├── sync.go │ └── utils.go ├── mtproto.go ├── network.go ├── schemes ├── api.tl ├── e2e.tl └── mtproto.tl └── telegram ├── auth.go ├── bots.go ├── buttons.go ├── cache.go ├── callbackquery.go ├── channels.go ├── client.go ├── const.go ├── conversation.go ├── enums_gen.go ├── formatting.go ├── helpers.go ├── init_gen.go ├── inlinequery.go ├── interfaces_gen.go ├── media.go ├── messages.go ├── methods_gen.go ├── methods_special.go ├── newmessage.go ├── participant.go ├── tgcalls.go ├── types_gen.go ├── updates.go ├── users.go └── utils.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "go" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | import_root = "github.com/AmarnathCJD/gogram" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: amarnathcjd 2 | buy_me_a_coffee: amarnathcjd 3 | -------------------------------------------------------------------------------- /.github/workflows/tlparser.yml: -------------------------------------------------------------------------------- 1 | name: Go TL Parser 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | workflow_dispatch: 9 | schedule: 10 | - cron: '0 */3 * * *' 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: '1.23' 23 | 24 | - name: Build & Run TL Parser 25 | run: cd ./internal/cmd/tlgen && go run . --doc 26 | 27 | - name: Lint 28 | run: gofmt -l -s -w . 29 | 30 | - uses: stefanzweifel/git-auto-commit-action@v5 31 | with: 32 | commit_message: "tlgen: update TL schema files" 33 | commit_options: "--no-verify" 34 | repository: . 35 | commit_user_name: AmarnathCJD 36 | commit_user_email: 72609355+AmarnathCJD@users.noreply.github.com 37 | commit_author: AmarnathCJD <72609355+AmarnathCJD+@users.noreply.github.com> 38 | 39 | 40 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "examples/tgcalls"] 2 | path = examples/tgcalls 3 | url = https://github.com/amarnathcjd/tgcalls.git 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | roseloverx@proton.me. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /examples/auth/userauth.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/amarnathcjd/gogram/telegram" 7 | ) 8 | 9 | const ( 10 | appID = 6 11 | appHash = "YOUR_APP_HASH" 12 | phoneNum = "YOUR_PHONE_NUMBER" 13 | ) 14 | 15 | func main() { 16 | // Create a new client 17 | client, _ := telegram.NewClient(telegram.ClientConfig{ 18 | AppID: appID, 19 | AppHash: appHash, 20 | LogLevel: telegram.LogInfo, 21 | // StringSession: "", // Uncomment this line to use string session 22 | // SessionFile: "session.session", To use session File. 23 | }) 24 | 25 | // Connect to the server 26 | if err := client.Connect(); err != nil { 27 | panic(err) 28 | } 29 | 30 | // Authenticate the client using the bot token 31 | // This will send a code to the phone number if it is not already authenticated 32 | if _, err := client.Login(phoneNum); err != nil { 33 | panic(err) 34 | } 35 | 36 | // Do something with the client 37 | // ... 38 | me, err := client.GetMe() 39 | if err != nil { 40 | panic(err) 41 | } 42 | client.SendMessage("me", fmt.Sprintf("Hello, %s!", me.FirstName)) 43 | fmt.Println("Logged in as", me.Username) 44 | } 45 | -------------------------------------------------------------------------------- /examples/bot/echobot.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/amarnathcjd/gogram/telegram" 5 | ) 6 | 7 | const ( 8 | appID = 6 9 | appHash = "YOUR_APP_HASH" 10 | botToken = "YOUR_BOT_TOKEN" 11 | ) 12 | 13 | func main() { 14 | // create a new client object 15 | client, _ := telegram.NewClient(telegram.ClientConfig{ 16 | AppID: appID, 17 | AppHash: appHash, 18 | LogLevel: telegram.LogInfo, 19 | }) 20 | 21 | client.LoginBot(botToken) 22 | 23 | client.On(telegram.OnMessage, func(message *telegram.NewMessage) error { 24 | message.Respond(message) 25 | return nil 26 | }, telegram.FilterPrivate) 27 | 28 | client.On("message:/start", func(message *telegram.NewMessage) error { 29 | message.Reply("Hello, I am a bot!") 30 | return nil 31 | }) 32 | 33 | // lock the main routine 34 | client.Idle() 35 | } 36 | -------------------------------------------------------------------------------- /examples/broadcast/broadcast.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/amarnathcjd/gogram/telegram" 5 | ) 6 | 7 | // Broadcasting to bot users / chats using updates.GetDifference 8 | // client.Broadcast returns peers in update history via channels 9 | // these peers can be used for broadcasting messages 10 | // no external database is required to store user / chat ids 11 | 12 | const ( 13 | appID = 6 14 | appHash = "YOUR_APP_HASH" 15 | botToken = "YOUR_BOT_TOKEN" 16 | ) 17 | 18 | func main() { 19 | // create a new client object 20 | client, _ := telegram.NewClient(telegram.ClientConfig{ 21 | AppID: appID, 22 | AppHash: appHash, 23 | LogLevel: telegram.LogInfo, 24 | }) 25 | 26 | client.LoginBot(botToken) 27 | 28 | users, chats, err := client.Broadcast() 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | for user := range users { 34 | client.SendMessage(user, "Hello, This is a broadcast message") 35 | } 36 | 37 | for chat := range chats { 38 | client.SendMessage(chat, "Hello, This is a broadcast message") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/buttons/replymarkup.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/amarnathcjd/gogram/telegram" 5 | ) 6 | 7 | const ( 8 | appID = 6 9 | appHash = "YOUR_APP_HASH" 10 | botToken = "YOUR_BOT_TOKEN" 11 | ) 12 | 13 | func main() { 14 | // create a new client object 15 | client, _ := telegram.NewClient(telegram.ClientConfig{ 16 | AppID: appID, 17 | AppHash: appHash, 18 | LogLevel: telegram.LogInfo, 19 | }) 20 | 21 | client.LoginBot(botToken) 22 | 23 | client.SendMessage("username", "Hello, This is ReplyMarkup Example", &telegram.SendOptions{ 24 | ReplyMarkup: telegram.NewKeyboard().AddRow( // adds a row of buttons 25 | telegram.Button.Data("Help", "help"), 26 | telegram.Button.URL("Google", "https://www.google.com"), 27 | ).Build(), //.AddRow( // adds another row of buttons) 28 | }) 29 | 30 | client.On("message:/start", func(message *telegram.NewMessage) error { 31 | message.Reply("Hello:>", telegram.SendOptions{ 32 | ReplyMarkup: telegram.NewKeyboard().NewGrid(2, 3, // 2 rows, 3 columns format 33 | telegram.Button.URL("Google", "https://www.google.com"), 34 | telegram.Button.Data("Help", "help"), 35 | telegram.Button.Data("Main Menu", "main"), 36 | telegram.Button.Data("Source", "source"), 37 | telegram.Button.SwitchInline("goinline", true, "query"), 38 | telegram.Button.Data("Back", "back")).Build(), 39 | }) 40 | return nil 41 | }) 42 | 43 | telegram.NewKeyboard().NewColumn(2, telegram.Button.URL("Google", "https://www.google.com"), 44 | telegram.Button.URL("Yahoo", "https://www.yahoo.com"), 45 | telegram.Button.URL("Bing", "https://www.bing.com"), 46 | telegram.Button.URL("DuckDuckGo", "https://www.duckduckgo.com"), 47 | ).Build() // 2 columns format, buttons equally divided into 2 columns 48 | 49 | telegram.NewKeyboard().NewRow(3, 50 | telegram.Button.URL("Google", "https://www.google.com"), 51 | telegram.Button.URL("Yahoo", "https://www.yahoo.com"), 52 | telegram.Button.URL("Bing", "https://www.bing.com"), 53 | telegram.Button.URL("DuckDuckGo", "https://www.duckduckgo.com"), 54 | ).Build() // 3 rows format, buttons equally divided into 3 rows 55 | 56 | client.On("callback:help", func(callback *telegram.CallbackQuery) error { 57 | callback.Answer("Help is on the way!") 58 | return nil 59 | }) 60 | 61 | client.Idle() 62 | } 63 | -------------------------------------------------------------------------------- /examples/cache/main.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/amarnathcjd/gogram/telegram" 5 | ) 6 | 7 | const ( 8 | appID = 6 9 | appHash = "YOUR_APP_HASH" 10 | botToken = "YOUR_BOT_TOKEN" 11 | ) 12 | 13 | func main() { 14 | // create a new client object 15 | client, _ := telegram.NewClient(telegram.ClientConfig{ 16 | AppID: appID, 17 | AppHash: appHash, 18 | LogLevel: telegram.LogInfo, 19 | Cache: telegram.NewCache("cache_file.db", &telegram.CacheConfig{ 20 | MaxSize: 1000, // TODO 21 | LogLevel: telegram.LogInfo, 22 | LogNoColor: true, // disable color in logs 23 | Memory: true, // disable writing to disk 24 | Disabled: false, // to totally disable cache 25 | }), // if left empty, it will use the default cache 'cache.db', with default config 26 | }) 27 | 28 | client.LoginBot(botToken) 29 | 30 | client.On(telegram.OnMessage, func(message *telegram.NewMessage) error { 31 | message.Respond(message) 32 | return nil 33 | }, telegram.FilterPrivate) 34 | 35 | client.On("message:/start", func(message *telegram.NewMessage) error { 36 | message.Reply("Hello, I am a bot!") 37 | return nil 38 | }) 39 | 40 | // lock the main routine 41 | client.Idle() 42 | } 43 | -------------------------------------------------------------------------------- /examples/conversation/conversation.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/amarnathcjd/gogram/telegram" 7 | ) 8 | 9 | const ( 10 | appID = 6 11 | appHash = "YOUR_APP_HASH" 12 | botToken = "YOUR_BOT_TOKEN" 13 | ) 14 | 15 | func main() { 16 | // create a new client object 17 | client, _ := telegram.NewClient(telegram.ClientConfig{ 18 | AppID: appID, 19 | AppHash: appHash, 20 | LogLevel: telegram.LogInfo, 21 | }) 22 | 23 | client.LoginBot(botToken) 24 | 25 | client.On("message", convEventHandler) 26 | 27 | // new conversation 28 | conv, _ := client.NewConversation("username or id", false, 30) // 30 is the timeout in seconds, false means it's not a private conversation 29 | defer conv.Close() 30 | 31 | _, err := conv.Respond("Hello, Please reply to this message") 32 | if err != nil { 33 | panic(err) 34 | } 35 | resp, err := conv.GetResponse() // wait for the response 36 | // resp, err := conv.GetReply() // wait for the reply 37 | // conv.MarkRead() // mark the conversation as read 38 | // conv.WaitEvent() // wait for any custom update 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | fmt.Println("response:", resp.Text()) 44 | } 45 | 46 | func convEventHandler(m *telegram.NewMessage) error { 47 | response, err := m.Ask("What's your name?") 48 | if err != nil { 49 | return err 50 | } 51 | 52 | response.Reply("Nice to meet you, " + response.Text()) 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /examples/fileid/fileid.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/amarnathcjd/gogram/telegram" 5 | ) 6 | 7 | // https://gist.github.com/AmarnathCJD/824907f8ec9e7aa9cec8278937fc437b 8 | // use this code to convert bot-api file id to gogram file id 9 | 10 | // https://gist.github.com/AmarnathCJD/27626c8fc1b5d5234576d1eecb5d651f 11 | // use this code to convert pyrogram file id to gogram file id 12 | 13 | const ( 14 | appID = 6 15 | appHash = "YOUR_APP_HASH" 16 | phoneNum = "YOUR_PHONE_NUMBER" 17 | ) 18 | 19 | func main() { 20 | // Create a new client 21 | client, _ := telegram.NewClient(telegram.ClientConfig{ 22 | AppID: appID, 23 | AppHash: appHash, 24 | LogLevel: telegram.LogInfo, 25 | }) 26 | 27 | client.Conn() // Connect to the telegram server 28 | 29 | // Authentication 30 | client.LoginBot("") 31 | 32 | client.On(telegram.OnMessage, func(msg *telegram.NewMessage) error { 33 | if msg.Media() != nil { 34 | fileID := msg.File.FileID // or telegram.PackBotFileID(msg.Media()) 35 | 36 | msg.Respond("File ID: " + fileID) 37 | 38 | fileRetained, err := telegram.ResolveBotFileID(fileID) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | msg.RespondMedia(fileRetained) 44 | } 45 | 46 | return nil 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /examples/getchats/getchats.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/amarnathcjd/gogram/telegram" 7 | ) 8 | 9 | const ( 10 | appID = 6 11 | appHash = "YOUR_APP_HASH" 12 | phoneNum = "YOUR_PHONE_NUMBER" 13 | ) 14 | 15 | func main() { 16 | // Create a new client 17 | client, _ := telegram.NewClient(telegram.ClientConfig{ 18 | AppID: appID, 19 | AppHash: appHash, 20 | LogLevel: telegram.LogInfo, 21 | // StringSession: "", // Uncomment this line to use string session 22 | }) 23 | 24 | // Connect to the server 25 | if err := client.Connect(); err != nil { 26 | panic(err) 27 | } 28 | 29 | // Authenticate the client using the bot token 30 | // This will send a code to the phone number if it is not already authenticated 31 | if _, err := client.Login(phoneNum); err != nil { 32 | panic(err) 33 | } 34 | 35 | // if you are using bot account, use client.Broadcast() 36 | 37 | dialogs, err := client.GetDialogs() 38 | if err != nil { 39 | panic(err) 40 | } 41 | for _, dialog := range dialogs { 42 | switch d := dialog.(type) { 43 | case *telegram.DialogObj: 44 | fmt.Println(d.TopMessage) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/gifts/main.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/amarnathcjd/gogram/telegram" 7 | ) 8 | 9 | const ( 10 | appID = 6 11 | appHash = "YOUR_APP_HASH" 12 | phoneNum = "YOUR_PHONE_NUMBER" 13 | ) 14 | 15 | func main() { 16 | client, _ := telegram.NewClient(telegram.ClientConfig{ 17 | AppID: appID, 18 | AppHash: appHash, 19 | }) 20 | client.Conn() 21 | client.AuthPrompt() 22 | 23 | // get all your unique gifts and send the first one to a user 24 | gifts, _ := client.GetMyGifts(true) 25 | for _, gift := range gifts { 26 | gft := (*gift).(*telegram.StarGiftUnique) 27 | 28 | // transfer the gift to the user 29 | client.SendGift("roseloverx", gft.ID, "Here is a gift for you!") 30 | 31 | break 32 | } 33 | 34 | // buying new gifts and sending them to users 35 | availGifts, _ := client.PaymentsGetStarGifts(0) 36 | for _, gift := range availGifts.(*telegram.PaymentsStarGiftsObj).Gifts { 37 | gft := gift.(*telegram.StarGiftObj) 38 | 39 | // buy and send the gift 40 | client.SendNewGift("roseloverx", gft.ID, "Here is a gift for you!") 41 | 42 | break 43 | } 44 | 45 | // add a handler on receiving gifts 46 | client.AddActionHandler(func(m *telegram.NewMessage) error { 47 | if action, ok := m.Action.(*telegram.MessageActionStarGift); ok { 48 | fmt.Println("Gift received", action.Gift) 49 | } else if action, ok := m.Action.(*telegram.MessageActionStarGiftUnique); ok { 50 | fmt.Println("Unique gift received", action.Gift) 51 | } 52 | 53 | return nil 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /examples/inline/inlinebot.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/amarnathcjd/gogram/telegram" 5 | ) 6 | 7 | const ( 8 | appID = 6 9 | appHash = "YOUR_APP_HASH" 10 | botToken = "YOUR_BOT_TOKEN" 11 | ) 12 | 13 | func main() { 14 | // Create a new client 15 | client, _ := telegram.NewClient(telegram.ClientConfig{ 16 | AppID: appID, 17 | AppHash: appHash, 18 | LogLevel: telegram.LogInfo, 19 | }) 20 | 21 | // Connect to the server 22 | if err := client.Connect(); err != nil { 23 | panic(err) 24 | } 25 | 26 | // Authenticate the client using the bot token 27 | if err := client.LoginBot(botToken); err != nil { 28 | panic(err) 29 | } 30 | 31 | // Add a inline query handler 32 | client.AddInlineHandler(telegram.OnInlineQuery, HelloWorld) 33 | 34 | // Start polling 35 | client.Idle() 36 | } 37 | 38 | func HelloWorld(i *telegram.InlineQuery) error { 39 | builder := i.Builder() 40 | builder.Article("Hello World", "Hello World", "This is a test article") 41 | _, err := i.Answer(builder.Results()) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /examples/multiple/main.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | // ExampleMultiple demonstrates how to use multiple clients in a single application. 4 | 5 | import ( 6 | "fmt" 7 | 8 | tg "github.com/amarnathcjd/gogram/telegram" 9 | ) 10 | 11 | func main() { 12 | // Create a new client with the default options 13 | client1, _ := tg.NewClient(tg.ClientConfig{ 14 | Cache: tg.NewCache("client1_cache"), 15 | Session: "client1_session", 16 | }) 17 | 18 | // Create a new client with the default options 19 | client2, _ := tg.NewClient(tg.ClientConfig{ 20 | Cache: tg.NewCache("client2_cache"), 21 | Session: "client2_session", 22 | }) 23 | 24 | client1.LoginBot("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") 25 | client2.LoginBot("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11") 26 | 27 | fmt.Println(client1.GetMe()) 28 | fmt.Println(client2.GetMe()) 29 | 30 | // This way, their cache and session files are stored separately. 31 | // Now you can use both clients in your application. 32 | } 33 | -------------------------------------------------------------------------------- /examples/progress/upload.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | // Youtube DL Bot Example; 4 | // https://gist.github.com/AmarnathCJD/bfceefe9efd1a079ab151da54ef3bba2 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/amarnathcjd/gogram/telegram" 11 | ) 12 | 13 | const ( 14 | appID = 6 15 | apiKey = "" 16 | botToken = "" 17 | ) 18 | 19 | func main() { 20 | // create a new client object 21 | client, _ := telegram.NewClient(telegram.ClientConfig{ 22 | AppID: appID, 23 | AppHash: apiKey, 24 | }) 25 | 26 | client.LoginBot(botToken) 27 | 28 | chat, _ := client.ResolvePeer("chatId") 29 | m, _ := client.SendMessage(chat, "Starting File Upload...") 30 | 31 | client.SendMedia(chat, "", &telegram.MediaOptions{ 32 | ProgressManager: telegram.NewProgressManager(5).SetMessage(m), 33 | }) 34 | 35 | // to use custom progress manager 36 | // pm := telegram.NewProgressManager(5) 37 | // pm.EditFunc(MediaDownloadProgress("", m, pm)) 38 | // client.SendMedia(chat, "", &telegram.MediaOptions{ 39 | // ProgressManager: pm, 40 | // }) 41 | 42 | // same goes for download 43 | // &DownloadOptions{ProgressManager: NewProgressManager(5).SetMessage(m)} 44 | // &SendOptions{ProgressManager: NewProgressManager(5).SetMessage(m)} 45 | // &MediaOptions{ProgressManager: NewProgressManager(5).SetMessage(m)} 46 | } 47 | 48 | func MediaDownloadProgress(fname string, editMsg *telegram.NewMessage, pm *telegram.ProgressManager) func(atotalBytes, currentBytes int64) { 49 | return func(totalBytes int64, currentBytes int64) { 50 | text := "" 51 | text += "📄 Name: %s\n" 52 | text += "💾 File Size: %.2f MiB\n" 53 | text += "⌛️ ETA: %s\n" 54 | text += "⏱ Speed: %s\n" 55 | text += "⚙️ Progress: %s %.2f%%" 56 | 57 | size := float64(totalBytes) / 1024 / 1024 58 | eta := pm.GetETA(currentBytes) 59 | speed := pm.GetSpeed(currentBytes) 60 | percent := pm.GetProgress(currentBytes) 61 | 62 | progressbar := strings.Repeat("■", int(percent/10)) + strings.Repeat("□", 10-int(percent/10)) 63 | 64 | message := fmt.Sprintf(text, fname, size, eta, speed, progressbar, percent) 65 | editMsg.Edit(message) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/amarnathcjd/gogram/telegram" 8 | ) 9 | 10 | // supported proxy types 11 | // socks5, sock4, http, https 12 | 13 | const ( 14 | appID = 6 15 | appHash = "YOUR_APP_HASH" 16 | phoneNum = "+YOUR_PHONE_NUMBER" 17 | ) 18 | 19 | func main() { 20 | socks5Proxy := &url.URL{ 21 | Scheme: "socks5", 22 | Host: "127.0.0.1:1080", 23 | // User: url.UserPassword("username", "password"), 24 | } 25 | 26 | // create a new client object 27 | client, _ := telegram.NewClient(telegram.ClientConfig{ 28 | AppID: appID, 29 | AppHash: appHash, 30 | LogLevel: telegram.LogInfo, 31 | Proxy: socks5Proxy, 32 | }) 33 | 34 | // authenticate the client using the bot token 35 | // this will send a code to the phone number if it is not already authenticated 36 | if _, err := client.Login(phoneNum); err != nil { 37 | panic(err) 38 | } 39 | 40 | // Do something with the client 41 | // ... 42 | me, err := client.GetMe() 43 | if err != nil { 44 | panic(err) 45 | } 46 | client.SendMessage("me", fmt.Sprintf("Hello, %s!", me.FirstName)) 47 | fmt.Println("Logged in as", me.Username) 48 | } 49 | -------------------------------------------------------------------------------- /examples/react/react.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/amarnathcjd/gogram/telegram" 7 | ) 8 | 9 | const ( 10 | apiKey = 6 11 | apiHash = "" 12 | botToken = "" 13 | ) 14 | 15 | func main() { 16 | // create a new client object 17 | client, _ := telegram.NewClient(telegram.ClientConfig{ 18 | AppID: apiKey, 19 | AppHash: apiHash, 20 | }) 21 | 22 | if err := client.LoginBot(botToken); err != nil { 23 | panic(err) 24 | } 25 | 26 | client.AddRawHandler(&telegram.UpdateBotMessageReaction{}, func(m telegram.Update, c *telegram.Client) error { 27 | fmt.Println(m.(*telegram.UpdateBotMessageReaction).NewReactions) 28 | return nil 29 | }) 30 | 31 | client.AddMessageHandler(telegram.OnNewMessage, func(msg *telegram.NewMessage) error { 32 | client.MessagesSendReaction(&telegram.MessagesSendReactionParams{ 33 | Big: true, 34 | AddToRecent: false, 35 | Peer: msg.Peer, 36 | MsgID: msg.ID, 37 | Reaction: []telegram.Reaction{ 38 | &telegram.ReactionEmoji{ 39 | Emoticon: "👍", 40 | }, 41 | }, 42 | }) 43 | 44 | // or 45 | 46 | // msg.React("👍") 47 | 48 | return nil 49 | }) 50 | 51 | client.Idle() 52 | } 53 | -------------------------------------------------------------------------------- /examples/sessions/pyrogram/main.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/amarnathcjd/gogram/telegram" 8 | ) 9 | 10 | // This function decodes the Pyrogram Session String to gogram's Session Format 11 | func decodePyrogramSessionString(encodedString string) (*telegram.Session, error) { 12 | // SESSION_STRING_FORMAT: Big-endian, uint8, uint32, bool, 256-byte array, uint64, bool 13 | const ( 14 | dcIDSize = 1 // uint8 15 | apiIDSize = 4 // uint32 16 | testModeSize = 1 // bool (uint8) 17 | authKeySize = 256 18 | userIDSize = 8 // uint64 19 | isBotSize = 1 // bool (uint8) 20 | ) 21 | 22 | // Add padding to the base64 string if necessary 23 | for len(encodedString)%4 != 0 { 24 | encodedString += "=" 25 | } 26 | 27 | packedData, err := base64.URLEncoding.DecodeString(encodedString) 28 | if err != nil { 29 | return nil, fmt.Errorf("failed to decode base64 string: %w", err) 30 | } 31 | 32 | expectedSize := dcIDSize + apiIDSize + testModeSize + authKeySize + userIDSize + isBotSize 33 | if len(packedData) != expectedSize { 34 | return nil, fmt.Errorf("unexpected data length: got %d, want %d", len(packedData), expectedSize) 35 | } 36 | 37 | return &telegram.Session{ 38 | Hostname: telegram.ResolveDataCenterIP(int(uint8(packedData[0])), packedData[5] != 0, false), 39 | AppID: int32(uint32(packedData[1])<<24 | uint32(packedData[2])<<16 | uint32(packedData[3])<<8 | uint32(packedData[4])), 40 | Key: packedData[6 : 6+authKeySize], 41 | }, nil 42 | } 43 | 44 | func main() { 45 | sessionString := "" 46 | 47 | sess, err := decodePyrogramSessionString(sessionString) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | client, err := telegram.NewClient(telegram.ClientConfig{ 53 | //AppID: 6, // 54 | StringSession: sess.Encode(), 55 | MemorySession: true, 56 | }) 57 | 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | me, err := client.GetMe() 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | fmt.Println(client.JSON(me, true)) 68 | } 69 | -------------------------------------------------------------------------------- /examples/sessions/string/main.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/amarnathcjd/gogram/telegram" 7 | ) 8 | 9 | func main() { 10 | client, err := telegram.NewClient(telegram.ClientConfig{ 11 | AppID: 6, // 12 | AppHash: "YOUR_APP_HASH", // 13 | // StringSession: "", 14 | MemorySession: true, 15 | }) 16 | 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | client.Conn() // 22 | client.AuthPrompt() // 23 | 24 | // Print the StringSession 25 | // This can be used to login without the need of reauthenticating again 26 | // 27 | // Doings so will allow Full Access to your Telegram Account 28 | fmt.Println("StringSession:", client.ExportSession()) 29 | 30 | me, err := client.GetMe() 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | fmt.Println(client.JSON(me, true)) 36 | } 37 | -------------------------------------------------------------------------------- /examples/sessions/telethon/main.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/binary" 6 | "fmt" 7 | "net" 8 | 9 | "github.com/amarnathcjd/gogram/telegram" 10 | ) 11 | 12 | // This function decodes the Telethon Session String to gogram's Session Format 13 | // Make sure the Schema Layer of Telethon Session matches with telegram.ApiVersion value 14 | func decodeTelethonSessionString(sessionString string) (*telegram.Session, error) { 15 | data, err := base64.URLEncoding.DecodeString(sessionString[1:]) 16 | if err != nil { 17 | return nil, fmt.Errorf("failed to decode base64: %v", err) 18 | } 19 | 20 | ipLen := 4 21 | if len(data) == 352 { 22 | ipLen = 16 23 | } 24 | 25 | expectedLen := 1 + ipLen + 2 + 256 26 | if len(data) != expectedLen { 27 | return nil, fmt.Errorf("invalid session string length") 28 | } 29 | 30 | // ">B{}sH256s" 31 | offset := 1 32 | 33 | // IP Address (4 or 16 bytes based on IPv4 or IPv6) 34 | ipData := data[offset : offset+ipLen] 35 | ip := net.IP(ipData) 36 | ipAddress := ip.String() 37 | offset += ipLen 38 | 39 | // Port (2 bytes, Big Endian) 40 | port := binary.BigEndian.Uint16(data[offset : offset+2]) 41 | offset += 2 42 | 43 | // Auth Key (256 bytes) 44 | var authKey [256]byte 45 | copy(authKey[:], data[offset:offset+256]) 46 | 47 | return &telegram.Session{ 48 | Hostname: ipAddress + ":" + fmt.Sprint(port), 49 | Key: authKey[:], 50 | }, nil 51 | } 52 | 53 | func main() { 54 | sessionString := "" 55 | 56 | sess, err := decodeTelethonSessionString(sessionString) 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | client, err := telegram.NewClient(telegram.ClientConfig{ 62 | AppID: 6, // 63 | StringSession: sess.Encode(), 64 | MemorySession: true, 65 | }) 66 | 67 | if err != nil { 68 | panic(err) 69 | } 70 | 71 | me, err := client.GetMe() 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | fmt.Println(client.JSON(me, true)) 77 | } 78 | -------------------------------------------------------------------------------- /examples/simple/simpleauth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | tg "github.com/amarnathcjd/gogram/telegram" 5 | ) 6 | 7 | func main() { 8 | client, _ := tg.NewClient(tg.ClientConfig{ 9 | AppID: 6, 10 | AppHash: "", 11 | }) 12 | client.Start() // this calls client.Connect() 13 | // Then asks for userInput for phone num or botToken 14 | // Further authing with that, fully automatic the auth code flow. 15 | // fmt.Println(client.ExportSession()) -> "Bwjiaw27sbss..." 16 | // client Do anything 17 | // client.Idle() 18 | } 19 | -------------------------------------------------------------------------------- /examples/stars/stars.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/amarnathcjd/gogram/telegram" 7 | ) 8 | 9 | var paidUsers = make(map[int64]int32) 10 | 11 | func main() { 12 | bot, _ := telegram.NewClient(telegram.ClientConfig{ 13 | AppID: 123456, 14 | AppHash: "YOUR_APP_HASH", 15 | }) 16 | 17 | bot.Conn() 18 | bot.LoginBot("YOUR_BOT_TOKEN") 19 | 20 | bot.AddRawHandler(&telegram.UpdateBotPrecheckoutQuery{}, preCheckoutQueryUpd) 21 | bot.On("edit", func(m *telegram.NewMessage) error { 22 | paidUsers[m.SenderID()] = m.Invoice().ReceiptMsgID 23 | return bot.E(m.Respond("Payment Successful!")) 24 | }, telegram.FilterFunc(func(upd *telegram.NewMessage) bool { 25 | return upd.Invoice() != nil && upd.Invoice().ReceiptMsgID != 0 26 | })) 27 | 28 | bot.On("message:pay", payCmd) 29 | bot.On("message:status", statusCmd) 30 | bot.On("message:refund", refundCmd) // new type of message handlers, eg<> 31 | } 32 | 33 | func payCmd(m *telegram.NewMessage) error { 34 | invoice := telegram.InputMediaInvoice{ 35 | Title: "Test Product", 36 | Description: "Test Description", 37 | Payload: []byte(""), 38 | Invoice: &telegram.Invoice{ 39 | Test: true, 40 | Currency: "USD", 41 | Prices: []*telegram.LabeledPrice{ 42 | { 43 | Amount: 1, 44 | Label: "1 USD", 45 | }, 46 | }, 47 | }, 48 | ProviderData: &telegram.DataJson{}, 49 | } 50 | 51 | m.ReplyMedia(&invoice) 52 | return nil 53 | } 54 | 55 | func preCheckoutQueryUpd(upd telegram.Update, c *telegram.Client) error { 56 | _upd := upd.(*telegram.UpdateBotPrecheckoutQuery) 57 | c.MessagesSetBotPrecheckoutResults(true, _upd.QueryID, "Success") 58 | return nil 59 | } 60 | 61 | func statusCmd(m *telegram.NewMessage) error { 62 | if _, ok := paidUsers[m.SenderID()]; ok { 63 | return m.E(m.Respond("You have paid!")) 64 | } 65 | return m.E(m.Respond("You have not paid!")) 66 | } 67 | 68 | func refundCmd(m *telegram.NewMessage) error { 69 | if recpt_id, ok := paidUsers[m.SenderID()]; ok { 70 | delete(paidUsers, m.SenderID()) 71 | u, _ := m.Client.GetSendableUser(m.SenderID()) 72 | m.Client.PaymentsRefundStarsCharge(u, fmt.Sprintf("%d", recpt_id)) 73 | return m.E(m.Respond("Refund Successful!")) 74 | } 75 | return m.E(m.Respond("You have not paid!")) 76 | } 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/amarnathcjd/gogram 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/pkg/errors v0.9.1 7 | golang.org/x/net v0.35.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 2 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 4 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 5 | -------------------------------------------------------------------------------- /handshake.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 RoseLoverX 2 | 3 | package gogram 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "fmt" 10 | "math/big" 11 | "time" 12 | 13 | ige "github.com/amarnathcjd/gogram/internal/aes_ige" 14 | "github.com/amarnathcjd/gogram/internal/encoding/tl" 15 | "github.com/amarnathcjd/gogram/internal/keys" 16 | "github.com/amarnathcjd/gogram/internal/math" 17 | "github.com/amarnathcjd/gogram/internal/mtproto/objects" 18 | "github.com/amarnathcjd/gogram/internal/utils" 19 | "github.com/pkg/errors" 20 | ) 21 | 22 | // https://core.telegram.org/mtproto/auth_key 23 | func (m *MTProto) makeAuthKey() error { 24 | m.serviceModeActivated = true 25 | 26 | maxRetries := 5 27 | nonceCreate: 28 | nonceFirst := tl.RandomInt128() 29 | var ( 30 | res *objects.ResPQ 31 | err error 32 | ) 33 | 34 | if m.cdn { 35 | res, err = m.reqPQMulti(nonceFirst) 36 | } else { 37 | res, err = m.reqPQ(nonceFirst) 38 | } 39 | 40 | if err != nil { 41 | return fmt.Errorf("reqPQ: %w", err) 42 | } 43 | 44 | if nonceFirst.Cmp(res.Nonce.Int) != 0 { 45 | if maxRetries > 0 { 46 | maxRetries-- 47 | time.Sleep(200 * time.Millisecond) 48 | goto nonceCreate 49 | } 50 | return fmt.Errorf("reqPQ: nonce mismatch (%v, %v)", nonceFirst, res.Nonce) 51 | } 52 | 53 | found := false 54 | for _, b := range res.Fingerprints { 55 | if m.cdn { 56 | for _, key := range m.cdnKeys { 57 | if uint64(b) == binary.LittleEndian.Uint64(keys.RSAFingerprint(key)) { 58 | found = true 59 | m.publicKey = key 60 | break 61 | } 62 | } 63 | } 64 | if uint64(b) == binary.LittleEndian.Uint64(keys.RSAFingerprint(m.publicKey)) { 65 | found = true 66 | break 67 | } 68 | } 69 | if !found { 70 | return fmt.Errorf("reqPQ: no matching fingerprint") 71 | } 72 | 73 | // (encoding) p_q_inner_data 74 | pq := big.NewInt(0).SetBytes(res.Pq) 75 | p, q := math.Fac(pq) 76 | nonceSecond := tl.RandomInt256() 77 | nonceServer := res.ServerNonce 78 | 79 | message, err := tl.Marshal(&objects.PQInnerData{ 80 | Pq: res.Pq, 81 | P: p.Bytes(), 82 | Q: q.Bytes(), 83 | Nonce: nonceFirst, 84 | ServerNonce: nonceServer, 85 | NewNonce: nonceSecond, 86 | }) 87 | if err != nil { 88 | m.Logger.Warn("makeAuthKey: failed to marshal pq inner data") 89 | return err 90 | } 91 | 92 | hashAndMsg := make([]byte, 255) 93 | copy(hashAndMsg, append(utils.Sha1(string(message)), message...)) 94 | 95 | encryptedMessage := math.DoRSAencrypt(hashAndMsg, m.publicKey) 96 | 97 | keyFingerprint := int64(binary.LittleEndian.Uint64(keys.RSAFingerprint(m.publicKey))) 98 | dhResponse, err := m.reqDHParams(nonceFirst, nonceServer, p.Bytes(), q.Bytes(), keyFingerprint, encryptedMessage) 99 | if err != nil { 100 | return fmt.Errorf("reqDHParams: %w", err) 101 | } 102 | dhParams, ok := dhResponse.(*objects.ServerDHParamsOk) 103 | if !ok { 104 | return fmt.Errorf("reqDHParams: invalid response") 105 | } 106 | 107 | if nonceFirst.Cmp(dhParams.Nonce.Int) != 0 { 108 | return fmt.Errorf("reqDHParams: nonce mismatch") 109 | } 110 | if nonceServer.Cmp(dhParams.ServerNonce.Int) != 0 { 111 | return fmt.Errorf("reqDHParams: server nonce mismatch") 112 | } 113 | 114 | // check of hash, random bytes trail removing occurs in this func already 115 | decodedMessage, err := ige.DecryptMessageWithTempKeys(dhParams.EncryptedAnswer, nonceSecond.Int, nonceServer.Int) 116 | if err != nil { 117 | m.Logger.Debug(err.Error() + " - retrying") 118 | return m.makeAuthKey() 119 | } 120 | 121 | data, err := tl.DecodeUnknownObject(decodedMessage) 122 | if err != nil { 123 | return fmt.Errorf("decode: %w", err) 124 | } 125 | 126 | dhi, ok := data.(*objects.ServerDHInnerData) 127 | if !ok { 128 | return fmt.Errorf("decode: invalid response") 129 | } 130 | if nonceFirst.Cmp(dhi.Nonce.Int) != 0 { 131 | return fmt.Errorf("decode: nonce mismatch") 132 | } 133 | if nonceServer.Cmp(dhi.ServerNonce.Int) != 0 { 134 | return fmt.Errorf("decode: server nonce mismatch") 135 | } 136 | 137 | // this apparently is just part of diffie hellman, so just leave it as it is, hope that it will just work 138 | _, gB, gAB := math.MakeGAB(dhi.G, big.NewInt(0).SetBytes(dhi.GA), big.NewInt(0).SetBytes(dhi.DhPrime)) 139 | 140 | authKey := gAB.Bytes() 141 | if authKey[0] == 0 { 142 | authKey = authKey[1:] 143 | } 144 | 145 | m.SetAuthKey(authKey) 146 | 147 | t4 := make([]byte, 32+1+8) 148 | copy(t4[0:], nonceSecond.Bytes()) 149 | t4[32] = 1 150 | copy(t4[33:], utils.Sha1Byte(m.GetAuthKey())[0:8]) 151 | nonceHash1 := utils.Sha1Byte(t4)[4:20] 152 | salt := make([]byte, tl.LongLen) 153 | copy(salt, nonceSecond.Bytes()[:8]) 154 | math.Xor(salt, nonceServer.Bytes()[:8]) 155 | m.serverSalt = int64(binary.LittleEndian.Uint64(salt)) 156 | 157 | // (encoding) client_DH_inner_data 158 | clientDHData, err := tl.Marshal(&objects.ClientDHInnerData{ 159 | Nonce: nonceFirst, 160 | ServerNonce: nonceServer, 161 | Retry: 0, 162 | GB: gB.Bytes(), 163 | }) 164 | if err != nil { 165 | m.Logger.Warn("makeAuthKey: failed to marshal client dh inner data") 166 | return err 167 | } 168 | 169 | encryptedMessage, err = ige.EncryptMessageWithTempKeys(clientDHData, nonceSecond.Int, nonceServer.Int) 170 | if err != nil { 171 | return errors.New("dh: " + err.Error()) 172 | } 173 | 174 | dhGenStatus, err := m.setClientDHParams(nonceFirst, nonceServer, encryptedMessage) 175 | if err != nil { 176 | return errors.New("dh: " + err.Error()) 177 | } 178 | 179 | dhg, ok := dhGenStatus.(*objects.DHGenOk) 180 | if !ok { 181 | return fmt.Errorf("invalid response") 182 | } 183 | if nonceFirst.Cmp(dhg.Nonce.Int) != 0 { 184 | return fmt.Errorf("handshake: Wrong nonce: %v, %v", nonceFirst, dhg.Nonce) 185 | } 186 | if nonceServer.Cmp(dhg.ServerNonce.Int) != 0 { 187 | return fmt.Errorf("handshake: Wrong server_nonce: %v, %v", nonceServer, dhg.ServerNonce) 188 | } 189 | if !bytes.Equal(nonceHash1, dhg.NewNonceHash1.Bytes()) { 190 | return fmt.Errorf( 191 | "handshake: Wrong new_nonce_hash1: %v, %v", 192 | hex.EncodeToString(nonceHash1), 193 | hex.EncodeToString(dhg.NewNonceHash1.Bytes()), 194 | ) 195 | } 196 | 197 | m.serviceModeActivated = false 198 | m.encrypted = true 199 | if err := m.SaveSession(m.memorySession); err != nil { 200 | m.Logger.Error("saving session: ", err) 201 | } 202 | return err 203 | } 204 | -------------------------------------------------------------------------------- /internal/aes_ige/aes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 RoseLoverX 2 | 3 | package ige 4 | 5 | import ( 6 | "bytes" 7 | "crypto/aes" 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "math/big" 11 | 12 | "github.com/amarnathcjd/gogram/internal/utils" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type AesBlock [aes.BlockSize]byte 17 | type AesKV [32]byte 18 | type AesIgeBlock [48]byte 19 | 20 | func MessageKey(authKey, msgPadded []byte, decode bool) []byte { 21 | var x int 22 | if decode { 23 | x = 8 24 | } else { 25 | x = 0 26 | } 27 | 28 | // `msg_key_large = SHA256 (substr (auth_key, 88+x, 32) + plaintext + random_padding);` 29 | var msgKeyLarge [sha256.Size]byte 30 | { 31 | h := sha256.New() 32 | 33 | substr := authKey[88+x:] 34 | _, _ = h.Write(substr[:32]) 35 | _, _ = h.Write(msgPadded) 36 | 37 | h.Sum(msgKeyLarge[:0]) 38 | } 39 | r := make([]byte, 16) 40 | // `msg_key = substr (msg_key_large, 8, 16);` 41 | copy(r, msgKeyLarge[8:8+16]) 42 | return r 43 | } 44 | 45 | func Encrypt(msg, authKey []byte) (out, msgKey []byte, _ error) { 46 | return encrypt(msg, authKey, false) 47 | } 48 | 49 | func encrypt(msg, authKey []byte, decode bool) (out, msgKey []byte, _ error) { 50 | padding := 16 + (16-(len(msg)%16))&15 51 | data := make([]byte, len(msg)+padding) 52 | n := copy(data, msg) 53 | 54 | // Fill padding using secure PRNG. 55 | // 56 | // See https://core.telegram.org/mtproto/description#encrypted-message-encrypted-data. 57 | if _, err := rand.Read(data[n:]); err != nil { 58 | return nil, nil, err 59 | } 60 | 61 | msgKey = MessageKey(authKey, data, decode) 62 | aesKey, aesIV := aesKeys(msgKey[:], authKey, decode) 63 | 64 | c, err := NewCipher(aesKey[:], aesIV[:]) 65 | if err != nil { 66 | return nil, nil, err 67 | } 68 | 69 | out = make([]byte, len(data)) 70 | if err := c.doAES256IGEencrypt(data, out); err != nil { 71 | return nil, nil, err 72 | } 73 | 74 | return out, msgKey, nil 75 | } 76 | 77 | func Decrypt(msg, authKey, checkData []byte) ([]byte, error) { 78 | return decrypt(msg, authKey, checkData, true) 79 | } 80 | 81 | func decrypt(msg, authKey, msgKey []byte, decode bool) ([]byte, error) { 82 | aesKey, aesIV := aesKeys(msgKey, authKey, decode) 83 | 84 | c, err := NewCipher(aesKey[:], aesIV[:]) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | out := make([]byte, len(msg)) 90 | if err := c.doAES256IGEdecrypt(msg, out); err != nil { 91 | return nil, err 92 | } 93 | 94 | return out, nil 95 | } 96 | 97 | func doAES256IGEencrypt(data, out, key, iv []byte) error { 98 | c, err := NewCipher(key, iv) 99 | if err != nil { 100 | return err 101 | } 102 | return c.doAES256IGEencrypt(data, out) 103 | } 104 | 105 | func doAES256IGEdecrypt(data, out, key, iv []byte) error { 106 | c, err := NewCipher(key, iv) 107 | if err != nil { 108 | return err 109 | } 110 | return c.doAES256IGEdecrypt(data, out) 111 | } 112 | 113 | // DecryptMessageWithTempKeys decrypts a message using temporary keys obtained during the Diffie-Hellman key exchange. 114 | func DecryptMessageWithTempKeys(msg []byte, nonceSecond, nonceServer *big.Int) ([]byte, error) { 115 | key, iv, errTemp := generateTempKeys(nonceSecond, nonceServer) 116 | if errTemp != nil { 117 | return nil, errTemp 118 | } 119 | decodedWithHash := make([]byte, len(msg)) 120 | err := doAES256IGEdecrypt(msg, decodedWithHash, key, iv) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | // decodedWithHash := SHA1(answer) + answer + (0-15); 16; 126 | decodedHash := decodedWithHash[:20] 127 | decodedMessage := decodedWithHash[20:] 128 | 129 | for i := len(decodedMessage) - 1; i > len(decodedMessage)-16; i-- { 130 | if bytes.Equal(decodedHash, utils.Sha1Byte(decodedMessage[:i])) { 131 | return decodedMessage[:i], nil 132 | } 133 | } 134 | 135 | return nil, errors.New("couldn't trim message: hashes incompatible on more than 16 tries") 136 | } 137 | 138 | // EncryptMessageWithTempKeys encrypts a message using temporary keys obtained during the Diffie-Hellman key exchange. 139 | func EncryptMessageWithTempKeys(msg []byte, nonceSecond, nonceServer *big.Int) ([]byte, error) { 140 | hash := utils.Sha1Byte(msg) 141 | 142 | totalLen := len(hash) + len(msg) 143 | overflowedLen := totalLen % 16 144 | needToAdd := 16 - overflowedLen 145 | 146 | msg = bytes.Join([][]byte{hash, msg, utils.RandomBytes(needToAdd)}, []byte{}) 147 | return encryptMessageWithTempKeys(msg, nonceSecond, nonceServer) 148 | } 149 | 150 | func encryptMessageWithTempKeys(msg []byte, nonceSecond, nonceServer *big.Int) ([]byte, error) { 151 | key, iv, errTemp := generateTempKeys(nonceSecond, nonceServer) 152 | if errTemp != nil { 153 | return nil, errTemp 154 | } 155 | 156 | encodedWithHash := make([]byte, len(msg)) 157 | err := doAES256IGEencrypt(msg, encodedWithHash, key, iv) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | return encodedWithHash, nil 163 | } 164 | 165 | // https://core.telegram.org/mtproto/auth_key#server-responds-in-two-ways 166 | // 167 | // generateTempKeys generates temporary keys for encryption during the key exchange process. 168 | func generateTempKeys(nonceSecond, nonceServer *big.Int) (key, iv []byte, err error) { 169 | if nonceSecond == nil { 170 | return nil, nil, errors.New("nonceSecond is nil") 171 | } 172 | if nonceServer == nil { 173 | return nil, nil, errors.New("nonceServer is nil") 174 | } 175 | 176 | // nonceSecond + nonceServer 177 | t1 := make([]byte, 48) 178 | copy(t1[0:], nonceSecond.Bytes()) 179 | copy(t1[32:], nonceServer.Bytes()) 180 | // SHA1 of nonceSecond + nonceServer 181 | hash1 := utils.Sha1Byte(t1) 182 | 183 | // nonceServer + nonceSecond 184 | t2 := make([]byte, 48) 185 | copy(t2[0:], nonceServer.Bytes()) 186 | copy(t2[16:], nonceSecond.Bytes()) 187 | // SHA1 of nonceServer + nonceSecond 188 | hash2 := utils.Sha1Byte(t2) 189 | 190 | // SHA1(nonceSecond + nonceServer) + substr (SHA1(nonceServer + nonceSecond), 0, 12); 191 | tmpAESKey := make([]byte, 32) 192 | // SHA1 of nonceSecond + nonceServer 193 | copy(tmpAESKey[0:], hash1) 194 | // substr (0 to 12) of SHA1 of nonceServer + nonceSecond 195 | copy(tmpAESKey[20:], hash2[0:12]) 196 | 197 | t3 := make([]byte, 64) // nonceSecond + nonceSecond 198 | copy(t3[0:], nonceSecond.Bytes()) 199 | copy(t3[32:], nonceSecond.Bytes()) 200 | hash3 := utils.Sha1Byte(t3) // SHA1 of nonceSecond + nonceSecond 201 | 202 | // substr (SHA1(server_nonce + new_nonce), 12, 8) + SHA1(new_nonce + new_nonce) + substr (new_nonce, 0, 4); 203 | tmpAESIV := make([]byte, 32) 204 | // substr (12 to 8 ) of SHA1 of nonceServer + nonceSecond 205 | copy(tmpAESIV[0:], hash2[12:12+8]) 206 | // SHA1 of nonceSecond + nonceSecond 207 | copy(tmpAESIV[8:], hash3) 208 | // substr (nonceSecond, 0, 4) 209 | copy(tmpAESIV[28:], nonceSecond.Bytes()[0:4]) 210 | 211 | return tmpAESKey, tmpAESIV, nil 212 | } 213 | -------------------------------------------------------------------------------- /internal/aes_ige/aes_common.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 RoseLoverX 2 | 3 | package ige 4 | 5 | import ( 6 | "bytes" 7 | "crypto/aes" 8 | "crypto/cipher" 9 | ) 10 | 11 | func EncryptAES(data []byte, key string) ([]byte, error) { 12 | block, err := aes.NewCipher([]byte(key)) 13 | if err != nil { 14 | return nil, err 15 | } 16 | b := pkcs5Padding(data, block.BlockSize()) 17 | blockMode := cipher.NewCBCEncrypter(block, []byte(key)[:block.BlockSize()]) 18 | crypted := make([]byte, len(b)) 19 | blockMode.CryptBlocks(crypted, b) 20 | return crypted, nil 21 | } 22 | 23 | func DecryptAES(data []byte, key string) ([]byte, error) { 24 | block, err := aes.NewCipher([]byte(key)) 25 | if err != nil { 26 | return nil, err 27 | } 28 | blockMode := cipher.NewCBCDecrypter(block, []byte(key)[:block.BlockSize()]) 29 | origData := make([]byte, len(data)) 30 | blockMode.CryptBlocks(origData, data) 31 | origData = pkcs5UnPadding(origData) 32 | return origData, nil 33 | } 34 | 35 | func pkcs5Padding(ciphertext []byte, blockSize int) []byte { 36 | padding := blockSize - len(ciphertext)%blockSize 37 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 38 | return append(ciphertext, padtext...) 39 | } 40 | 41 | func pkcs5UnPadding(origData []byte) []byte { 42 | length := len(origData) 43 | if length == 0 { 44 | return origData 45 | } 46 | unpadding := int(origData[length-1]) 47 | return origData[:(length - unpadding)] 48 | } 49 | -------------------------------------------------------------------------------- /internal/aes_ige/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 RoseLoverX 2 | 3 | package ige 4 | 5 | import "github.com/pkg/errors" 6 | 7 | var ( 8 | ErrDataTooSmall = errors.New("AES256IGE: data too small") 9 | ErrDataNotDivisible = errors.New("AES256IGE: data not divisible by block size") 10 | ) 11 | -------------------------------------------------------------------------------- /internal/aes_ige/ige_cipher.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 RoseLoverX 2 | 3 | package ige 4 | 5 | import ( 6 | "crypto/aes" 7 | "crypto/cipher" 8 | "crypto/sha256" 9 | 10 | "github.com/amarnathcjd/gogram/internal/utils" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type Cipher struct { 15 | block cipher.Block 16 | v [3]AesBlock 17 | t, x, y []byte 18 | } 19 | 20 | // NewCipher 21 | func NewCipher(key, iv []byte) (*Cipher, error) { 22 | const ( 23 | firstBlock = iota 24 | secondBlock 25 | thirdBlock 26 | ) 27 | 28 | var err error 29 | 30 | c := new(Cipher) 31 | c.block, err = aes.NewCipher(key) 32 | if err != nil { 33 | return nil, errors.Wrap(err, "creating new cipher") 34 | } 35 | 36 | c.t = c.v[firstBlock][:] 37 | c.x = c.v[secondBlock][:] 38 | c.y = c.v[thirdBlock][:] 39 | copy(c.x, iv[:aes.BlockSize]) 40 | copy(c.y, iv[aes.BlockSize:]) 41 | 42 | return c, nil 43 | } 44 | 45 | func (c *Cipher) doAES256IGEencrypt(in, out []byte) error { //nolint:dupl 46 | if err := isCorrectData(in); err != nil { 47 | return err 48 | } 49 | 50 | for i := 0; i < len(in); i += aes.BlockSize { 51 | utils.Xor(c.x, in[i:i+aes.BlockSize]) 52 | c.block.Encrypt(c.t, c.x) 53 | utils.Xor(c.t, c.y) 54 | c.x, c.y = c.t, in[i:i+aes.BlockSize] 55 | copy(out[i:], c.t) 56 | } 57 | return nil 58 | } 59 | 60 | func (c *Cipher) doAES256IGEdecrypt(in, out []byte) error { //nolint:dupl 61 | if err := isCorrectData(in); err != nil { 62 | return err 63 | } 64 | 65 | for i := 0; i < len(in); i += aes.BlockSize { 66 | utils.Xor(c.y, in[i:i+aes.BlockSize]) 67 | c.block.Decrypt(c.t, c.y) 68 | utils.Xor(c.t, c.x) 69 | c.y, c.x = c.t, in[i:i+aes.BlockSize] 70 | copy(out[i:], c.t) 71 | } 72 | return nil 73 | } 74 | 75 | func isCorrectData(data []byte) error { 76 | if len(data) < aes.BlockSize { 77 | return ErrDataTooSmall 78 | } 79 | if len(data)%aes.BlockSize != 0 { 80 | return ErrDataNotDivisible 81 | } 82 | return nil 83 | } 84 | 85 | // -------------------------------------------------------------------------------------------------- 86 | 87 | func aesKeys(msgKey, authKey []byte, decode bool) (aesKey, aesIv [32]byte) { 88 | var x int 89 | if decode { 90 | x = 8 91 | } else { 92 | x = 0 93 | } 94 | 95 | // aes_key = substr (sha256_a, 0, 8) + substr (sha256_b, 8, 16) + substr (sha256_a, 24, 8); 96 | computeAesKey := func(sha256a, sha256b []byte) (v [32]byte) { 97 | n := copy(v[:], sha256a[:8]) 98 | n += copy(v[n:], sha256b[8:16+8]) 99 | copy(v[n:], sha256a[24:24+8]) 100 | return v 101 | } 102 | // aes_iv = substr (sha256_b, 0, 8) + substr (sha256_a, 8, 16) + substr (sha256_b, 24, 8); 103 | computeAesIV := func(sha256b, sha256a []byte) (v [32]byte) { 104 | n := copy(v[:], sha256a[:8]) 105 | n += copy(v[n:], sha256b[8:16+8]) 106 | copy(v[n:], sha256a[24:24+8]) 107 | return v 108 | } 109 | 110 | var sha256a, sha256b [256]byte 111 | // sha256_a = SHA256 (msg_key + substr (auth_key, x, 36)); 112 | { 113 | h := sha256.New() 114 | 115 | _, _ = h.Write(msgKey) 116 | _, _ = h.Write(authKey[x : x+36]) 117 | 118 | h.Sum(sha256a[:0]) 119 | } 120 | // sha256_b = SHA256 (substr (auth_key, 40+x, 36) + msg_key); 121 | { 122 | h := sha256.New() 123 | 124 | substr := authKey[40+x:] 125 | _, _ = h.Write(substr[:36]) 126 | _, _ = h.Write(msgKey) 127 | 128 | h.Sum(sha256b[:0]) 129 | } 130 | 131 | return computeAesKey(sha256a[:], sha256b[:]), computeAesIV(sha256a[:], sha256b[:]) 132 | } 133 | -------------------------------------------------------------------------------- /internal/aes_ige/password.go: -------------------------------------------------------------------------------- 1 | package ige 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "crypto/sha512" 8 | "hash" 9 | "math/big" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func GetInputCheckPassword(password string, srpB []byte, mp *ModPow, random []byte) (*SrpAnswer, error) { 15 | if password == "" { 16 | return nil, nil 17 | } 18 | err := validateCurrentAlgo(srpB, mp) 19 | if err != nil { 20 | return nil, errors.Wrap(err, "validating CurrentAlgo") 21 | } 22 | p := BytesToBig(mp.P) 23 | g := big.NewInt(int64(mp.G)) 24 | gBytes := Pad256(g.Bytes()) 25 | a := BytesToBig(random) 26 | ga := Pad256(BigExp(g, a, p).Bytes()) 27 | gb := Pad256(srpB) 28 | u := BytesToBig(calcSHA256(ga, gb)) 29 | x := BytesToBig(PasswordHash2([]byte(password), mp.Salt1, mp.Salt2)) 30 | v := BigExp(g, x, p) 31 | k := BytesToBig(calcSHA256(mp.P, gBytes)) 32 | kv := k.Mul(k, v).Mod(k, p) 33 | t := BytesToBig(srpB) 34 | if t.Sub(t, kv).Cmp(big.NewInt(0)) == -1 { 35 | t.Add(t, p) 36 | } 37 | 38 | sa := Pad256(BigExp(t, u.Mul(u, x).Add(u, a), p).Bytes()) 39 | 40 | ka := calcSHA256(sa) 41 | 42 | M1 := calcSHA256( 43 | BytesXor(calcSHA256(mp.P), calcSHA256(gBytes)), 44 | calcSHA256(mp.Salt1), 45 | calcSHA256(mp.Salt2), 46 | ga, 47 | gb, 48 | ka, 49 | ) 50 | 51 | return &SrpAnswer{ 52 | GA: ga, 53 | M1: M1, 54 | }, nil 55 | } 56 | 57 | type ModPow struct { 58 | Salt1 []byte 59 | Salt2 []byte 60 | G int32 61 | P []byte 62 | } 63 | 64 | type SrpAnswer struct { 65 | GA []byte 66 | M1 []byte 67 | } 68 | 69 | func validateCurrentAlgo(srpB []byte, mp *ModPow) error { 70 | if dhHandshakeCheckConfigIsError(mp.G, mp.P) { 71 | return errors.New("receive invalid config g") 72 | } 73 | 74 | p := BytesToBig(mp.P) 75 | gb := BytesToBig(srpB) 76 | 77 | if big.NewInt(0).Cmp(gb) != -1 || gb.Cmp(p) != -1 || len(srpB) < 248 || len(srpB) > 256 { 78 | return errors.New("receive invalid value of B") 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func saltingHashing(data, salt []byte) []byte { 85 | return calcSHA256(salt, data, salt) 86 | } 87 | 88 | func passwordHash1(password, salt1, salt2 []byte) []byte { 89 | return saltingHashing(saltingHashing(password, salt1), salt2) 90 | } 91 | 92 | func PasswordHash2(password, salt1, salt2 []byte) []byte { 93 | return saltingHashing(pbkdf2sha512(passwordHash1(password, salt1, salt2), salt1, 100000), salt2) 94 | } 95 | 96 | func pbkdf2sha512(hash1, salt1 []byte, i int) []byte { 97 | return AlgoKey(hash1, salt1, i, 64, sha512.New) 98 | } 99 | 100 | func Pad256(b []byte) []byte { 101 | if len(b) >= 256 { 102 | return b[len(b)-256:] 103 | } 104 | 105 | tmp := make([]byte, 256) 106 | copy(tmp[256-len(b):], b) 107 | 108 | return tmp 109 | } 110 | 111 | func calcSHA256(arrays ...[]byte) []byte { 112 | h := sha256.New() 113 | for _, arr := range arrays { 114 | h.Write(arr) 115 | } 116 | return h.Sum(nil) 117 | } 118 | 119 | func BytesToBig(b []byte) *big.Int { 120 | return new(big.Int).SetBytes(b) 121 | } 122 | 123 | func BigExp(x, y, m *big.Int) *big.Int { 124 | return new(big.Int).Exp(x, y, m) 125 | } 126 | 127 | func dhHandshakeCheckConfigIsError(_ int32, _ []byte) bool { 128 | return false 129 | } 130 | 131 | func AlgoKey(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte { 132 | prf := hmac.New(h, password) 133 | hashLen := prf.Size() 134 | numBlocks := (keyLen + hashLen - 1) / hashLen 135 | 136 | var buf [4]byte 137 | dk := make([]byte, 0, numBlocks*hashLen) 138 | U := make([]byte, hashLen) 139 | for block := 1; block <= numBlocks; block++ { 140 | prf.Reset() 141 | prf.Write(salt) 142 | buf[0] = byte(block >> 24) 143 | buf[1] = byte(block >> 16) 144 | buf[2] = byte(block >> 8) 145 | buf[3] = byte(block) 146 | prf.Write(buf[:4]) 147 | dk = prf.Sum(dk) 148 | T := dk[len(dk)-hashLen:] 149 | copy(U, T) 150 | 151 | for n := 2; n <= iter; n++ { 152 | prf.Reset() 153 | prf.Write(U) 154 | U = U[:0] 155 | U = prf.Sum(U) 156 | for x := range U { 157 | T[x] ^= U[x] 158 | } 159 | } 160 | } 161 | return dk[:keyLen] 162 | } 163 | 164 | func BytesXor(a, b []byte) []byte { 165 | res := make([]byte, len(a)) 166 | copy(res, a) 167 | for i := range res { 168 | res[i] ^= b[i] 169 | } 170 | return res 171 | } 172 | 173 | func RandomBytes(size int) []byte { 174 | b := make([]byte, size) 175 | _, _ = rand.Read(b) 176 | return b 177 | } 178 | -------------------------------------------------------------------------------- /internal/cmd/tlgen/gen/common.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dave/jennifer/jen" 7 | 8 | "github.com/amarnathcjd/gogram/internal/cmd/tlgen/tlparser" 9 | ) 10 | 11 | func (*Generator) generateMethodCallerFunc(method tlparser.Method) *jen.Statement { 12 | resp := createParamsStructFromMethod(method) 13 | maximumPositionalArguments := 0 14 | if haveOptionalParams(resp.Parameters) { 15 | maximumPositionalArguments++ 16 | } 17 | 18 | funcParameters := make([]jen.Code, 0) 19 | methodName := goify(method.Name, true) 20 | typeName := methodName + "Params" 21 | 22 | argsAsSingleItem := false 23 | if len(resp.Parameters) > maximumPositionalArguments { 24 | argsAsSingleItem = true 25 | funcParameters = []jen.Code{jen.Id("params").Id("*" + typeName)} 26 | } 27 | 28 | parameters := jen.Dict{} 29 | for _, arg := range funcParameters { 30 | parameters[arg] = arg 31 | } 32 | 33 | requestStruct := jen.Op("&").Id(typeName).Values(parameters) 34 | if argsAsSingleItem { 35 | requestStruct = jen.Id("params") 36 | } 37 | 38 | assertedType := goify(method.Response.Type, true) 39 | //firstErrorReturn := jen.Code(jen.Nil()) 40 | if assertedType == "Bool" { 41 | assertedType = "bool" 42 | //firstErrorReturn = jen.False() 43 | } 44 | if assertedType == "Long" { 45 | assertedType = "int64" 46 | //firstErrorReturn = jen.Lit(0) 47 | } 48 | if assertedType == "Int" { 49 | assertedType = "int" 50 | //firstErrorReturn = jen.Lit(0) 51 | } 52 | if assertedType == "int256" { 53 | assertedType = "*tl.Int256" 54 | } 55 | 56 | if method.Response.IsList { 57 | assertedType = "[]" + assertedType 58 | } 59 | 60 | return jen.Func().Params(jen.Id("c").Id("*Client")).Id(methodName).Params(funcParameters...).Params(jen.Id(assertedType), jen.Error()).Block( 61 | jen.Var().Id("resp").Id(assertedType), 62 | jen.Err().Op(":=").Id("c.MakeRequest").Call(requestStruct, jen.Id("&resp")), 63 | jen.Return(jen.Id("resp"), jen.Err()), 64 | ) 65 | } 66 | 67 | func createCrcFunc(typ string, crc uint32) *jen.Statement { 68 | hex := fmt.Sprintf("0x%x", crc) 69 | return jen.Func().Params(jen.Id(typ)).Id("CRC").Params().Uint32(). 70 | Id("{" + jen.Return(jen.Id(hex)).GoString() + "}") 71 | } 72 | -------------------------------------------------------------------------------- /internal/cmd/tlgen/gen/gen.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/dave/jennifer/jen" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/amarnathcjd/gogram/internal/cmd/tlgen/tlparser" 13 | ) 14 | 15 | var capitalizePatterns = []string{ 16 | "id", 17 | "api", 18 | "url", 19 | "p2p", 20 | "sha", 21 | "srp", 22 | } 23 | 24 | type Generator struct { 25 | schema *internalSchema 26 | outdir string 27 | 28 | PackageName string 29 | PackageHeader string 30 | } 31 | 32 | func NewGenerator(tlschema *tlparser.Schema, licenseHeader, outdir string) (*Generator, error) { 33 | internalSchema, err := createInternalSchema(tlschema) 34 | if err != nil { 35 | return nil, errors.Wrap(err, "analyzing schema") 36 | } 37 | 38 | return &Generator{ 39 | schema: internalSchema, 40 | outdir: outdir, 41 | PackageName: "telegram", 42 | PackageHeader: licenseHeader + "\nCode generated by tlgen; DO NOT EDIT.", 43 | }, nil 44 | } 45 | 46 | func (g *Generator) Generate(d bool) error { 47 | err := g.generateFile(g.generateEnumDefinitions, filepath.Join(g.outdir, "enums_gen.go"), d) 48 | if err != nil { 49 | return fmt.Errorf("generate enums: %w", err) 50 | } 51 | 52 | err = g.generateFile(g.generateSpecificStructs, filepath.Join(g.outdir, "types_gen.go"), d) 53 | if err != nil { 54 | return fmt.Errorf("generate types: %w", err) 55 | } 56 | 57 | err = g.generateFile(g.generateInterfaces, filepath.Join(g.outdir, "interfaces_gen.go"), d) 58 | if err != nil { 59 | return fmt.Errorf("generate interfaces: %w", err) 60 | } 61 | 62 | err = g.generateFile(g.generateMethods, filepath.Join(g.outdir, "methods_gen.go"), d) 63 | if err != nil { 64 | return fmt.Errorf("generate methods: %w", err) 65 | } 66 | 67 | err = g.generateFile(g.generateInit, filepath.Join(g.outdir, "init_gen.go"), d) 68 | if err != nil { 69 | return fmt.Errorf("generate init: %w", err) 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func (*Generator) generateFile(f func(file *jen.File, d bool), filename string, genDocs bool) error { 76 | file := jen.NewFile("telegram") 77 | file.HeaderComment("Code generated by TLParser; DO NOT EDIT. (c) @amarnathcjd") 78 | f(file, genDocs) 79 | 80 | buf := bytes.NewBuffer([]byte{}) 81 | if err := file.Render(buf); err != nil { 82 | return err 83 | } 84 | 85 | return os.WriteFile(filename, buf.Bytes(), 0600) 86 | } 87 | -------------------------------------------------------------------------------- /internal/cmd/tlgen/gen/schema.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/amarnathcjd/gogram/internal/cmd/tlgen/tlparser" 7 | ) 8 | 9 | type goifiedName = string 10 | type nativeName = string 11 | 12 | type internalSchema struct { 13 | InterfaceCommnets map[nativeName]string 14 | Types map[nativeName][]tlparser.Object 15 | SingleInterfaceTypes []tlparser.Object 16 | Enums map[nativeName][]enum 17 | Methods []tlparser.Method 18 | } 19 | 20 | type enum struct { 21 | Name nativeName 22 | CRC uint32 23 | } 24 | 25 | func createInternalSchema(nativeSchema *tlparser.Schema) (*internalSchema, error) { 26 | internalSchema := &internalSchema{ 27 | InterfaceCommnets: make(map[string]string), 28 | Enums: make(map[string][]enum), 29 | Types: make(map[string][]tlparser.Object), 30 | SingleInterfaceTypes: make([]tlparser.Object, 0), 31 | Methods: make([]tlparser.Method, 0), 32 | } 33 | 34 | reversedObjects := make(map[string][]tlparser.Object) 35 | for _, obj := range nativeSchema.Objects { 36 | if reversedObjects[obj.Interface] == nil { 37 | reversedObjects[obj.Interface] = make([]tlparser.Object, 0) 38 | } 39 | 40 | reversedObjects[obj.Interface] = append(reversedObjects[obj.Interface], obj) 41 | } 42 | 43 | for interfaceName, objects := range reversedObjects { 44 | for _, obj := range objects { 45 | if strings.EqualFold(obj.Name, obj.Interface) { 46 | obj.Name += "Obj" 47 | } 48 | } 49 | 50 | if interfaceIsEnum(objects) { 51 | enums := make([]enum, len(objects)) 52 | for i, obj := range objects { 53 | enums[i] = enum{ 54 | Name: obj.Name, 55 | CRC: obj.CRC, 56 | } 57 | } 58 | 59 | internalSchema.Enums[interfaceName] = enums 60 | continue 61 | } 62 | 63 | if len(objects) == 1 { 64 | internalSchema.SingleInterfaceTypes = append(internalSchema.SingleInterfaceTypes, objects[0]) 65 | // delete(reversedObjects, interfaceName) 66 | continue 67 | } 68 | 69 | internalSchema.Types[interfaceName] = objects 70 | } 71 | 72 | internalSchema.Methods = nativeSchema.Methods 73 | internalSchema.InterfaceCommnets = nativeSchema.TypeComments 74 | return internalSchema, nil 75 | } 76 | 77 | func (g *Generator) getAllConstructors() (structs, enums []goifiedName) { 78 | structs, enums = make([]string, 0), make([]string, 0) 79 | 80 | for _, items := range g.schema.Types { 81 | for _, _struct := range items { 82 | t := goify(_struct.Name, true) 83 | if goify(_struct.Name, true) == goify(_struct.Interface, true) { 84 | t = goify(_struct.Name+"Obj", true) 85 | } 86 | structs = append(structs, t) 87 | } 88 | } 89 | for _, _struct := range g.schema.SingleInterfaceTypes { 90 | structs = append(structs, goify(_struct.Name, true)) 91 | } 92 | for _, method := range g.schema.Methods { 93 | structs = append(structs, goify(method.Name+"Params", true)) 94 | } 95 | 96 | for _, items := range g.schema.Enums { 97 | for _, enum := range items { 98 | enums = append(enums, goify(enum.Name, true)) 99 | } 100 | } 101 | 102 | return structs, enums 103 | } 104 | 105 | func interfaceIsEnum(in []tlparser.Object) bool { 106 | for _, obj := range in { 107 | if len(obj.Parameters) > 0 { 108 | return false 109 | } 110 | } 111 | 112 | return true 113 | } 114 | -------------------------------------------------------------------------------- /internal/cmd/tlgen/gen/tl_gen_enums.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/dave/jennifer/jen" 8 | ) 9 | 10 | func (g *Generator) generateEnumDefinitions(file *jen.File, _ bool) { 11 | enumTypes := make([]string, len(g.schema.Enums)) 12 | enumIndex := 0 13 | for _type := range g.schema.Enums { 14 | enumTypes[enumIndex] = _type 15 | enumIndex++ 16 | } 17 | 18 | sort.Strings(enumTypes) 19 | 20 | for _, enumType := range enumTypes { 21 | values := g.schema.Enums[enumType] 22 | sort.Slice(values, func(i, j int) bool { 23 | return values[i].Name < values[j].Name 24 | }) 25 | 26 | file.Add(g.generateSpecificEnum(enumType, values)...) 27 | } 28 | } 29 | 30 | func (*Generator) generateSpecificEnum(enumType string, enumValues []enum) []jen.Code { 31 | total := make([]jen.Code, 0) 32 | 33 | typeID := goify(enumType, true) 34 | 35 | enumDef := jen.Type().Id(typeID).Uint32() 36 | total = append(total, enumDef, jen.Line()) 37 | 38 | opc := make([]jen.Code, len(enumValues)) 39 | cases := make([]jen.Code, len(enumValues)) 40 | for i, id := range enumValues { 41 | name := goify(id.Name, true) 42 | 43 | opc[i] = jen.Id(name).Id(typeID).Op("=").Id(fmt.Sprintf("%#v", id.CRC)) 44 | cases[i] = jen.Case(jen.Id(typeID).Call(jen.Id(fmt.Sprintf("%#v", id.CRC)))).Block(jen.Return(jen.Lit(id.Name))) 45 | } 46 | 47 | total = append(total, jen.Const().Defs(opc...), jen.Line()) 48 | 49 | cases = append(cases, jen.Default().Block(jen.Return(jen.Lit("")))) 50 | 51 | stringFunc := jen.Func().Params(jen.Id("e").Id(typeID)).Id("String").Params().String().Block( 52 | jen.Switch(jen.Id("e")).Block(cases...), 53 | ) 54 | 55 | crcFunc := jen.Func().Params(jen.Id("e").Id(typeID)).Id("CRC").Params().Uint32(). 56 | Id("{ return uint32(e) }") 57 | 58 | total = append(total, 59 | stringFunc, 60 | jen.Line(), 61 | jen.Line(), 62 | crcFunc, 63 | jen.Line(), 64 | jen.Line(), 65 | ) 66 | 67 | return total 68 | } 69 | -------------------------------------------------------------------------------- /internal/cmd/tlgen/gen/tl_gen_init.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/dave/jennifer/jen" 7 | ) 8 | 9 | var tlPackagePath = "github.com/amarnathcjd/gogram/internal/encoding/tl" 10 | var errorsPackagePath = "github.com/pkg/errors" 11 | 12 | func (g *Generator) generateInit(file *jen.File, _ bool) { 13 | structs, enums := g.getAllConstructors() 14 | 15 | initFunc := jen.Func().Id("init").Params().Block( 16 | g.createInitStructs(structs...), 17 | jen.Line(), 18 | g.createInitEnums(enums...), 19 | ) 20 | 21 | file.Add(initFunc) 22 | } 23 | 24 | func (*Generator) createInitStructs(itemNames ...string) jen.Code { 25 | sort.Strings(itemNames) 26 | 27 | structs := make([]jen.Code, len(itemNames)) 28 | for i, item := range itemNames { 29 | structs[i] = jen.Op("&").Id(item).Block() 30 | } 31 | 32 | return jen.Qual(tlPackagePath, "RegisterObjects").Call( 33 | structs..., 34 | ) 35 | } 36 | 37 | func (*Generator) createInitEnums(itemNames ...string) jen.Code { 38 | sort.Strings(itemNames) 39 | 40 | enums := make([]jen.Code, len(itemNames)) 41 | for i, item := range itemNames { 42 | enums[i] = jen.Id(item) 43 | } 44 | 45 | return jen.Qual(tlPackagePath, "RegisterEnums").Call( 46 | enums..., 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /internal/cmd/tlgen/gen/tl_gen_interfaces.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | 7 | "github.com/amarnathcjd/gogram/internal/cmd/tlgen/tlparser" 8 | "github.com/dave/jennifer/jen" 9 | ) 10 | 11 | func (g *Generator) generateInterfaces(f *jen.File, d bool) { 12 | keys := make([]string, 0, len(g.schema.Types)) 13 | for key := range g.schema.Types { 14 | keys = append(keys, key) 15 | } 16 | sort.Strings(keys) 17 | 18 | // wg := sync.WaitGroup{} 19 | // for _, key := range keys { 20 | // structs := g.schema.Types[key] 21 | 22 | // fmt.Println("Gk", key) 23 | // wg.Add(1) 24 | // go func(structs []tlparser.Object, key string) { 25 | // defer wg.Done() 26 | // for j, _type := range structs { 27 | // go func(_type tlparser.Object, j int, key string) { 28 | // g.schema.Types[key][j].Comment = g.generateComment(_type.Name, "constructor") 29 | 30 | // fmt.Println("Gk", key, "Comment", g.schema.Types[key][j].Comment) 31 | // }(_type, j, key) 32 | // } 33 | // }(structs, key) 34 | // } 35 | 36 | // wg.Wait() 37 | 38 | for _, i := range keys { 39 | f.Add(jen.Type().Id(goify(i, true)).Interface( 40 | jen.Qual(tlPackagePath, "Object"), 41 | jen.Id("Implements"+goify(i, true)).Params(), 42 | )) 43 | 44 | structs := g.schema.Types[i] 45 | 46 | sort.Slice(structs, func(i, j int) bool { 47 | return structs[i].Name < structs[j].Name 48 | }) 49 | 50 | if d { 51 | wg := sync.WaitGroup{} 52 | for i, _type := range structs { 53 | wg.Add(1) 54 | go func(_type tlparser.Object, i int) { 55 | defer wg.Done() 56 | comment, pComments := g.generateComment(_type.Name, "constructor") 57 | structs[i].Comment = comment 58 | 59 | if pComments != nil && len(pComments) == len(_type.Parameters) { 60 | for j := range _type.Parameters { 61 | pComments[j] = cleanComment(pComments[j]) 62 | structs[i].Parameters[j].Comment = pComments[j] 63 | } 64 | } 65 | }(_type, i) 66 | } 67 | 68 | wg.Wait() 69 | } 70 | 71 | for _, _type := range structs { 72 | if goify(_type.Name, true) == goify(i, true) { 73 | _type.Name += "Obj" 74 | } 75 | 76 | f.Add(g.generateStructTypeAndMethods(_type, []string{goify(i, true)})) 77 | f.Line() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/cmd/tlgen/gen/tl_gen_methods.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "regexp" 8 | "sort" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/dave/jennifer/jen" 13 | 14 | "github.com/amarnathcjd/gogram/internal/cmd/tlgen/tlparser" 15 | ) 16 | 17 | var maximumPositionalArguments = 5 18 | 19 | func (g *Generator) generateMethods(f *jen.File, d bool) { 20 | sort.Slice(g.schema.Methods, func(i, j int) bool { 21 | return g.schema.Methods[i].Name < g.schema.Methods[j].Name 22 | }) 23 | 24 | if d { 25 | wg := sync.WaitGroup{} 26 | for i, method := range g.schema.Methods { 27 | wg.Add(1) 28 | go func(method tlparser.Method, i int) { 29 | defer wg.Done() 30 | g.schema.Methods[i].Comment, _ = g.generateComment(method.Name, "method") 31 | }(method, i) 32 | } 33 | 34 | wg.Wait() 35 | } 36 | 37 | for _, method := range g.schema.Methods { 38 | 39 | f.Add(g.generateStructTypeAndMethods(tlparser.Object{ 40 | Name: method.Name + "Params", 41 | Comment: "", 42 | CRC: method.CRC, 43 | Parameters: method.Parameters, 44 | }, nil)) 45 | f.Line() 46 | if method.Comment != "" { 47 | f.Comment(method.Comment) 48 | } 49 | f.Add(g.generateMethodFunction(&method)) 50 | f.Line() 51 | } 52 | 53 | // sort.Strings(keys) 54 | // 55 | // for _, i := range keys { 56 | // structs := g.schema.Types[i] 57 | // 58 | // sort.Slice(structs, func(i, j int) bool { 59 | // return structs[i].Name < structs[j].Name 60 | // }) 61 | // 62 | // for _, _type := range structs { 63 | // f.Add(g.generateStructTypeAndMethods(_type, []string{goify(i, true)})) 64 | // f.Line() 65 | // } 66 | // } 67 | } 68 | 69 | func (g *Generator) generateComment(name, _type string) (string, []string) { 70 | var base = "https://corefork.telegram.org/" + _type + "/" 71 | req, _ := http.NewRequest("GET", base+name, http.NoBody) 72 | log.Println("tlgen: fetching", req.URL.String()) 73 | 74 | resp, err := http.DefaultClient.Do(req) 75 | if err != nil { 76 | return "", nil 77 | } 78 | 79 | if resp.StatusCode != 200 { 80 | return "", nil 81 | } 82 | 83 | body, err := io.ReadAll(resp.Body) 84 | if err != nil { 85 | return "", nil 86 | } 87 | 88 | ack := string(body) 89 | 90 | matches := regexTableTag.FindAllStringSubmatch(ack, -1) 91 | 92 | var descs []string 93 | 94 | for _, match := range matches { 95 | descs = append(descs, match[1]) 96 | } 97 | 98 | ack = strings.Split(ack, "
")[1] 99 | ack = strings.Split(ack, "

")[0] 100 | ack = strings.ReplaceAll(ack, "

", "") 101 | //ack = strings.ReplaceAll(ack, "see .", "") 102 | ack = regexLinkTag.ReplaceAllString(ack, "[$2](https://corefork.telegram.org$1)") 103 | ack = regexp.MustCompile(`\[(.*?)\]\(.*?\)`).ReplaceAllString(ack, "$1") 104 | 105 | ack = regexCodeTag.ReplaceAllString(ack, "`$1`") 106 | 107 | ack = strings.TrimSpace(ack) 108 | 109 | if strings.Contains(ack, "The page has not been saved") { 110 | return "", nil 111 | } 112 | 113 | if strings.Contains(ack, `