Add /vcard command

This commit is contained in:
Bohdan Horbeshko 2023-06-01 16:37:38 -04:00
parent 75f0532193
commit 8663a29e15
4 changed files with 177 additions and 80 deletions

View file

@ -15,7 +15,7 @@ import (
goxmpp "gosrc.io/xmpp" goxmpp "gosrc.io/xmpp"
) )
var version string = "1.5.0" var version string = "1.6.0-dev"
var commit string var commit string
var sm *goxmpp.StreamManager var sm *goxmpp.StreamManager

View file

@ -64,6 +64,7 @@ var chatCommands = map[string]command{
"silent": command{"message", "send a message without sound"}, "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"}, "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"}, "forward": command{"message_id target_chat", "forwards a message"},
"vcard": command{"", "print vCard as text"},
"add": command{"@username", "add @username to your chat list"}, "add": command{"@username", "add @username to your chat list"},
"join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"}, "join": command{"https://t.me/invite_link", "join to chat via invite link or @publicname"},
"group": command{"title", "create groupchat «title» with current user"}, "group": command{"title", "create groupchat «title» with current user"},
@ -172,6 +173,10 @@ func rawCmdArguments(cmdline string, start uint8) string {
return "" return ""
} }
func keyValueString(key, value string) string {
return fmt.Sprintf("%s: %s", key, value)
}
func (c *Client) unsubscribe(chatID int64) error { func (c *Client) unsubscribe(chatID int64) error {
return gateway.SendPresence( return gateway.SendPresence(
c.xmpp, c.xmpp,
@ -636,6 +641,21 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool)
c.ProcessIncomingMessage(targetChatId, message) 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 // add @contact
case "add": case "add":
return c.cmdAdd(args), true return c.cmdAdd(args), true

View file

@ -24,6 +24,16 @@ import (
"github.com/zelenin/go-tdlib/client" "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 errOffline = errors.New("TDlib instance is offline")
var spaceRegex = regexp.MustCompile(`\s+`) var spaceRegex = regexp.MustCompile(`\s+`)
@ -207,7 +217,7 @@ func (c *Client) ProcessStatusUpdate(chatID int64, status string, show string, o
var photo string var photo string
if chat != nil && chat.Photo != nil { 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 { if err == nil {
defer file.Close() 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) { 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) log.Debugf("file: %#v", file)
if file == nil || file.Local == nil || file.Remote == nil { if file == nil || file.Local == nil || file.Remote == nil {
return "", "" return "", ""
@ -434,6 +458,45 @@ func (c *Client) formatFile(file *client.File, compact bool) (string, string) {
dest := c.content.Path + "/" + basename // destination path dest := c.content.Path + "/" + basename // destination path
link = c.content.Link + "/" + basename // download link link = c.content.Link + "/" + basename // download link
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 "<ERROR>", ""
}
}
defer tempFile.Close()
// copy
_, err = io.Copy(tempFile, file)
if err != nil {
log.Errorf("File copying error: %v", err)
return "<ERROR>", ""
}
} else if path != "" {
log.Errorf("Source file does not exist: %v", path)
return "<ERROR>", ""
} else {
log.Errorf("PHOTO: %#v", err.Error())
return "<ERROR>", ""
}
} else {
// move // move
err = os.Rename(src, dest) err = os.Rename(src, dest)
if err != nil { if err != nil {
@ -445,7 +508,7 @@ func (c *Client) formatFile(file *client.File, compact bool) (string, string) {
return "<ERROR>", "" return "<ERROR>", ""
} }
} }
gateway.CachedStorageSize += size64 }
// chown // chown
if c.content.User != "" { 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) log.Errorf("Wrong user name for chown: %v", err)
} }
} }
// copy or move should have succeeded at this point
gateway.CachedStorageSize += size64
} }
if compact { return src, link
return link, link
} else {
return fmt.Sprintf("%s (%v kbytes) | %s", filepath.Base(src), file.Size/1024, link), link
}
} }
func (c *Client) formatBantime(hours int64) int32 { 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 // ForceOpenFile reliably obtains a file if possible
func (c *Client) OpenPhotoFile(photoFile *client.File, priority int32) (*os.File, string, error) { func (c *Client) ForceOpenFile(tgFile *client.File, priority int32) (*os.File, string, error) {
if photoFile == nil { if tgFile == nil {
return nil, "", errors.New("Photo file not found") return nil, "", errors.New("File not found")
} }
path := photoFile.Local.Path path := tgFile.Local.Path
file, err := os.Open(path) file, err := os.Open(path)
if err == nil { if err == nil {
return file, path, nil return file, path, nil
} else } else
// obtain the photo right now if still not downloaded // obtain the photo right now if still not downloaded
if !photoFile.Local.IsDownloadingCompleted { if !tgFile.Local.IsDownloadingCompleted {
tdFile, tdErr := c.DownloadFile(photoFile.Id, priority, true) tdFile, tdErr := c.DownloadFile(tgFile.Id, priority, true)
if tdErr == nil { if tdErr == nil {
path = tdFile.Local.Path path = tdFile.Local.Path
file, err = os.Open(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
}

View file

@ -10,6 +10,7 @@ import (
"strings" "strings"
"dev.narayana.im/narayana/telegabber/persistence" "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/extensions"
"dev.narayana.im/narayana/telegabber/xmpp/gateway" "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") log.Error("Invalid IQ to")
return return
} }
chat, user, err := session.GetContactByID(toID, nil) info, err := session.GetVcardInfo(toID)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return 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{ answer := stanza.IQ{
Attrs: stanza.Attrs{ Attrs: stanza.Attrs{
From: iq.To, From: iq.To,
@ -365,7 +333,7 @@ func handleGetVcardIq(s xmpp.Sender, iq *stanza.IQ, typ byte) {
Id: iq.Id, Id: iq.Id,
Type: "result", 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) log.Debugf("%#v", answer)
@ -426,53 +394,75 @@ func toToID(to string) (int64, bool) {
return toID, true 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 { if typ == TypeVCardTemp {
vcard := &extensions.IqVcardTemp{} vcard := &extensions.IqVcardTemp{}
vcard.Fn.Text = fn vcard.Fn.Text = info.Fn
if photo != "" { if base64Photo != "" {
vcard.Photo.Type.Text = "image/jpeg" vcard.Photo.Type.Text = "image/jpeg"
vcard.Photo.Binval.Text = photo vcard.Photo.Binval.Text = base64Photo
} }
vcard.Nickname.Text = nickname vcard.Nickname.Text = info.Nickname
vcard.N.Given.Text = given vcard.N.Given.Text = info.Given
vcard.N.Family.Text = family vcard.N.Family.Text = info.Family
vcard.Tel.Number.Text = tel vcard.Tel.Number.Text = info.Tel
vcard.Desc.Text = info vcard.Desc.Text = info.Info
return vcard return vcard
} else if typ == TypeVCard4 { } else if typ == TypeVCard4 {
nodes := []stanza.Node{} nodes := []stanza.Node{}
if fn != "" { if info.Fn != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "fn"}, XMLName: xml.Name{Local: "fn"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "text"}, XMLName: xml.Name{Local: "text"},
Content: fn, Content: info.Fn,
}, },
}, },
}) })
} }
if photo != "" { if base64Photo != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "photo"}, XMLName: xml.Name{Local: "photo"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "uri"}, 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{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "nickname"}, XMLName: xml.Name{Local: "nickname"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "text"}, XMLName: xml.Name{Local: "text"},
Content: nickname, Content: info.Nickname,
}, },
}, },
}, stanza.Node{ }, stanza.Node{
@ -480,44 +470,44 @@ func makeVCardPayload(typ byte, id, fn, photo, nickname, given, family, tel, inf
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "uri"}, 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{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "n"}, XMLName: xml.Name{Local: "n"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "surname"}, XMLName: xml.Name{Local: "surname"},
Content: family, Content: info.Family,
}, },
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "given"}, XMLName: xml.Name{Local: "given"},
Content: given, Content: info.Given,
}, },
}, },
}) })
} }
if tel != "" { if info.Tel != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "tel"}, XMLName: xml.Name{Local: "tel"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "uri"}, XMLName: xml.Name{Local: "uri"},
Content: "tel:" + tel, Content: "tel:" + info.Tel,
}, },
}, },
}) })
} }
if info != "" { if info.Info != "" {
nodes = append(nodes, stanza.Node{ nodes = append(nodes, stanza.Node{
XMLName: xml.Name{Local: "note"}, XMLName: xml.Name{Local: "note"},
Nodes: []stanza.Node{ Nodes: []stanza.Node{
stanza.Node{ stanza.Node{
XMLName: xml.Name{Local: "text"}, XMLName: xml.Name{Local: "text"},
Content: info, Content: info.Info,
}, },
}, },
}) })