diff --git a/example/rich_message/rich_message.go b/example/rich_message/rich_message.go new file mode 100644 index 0000000..8ae07f9 --- /dev/null +++ b/example/rich_message/rich_message.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + + tdlib "github.com/c0re100/gotdlib/client" + "github.com/c0re100/gotdlib/helper" +) + +func GetTdParameters() *tdlib.SetTdlibParametersRequest { + return &tdlib.SetTdlibParametersRequest{ + UseTestDc: false, + DatabaseDirectory: "./tdlib-db", + FilesDirectory: "./tdlib-files", + UseFileDatabase: true, + UseChatInfoDatabase: true, + UseMessageDatabase: true, + UseSecretChats: false, + ApiId: 132712, + ApiHash: "e82c07ad653399a37baca8d1e498e472", + SystemLanguageCode: "en", + DeviceModel: "HuskyNG", + SystemVersion: "3.0", + ApplicationVersion: "3.0", + } +} + +func main() { + tdlib.SetLogLevel(0) + tdlib.SetFilePath("./errors.txt") + + botToken := "your_bot_token" + authorizer := tdlib.BotAuthorizer(botToken) + + authorizer.TdlibParameters <- GetTdParameters() + + client, err := tdlib.NewClient(authorizer) + if err != nil { + log.Fatalf("NewClient error: %s", err) + } + + // Handle SIGINT + ch := make(chan os.Signal, 2) + signal.Notify(ch, os.Interrupt, syscall.SIGINT) + signal.Notify(ch, os.Interrupt, syscall.SIGKILL) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + signal.Notify(ch, os.Interrupt, syscall.SIGQUIT) + signal.Notify(ch, os.Interrupt, syscall.SIGSEGV) + go func() { + <-ch + client.Close() + }() + + me, err := client.GetMe() + if err != nil { + log.Fatalf("GetMe error: %s", err) + } + + log.Printf("%v connected", me.Usernames) + + listener := client.AddEventReceiver(&tdlib.UpdateNewMessage{}, 1000) + + defer listener.Close() + for update := range listener.Updates { + updateMsg := update.(*tdlib.UpdateNewMessage) + + switch updateMsg.Message.Content.MessageContentType() { + case tdlib.TypeMessageRichMessage: + richMsg := updateMsg.Message.Content.(*tdlib.MessageRichMessage).Message + formatted, gErr := helper.GetTextAndEntitiesFromRichMessage(richMsg) + if gErr != nil { + log.Println(gErr) + return + } + msgText := formatted.Text + fmt.Println(msgText) + } + } +} diff --git a/go.mod b/go.mod index 2b1a523..cbc138a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,13 @@ module github.com/c0re100/gotdlib -go 1.16 +go 1.25.0 require ( github.com/google/uuid v1.6.0 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.55.0 +) + +require ( + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect ) diff --git a/go.sum b/go.sum index 131bad5..da6eadf 100644 --- a/go.sum +++ b/go.sum @@ -1,69 +1,8 @@ -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= diff --git a/helper/get_text_and_entities.go b/helper/get_text_and_entities.go new file mode 100644 index 0000000..dcfbaea --- /dev/null +++ b/helper/get_text_and_entities.go @@ -0,0 +1,240 @@ +package helper + +import ( + "encoding/json" + "strings" + "unicode/utf16" + + tdlib "github.com/c0re100/gotdlib/client" +) + +func richMsg2JSON(data any) []byte { + if data == nil { + return []byte("[]") + } + respByte, err := json.Marshal(data) + if err != nil { + return []byte("[]") + } + return respByte +} + +type RawRichText struct { + Type string `json:"@type"` + Text json.RawMessage `json:"text,omitempty"` + Texts []RawRichText `json:"texts,omitempty"` + URL string `json:"url,omitempty"` + Language string `json:"language,omitempty"` +} + +type RawCaption struct { + Type string `json:"@type"` + Text RawRichText `json:"text"` + Credit *RawRichText `json:"credit,omitempty"` +} + +type RawPageBlock struct { + Type string `json:"@type"` + Text *RawRichText `json:"text,omitempty"` + Caption *RawCaption `json:"caption,omitempty"` + Blocks []RawPageBlock `json:"blocks,omitempty"` + Language string `json:"language,omitempty"` +} + +type formattedBuilder struct { + sb strings.Builder + utf16Len int32 + entities []*tdlib.TextEntity +} + +func (b *formattedBuilder) appendText(str string) { + b.sb.WriteString(str) + b.utf16Len += int32(len(utf16.Encode([]rune(str)))) +} + +func (b *formattedBuilder) walkRichText(rt *RawRichText) { + if rt == nil { + return + } + + startOffset := b.utf16Len + + if rt.Type == "richTexts" && len(rt.Texts) > 0 { + for i := range rt.Texts { + b.walkRichText(&rt.Texts[i]) + } + return + } + + if len(rt.Text) > 0 { + var plainStr string + if err := json.Unmarshal(rt.Text, &plainStr); err == nil { + b.appendText(plainStr) + } else { + var childRaw RawRichText + if err := json.Unmarshal(rt.Text, &childRaw); err == nil { + b.walkRichText(&childRaw) + } + } + } + + length := b.utf16Len - startOffset + if length == 0 { + return + } + + var entityType tdlib.TextEntityType + switch rt.Type { + case "richTextBold": + entityType = &tdlib.TextEntityTypeBold{} + case "richTextItalic": + entityType = &tdlib.TextEntityTypeItalic{} + case "richTextUnderline": + entityType = &tdlib.TextEntityTypeUnderline{} + case "richTextStrikethrough": + entityType = &tdlib.TextEntityTypeStrikethrough{} + case "richTextSpoiler": + entityType = &tdlib.TextEntityTypeSpoiler{} + case "richTextCode": + entityType = &tdlib.TextEntityTypeCode{} + case "richTextUrl": + entityType = &tdlib.TextEntityTypeTextUrl{ + Url: rt.URL, + } + case "richTextEmailAddress": + entityType = &tdlib.TextEntityTypeEmailAddress{} + case "richTextFixed", "richTextPre": + entityType = &tdlib.TextEntityTypePreCode{ + Language: rt.Language, + } + } + + if entityType != nil { + b.entities = append(b.entities, &tdlib.TextEntity{ + Offset: startOffset, + Length: length, + Type: entityType, + }) + } +} + +func (b *formattedBuilder) walkBlock(block *RawPageBlock) { + if block == nil { + return + } + + startOffset := b.utf16Len + + if block.Text != nil { + b.walkRichText(block.Text) + } + + if block.Caption != nil { + b.walkRichText(&block.Caption.Text) + if block.Caption.Credit != nil { + b.walkRichText(block.Caption.Credit) + } + } + + length := b.utf16Len - startOffset + if length > 0 { + switch block.Type { + case "pageBlockPreformatted": + b.entities = append(b.entities, &tdlib.TextEntity{ + Offset: startOffset, + Length: length, + Type: &tdlib.TextEntityTypePreCode{ + Language: block.Language, + }, + }) + case "pageBlockBlockQuote", "pageBlockPullQuote": + b.entities = append(b.entities, &tdlib.TextEntity{ + Offset: startOffset, + Length: length, + Type: &tdlib.TextEntityTypeBlockQuote{}, + }) + } + } + + for i := range block.Blocks { + b.walkBlock(&block.Blocks[i]) + } +} + +func GetTextAndEntitiesFromRichMessage(msg *tdlib.RichMessage) (*tdlib.FormattedText, error) { + jsonBytes := richMsg2JSON(msg.Blocks) + + var blocks []RawPageBlock + if err := json.Unmarshal(jsonBytes, &blocks); err != nil { + return nil, err + } + + builder := &formattedBuilder{} + + for i := range blocks { + prevLen := builder.sb.Len() + + builder.walkBlock(&blocks[i]) + + if builder.sb.Len() > prevLen && i < len(blocks)-1 { + builder.appendText("\n") + } + } + + if builder.entities == nil { + builder.entities = []*tdlib.TextEntity{} + } + + return &tdlib.FormattedText{ + Text: builder.sb.String(), + Entities: builder.entities, + }, nil +} + +func GetTextAndEntities(msg *tdlib.Message) (string, []*tdlib.TextEntity, string) { + var msgText string + var msgEnt []*tdlib.TextEntity + msgType := strings.ReplaceAll(msg.Content.MessageContentType(), "message", "") + + switch msg.Content.MessageContentType() { + case tdlib.TypeMessageText: + msgText = msg.Content.(*tdlib.MessageText).Text.Text + msgEnt = msg.Content.(*tdlib.MessageText).Text.Entities + case tdlib.TypeMessageRichMessage: + richMsg := msg.Content.(*tdlib.MessageRichMessage).Message + formatted, err := GetTextAndEntitiesFromRichMessage(richMsg) + if err != nil { + return "", []*tdlib.TextEntity{}, msgType + } + msgText = formatted.Text + msgEnt = formatted.Entities + case tdlib.TypeMessageAnimation: + msgText = msg.Content.(*tdlib.MessageAnimation).Caption.Text + msgEnt = msg.Content.(*tdlib.MessageAnimation).Caption.Entities + case tdlib.TypeMessageAudio: + msgText = msg.Content.(*tdlib.MessageAudio).Caption.Text + msgEnt = msg.Content.(*tdlib.MessageAudio).Caption.Entities + case tdlib.TypeMessageDocument: + msgText = msg.Content.(*tdlib.MessageDocument).Caption.Text + msgEnt = msg.Content.(*tdlib.MessageDocument).Caption.Entities + case tdlib.TypeMessagePhoto: + msgText = msg.Content.(*tdlib.MessagePhoto).Caption.Text + msgEnt = msg.Content.(*tdlib.MessagePhoto).Caption.Entities + case tdlib.TypeMessagePaidMedia: + msgText = msg.Content.(*tdlib.MessagePaidMedia).Caption.Text + msgEnt = msg.Content.(*tdlib.MessagePaidMedia).Caption.Entities + case tdlib.TypeMessageVideo: + msgText = msg.Content.(*tdlib.MessageVideo).Caption.Text + msgEnt = msg.Content.(*tdlib.MessageVideo).Caption.Entities + case tdlib.TypeMessageVoiceNote: + msgText = msg.Content.(*tdlib.MessageVoiceNote).Caption.Text + msgEnt = msg.Content.(*tdlib.MessageVoiceNote).Caption.Entities + case tdlib.TypeMessageContact: + msgText = msg.Content.(*tdlib.MessageContact).Contact.FirstName + msg.Content.(*tdlib.MessageContact).Contact.LastName + msgEnt = []*tdlib.TextEntity{} + default: + return "", []*tdlib.TextEntity{}, msgType + } + + return msgText, msgEnt, msgType +}