Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Access to Evolution-DataServer (contacts, calendars) through GObject #39

Open
khurshid-alam opened this issue Feb 14, 2017 · 29 comments

Comments

@khurshid-alam
Copy link

khurshid-alam commented Feb 14, 2017

Evolution Data Server can be accessed with Python using gobject introspection. On Ubuntu they the libraries are gir1.x-ebookcontacts-1.x, gir1.x-ebook-1.x. So it is possible to search contact names and open it with respective application. i.e gnome-contacts or evolution (both uses eds as backend). Old kupfer had a evolution plugin which could do the same.

Thanks.

@bluss
Copy link
Member

bluss commented Feb 14, 2017

Our project is looking for maintainers. I am working on fixing bugs in kupfer all the time, and I love to provide quality, but we need interested parties to work on plugins they like.

@khurshid-alam
Copy link
Author

@bluss In sort, it is a request to port old evolution plugin which is defunct now. The only way is through gobject. Anyway, I will look if anyone wants to have a go at this.

@khurshid-alam
Copy link
Author

khurshid-alam commented Mar 11, 2017

Ideas

Ok, I think it's better to use Gnome Contacts and Gnome Calendar instead of complicated gobject method for accessing calendar and contacts data. Both uses EDS as backend anyway.

Both Calendar and Contacts provides shell search dbus backend. We can take advantage of that. However shell-search provider for Gnome Contacts won't be enough we will need to depend of libfolks. I will come back to contacts later, first calendar.

sudo apt-get install gnome-calendar

Calendar

Usage:
  gnome-calendar [OPTION…] — Calendar management

Help Options:
  -h, --help        Show help options

Application Options:
  -v, --version     Display version number
  -d, --date        Open calendar on the passed date
  -u, --uuid        Open calendar showing the passed event

As you can see gnome-calendar can open any existing event in eds if we pass the uuid of that event to the command line option we can take advantage of that. First we can use shell-search provider dbus interface to get all events, extract uuids and then we can open it with gnome-calendar.

#!/usr/bin/python3

import os
import dbus
import subprocess

from subprocess import call


CALENDAR_ID = "gnome_calendar"
_SERVICE_NAME2 = 'org.gnome.Calendar'
_OBJECT_NAME2 = '/org/gnome/Calendar/SearchProvider'
_IFACE_NAME2 = 'org.gnome.Shell.SearchProvider2'



bus = dbus.SessionBus()

proxy = bus.get_object("org.gnome.Calendar", 
                       "/org/gnome/Calendar/SearchProvider")
iface = dbus.Interface(proxy, "org.gnome.Shell.SearchProvider2")

oEvent = []

event_uids = iface.GetInitialResultSet([""])
for event_uid in event_uids:
    print (event_uid)
    event = iface.GetResultMetas([event_uid])
    event_title = event[0]['name']
    event_id = event[0]['id']
    oEvent.append((event[0]['id'], event_title, event[0]['description']))

#print (oEvent)

#now that we got event uuids we can pass it to gnome-calendar

#subprocess.Popen(args=["sh", "-c", "dbus-launch gnome-calendar -u 1487707882.23335.1@ubuntu-dev:[email protected]"])




#example
event_uid1 = "1487707452.23051.0@ubuntu-dev:[email protected]"
iface.ActivateResult(event_uid1, [], 145675656)

@khurshid-alam
Copy link
Author

Contacts

I am more interested in contacts, but shell search provider for Gnome Contacts is not enough as it only returns search-ids instead of actual individual ids required by contacts.

Usage:
  gnome-contacts [OPTION…] — contact management

Help Options:
  -h, --help               Show help options
  --help-all               Show all help options
  --help-gtk               Show GTK+ Options

Application Options:
  -i, --individual         Show contact with this individual id
  -e, --email              Show contact with this email address
  -s, --search             
  --display=DISPLAY        X display to use

That's why we need to depend on libfolks (https://github.com/GNOME/folks). libfolks is a library that aggregates people from multiple sources (eg, Telepathy connection managers, eds) to create metacontacts. Gnome-Contacts has hardcore dependency fot it. It is installed by default by most distro/environment including, Fedora, Arch, Debian/Ubuntu and even Kde. On Ubuntu/Debian the package name is libfolks25 and installed by default. It provides a nice command-line utility folks-inspect which is available under folks-tools package.

sudo apt-get install folks-tools

eds-adbs

As you can see I have several addressbooks in evolution. We can get ids of those addressbooks by:

user@ubuntu-dev:~$ folks-inspect backends eds
Backend 'eds' with 4 persona stores (type ID, ID ('display name')):
  eds, 1451380226.10720.2@Saturnica ('TestAdb')
  eds, 1426007665.3207.2@saturnica ('Unnamed')
  eds, 1428657577.4552.2@saturnica ('Android')
  eds, system-address-book ('Personal')

1451380226.10720.2@Saturnica is one of the ids which is the same as the folder name under ~/.local/evolution/addressbooks.

Individuals ids can be found by running command folks-inspect individuals

Metadata about a particular contact can be found by

user@ubuntu-dev:~$ folks-inspect individuals f87a1a84300a745c21201cfb2bf7adfebf85f1b1
Individual 'f87a1a84300a745c21201cfb2bf7adfebf85f1b1' with 1 personas:
  trust-level           FOLKS_TRUST_LEVEL_PERSONAS
  avatar                (null)
  presence-type         FOLKS_PRESENCE_TYPE_UNSET
  presence-status       
  presence-message      
  client-types          {  }
  is-user               FALSE
  id                    f87a1a84300a745c21201cfb2bf7adfebf85f1b1
  display-name          Tony Stark
  alias                 
  structured-name       Tony Stark
  full-name             Tony Stark
  nickname              
  gender                FOLKS_GENDER_UNSPECIFIED
  urls                  {  }
  phone-numbers         { 03174987789, 9999555555 }
  email-addresses       { [email protected] }
  roles                 { Title: , Organisation: Stark Industries, Role: (null) }
  local-ids             { '1451380226.10720.2@Saturnica:pas-id-56824FD000000001' }
  location              (nil)
  birthday              1988-01-15T18:30:00+0000
  calendar-event-id     (null)
  notes                 {  }
  postal-addresses      { , , Iron Man Villa, 99 Stark Street, New York, , , US }
  
  Personas:
    Persona 'eds:1451380226.10720.2@Saturnica:pas-id-56824FD000000001':
      iid                   1451380226.10720.2@Saturnica:pas-id-56824FD000000001
      uid                   eds:1451380226.10720.2@Saturnica:pas-id-56824FD000000001
      display-id            1451380226.10720.2@Saturnica:pas-id-56824FD000000001

Interestingly, Here the individual id f87a1a84300a745c21201cfb2bf7adfebf85f1b1 is nothing but SHA1 hash of uid listed under Personas:, i.e

import hashlib
m = hashlib.sha1()
m.update(("eds:1451380226.10720.2@Saturnica:pas-id-56824FD000000001").encode('utf-8'))
print(m.hexdigest())  #and it will print the individual id

gnome-contacts -i f87a1a84300a745c21201cfb2bf7adfebf85f1b1

We can extract relevant data like email addresses and the use it like mailto:[email protected] for one of kupfer action. It will start composing messages using default email client.

@khurshid-alam
Copy link
Author

khurshid-alam commented Mar 11, 2017

Contact (Without using folks-inspect)

It is also possible to get contact metadata without using any command line tool. But for that purpose we have to dbus backend of eds. evolution-dataserver is installed by default it launch two processes during boot:

  1. /usr/lib/evolution/evolution-addressbook-factory

  2. /usr/lib/evolution/evolution-addressbook-factory-subprocess

These procceses has bus_names like:

org.gnome.evolution.dataserver.AddressBook9
org.gnome.evolution.dataserver.Subprocess.Backend.AddressBookx1999x2 

9 and 1999x2 depends on the session. I am using simple filter to search through bus names but I am sure there is better method available.

We also know that directory names under ~/.local/share/evolution/addressbook gives us the Address book UIDS. (It's rather crude method, but I couldn't find any other method to get uid without using libfolks or gobject.)

We will also need python3-vobject to parse vcard file. sudo apt-get install python3-vobject

#!/usr/bin/python3

import os
import dbus
import subprocess
import hashlib
import vobject
from collections import defaultdict
from subprocess import call

__all__ = (
	"HOME",
	"XDG_DATA_HOME",
)

# Always points to the user's home directory
HOME = os.path.expanduser("~")
XDG_DATA_EVOLUTION = os.environ.get("XDG_DATA_HOME", os.path.join(HOME, ".local", "share", "evolution", "addressbook"))

addressbook_uids = []
for dir in os.listdir(XDG_DATA_EVOLUTION):
  addressbook_uids += [dir]  #To-do: Don't include trash folder

#we should loop through each addessbook but as example I am using a single adb.
addressbook_uid = addressbook_uids[1]


#get eds factory bus
bus = dbus.SessionBus()
proxy = bus.get_object("org.freedesktop.DBus", 
                       "/")
iface = dbus.Interface(proxy, "org.freedesktop.DBus")

bus_names = iface.ListActivatableNames()
eds_adb = list(filter(lambda x:'org.gnome.evolution.dataserver.AddressBook' in x, bus_names))
EDS_FACTORY_BUS = eds_adb[0]

#get eds subprocess bus
proxy = bus.get_object(EDS_FACTORY_BUS, "/org/gnome/evolution/dataserver/AddressBookFactory")
iface = dbus.Interface(proxy, "org.gnome.evolution.dataserver.AddressBookFactory")

EDS_SUBPROCESS_OBJ_PATH, EDS_SUBPROCESS_BUS  = iface.OpenAddressBook(addressbook_uid)

#Now that we got the required bus names and object path we can get contact pass-ids
proxy = bus.get_object(EDS_SUBPROCESS_BUS, EDS_SUBPROCESS_OBJ_PATH)
iface = dbus.Interface(proxy, "org.gnome.evolution.dataserver.AddressBook")

iface.Open() #otherwise it may fail
contact_pass_ids = iface.GetContactListUids("") #this will give contact_pass_ids

contact_final_obj = {}
for contact_pass_id in contact_pass_ids:
    #Lets form contact_uid from pass_id
    contact_uid = "eds:" + addressbook_uid + ":" + contact_pass_id
    #We also know individual id is just sha1 hash of uid, so
    m = hashlib.sha1()
    m.update((contact_uid).encode('utf-8'))
    contact_individual_id = m.hexdigest()
    print (contact_individual_id)
    print ("\n")

    #we get contact vcard and parse it using python3-vobject
    contact_vcard = iface.GetContact(contact_pass_id)
    vcard = vobject.readOne( contact_vcard )
    full_name = vcard.contents['fn'][0].value
    email_ids = [email.value for email in vcard.contents['email']]
    contact_final_obj[contact_individual_id] = (full_name, email_ids)

print (contact_final_obj)

This gives us nice dictionary object like:

{'ddb389a5ea243dbcab711a4b6cd6682d41b4e561': ('Tony Stark', ['[email protected]']), 'f68015debcdcf5f1c41e0a1e4d7a8ef6db039684': ('Bruce Banner', ['[email protected]'])}

Now we can open contact using

gnome-contacts -i ddb389a5ea243dbcab711a4b6cd6682d41b4e561

@khurshid-alam
Copy link
Author

Kupfer workflow for Contact

  1. User can search contact by contact names (or even email ?)

  2. By default it will have two default action:
    A) Open: Open contacts in gnome-contacts
    B) compose email: it will use default mail client

  3. For multiple emails, kupfer should let user to select a email id through third action pane.

  4. More actions are possible for contacts, but that requires deeper digging.

@bluss
Copy link
Member

bluss commented Mar 14, 2017

Thanks a lot for this! Extracting contacts seems far from straightforward, but maybe we can cut off a few of the corners here and find a good way.

@khurshid-alam
Copy link
Author

khurshid-alam commented Mar 14, 2017

@bluss

I have created a very basic plugin, but I have really hit the wall here. It has some issues. I guess it requires your superior skill to fix it.

  1. it can open any contact in gnome-contacts

  2. it can send email for a contact.......but I want to use a third kuper pane to select a email. I am not sure how to do this. may be separate email source within compose email action?

On Ubuntu/Debian

sudo apt-get install python3-vobject evolution-data-server evolution gnome-contacts

  1. Open evolution , create a seperate addressbook, and few dummy contacts within those.

  2. Open gnome-contacts, it will ask you to choose a address-book. But it has a bug, actually it uses libfolks which aggregates all contacts from all address-books.

eds_contacts.py

# -*- encoding: UTF-8 -*-
__kupfer_name__ = _("Gnome Contacts")
__kupfer_sources__ = ("ContactsSource", )
__kupfer_actions__ = ("NewMailAction", )
__description__ = _("Search and open contact with Gnome-Contacts")
__version__ = "2017.2"
__author__ = ""


import sys
import os
import dbus
import gi
import subprocess
import hashlib
import vobject
import xdg.BaseDirectory as base
gi.require_version('Gtk', '3.0')

from kupfer import plugin_support
from kupfer import pretty, utils
from kupfer import textutils
from kupfer.objects import Leaf, Action, Source
from kupfer.objects import TextLeaf, NotAvailableError, AppLeaf
from kupfer.objects import UrlLeaf, RunnableLeaf, FileLeaf
from kupfer.obj.apps import AppLeafContentMixin
from kupfer.obj.grouping import ToplevelGroupingSource
from kupfer.obj.contacts import ContactLeaf, EmailContact, is_valid_email
from kupfer.obj.contacts import EMAIL_KEY, NAME_KEY
from kupfer.weaklib import dbus_signal_connect_weakly

plugin_support.check_dbus_connection()



Contact_ID = "org.gnome.Contacts"

#get Addressbook UIDs
EDS_ADB_PATH = (os.path.join(base.xdg_data_home, "evolution/addressbook"))
addressbook_uids = []
for dir in os.listdir(EDS_ADB_PATH):
    if dir != "trash":
        addressbook_uids += [dir]

addressbook_uids = [word.replace('system','system-address-book') for word in addressbook_uids]
#print (addressbook_uids)

#get EDS_FACTORY_BUS
EDS_FACTORY_OBJ = "/org/gnome/evolution/dataserver/AddressBookFactory"
EDS_FACTORY_IFACE = "org.gnome.evolution.dataserver.AddressBookFactory"

EDS_SUBPROCESS_IFACE = "org.gnome.evolution.dataserver.AddressBook"
INDIVIDUAL_ID_KEY = "CID"
CONTACT_NAME = "CONTACT_NAME"
CONTACT_EMAILS = "CONTACT_EMAILS"


def _search_bus_name(SERVICE_NAME_FILTER, activate=False):

    interface = None
    obj = None
    sbus = dbus.SessionBus()

    try:
        #check for running pidgin (code from note.py)
        proxy_obj = sbus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
        dbus_iface = dbus.Interface(proxy_obj, 'org.freedesktop.DBus')
        bus_names = dbus_iface.ListActivatableNames()
        eds_adb = list(filter(lambda x:SERVICE_NAME_FILTER in x, bus_names))
        if eds_adb:
            EDS_FACTORY_BUS = eds_adb[0]
    except dbus.exceptions.DBusException as err:
        pretty.print_debug(err)
    return EDS_FACTORY_BUS
                
EDS_FACTORY_BUS = _search_bus_name("org.gnome.evolution.dataserver.AddressBook")



def _create_dbus_connection(SERVICE_NAME, OBJECT_NAME, IFACE_NAME, activate=False):
    ''' Create dbus connection to Pidgin
    @activate: true=starts pidgin if not running
    '''
    interface = None
    obj = None
    sbus = dbus.SessionBus()

    try:
        #check for running pidgin (code from note.py)
        proxy_obj = sbus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
        dbus_iface = dbus.Interface(proxy_obj, 'org.freedesktop.DBus')
        if activate or dbus_iface.NameHasOwner(SERVICE_NAME):
            obj = sbus.get_object(SERVICE_NAME, OBJECT_NAME)
        if obj:
            interface = dbus.Interface(obj, IFACE_NAME)
    except dbus.exceptions.DBusException as err:
        pretty.print_debug(err)
    return interface


class ComposeMail(RunnableLeaf):
    ''' Create new mail without recipient '''
    def __init__(self):
        RunnableLeaf.__init__(self, name=_("Compose New Email"))

    def run(self):
        utils.spawn_async_notify_as("evolution.desktop",
                                   ['xdg-open', 'mailto:'])

    def get_description(self):
        return _("Compose a new message in Evolution")

    def get_icon_name(self):
        return "mail-message-new"




def _load_contacts(addressbook_uids):
    ''' Get service & ifcace name for each addressbooks and then load all contacts '''
    for addressbook_uid in addressbook_uids:
        iface = _create_dbus_connection(EDS_FACTORY_BUS, EDS_FACTORY_OBJ, EDS_FACTORY_IFACE)
        EDS_SUBPROCESS_OBJ, EDS_SUBPROCESS_BUS  = iface.OpenAddressBook(addressbook_uid)
        interface = _create_dbus_connection(EDS_SUBPROCESS_BUS, EDS_SUBPROCESS_OBJ, EDS_SUBPROCESS_IFACE)
        interface.Open() #otherwise it may fail
        contact_pass_ids = interface.GetContactListUids("")
        for contact_pass_id in contact_pass_ids:
            #Lets form contact_uid from pass_id
            contact_uid = "eds:" + addressbook_uid + ":" + contact_pass_id
            #We also know individual id is just sha1 hash of uid, so
            m = hashlib.sha1()
            m.update(contact_uid.encode('UTF-8'))
            contact_individual_id = str(m.hexdigest())

            #we get contact vcard and parse it using python3-vobject
            contact_vcard = interface.GetContact(contact_pass_id)
            vcard = vobject.readOne( contact_vcard )
            if 'email' in vcard.contents:
                emails = [email.value for email in vcard.contents['email']]
            else:
                emails = [""]
            if 'tel' in vcard.contents:
                telephones = [tel.value for tel in vcard.contents['tel']]
            else:
                telephones = [""]
            cobj = {"EMAIL": emails, "TEL": telephones}

            if "FN" in vcard.behavior.knownChildren:
                full_name = vcard.fn.value
            elif "N" in vcard.behavior.knownChildren:
                full_name = vcard.n.value
            else:
                continue

            ocontact = Contact(contact_individual_id, full_name, cobj)
            yield ocontact
            yield ComposeMail()




class Contact (Leaf):
    def __init__(self, contact_individual_id, full_name, cobj):
        Leaf.__init__(self, contact_individual_id, full_name)
        self.cid = contact_individual_id
        self.name = full_name
        self.cobj = cobj
        self.emails = cobj['EMAIL']
        self.telephones = cobj['TEL']

    def get_description(self):
        descr = []
        if self.telephones:
            descr.append("Telephones: %s" % self.telephones[0])
        else:
            descr.append("This contact doesn't have any telephone no")
        '''
        if self.emails:
            if len(self.emails) > 1:
                eid = ",".join(e for e in self.emails)
                descr.append("Emails: %s" % eid)
            else:
                descr.append("Emails: %s" % self.emails[0])
        '''
        return "  ".join(descr)

    def get_icon_name(self):
        return 'evolution'

    def get_actions(self):
        yield OpenContact()
        yield NewMailAction()





def spawn_async(argv):
    try:
        utils.spawn_async_raise(argv)
    except utils.SpawnError as exc:
        raise OperationError(exc)



class OpenContact (Action):
    rank_adjust = 1
    action_accelerator = "o"

    def __init__(self):
        Action.__init__(self, _("Open"))

    def activate(self, leaf):
        #interface = _create_dbus_connection(True)
        spawn_async(("gnome-contacts", "-i", leaf.cid))

    def get_icon_name(self):
        return 'x-office-address-book'

    def get_description(self):
        return _("Open contact in Gnome Contact")


class NewMailAction(Action):
    ''' Create new mail to selected leaf'''
    def __init__(self):
        Action.__init__(self, _('Compose Email'))

    #def activate(self, leaf):
        #self.activate_multiple((leaf, ))

    def activate(self, leaf):
        print(leaf.telephones)
        if len(leaf.emails) > 1:
            ems = leaf.emails
            eids = ",".join(e for e in ems)
            spawn_async(["xdg-open", "mailto:%s" % eids])
        else:
            spawn_async(["xdg-open", "mailto:%s" % leaf.emails[0]])

    def valid_for_item(self, item):
        return bool(is_valid_email(item.emails[0]) and item.emails[0])

    def get_icon_name(self):
        return "mail-message-new"

    def item_types(self):
        yield ContactLeaf
        # we can enter email
        #yield TextLeaf
        #yield UrlLeaf


class ContactsSource (AppLeafContentMixin, ToplevelGroupingSource, Source):
    appleaf_content_id = Contact_ID

    def __init__(self, name=None):
        ToplevelGroupingSource.__init__(self, name, _("Contacts"))
        self._contacts = []
        self._version = 3

    def initialize(self):
        ToplevelGroupingSource.initialize(self)


    def get_items(self):
        self._contacts = list(_load_contacts(addressbook_uids))
        return self._contacts

    def get_icon_name(self):
        return 'evolution'

    def provides(self):
        yield Contact
        yield RunnableLeaf

@bluss
Copy link
Member

bluss commented Mar 14, 2017

Maybe templates.py action CreateNewDocument can be an example - it uses a custom source for the third pane's object. Yes the source can use the value of the primary object to build itself.

By the way, through a quick look I see that you can just use the name_has_owner and list_activatable_names method of the SessionBus object.

@bluss
Copy link
Member

bluss commented Mar 14, 2017

Sidenote I'm not really happy with how contacts grouping works, maybe you have an idea how to improve it.

@khurshid-alam
Copy link
Author

khurshid-alam commented Mar 14, 2017

By the way, through a quick look I see that you can just use the name_has_owner and list_activatable_names method of the SessionBus object.

I am using list_activatable_names method and the filtering out the service bus which changes on each boot. Please modify as you see fit.

Sidenote I'm not really happy with how contacts grouping works, maybe you have an idea how to improve it.

I am using ocontact = Contact(contact_individual_id, full_name, emails) where emails has a list of emails. However when I try to include more things like telephones or birthdays, it gets complected. And I couldn't find a way to include addresses, specially when it contains Unicode characters. Also _toutf8 from kupfersting is not working for me.

@bluss
Copy link
Member

bluss commented Mar 14, 2017

  proxy_obj = sbus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
        dbus_iface = dbus.Interface(proxy_obj, 'org.freedesktop.DBus')
        bus_names = dbus_iface.ListActivatableNames()

The tip is that this can be removed and replaced with sbus.list_activatable_names().

Not working is not specific enough. _toutf8 is maybe not what you want — some of the kupferstring functions are not updated after the Python 3 port. You just want regular str everywhere, should get that natively from d-bus.

I'm not sure the third pane should not be used for selecting email? Currently ContactLeaf is based around multiple identities of a contact being merged and you can select which “sub-contact” in the primary pane. I know this is far from ideal, so maybe the third pane idea is a better way.

@khurshid-alam
Copy link
Author

I'm not sure the third pane should not be used for selecting email? Currently ContactLeaf is based around multiple identities of a contact being merged and you can select which “sub-contact” in the primary pane. I know this is far from ideal, so maybe the third pane idea is a better way.

Yes. Third pane is better.

The code I put there is very raw. I think it also need repr_key otherwise it will fail for multiple contacts with same name.

@bluss
Copy link
Member

bluss commented Mar 14, 2017

Can you use ContactLeaf instead of defining a new Contact? Or is it needed to keep the integration with gnome contacts

@khurshid-alam
Copy link
Author

khurshid-alam commented Mar 14, 2017

Gnome contacts does nothing really here except opening a contact.

So class Contact (Leaf) becomes class GnomeContact (ContactLeaf) ?

@bluss
Copy link
Member

bluss commented Mar 14, 2017

What I mean is that ideally Contact and GnomeContact don't exist.

The current design of ContactLeaf is that contacts from different sources can be merged. Being a gnome contact is then only an aspect of a contact (just like the email field on a contact), so all contact leaves are inspected if they have the right aspect.

For example in the Pidgin plugin:

class ContactAction (Action):
    def get_required_slots(self):
        return ()
    def item_types(self):
        yield ContactLeaf
    def valid_for_item(self, leaf):
        return all(slot in leaf for slot in self.get_required_slots())

class OpenChat(ContactAction):
    """ Open Chat Conversation Window with jid """
    # consider it as main action for pidgin contacts
    rank_adjust = 5

    def __init__(self):
        Action.__init__(self, _('Open Chat'))

    def activate(self, leaf):
        _send_message_to_contact(leaf, "", present=True)

    def get_required_slots(self):
        return [PIDGIN_ACCOUNT, PIDGIN_JID]

So it offers the OpenChat action for all contacts that have slots PIDGIN_ACCOUNT, PIDGIN_JID.

Kupfer is the best when everything works together. I'm reminded of the previous discussion about listing png files: When we produce files, we should use the common FileLeaf. Then all plugins that can work with files apply, we don't have to build the whole interaction again for every plugin. The idea is the same here: This plugin has a source that produces contacts with emails and stuff. So it should use ContactLeaf so that all other plugins that know how to use emails can work with that.

@bluss
Copy link
Member

bluss commented Mar 14, 2017

Pidgin plugin might be good to look at. I can use the thunderbird email action on a pidgin contact that has an email attached.

@khurshid-alam
Copy link
Author

khurshid-alam commented Mar 14, 2017

Wow. I didn't know we could do that. So, In case of contacts it could be

def get_required_slots(self):
        return [CONTACT_EMAILS]

That way , Compose Email won't show up for contacts which doesn't even have a email in the first place. Nice. :)

I think you should take over now. To be honest, there are still lots of things I don't know about kupfer. Still learning. And I still have a long way to go before I can create a full fledged plugin of this magnitude on my own. I will be glad to leave this to you.

@bluss
Copy link
Member

bluss commented Mar 14, 2017

Yes, something like that. Sure, I don't think it's easy to get the hang of the system here it might be a bit idiosyncratic. I'll have a look at this at some point.

@khurshid-alam
Copy link
Author

Thank you.

@bluss
Copy link
Member

bluss commented Mar 15, 2017

It may be good to write down some general principles about Kupfer design, so https://github.com/kupferlauncher/kupfer/wiki/Plugin-Design it starts on the wiki and can go into docs later.

@khurshid-alam
Copy link
Author

khurshid-alam commented Mar 15, 2017

There is a EmailContact class available from contacts.py, but it's too basic and can only take two arguments.

May be in there we can define a more proper class like class GnomeContact (ContactLeaf) which will merge contacts based on INDIVIDUAL_ID_KEY and not by NAME_KEY or EMAIL_KEY. It will be similar to what JabberContact provides.

Then in plugin for any kind of contactleaf we can simply use class Contact (GnomeContact).

@bluss
Copy link
Member

bluss commented Mar 15, 2017

That sounds right

@hugosenari
Copy link

@khurshid-alam based on your suggestion I made a basic contact plugin using libfolks.

The problem is that libfolks uses GeeLib that won't work well with GI-Instrospection, to use it you need some voodoo (ctypes). This voodoo isn't stable to create a plugin but was good way to learn a lot about ctypes. Thank you. ;-)

Basically every time you get some libgee object like Map or List, you need discover what object it holds using get_type (or similar) and convert returned value (int pointer) to correct object.

Unfortunately get_type will not return a GType but a int, what is fine in C but pygobject wraps it and to get GType you need more voodoo. :-D

@khurshid-alam
Copy link
Author

@hugosenari Wow! Wonderful. I will try.

@khurshid-alam
Copy link
Author

@hugosenari I am getting

gee_iterator_has_next: assertion 'self != NULL' failed But it works!

Also libfolks aggregate both eds and telepathy contacts. As a result for a contact with same name and same email it displays only one type of contact. I rather not pull contacts from telepathy backend. Thoughts?

@hugosenari
Copy link

@khurshid-alam as I said this solution wasn't stable. ;)
This assertion error bothers me, if you getting next at an iterator and it is NULL how can you get value anyway? 🗡
And more strange, iteration don't use any crazy code, is just

while it and it.has_next():
    it.next()
    yield it.get_value()

To only list EDS, start IndividalAggregator with your own BackandStore instance that has only EDS enabled.

@khurshid-alam
Copy link
Author

@hugosenari

if you getting next at an iterator and it is NULL how can you get value anyway?

I noticed..It surprised me too....but I guess it finally got not NULL value.

  1. It only gets single email. If a contact has multiple emails, it should open third object pane listing all email to choose from in "Compose Email " action. I opened a bug on your repo.

  2. Since you are using GObject for libfolks and has better understanding of the api, wouldn't it be better to directly use GObject introspection for EBook and ECalendar? Although the api is far more complex.

Example (for calendar):

#! /usr/bin/python3
# -*- coding: utf-8 -*-

import gi
gi.require_version('EBook', '1.2')

from gi.repository import EBook
from gi.repository import EDataServer
#from gi.repository import ECalendar

# Open a registry and get a list of all the calendars in EDS
registry = EDataServer.SourceRegistry.new_sync(None)
sources = EDataServer.SourceRegistry.list_sources(registry, EDataServer.SOURCE_EXTENSION_CALENDAR)
print("sources")
'''
# Open each calendar containing events and get a list of all objects in them
for source in sources:
    client = EBook.CalClient.new(source, EBook.CalSourceType.EVENT)
    client.open_sync(False, None)

    # ret is true or false depending if events are found or not
    # values is a list of events
    ret, values = client.get_object_list_as_comps_sync("#t", None)
    if ret:
        for value in values:
            event = value.get_as_string()
            print("event")
'''

@hugosenari
Copy link

@khurshid-alam In your case, is better keep it with dbus. :)
In my case I think you're right except that libfolks has less dependency than Gnome-Contacts.
Less dependency with shit code or more dependency with less kludge? 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants