From 8663a29e157aae6e68cc880a86b3a666da37bfc9 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 1 Jun 2023 16:37:38 -0400 Subject: [PATCH] Add /vcard command --- telegabber.go | 2 +- telegram/commands.go | 20 +++++++ telegram/utils.go | 129 ++++++++++++++++++++++++++++++++++++------- xmpp/handlers.go | 106 ++++++++++++++++------------------- 4 files changed, 177 insertions(+), 80 deletions(-) diff --git a/telegabber.go b/telegabber.go index 72353bb..c8d6a8f 100644 --- a/telegabber.go +++ b/telegabber.go @@ -15,7 +15,7 @@ import ( goxmpp "gosrc.io/xmpp" ) -var version string = "1.5.0" +var version string = "1.6.0-dev" var commit string var sm *goxmpp.StreamManager diff --git a/telegram/commands.go b/telegram/commands.go index 2a72219..ec06f36 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -64,6 +64,7 @@ var chatCommands = map[string]command{ "silent": command{"message", "send a message without sound"}, "schedule": command{"{online | 2006-01-02T15:04:05 | 15:04:05} message", "schedules a message either to timestamp or to whenever the user goes online"}, "forward": command{"message_id target_chat", "forwards a message"}, + "vcard": command{"", "print vCard as text"}, "add": command{"@username", "add @username to your chat list"}, "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, "group": command{"title", "create groupchat «title» with current user"}, @@ -172,6 +173,10 @@ func rawCmdArguments(cmdline string, start uint8) string { return "" } +func keyValueString(key, value string) string { + return fmt.Sprintf("%s: %s", key, value) +} + func (c *Client) unsubscribe(chatID int64) error { return gateway.SendPresence( c.xmpp, @@ -636,6 +641,21 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool) c.ProcessIncomingMessage(targetChatId, message) } } + // print vCard + case "vcard": + info, err := c.GetVcardInfo(chatID) + if err != nil { + return err.Error(), true + } + _, link := c.PermastoreFile(info.Photo, true) + entries := []string{ + keyValueString("Chat title", info.Fn), + keyValueString("Photo", link), + keyValueString("Username", info.Nickname), + keyValueString("Full name", info.Given + " " + info.Family), + keyValueString("Phone number", info.Tel), + } + return strings.Join(entries, "\n"), true // add @contact case "add": return c.cmdAdd(args), true diff --git a/telegram/utils.go b/telegram/utils.go index 9a247c4..851c6c1 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -24,6 +24,16 @@ import ( "github.com/zelenin/go-tdlib/client" ) +type VCardInfo struct { + Fn string + Photo *client.File + Nickname string + Given string + Family string + Tel string + Info string +} + var errOffline = errors.New("TDlib instance is offline") var spaceRegex = regexp.MustCompile(`\s+`) @@ -207,7 +217,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o var photo string if chat != nil && chat.Photo != nil { - file, path, err := c.OpenPhotoFile(chat.Photo.Small, 1) + file, path, err := c.ForceOpenFile(chat.Photo.Small, 1) if err == nil { defer file.Close() @@ -408,6 +418,20 @@ func (c *Client) formatForward(fwd *client.MessageForwardInfo) string { } func (c *Client) formatFile(file *client.File, compact bool) (string, string) { + if file == nil { + return "", "" + } + src, link := c.PermastoreFile(file, false) + + if compact { + return link, link + } else { + return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link), link + } +} + +// PermastoreFile steals a file out of TDlib control into an independent shared directory +func (c *Client) PermastoreFile(file *client.File, clone bool) (string, string) { log.Debugf("file: %#v", file) if file == nil || file.Local == nil || file.Remote == nil { return "", "" @@ -434,18 +458,57 @@ func (c *Client) formatFile(file *client.File, compact bool) (string, string) { dest := c.content.Path + "/" + basename // destination path link = c.content.Link + "/" + basename // download link - // move - err = os.Rename(src, dest) - if err != nil { - linkErr := err.(*os.LinkError) - if linkErr.Err.Error() == "file exists" { - log.Warn(err.Error()) + if clone { + file, path, err := c.ForceOpenFile(file, 1) + if err == nil { + defer file.Close() + + // mode + mode := os.FileMode(0644) + fi, err := os.Stat(path) + if err == nil { + mode = fi.Mode().Perm() + } + + // create destination + tempFile, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode) + if err != nil { + pathErr := err.(*os.PathError) + if pathErr.Err.Error() == "file exists" { + log.Warn(err.Error()) + return src, link + } else { + log.Errorf("File creation error: %v", err) + return "", "" + } + } + defer tempFile.Close() + // copy + _, err = io.Copy(tempFile, file) + if err != nil { + log.Errorf("File copying error: %v", err) + return "", "" + } + } else if path != "" { + log.Errorf("Source file does not exist: %v", path) + return "", "" } else { - log.Errorf("File moving error: %v", err) + log.Errorf("PHOTO: %#v", err.Error()) return "", "" } + } else { + // move + err = os.Rename(src, dest) + if err != nil { + linkErr := err.(*os.LinkError) + if linkErr.Err.Error() == "file exists" { + log.Warn(err.Error()) + } else { + log.Errorf("File moving error: %v", err) + return "", "" + } + } } - gateway.CachedStorageSize += size64 // chown if c.content.User != "" { @@ -464,13 +527,12 @@ func (c *Client) formatFile(file *client.File, compact bool) (string, string) { log.Errorf("Wrong user name for chown: %v", err) } } + + // copy or move should have succeeded at this point + gateway.CachedStorageSize += size64 } - if compact { - return link, link - } else { - return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link), link - } + return src, link } func (c *Client) formatBantime(hours int64) int32 { @@ -1148,20 +1210,20 @@ func (c *Client) DownloadFile(id int32, priority int32, synchronous bool) (*clie }) } -// OpenPhotoFile reliably obtains a photo if possible -func (c *Client) OpenPhotoFile(photoFile *client.File, priority int32) (*os.File, string, error) { - if photoFile == nil { - return nil, "", errors.New("Photo file not found") +// ForceOpenFile reliably obtains a file if possible +func (c *Client) ForceOpenFile(tgFile *client.File, priority int32) (*os.File, string, error) { + if tgFile == nil { + return nil, "", errors.New("File not found") } - path := photoFile.Local.Path + path := tgFile.Local.Path file, err := os.Open(path) if err == nil { return file, path, nil } else // obtain the photo right now if still not downloaded - if !photoFile.Local.IsDownloadingCompleted { - tdFile, tdErr := c.DownloadFile(photoFile.Id, priority, true) + if !tgFile.Local.IsDownloadingCompleted { + tdFile, tdErr := c.DownloadFile(tgFile.Id, priority, true) if tdErr == nil { path = tdFile.Local.Path file, err = os.Open(path) @@ -1248,3 +1310,28 @@ func (c *Client) prepareDiskSpace(size uint64) { } } } + +func (c *Client) GetVcardInfo(toID int64) (VCardInfo, error) { + var info VCardInfo + chat, user, err := c.GetContactByID(toID, nil) + if err != nil { + return info, err + } + + if chat != nil { + info.Fn = chat.Title + + if chat.Photo != nil { + info.Photo = chat.Photo.Small + } + info.Info = c.GetChatDescription(chat) + } + if user != nil { + info.Nickname = user.Username + info.Given = user.FirstName + info.Family = user.LastName + info.Tel = user.PhoneNumber + } + + return info, nil +} diff --git a/xmpp/handlers.go b/xmpp/handlers.go index db2b6ea..780478a 100644 --- a/xmpp/handlers.go +++ b/xmpp/handlers.go @@ -10,6 +10,7 @@ import ( "strings" "dev.narayana.im/narayana/telegabber/persistence" + "dev.narayana.im/narayana/telegabber/telegram" "dev.narayana.im/narayana/telegabber/xmpp/extensions" "dev.narayana.im/narayana/telegabber/xmpp/gateway" @@ -319,45 +320,12 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { log.Error("Invalid IQ to") return } - chat, user, err := session.GetContactByID(toID, nil) + info, err := session.GetVcardInfo(toID) if err != nil { log.Error(err) return } - var fn, photo, nickname, given, family, tel, info string - if chat != nil { - fn = chat.Title - - if chat.Photo != nil { - file, path, err := session.OpenPhotoFile(chat.Photo.Small, 32) - if err == nil { - defer file.Close() - - buf := new(bytes.Buffer) - binval := base64.NewEncoder(base64.StdEncoding, buf) - _, err = io.Copy(binval, file) - binval.Close() - if err == nil { - photo = buf.String() - } else { - log.Errorf("Error calculating base64: %v", path) - } - } else if path != "" { - log.Errorf("Photo does not exist: %v", path) - } else { - log.Errorf("PHOTO: %#v", err.Error()) - } - } - info = session.GetChatDescription(chat) - } - if user != nil { - nickname = user.Username - given = user.FirstName - family = user.LastName - tel = user.PhoneNumber - } - answer := stanza.IQ{ Attrs: stanza.Attrs{ From: iq.To, @@ -365,7 +333,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) { Id: iq.Id, Type: "result", }, - Payload: makeVCardPayload(typ, iq.To, fn, photo, nickname, given, family, tel, info), + Payload: makeVCardPayload(typ, iq.To, info, session), } log.Debugf("%#v", answer) @@ -426,53 +394,75 @@ func toToID(to string) (int64, bool) { return toID, true } -func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, info string) stanza.IQPayload { +func makeVCardPayload(typ byte, id string, info telegram.VCardInfo, session *telegram.Client) stanza.IQPayload { + var base64Photo string + if info.Photo != nil { + file, path, err := session.ForceOpenFile(info.Photo, 32) + if err == nil { + defer file.Close() + + buf := new(bytes.Buffer) + binval := base64.NewEncoder(base64.StdEncoding, buf) + _, err = io.Copy(binval, file) + binval.Close() + if err == nil { + base64Photo = buf.String() + } else { + log.Errorf("Error calculating base64: %v", path) + } + } else if path != "" { + log.Errorf("Photo does not exist: %v", path) + } else { + log.Errorf("PHOTO: %#v", err.Error()) + } + } + if typ == TypeVCardTemp { vcard := &extensions.IqVcardTemp{} - vcard.Fn.Text = fn - if photo != "" { + vcard.Fn.Text = info.Fn + if base64Photo != "" { vcard.Photo.Type.Text = "image/jpeg" - vcard.Photo.Binval.Text = photo + vcard.Photo.Binval.Text = base64Photo } - vcard.Nickname.Text = nickname - vcard.N.Given.Text = given - vcard.N.Family.Text = family - vcard.Tel.Number.Text = tel - vcard.Desc.Text = info + vcard.Nickname.Text = info.Nickname + vcard.N.Given.Text = info.Given + vcard.N.Family.Text = info.Family + vcard.Tel.Number.Text = info.Tel + vcard.Desc.Text = info.Info return vcard } else if typ == TypeVCard4 { nodes := []stanza.Node{} - if fn != "" { + if info.Fn != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "fn"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, - Content: fn, + Content: info.Fn, }, }, }) } - if photo != "" { + if base64Photo != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "photo"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "uri"}, - Content: "data:image/jpeg;base64," + photo, + Content: "data:image/jpeg;base64," + base64Photo, }, }, }) } - if nickname != "" { + if info.Nickname != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "nickname"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, - Content: nickname, + Content: info.Nickname, }, }, }, stanza.Node{ @@ -480,44 +470,44 @@ func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, inf Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "uri"}, - Content: "https://t.me/" + nickname, + Content: "https://t.me/" + info.Nickname, }, }, }) } - if family != "" || given != "" { + if info.Family != "" || info.Given != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "n"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "surname"}, - Content: family, + Content: info.Family, }, stanza.Node{ XMLName: xml.Name{Local: "given"}, - Content: given, + Content: info.Given, }, }, }) } - if tel != "" { + if info.Tel != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "tel"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "uri"}, - Content: "tel:" + tel, + Content: "tel:" + info.Tel, }, }, }) } - if info != "" { + if info.Info != "" { nodes = append(nodes, stanza.Node{ XMLName: xml.Name{Local: "note"}, Nodes: []stanza.Node{ stanza.Node{ XMLName: xml.Name{Local: "text"}, - Content: info, + Content: info.Info, }, }, })