-
Notifications
You must be signed in to change notification settings - Fork 64
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
Comments
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. |
@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. |
IdeasOk, 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.
Calendar
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) |
ContactsI 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.
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
As you can see I have several addressbooks in evolution. We can get ids of those addressbooks by:
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 Metadata about a particular contact can be found by
Interestingly, Here the individual id 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
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. |
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:
These procceses has bus_names like:
We also know that directory names under We will also need #!/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:
Now we can open contact using gnome-contacts -i ddb389a5ea243dbcab711a4b6cd6682d41b4e561 |
Kupfer workflow for Contact
|
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. |
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.
On Ubuntu/Debian
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 |
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. |
Sidenote I'm not really happy with how contacts grouping works, maybe you have an idea how to improve it. |
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.
I am using |
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 Not working is not specific enough. 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 |
Can you use ContactLeaf instead of defining a new Contact? Or is it needed to keep the integration with gnome contacts |
Gnome contacts does nothing really here except opening a contact. So |
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 |
Pidgin plugin might be good to look at. I can use the thunderbird email action on a pidgin contact that has an email attached. |
Wow. I didn't know we could do that. So, In case of contacts it could be
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. |
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. |
Thank you. |
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. |
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 Then in plugin for any kind of contactleaf we can simply use class |
That sounds right |
@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 |
@hugosenari Wow! Wonderful. I will try. |
@hugosenari I am getting
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? |
@khurshid-alam as I said this solution wasn't stable. ;) 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. |
I noticed..It surprised me too....but I guess it finally got not NULL value.
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")
''' |
@khurshid-alam In your case, is better keep it with dbus. :) |
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.
The text was updated successfully, but these errors were encountered: