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"
)
var version string = "1.5.0"
var version string = "1.6.0-dev"
var commit string
var sm *goxmpp.StreamManager

View file

@ -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

View file

@ -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 "<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("File moving error: %v", err)
log.Errorf("PHOTO: %#v", err.Error())
return "<ERROR>", ""
}
} 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 "<ERROR>", ""
}
}
}
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
}

View file

@ -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,
},
},
})