summaryrefslogtreecommitdiff
path: root/datahub
diff options
context:
space:
mode:
authorRico Tzschichholz <ricotz@t-online.de>2013-03-15 17:38:16 -0700
committerManish Sinha <manishsinha@ubuntu.com>2013-03-15 17:38:16 -0700
commit013e2f4e5ec27301d30bd8412f2374976f2f81ab (patch)
tree099ec130bc1b46a7141c4bc687ff903861693e6f /datahub
parenteb1aac4898a6e645769dd3536369f26d37bf38d6 (diff)
downloadzeitgeist-013e2f4e5ec27301d30bd8412f2374976f2f81ab.tar.gz
Moved datahub inside the zeitgeist codebase
Diffstat (limited to 'datahub')
-rw-r--r--datahub/Makefile.am72
-rw-r--r--datahub/configuration.vala30
-rw-r--r--datahub/data-provider.vala46
-rw-r--r--datahub/desktop-launch-listener.vala171
-rw-r--r--datahub/downloads-directory-provider.vala150
-rw-r--r--datahub/kde-recent-document-provider.vala273
-rw-r--r--datahub/recent-manager-provider.vala233
-rw-r--r--datahub/telepathy-observer.vala525
-rw-r--r--datahub/utils.vala272
-rwxr-xr-xdatahub/zeitgeist-datahub228
-rw-r--r--datahub/zeitgeist-datahub.vala291
11 files changed, 2291 insertions, 0 deletions
diff --git a/datahub/Makefile.am b/datahub/Makefile.am
new file mode 100644
index 00000000..c6a1bbc4
--- /dev/null
+++ b/datahub/Makefile.am
@@ -0,0 +1,72 @@
+NULL =
+
+bin_PROGRAMS = zeitgeist-datahub
+
+AM_CPPFLAGS = \
+ $(ZEITGEIST_DATAHUB_CFLAGS) \
+ -include $(CONFIG_HEADER) \
+ -I$(top_builddir)/libzeitgeist \
+ -w \
+ $(NULL)
+
+AM_VALAFLAGS = \
+ --target-glib=2.26 \
+ --pkg gio-2.0 \
+ --pkg gio-unix-2.0 \
+ --pkg gtk+-3.0 \
+ --pkg json-glib-1.0 \
+ $(top_builddir)/libzeitgeist/zeitgeist-datamodel-2.0.vapi \
+ $(top_builddir)/libzeitgeist/zeitgeist-2.0.vapi \
+ $(top_srcdir)/config.vapi \
+ $(srcdir)/glib-extra.vapi \
+ -C \
+ $(NULL)
+
+BUILT_SOURCES = \
+ zeitgeist_datahub_vala.stamp \
+ $(NULL)
+
+zeitgeist_datahub_VALASOURCES = \
+ configuration.vala \
+ data-provider.vala \
+ desktop-launch-listener.vala \
+ downloads-directory-provider.vala \
+ kde-recent-document-provider.vala \
+ recent-manager-provider.vala \
+ utils.vala \
+ zeitgeist-datahub.vala \
+ $(NULL)
+
+if ENABLE_TELEPATHY
+AM_VALAFLAGS += --pkg telepathy-glib
+zeitgeist_datahub_VALASOURCES += telepathy-observer.vala
+endif
+
+nodist_zeitgeist_datahub_SOURCES = \
+ $(BUILT_SOURCES) \
+ $(zeitgeist_datahub_VALASOURCES:.vala=.c) \
+ $(NULL)
+
+zeitgeist_datahub_LDADD = $(top_builddir)/libzeitgeist/libzeitgeist-2.0.la $(ZEITGEIST_DATAHUB_LIBS)
+zeitgeist_datahub_LDFLAGS = -export-dynamic -no-undefined
+
+zeitgeist_datahub_vala.stamp: $(zeitgeist_datahub_VALASOURCES) Makefile
+ $(AM_V_VALA)$(VALAC) \
+ $(AM_VALAFLAGS) \
+ $(filter %.vala %.c,$^)
+ $(AM_V_at)touch $@
+
+EXTRA_DIST = \
+ $(zeitgeist_datahub_VALASOURCES) \
+ $(NULL)
+
+CLEANFILES = \
+ $(nodist_zeitgeist_datahub_SOURCES) \
+ $(NULL)
+
+distclean-local:
+ rm -f *.c *.o *.stamp *.~[0-9]~
+
+VALA_V = $(VALA_V_$(V))
+VALA_V_ = $(VALA_V_$(AM_DEFAULT_VERBOSITY))
+VALA_V_0 = @echo " VALAC " $^;
diff --git a/datahub/configuration.vala b/datahub/configuration.vala
new file mode 100644
index 00000000..747bdd6b
--- /dev/null
+++ b/datahub/configuration.vala
@@ -0,0 +1,30 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by Michal Hruby <michal.mhr@gmail.com>
+ *
+ */
+
+using Zeitgeist;
+
+[DBus (name = "org.gnome.zeitgeist.datahub")]
+interface DataProviderService : Object
+{
+ public abstract string[] get_data_providers () throws DBusError;
+}
+
diff --git a/datahub/data-provider.vala b/datahub/data-provider.vala
new file mode 100644
index 00000000..1334eff4
--- /dev/null
+++ b/datahub/data-provider.vala
@@ -0,0 +1,46 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by Michal Hruby <michal.mhr@gmail.com>
+ *
+ */
+
+using Zeitgeist;
+
+public abstract class DataProvider : Object
+{
+ public abstract string unique_id { get; construct set; }
+ public abstract string name { get; construct set; }
+ public abstract string description { get; construct set; }
+
+ public abstract DataHub datahub { get; construct set; }
+ public abstract bool enabled { get; set; default = true; }
+ public abstract bool register { get; construct set; default = true; }
+ public int64 last_timestamp { get; set; }
+
+ public virtual void start ()
+ {
+ }
+
+ public virtual void stop ()
+ {
+ }
+
+ public signal void items_available (GenericArray<Event> events);
+}
+
diff --git a/datahub/desktop-launch-listener.vala b/datahub/desktop-launch-listener.vala
new file mode 100644
index 00000000..ddcc613a
--- /dev/null
+++ b/datahub/desktop-launch-listener.vala
@@ -0,0 +1,171 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2010, 2012 Michal Hruby <michal.mhr@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by Michal Hruby <michal.mhr@gmail.com>
+ *
+ */
+
+using Zeitgeist;
+
+public class DesktopLaunchListener : DataProvider
+{
+ public DesktopLaunchListener (DataHub datahub)
+ {
+ GLib.Object (unique_id: "com.zeitgeist-project,datahub,gio-launch-listener",
+ name: "Launched desktop files",
+ description: "Logs events about launched desktop files using GIO",
+ datahub: datahub);
+ }
+
+ // if vala didn't have bug in construct-only properties, the properties
+ // would be construct-only
+ public override string unique_id { get; construct set; }
+ public override string name { get; construct set; }
+ public override string description { get; construct set; }
+
+ public override DataHub datahub { get; construct set; }
+ public override bool enabled { get; set; default = true; }
+ public override bool register { get; construct set; default = true; }
+
+ private GLib.DBusConnection bus;
+ private uint launched_signal_id = 0;
+
+ construct
+ {
+ try
+ {
+ bus = GLib.Bus.get_sync (GLib.BusType.SESSION);
+ }
+ catch (IOError err)
+ {
+ warning ("%s", err.message);
+ }
+
+ unowned string desktop_env = Environment.get_variable ("XDG_CURRENT_DESKTOP");
+ if (desktop_env != null)
+ {
+ DesktopAppInfo.set_desktop_env (desktop_env);
+ return;
+ }
+ }
+
+ public override void start ()
+ {
+ if (launched_signal_id != 0) return;
+
+ launched_signal_id = bus.signal_subscribe (null,
+ "org.gtk.gio.DesktopAppInfo",
+ "Launched",
+ "/org/gtk/gio/DesktopAppInfo",
+ null,
+ 0,
+ this.signal_received);
+ }
+
+ private void signal_received (GLib.DBusConnection connection,
+ string sender,
+ string object_path,
+ string interface_name,
+ string signal_name,
+ Variant parameters)
+ {
+ // unpack the variant
+ Variant desktop_variant;
+ VariantIter uris;
+ Variant dict;
+ int64 pid;
+
+ parameters.get ("(@aysxas@a{sv})", out desktop_variant, null,
+ out pid, out uris, out dict);
+
+ string desktop_file = desktop_variant.get_bytestring ();
+ if (desktop_file == "") return;
+
+ // are we going to do anything with these?
+ string uri;
+ while (uris.next ("s", out uri))
+ {
+ debug ("ran with uri: %s", uri);
+ }
+
+ // here we should be able to get info about the origin of the launch
+ HashTable<string, Variant> extra_params = (HashTable<string, Variant>) dict;
+
+ DesktopAppInfo? dai;
+ string launched_uri = Utils.get_actor_for_desktop_file (desktop_file,
+ out dai);
+ if (launched_uri == null)
+ {
+ warning ("Unable to open desktop file '%s'", desktop_file);
+ return;
+ }
+
+ string? launcher_uri = null;
+ unowned Variant origin_df = extra_params.lookup ("origin-desktop-file");
+ if (origin_df != null)
+ {
+ launcher_uri = Utils.get_actor_for_desktop_file (origin_df.get_bytestring ());
+ }
+ else
+ {
+ unowned Variant origin_prgname = extra_params.lookup ("origin-prgname");
+ if (origin_prgname != null)
+ {
+ unowned string? prgname = origin_prgname.get_bytestring ();
+ string origin_desktop_id = prgname + ".desktop";
+ DesktopAppInfo id_check = new DesktopAppInfo (origin_desktop_id);
+ if (id_check != null) launcher_uri = "application://%s".printf (origin_desktop_id);
+ }
+ }
+
+ if (!dai.should_show ())
+ {
+ // FIXME: do something else? Log with WORLD_EVENT?
+ return;
+ }
+
+ var event = new Zeitgeist.Event ();
+ var subject = new Zeitgeist.Subject ();
+
+ event.actor = launcher_uri;
+ event.interpretation = ZG.ACCESS_EVENT;
+ event.manifestation = ZG.USER_ACTIVITY;
+ event.add_subject (subject);
+
+ subject.uri = launched_uri;
+ subject.interpretation = NFO.SOFTWARE;
+ subject.manifestation = NFO.SOFTWARE_ITEM;
+ subject.mimetype = "application/x-desktop";
+ subject.text = dai.get_display_name ();
+
+ var arr = new GenericArray<Event> ();
+ arr.add (event);
+
+ items_available (arr);
+ }
+
+ public override void stop ()
+ {
+ if (launched_signal_id != 0)
+ {
+ bus.signal_unsubscribe (launched_signal_id);
+ launched_signal_id = 0;
+ }
+ }
+}
+
diff --git a/datahub/downloads-directory-provider.vala b/datahub/downloads-directory-provider.vala
new file mode 100644
index 00000000..6cf15912
--- /dev/null
+++ b/datahub/downloads-directory-provider.vala
@@ -0,0 +1,150 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
+ * Copyright (C) 2012 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by Michal Hruby <michal.mhr@gmail.com>
+ * Authored by Siegfried-A. Gevatter <siegfried.gevatter@collabora.co.uk>
+ *
+ */
+
+using Zeitgeist;
+
+public class DownloadsDirectoryMonitor : DataProvider
+{
+ public DownloadsDirectoryMonitor (DataHub datahub) throws GLib.Error
+ {
+ GLib.Object (unique_id: "com.zeitgeist-project,datahub,downloads-monitor",
+ name: "Downloads Directory Monitor",
+ description: "Logs files in the XDG downloads directory",
+ datahub: datahub);
+ }
+
+ // if vala didn't have bug in construct-only properties, the properties
+ // would be construct-only
+ public override string unique_id { get; construct set; }
+ public override string name { get; construct set; }
+ public override string description { get; construct set; }
+
+ public override DataHub datahub { get; construct set; }
+ public override bool enabled { get; set; default = true; }
+ public override bool register { get; construct set; default = true; }
+
+ private string? downloads_path;
+ private GLib.File downloads_directory;
+ private GLib.FileMonitor monitor;
+ private string[] ignored_actors;
+
+ construct
+ {
+ downloads_path = GLib.Environment.get_user_special_dir (
+ GLib.UserDirectory.DOWNLOAD);
+ if (downloads_path != null)
+ {
+ downloads_directory = File.new_for_path (downloads_path);
+ try
+ {
+ monitor = downloads_directory.monitor_directory (
+ GLib.FileMonitorFlags.NONE/*SEND_MOVED*/);
+ }
+ catch (GLib.Error err)
+ {
+ warning ("Couldn't set up monitor on Downloads directory: %s", err.message);
+ }
+ }
+ }
+
+ public override void start ()
+ {
+ if (downloads_path != null)
+ {
+ ignored_actors = datahub.get_data_source_actors ();
+ monitor.changed.connect (this.process_event);
+ }
+ }
+
+ public override void stop ()
+ {
+ if (downloads_path != null)
+ {
+ monitor.changed.disconnect (this.process_event);
+ }
+ }
+
+ private const string ATTRIBUTES =
+ FileAttribute.STANDARD_FAST_CONTENT_TYPE + "," +
+ FileAttribute.STANDARD_IS_HIDDEN + "," +
+ FileAttribute.STANDARD_IS_BACKUP + ",";
+
+ private async void process_event (GLib.File file, GLib.File? other_file,
+ GLib.FileMonitorEvent event_type)
+ {
+ // FIXME: add MOVED once libzg supports current_uri (not that they are
+ // very useful, inotify won't tell us about moves to outside ~/Downloads)
+ if (event_type != GLib.FileMonitorEvent.CREATED)
+ {
+ // We're ignoring DELETE since we can't get the mime-type for it, and who
+ // cares anyway if we only have them for ~/Downloads?
+ return;
+ }
+
+ // Skip temporary files (eg. in-progress Downloads)
+ string uri = file.get_uri ();
+ if (uri.has_suffix (".part") || uri.has_suffix (".crdownload"))
+ return;
+
+ GLib.FileInfo subject_info;
+ try
+ {
+ subject_info = yield file.query_info_async (ATTRIBUTES,
+ GLib.FileQueryInfoFlags.NONE);
+ if (subject_info.get_is_hidden () || subject_info.get_is_backup ())
+ return;
+ }
+ catch (GLib.Error err)
+ {
+ warning ("Couldn't process %s: %s", file.get_path (), err.message);
+ return;
+ }
+
+ string mimetype = subject_info.get_attribute_string (
+ FileAttribute.STANDARD_FAST_CONTENT_TYPE);
+ string origin = Path.get_dirname (uri);
+ string basename = Path.get_basename (file.get_path ());
+
+ var subject = new Subject.full (uri,
+ interpretation_for_mimetype (mimetype),
+ manifestation_for_uri (uri),
+ mimetype,
+ origin,
+ basename,
+ ""); // storage will be figured out by Zeitgeist
+
+ string actor = ""; // unknown
+ Event event = new Event.full (ZG.CREATE_EVENT, ZG.WORLD_ACTIVITY,
+ actor, null, null);
+ event.add_subject (subject);
+
+ if (event != null)
+ {
+ GenericArray<Event> events = new GenericArray<Event> ();
+ events.add ((owned) event);
+ items_available (events);
+ }
+ }
+
+}
diff --git a/datahub/kde-recent-document-provider.vala b/datahub/kde-recent-document-provider.vala
new file mode 100644
index 00000000..736f77bc
--- /dev/null
+++ b/datahub/kde-recent-document-provider.vala
@@ -0,0 +1,273 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
+ * Copyright (C) 2012 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by Michal Hruby <michal.mhr@gmail.com>
+ * Authored by Siegfried-A. Gevatter <siegfried.gevatter@collabora.co.uk>
+ *
+ */
+
+using Zeitgeist;
+
+public class RecentDocumentsKDE : DataProvider
+{
+ public RecentDocumentsKDE (DataHub datahub) throws GLib.Error
+ {
+ GLib.Object (unique_id: "com.zeitgeist-project,datahub,kde-recent",
+ name: "Recently Used Documents (KDE)",
+ description: "Logs events from KRecentDocument",
+ datahub: datahub);
+ }
+
+ private const string RECENT_DOCUMENTS_PATH =
+ "/.kde/share/apps/RecentDocuments";
+ private const string RECENT_FILE_GROUP = "Desktop Entry";
+
+ private const string ATTRIBUTE_SEPARATOR = ",";
+ private const string FILE_ATTRIBUTE_QUERY_RECENT =
+ GLib.FileAttribute.STANDARD_TYPE + ATTRIBUTE_SEPARATOR +
+ GLib.FileAttribute.TIME_MODIFIED + ATTRIBUTE_SEPARATOR +
+ GLib.FileAttribute.TIME_MODIFIED_USEC;
+ private const string FILE_ATTRIBUTE_QUERY_SUBJECT =
+ GLib.FileAttribute.STANDARD_CONTENT_TYPE + ATTRIBUTE_SEPARATOR +
+ GLib.FileAttribute.TIME_MODIFIED + ATTRIBUTE_SEPARATOR +
+ GLib.FileAttribute.TIME_MODIFIED_USEC + ATTRIBUTE_SEPARATOR +
+ GLib.FileAttribute.TIME_CHANGED + ATTRIBUTE_SEPARATOR +
+ GLib.FileAttribute.TIME_CHANGED_USEC;
+
+ private const int TIME_EPSILON = 100; // msec
+
+ // if vala didn't have bug in construct-only properties, the properties
+ // would be construct-only
+ public override string unique_id { get; construct set; }
+ public override string name { get; construct set; }
+ public override string description { get; construct set; }
+
+ public override DataHub datahub { get; construct set; }
+ public override bool enabled { get; set; default = true; }
+ public override bool register { get; construct set; default = true; }
+
+ private string recent_document_path;
+ private GLib.File recent_documents_directory;
+ private GLib.FileMonitor monitor;
+ private string[] ignored_actors;
+
+ private GLib.Regex recent_regex;
+ private GLib.Regex url_regex;
+ private const string RECENT_REGEX_REPLACEMENT = "URL=";
+
+ construct
+ {
+ //FIXME: is done properly ?
+ try
+ {
+ recent_regex = new Regex ("URL\\[[^]]+\\]=");
+ url_regex = new Regex ("\\$HOME");
+ }
+ catch (RegexError err)
+ {
+ warning ("Couldn't process regex: %s", err.message);
+ }
+ recent_document_path = Environment.get_home_dir () + RECENT_DOCUMENTS_PATH;
+ recent_documents_directory = File.new_for_path (recent_document_path);
+ try
+ {
+ monitor = recent_documents_directory.monitor_directory (
+ GLib.FileMonitorFlags.NONE);
+ }
+ catch (GLib.IOError err)
+ {
+ warning ("Couldn't set up monitor: %s", err.message);
+ }
+ }
+
+ public override void start ()
+ {
+ ignored_actors = datahub.get_data_source_actors ();
+ monitor.changed.connect (this.process_event);
+
+ crawl_all_items ();
+ }
+
+ public override void stop ()
+ {
+ monitor.changed.disconnect (this.process_event);
+ }
+
+ private async void process_event (GLib.File file, GLib.File? other_file,
+ GLib.FileMonitorEvent event_type)
+ {
+ if (event_type == GLib.FileMonitorEvent.CREATED ||
+ event_type == GLib.FileMonitorEvent.CHANGED ||
+ event_type == GLib.FileMonitorEvent.ATTRIBUTE_CHANGED)
+ {
+ try
+ {
+ Event? event = yield parse_file (file);
+ if (event != null)
+ {
+ GenericArray<Event> events = new GenericArray<Event> ();
+ events.add ((owned) event);
+ items_available (events);
+ }
+ }
+ catch (GLib.Error err)
+ {
+ warning ("Couldn't process %s: %s", file.get_path (), err.message);
+ }
+ }
+ }
+
+ private async Event? parse_file (GLib.File file) throws GLib.Error
+ {
+ TimeVal timeval;
+
+ if (!file.get_basename ().has_suffix (".desktop"))
+ return null;
+
+ var recent_info = yield file.query_info_async (
+ FILE_ATTRIBUTE_QUERY_RECENT, GLib.FileQueryInfoFlags.NONE);
+
+ GLib.FileType file_type = (GLib.FileType) recent_info.get_attribute_uint32 (
+ GLib.FileAttribute.STANDARD_TYPE);
+ if (file_type != GLib.FileType.REGULAR)
+ return null;
+
+ timeval = recent_info.get_modification_time ();
+ int64 event_time = Timestamp.from_timeval (timeval);
+
+ string? content = Utils.get_file_contents (file);
+ if (content == null)
+ return null;
+ content = recent_regex.replace (content, content.length, 0,
+ RECENT_REGEX_REPLACEMENT);
+
+ KeyFile recent_file = new KeyFile ();
+ recent_file.load_from_data (content, content.length, KeyFileFlags.NONE);
+ string basename = recent_file.get_string (RECENT_FILE_GROUP, "Name");
+ string uri = recent_file.get_string (RECENT_FILE_GROUP, "URL");
+ string desktop_entry_name = recent_file.get_string (RECENT_FILE_GROUP,
+ "X-KDE-LastOpenedWith");
+
+ // URL may contain environment variables. In practice, KConfigGroup
+ // only uses $HOME.
+ uri = url_regex.replace (uri, uri.length, 0, Environment.get_home_dir ());
+
+ string? actor = get_actor_for_desktop_entry_name (desktop_entry_name);
+ if (actor == null)
+ {
+ warning ("Couldn't find actor for '%s'.", desktop_entry_name);
+ return null;
+ }
+ if (actor in ignored_actors)
+ return null;
+
+ GLib.File subject_file = File.new_for_uri (uri);
+ var subject_info = subject_file.query_info (
+ FILE_ATTRIBUTE_QUERY_SUBJECT, GLib.FileQueryInfoFlags.NONE);
+
+ timeval = subject_info.get_modification_time ();
+ int64 modification_time = Timestamp.from_timeval (timeval);
+
+ timeval.tv_sec = (long) subject_info.get_attribute_uint64 (
+ GLib.FileAttribute.TIME_CHANGED);
+ timeval.tv_usec = subject_info.get_attribute_uint32 (
+ GLib.FileAttribute.TIME_CHANGED_USEC);
+ int64 creation_time = Timestamp.from_timeval (timeval);
+
+ string mimetype = subject_info.get_attribute_string (
+ FileAttribute.STANDARD_CONTENT_TYPE);
+
+ string event_interpretation;
+ int64 creation_diff = event_time - creation_time;
+ int64 modification_diff = event_time - modification_time;
+ if (creation_diff.abs () < TIME_EPSILON)
+ event_interpretation = ZG.CREATE_EVENT;
+ else if (modification_diff.abs () < TIME_EPSILON)
+ event_interpretation = ZG.MODIFY_EVENT;
+ else
+ event_interpretation = ZG.ACCESS_EVENT;
+
+ string origin = Path.get_dirname (uri);
+ var subject =
+ new Subject.full (uri,
+ interpretation_for_mimetype (mimetype),
+ manifestation_for_uri (uri),
+ mimetype,
+ origin,
+ basename,
+ ""); // storage will be figured out by Zeitgeist
+
+ Event event = new Event.full (event_interpretation, ZG.USER_ACTIVITY,
+ actor, null, null);
+ event.add_subject (subject);
+ event.timestamp = event_time;
+
+ return event;
+ }
+
+ private string? get_actor_for_desktop_entry_name (string desktop_entry_name)
+ {
+ const string desktop_prefixes[] = { "", "kde-", "kde4-" };
+
+ DesktopAppInfo dae = null;
+ string desktop_id = null;
+ foreach (unowned string prefix in desktop_prefixes)
+ {
+ desktop_id = "%s%s.desktop".printf (prefix, desktop_entry_name);
+ dae = new DesktopAppInfo (desktop_id);
+ if (dae != null)
+ break;
+ }
+
+ if (dae != null)
+ {
+ return "application://%s".printf (desktop_id);
+ }
+
+ return null;
+ }
+
+ private async void crawl_all_items () throws GLib.Error
+ {
+ GenericArray<Event> events = new GenericArray<Event> ();
+
+ GLib.File directory = GLib.File.new_for_path (recent_document_path);
+ GLib.FileEnumerator enumerator = directory.enumerate_children (
+ FileAttribute.STANDARD_NAME, GLib.FileQueryInfoFlags.NONE);
+ GLib.FileInfo fi;
+ while ((fi = enumerator.next_file ()) != null)
+ {
+ var file = directory.get_child (fi.get_name ());
+ try
+ {
+ Event? event = yield parse_file (file);
+ if (event != null)
+ events.add ((owned) event);
+ }
+ catch (GLib.Error err)
+ {
+ // Silently ignore. The files may be gone by now - who cares?
+ }
+ }
+ enumerator.close ();
+
+ // Zeitgeist will take care of ignoring the duplicates
+ items_available (events);
+ }
+}
diff --git a/datahub/recent-manager-provider.vala b/datahub/recent-manager-provider.vala
new file mode 100644
index 00000000..810ed6a0
--- /dev/null
+++ b/datahub/recent-manager-provider.vala
@@ -0,0 +1,233 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
+ * Copyright (C) 2012 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by Michal Hruby <michal.mhr@gmail.com>
+ * Authored by Siegfried-A. Gevatter <siegfried.gevatter@collabora.co.uk>
+ *
+ */
+
+using Zeitgeist;
+
+public class RecentManagerGtk : DataProvider
+{
+ public RecentManagerGtk (DataHub datahub)
+ {
+ GLib.Object (unique_id: "com.zeitgeist-project,datahub,recent",
+ name: "Recently Used Documents",
+ description: "Logs events from GtkRecentlyUsed",
+ datahub: datahub);
+ }
+
+ // if vala didn't have bug in construct-only properties, the properties
+ // would be construct-only
+ public override string unique_id { get; construct set; }
+ public override string name { get; construct set; }
+ public override string description { get; construct set; }
+
+ public override DataHub datahub { get; construct set; }
+ public override bool enabled { get; set; default = true; }
+ public override bool register { get; construct set; default = true; }
+
+ private unowned Gtk.RecentManager recent_manager;
+ private uint idle_id = 0;
+
+ construct
+ {
+ recent_manager = Gtk.RecentManager.get_default ();
+ }
+
+ public override void start ()
+ {
+ recent_manager.changed.connect (this.items_changed);
+
+ items_available (get_items ());
+ }
+
+ public override void stop ()
+ {
+ recent_manager.changed.disconnect (this.items_changed);
+ }
+
+ private void items_changed ()
+ {
+ if (idle_id == 0)
+ {
+ idle_id = Idle.add (() =>
+ {
+ items_available (get_items ());
+ idle_id = 0;
+ return false;
+ });
+ }
+ }
+
+ protected GenericArray<Event> get_items ()
+ {
+ GenericArray<Event> events = new GenericArray<Event> ();
+
+ int64 signal_time = Timestamp.from_now ();
+ string[] ignored_actors = datahub.get_data_source_actors ();
+
+ foreach (Gtk.RecentInfo ri in recent_manager.get_items ())
+ {
+ // GFile and GtkRecentInfo use different encoding of the uris, so we'll
+ // do this
+ File file_obj = File.new_for_uri (ri.get_uri ());
+ string uri = file_obj.get_uri ();
+ if (ri.get_private_hint () || uri.has_prefix ("file:///tmp/"))
+ continue;
+ if (ri.is_local () && !ri.exists ())
+ continue;
+
+ var last_app = ri.last_application ().strip ();
+ unowned string exec_str;
+ uint count;
+ ulong time_;
+ bool registered = ri.get_application_info (last_app, out exec_str,
+ out count, out time_);
+ if (!registered)
+ {
+ warning ("%s was not registered in RecentInfo item %p", last_app, ri);
+ continue;
+ }
+
+ string[] exec = exec_str.split_set (" \t\n", 2);
+
+ string? desktop_file;
+ if (exec[0] == "soffice" || exec[0] == "ooffice")
+ {
+ // special case OpenOffice... since it must do everything differently
+ desktop_file = Utils.get_ooo_desktop_file_for_mimetype (ri.get_mime_type ());
+ }
+ else
+ {
+ desktop_file = Utils.find_desktop_file_for_app (exec[0]);
+
+ // Thunderbird also likes doing funny stuff...
+ if (desktop_file == null && exec[0].has_suffix ("-bin"))
+ {
+ desktop_file = Utils.find_desktop_file_for_app (
+ exec[0].substring(0, exec[0].length - 4));
+ }
+ }
+
+ if (desktop_file == null)
+ {
+ warning ("Desktop file for \"%s\" was not found, exec: %s, mime_type: %s",
+ uri, exec[0], ri.get_mime_type ());
+ continue; // this makes us sad panda
+ }
+
+ var actor = "application://%s".printf (Path.get_basename (desktop_file));
+ if (actor in ignored_actors)
+ {
+ continue;
+ }
+
+ var parent_file = file_obj.get_parent ();
+ string origin = parent_file != null ?
+ parent_file.get_uri () : Path.get_dirname (uri);
+ var subject =
+ new Subject.full (uri,
+ interpretation_for_mimetype (ri.get_mime_type ()),
+ manifestation_for_uri (uri),
+ ri.get_mime_type (),
+ origin,
+ ri.get_display_name (),
+ ""); // FIXME: storage?!
+
+ Event event;
+ int64 timestamp;
+
+ // Zeitgeist checks for duplicated events, so we can just inserted
+ // all events every time.
+ bool log_create = true;
+ bool log_modify = true;
+ bool log_access = true;
+
+ // However, we don't really want duplicate events with the same
+ // timestamp but different interpretations...
+ if (ri.get_added () == ri.get_modified ())
+ {
+ // Creation also changes modified (and visited). If they are the
+ // same, we only log the former.
+ log_modify = false;
+ }
+ if (ri.get_modified () == ri.get_visited ())
+ {
+ // Modification also updated visited. If they are the same, we
+ // only log the former.
+ log_access = false;
+ }
+
+ if (log_create)
+ {
+ event = new Event.full (ZG.ACCESS_EVENT,
+ ZG.USER_ACTIVITY,
+ actor,
+ null, null);
+ event.add_subject (subject);
+ timestamp = ri.get_added ();
+ timestamp *= 1000;
+ event.timestamp = timestamp;
+ if (timestamp > last_timestamp && timestamp >= 0)
+ {
+ events.add ((owned) event);
+ }
+ }
+
+ if (log_modify)
+ {
+ event = new Event.full (ZG.MODIFY_EVENT,
+ ZG.USER_ACTIVITY,
+ actor,
+ null , null);
+ event.add_subject (subject);
+ timestamp = ri.get_modified ();
+ timestamp *= 1000;
+ event.timestamp = timestamp;
+ if (timestamp > last_timestamp && timestamp >= 0)
+ {
+ events.add ((owned) event);
+ }
+ }
+
+ if (log_access)
+ {
+ event = new Event.full (ZG.ACCESS_EVENT,
+ ZG.USER_ACTIVITY,
+ actor,
+ null, null);
+ event.add_subject (subject);
+ timestamp = ri.get_visited ();
+ timestamp *= 1000;
+ event.timestamp = timestamp;
+ if (timestamp > last_timestamp && timestamp >= 0)
+ {
+ events.add ((owned) event);
+ }
+ }
+
+ }
+
+ last_timestamp = signal_time;
+
+ return events;
+ }
+}
diff --git a/datahub/telepathy-observer.vala b/datahub/telepathy-observer.vala
new file mode 100644
index 00000000..1500ef23
--- /dev/null
+++ b/datahub/telepathy-observer.vala
@@ -0,0 +1,525 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2012 Collabora Ltd.
+ * Authored by: Seif Lotfy <seif.lotfy@collabora.co.uk>
+ * Copyright (C) 2012 Eslam Mostafa <cseslam@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+using Zeitgeist;
+using TelepathyGLib;
+using Json;
+
+public class TelepathyObserver : DataProvider
+{
+
+ private const string actor = "dbus://org.freedesktop.Telepathy.Logger.service";
+ private const string tp_account_path = "x-telepathy-account-path:%s";
+ private const string tp_identifier = "x-telepathy-identifier:%s";
+ private const string ft_json_domain = "http://zeitgeist-project.com/1.0/telepathy/filetransfer";
+ private const string call_json_domain = "http://zeitgeist-project.com/1.0/telepathy/call";
+
+ private TelepathyGLib.DBusDaemon dbus = null;
+ private TelepathyGLib.AutomaticClientFactory factory = null;
+ private TelepathyGLib.SimpleObserver observer = null;
+ private HashTable<string, Timer> call_timers = null;
+
+ public TelepathyObserver (DataHub datahub) throws GLib.Error
+ {
+ GLib.Object (unique_id: "com.zeitgeist-project,datahub,telepathy-observer",
+ name: "Telepathy Observer",
+ description: "Logs IM, call and filetransfer from telepathy",
+ datahub: datahub);
+ }
+
+ construct
+ {
+ call_timers = new HashTable<string, Timer> (str_hash, str_equal);
+ try {
+ dbus = TelepathyGLib.DBusDaemon.dup ();
+ }
+ catch (GLib.Error err)
+ {
+ warning ("Couldn't dup DBusDaemon: %s", err.message);
+ return;
+ }
+ factory = new TelepathyGLib.AutomaticClientFactory (dbus);
+
+ Quark[] channel_quark = {TelepathyGLib.Channel.get_feature_quark_contacts ()};
+ TelepathyGLib.ContactFeature[] contact_quark = {TelepathyGLib.ContactFeature.ALIAS};
+
+ factory.add_channel_features (channel_quark);
+ factory.add_contact_features (contact_quark);
+ }
+
+ // if vala didn't have bug in construct-only properties, the properties
+ // would be construct-only
+ public override string unique_id { get; construct set; }
+ public override string name { get; construct set; }
+ public override string description { get; construct set; }
+
+ public override DataHub datahub { get; construct set; }
+ public override bool enabled { get; set; default = true; }
+ public override bool register { get; construct set; default = true; }
+
+ private void push_event (Event event)
+ {
+ GenericArray<Event> events = new GenericArray<Event> ();
+ events.add (event);
+ items_available (events);
+ }
+
+ /*
+ * Create a standard template for text channel based events
+ */
+ private Event create_text_event (Account account, Channel channel)
+ {
+ var target = channel.get_target_contact ();
+ var obj_path = account.get_object_path ();
+ obj_path = tp_account_path.printf(obj_path[
+ TelepathyGLib.ACCOUNT_OBJECT_PATH_BASE.length : obj_path.length]);
+ Event event_template = new Event.full (
+ ZG.ACCESS_EVENT,
+ "",
+ actor,
+ null,
+ obj_path);
+
+ /*
+ * Whether user initiated the chat or not
+ */
+ if (!channel.requested)
+ event_template.manifestation = ZG.WORLD_ACTIVITY;
+ else
+ event_template.manifestation = ZG.USER_ACTIVITY;
+
+ /*
+ * Create IM subject for the event
+ */
+ event_template.add_subject (
+ new Subject.full (
+ "",
+ NMO.IMMESSAGE,
+ NFO.SOFTWARE_SERVICE,
+ "plain/text",
+ tp_identifier.printf (target.get_identifier ()),
+ "",
+ "net")
+ );
+
+ /*
+ * Create Contact subject for the event
+ */
+ event_template.add_subject (
+ new Subject.full (
+ tp_identifier.printf (target.get_identifier ()),
+ NCO.CONTACT,
+ NCO.CONTACT_LIST_DATA_OBJECT,
+ "",
+ obj_path,
+ target.get_alias (),
+ "net")
+ );
+ return event_template;
+ }
+
+ private void observe_text_channel (SimpleObserver observer, Account account,
+ Connection connection, Channel b_channel,
+ ChannelDispatchOperation? dispatch_operation,
+ List<ChannelRequest> requests,
+ ObserveChannelsContext context)
+ {
+ /*
+ * Channel has been created
+ */
+ TextChannel channel = (TextChannel) b_channel;
+ var target = channel.get_target_contact ();
+ if (target != null)
+ {
+ /*
+ * Create an event representing conversation start
+ */
+ var event_template = this.create_text_event (account, channel);
+ this.push_event (event_template);
+ /*
+ * Process pending messages
+ */
+ foreach (var message in channel.get_pending_messages ())
+ {
+ if (!message.is_delivery_report ())
+ {
+ event_template = this.create_text_event (account, channel);
+ event_template.interpretation = ZG.RECEIVE_EVENT;
+ event_template.manifestation = ZG.WORLD_ACTIVITY;
+ this.push_event (event_template);
+ }
+ // FIXME: what about sent messages? what happens with them?
+ }
+ /*
+ * Connect to the signal representing conversation end
+ */
+ channel.invalidated.connect (() => {
+ event_template = this.create_text_event (account, channel);
+ // manifestation depends on the chat creator, unless we can
+ // get a better value.
+ event_template.interpretation = ZG.LEAVE_EVENT;
+ this.push_event (event_template);
+ });
+ /*
+ * Connect to receive message signals of the channel
+ */
+ channel.message_received.connect (() => {
+ event_template = this.create_text_event (account, channel);
+ event_template.interpretation = ZG.RECEIVE_EVENT;
+ event_template.manifestation = ZG.WORLD_ACTIVITY;
+ this.push_event (event_template);
+ });
+ /*
+ * Connect to send message signals of the channel
+ */
+ channel.message_sent.connect (() => {
+ event_template = this.create_text_event (account, channel);
+ event_template.interpretation = ZG.SEND_EVENT;
+ event_template.manifestation = ZG.USER_ACTIVITY;
+ this.push_event (event_template);
+ });
+ }
+ }
+
+ /*
+ * Create a standard template for call channel based events
+ */
+ private Event? create_call_event (Account account, CallChannel channel)
+ {
+ var targets = channel.get_members ();
+ if (targets == null)
+ return null;
+ weak TelepathyGLib.Contact? target = targets.get_keys ().data;
+
+ var obj_path = account.get_object_path ();
+ obj_path = tp_account_path.printf(obj_path[
+ TelepathyGLib.ACCOUNT_OBJECT_PATH_BASE.length : obj_path.length]);
+ Event event_template = new Event.full (
+ ZG.ACCESS_EVENT,
+ ZG.USER_ACTIVITY,
+ actor,
+ null,
+ obj_path);
+ if (!channel.requested)
+ event_template.manifestation = ZG.WORLD_ACTIVITY;
+ /*
+ * Create Call subject for the event
+ */
+ event_template.add_subject (
+ new Subject.full (
+ "",
+ NFO.AUDIO,
+ NFO.MEDIA_STREAM,
+ "x-telepathy/call",
+ tp_identifier.printf (target.get_identifier ()),
+ target.get_alias (),
+ "net")
+ );
+ /*
+ * Create Contact subject for the event
+ */
+ event_template.add_subject (
+ new Subject.full (
+ tp_identifier.printf(target.get_identifier ()),
+ NCO.CONTACT,
+ NCO.CONTACT_LIST_DATA_OBJECT,
+ "",
+ obj_path,
+ target.get_alias (),
+ "net")
+ );
+ return event_template;
+ }
+
+ private void observe_call_channel (SimpleObserver observer, Account account,
+ Connection connection, Channel b_channel,
+ ChannelDispatchOperation? dispatch_operation,
+ List<ChannelRequest> requests,
+ ObserveChannelsContext context)
+ {
+ CallChannel channel = (CallChannel) b_channel;
+
+ channel.state_changed.connect (() =>
+ {
+ CallFlags flags;
+ TelepathyGLib.CallStateReason reason;
+ CallState state = channel.get_state (out flags, null, out reason);
+
+ /*
+ * Create an Event template for call events
+ */
+ var event_template = this.create_call_event (account, channel);
+
+ /*
+ * Start operating once the call state is initialized
+ */
+ if (state == TelepathyGLib.CallState.INITIALISED)
+ {
+ event_template.interpretation = ZG.CREATE_EVENT;
+ Timer t = new Timer ();
+ t.stop ();
+ call_timers.insert (channel.get_object_path (), (owned) t);
+ this.push_event (event_template);
+ }
+ /*
+ * Act only on call active or call end
+ */
+ else if ((state == TelepathyGLib.CallState.ACTIVE || state == TelepathyGLib.CallState.ENDED)
+ && call_timers.contains (channel.get_object_path ()))
+ {
+ if (state == TelepathyGLib.CallState.ACTIVE)
+ {
+ event_template.interpretation = ZG.ACCESS_EVENT;
+ call_timers.lookup (channel.get_object_path ()).start();
+ this.push_event (event_template);
+ }
+ else if (state == TelepathyGLib.CallState.ENDED)
+ {
+ event_template.interpretation = ZG.LEAVE_EVENT;
+
+ /* Call was created by user but was rejected or not answered */
+ if (reason.reason == TelepathyGLib.CallStateChangeReason.REJECTED
+ || reason.reason == TelepathyGLib.CallStateChangeReason.NO_ANSWER)
+ {
+ if (channel.requested)
+ event_template.manifestation = ZG.WORLD_ACTIVITY;
+ else
+ event_template.interpretation = ZG.USER_ACTIVITY;
+
+ if (reason.reason == TelepathyGLib.CallStateChangeReason.NO_ANSWER)
+ event_template.interpretation = ZG.EXPIRE_EVENT;
+ else
+ event_template.interpretation = ZG.DENY_EVENT;
+ }
+
+ var duration = call_timers.lookup (channel.get_object_path ()).elapsed ();
+ call_timers.lookup (channel.get_object_path ()).stop;
+ call_timers.remove (channel.get_object_path ());
+ /*
+ * Create JSON payload representing the call metadata including
+ * duration and termination reasons of the call.
+ */
+ var gen = new Generator();
+ var root = new Json.Node(NodeType.OBJECT);
+ var object = new Json.Object();
+ root.set_object(object);
+ gen.set_root(root);
+ gen.pretty = true;
+
+ var details_obj = new Json.Object ();
+ details_obj.set_int_member ("state", state);
+ details_obj.set_int_member ("reason", reason.reason);
+ details_obj.set_boolean_member ("requested", channel.requested);
+ details_obj.set_double_member ("duration", duration);
+ size_t length;
+ object.set_object_member (call_json_domain, details_obj);
+ string payload_string = gen.to_data(out length);
+ event_template.payload = new GLib.ByteArray.take (payload_string.data);
+ this.push_event (event_template);
+ }
+ }
+ });
+ }
+
+ private async void handle_ftchannel_change (SimpleObserver observer,
+ Account account,
+ Connection connection,
+ FileTransferChannel channel,
+ ChannelDispatchOperation? dispatch_operation,
+ List<ChannelRequest> requests,
+ ObserveChannelsContext context)
+ {
+ if (channel.state == TelepathyGLib.FileTransferState.COMPLETED
+ || channel.state == TelepathyGLib.FileTransferState.CANCELLED)
+ {
+ var target = channel.get_target_contact ();
+ var attr = "%s, %s, %s".printf (FileAttribute.STANDARD_DISPLAY_NAME,
+ FileAttribute.STANDARD_CONTENT_TYPE, FileAttribute.STANDARD_SIZE);
+ FileInfo info = null;
+ try
+ {
+ info = yield channel.file.query_info_async (attr, 0);
+ }
+ catch (GLib.Error err)
+ {
+ warning ("Couldn't process %s: %s", channel.file.get_path (), err.message);
+ return;
+ }
+ var obj_path = account.get_object_path ();
+ obj_path = tp_account_path.printf("%s",
+ obj_path [TelepathyGLib.ACCOUNT_OBJECT_PATH_BASE.length:
+ obj_path.length]);
+ /* Create Event template */
+ var event_template = new Event ();
+ if (channel.requested)
+ {
+ event_template.interpretation = ZG.SEND_EVENT;
+ event_template.manifestation = ZG.USER_ACTIVITY;
+ }
+ else
+ {
+ event_template.interpretation = ZG.RECEIVE_EVENT;
+ event_template.manifestation = ZG.WORLD_ACTIVITY;
+ }
+ event_template.actor = actor;
+ /*
+ * Create Subject representing the sent/received file
+ */
+ var subj = new Subject ();
+ subj.uri = channel.file.get_uri ();
+ subj.interpretation = interpretation_for_mimetype (info.get_content_type ());
+ subj.manifestation = NFO.FILE_DATA_OBJECT;
+ subj.text = info.get_display_name ();
+ subj.mimetype = info.get_content_type ();
+ if (channel.requested == true)
+ {
+ var split_uri = channel.file.get_uri ().split ("/");
+ var uri = "%s/".printf(string.join ("/", split_uri[0:split_uri.length-1]));
+ subj.origin = uri;
+ }
+ else
+ subj.origin = tp_identifier.printf (target.get_identifier ());
+ event_template.add_subject (subj);
+
+ /*
+ * Create Subject representing contact received from or sent to
+ */
+ event_template.add_subject (
+ new Subject.full (tp_identifier.printf(target.get_identifier ()),
+ NCO.CONTACT,
+ NCO.CONTACT_LIST_DATA_OBJECT,
+ "",
+ obj_path,
+ target.get_alias (),
+ "net"));
+ /*
+ * Create Payload
+ */
+ var gen = new Generator();
+ var root = new Json.Node(NodeType.OBJECT);
+ var object = new Json.Object();
+ root.set_object(object);
+ gen.set_root(root);
+ gen.pretty = true;
+ var details_obj = new Json.Object ();
+ TelepathyGLib.FileTransferStateChangeReason reason;
+ var state = channel.get_state (out reason);
+ details_obj.set_int_member ("state", state);
+ details_obj.set_int_member ("reason", reason);
+ details_obj.set_boolean_member ("requested", channel.requested);
+ details_obj.set_string_member ("description", channel.get_description ());
+ details_obj.set_double_member ("size", (int64)channel.get_size ());
+ details_obj.set_string_member ("service", channel.get_service_name ());
+ size_t length;
+ object.set_object_member (ft_json_domain, details_obj);
+ string payload_string = gen.to_data (out length);
+ event_template.payload = new GLib.ByteArray.take (payload_string.data);
+ this.push_event (event_template);
+ }
+ }
+
+ private void observe_ft_channel (SimpleObserver observer, Account account,
+ Connection connection, Channel b_channel,
+ ChannelDispatchOperation? dispatch_operation,
+ List<ChannelRequest> requests,
+ ObserveChannelsContext context)
+ {
+ FileTransferChannel channel = (FileTransferChannel) b_channel;
+ channel.notify["state"].connect (() => {
+ this.handle_ftchannel_change (observer, account, connection, channel,
+ dispatch_operation, requests, context);
+ });
+ }
+
+ private void observe_channels (SimpleObserver observer, Account account,
+ Connection connection, List<Channel> channels,
+ ChannelDispatchOperation? dispatch_operation,
+ List<ChannelRequest> requests,
+ ObserveChannelsContext context)
+ {
+ try
+ {
+ foreach (var channel in channels)
+ {
+ if (channel is TelepathyGLib.TextChannel)
+ this.observe_text_channel (observer, account, connection, channel,
+ dispatch_operation, requests, context);
+ else if (channel is TelepathyGLib.CallChannel)
+ this.observe_call_channel (observer, account, connection, channel,
+ dispatch_operation, requests, context);
+ else if (channel is TelepathyGLib.FileTransferChannel)
+ this.observe_ft_channel (observer, account, connection, channel,
+ dispatch_operation, requests, context);
+ }
+ }
+ finally
+ {
+ context.accept ();
+ }
+ }
+
+ public override void start ()
+ {
+ observer = new TelepathyGLib.SimpleObserver.with_factory (factory,
+ true,
+ "Zeitgeist",
+ false,
+ observe_channels);
+ /*
+ * Add Call Channel Filters
+ */
+ HashTable<string,Value?> call_filter = new HashTable<string,Value?> (str_hash, str_equal);
+ call_filter.insert (TelepathyGLib.PROP_CHANNEL_CHANNEL_TYPE,
+ TelepathyGLib.IFACE_CHANNEL_TYPE_CALL);
+ call_filter.insert (TelepathyGLib.PROP_CHANNEL_TARGET_HANDLE_TYPE, 1); // 1 => TP_HANDLE_TYPE_CONTACT, somehow vala fails to compile when using the constant
+ observer.add_observer_filter (call_filter);
+ /*
+ * Add Text Channel Filters
+ */
+ HashTable<string,Value?> text_filter = new HashTable<string,Value?> (str_hash, str_equal);
+ text_filter.insert (TelepathyGLib.PROP_CHANNEL_CHANNEL_TYPE,
+ TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT);
+ text_filter.insert (TelepathyGLib.PROP_CHANNEL_TARGET_HANDLE_TYPE, 1); // 1 => TP_HANDLE_TYPE_CONTACT, somehow vala fails to compile when using the constant
+ observer.add_observer_filter (text_filter);
+ /*
+ * Add FileTransfer Channel Filters
+ */
+ HashTable<string,Value?> ft_filter = new HashTable<string,Value?> (str_hash, str_equal);
+ ft_filter.insert (TelepathyGLib.PROP_CHANNEL_CHANNEL_TYPE,
+ TelepathyGLib.IFACE_CHANNEL_TYPE_FILE_TRANSFER);
+ ft_filter.insert (TelepathyGLib.PROP_CHANNEL_TARGET_HANDLE_TYPE, 1); // 1 => TP_HANDLE_TYPE_CONTACT, somehow vala fails to compile when using the constant
+ observer.add_observer_filter (ft_filter);
+ try
+ {
+ observer.register ();
+ }
+ catch (GLib.Error err)
+ {
+ warning ("Couldn't register observer: %s", err.message);
+ }
+ }
+
+ public override void stop ()
+ {
+ observer.unregister ();
+ }
+}
diff --git a/datahub/utils.vala b/datahub/utils.vala
new file mode 100644
index 00000000..998eb2b4
--- /dev/null
+++ b/datahub/utils.vala
@@ -0,0 +1,272 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
+ * Copyright (C) 2012 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by Michal Hruby <michal.mhr@gmail.com>
+ * Authored by Siegfried-A. Gevatter <siegfried.gevatter@collabora.co.uk>
+ *
+ */
+
+using Zeitgeist;
+
+public class Utils : Object
+{
+ private static HashTable<string, string> app_to_desktop_file = null;
+ private static string[] desktop_file_prefixes = null;
+
+ // FIXME: Do we want to make this async?
+ // FIXME: this can throw GLib.Error, but if we use try/catch or throws
+ // it makes segfaults :(
+ public static string? get_file_contents (GLib.File file)
+ {
+ string contents;
+#if VALA_0_14
+ uint8[] contents_array;
+ try
+ {
+ if (!file.load_contents (null, out contents_array, null))
+ return null;
+ }
+ catch (Error err)
+ {
+ warning ("Couldn't get file contents %s: %s", file.get_path (), err.message);
+ }
+ contents = (string) contents_array;
+#else
+ if (!file.load_contents (null, out contents, null, null))
+ return null;
+#endif
+ return contents;
+ }
+
+ /*
+ * Configures DesktopAppInfo and initializes the list of places where we
+ * may find .desktop files.
+ */
+ private static void init_desktop_id ()
+ {
+ if (desktop_file_prefixes != null)
+ return;
+
+ unowned string session_var;
+
+ session_var = Environment.get_variable ("XDG_CURRENT_DESKTOP");
+ if (session_var != null)
+ {
+ DesktopAppInfo.set_desktop_env (session_var);
+ }
+ else
+ {
+ session_var = Environment.get_variable ("DESKTOP_SESSION");
+ if (session_var == null)
+ {
+ // let's assume it's GNOME
+ DesktopAppInfo.set_desktop_env ("GNOME");
+ }
+ else
+ {
+ string desktop_session = session_var.up ();
+ if (desktop_session.has_prefix ("GNOME"))
+ {
+ DesktopAppInfo.set_desktop_env ("GNOME");
+ }
+ else if (desktop_session.has_prefix ("KDE"))
+ {
+ DesktopAppInfo.set_desktop_env ("KDE");
+ }
+ else if (desktop_session.has_prefix ("XFCE"))
+ {
+ DesktopAppInfo.set_desktop_env ("XFCE");
+ }
+ else
+ {
+ // assume GNOME
+ DesktopAppInfo.set_desktop_env ("GNOME");
+ }
+ }
+ }
+
+ foreach (unowned string data_dir in Environment.get_system_data_dirs ())
+ {
+ desktop_file_prefixes += Path.build_path (Path.DIR_SEPARATOR_S,
+ data_dir,
+ "applications",
+ Path.DIR_SEPARATOR_S, null);
+ }
+ }
+
+ /*
+ * Takes a path to a .desktop file and returns the Desktop ID for it.
+ * This isn't simply the basename, but may contain part of the path;
+ * eg. kde4-kate.desktop for /usr/share/applications/kde4/kate.desktop.
+ * */
+ private static string extract_desktop_id (string path)
+ {
+ if (!path.has_prefix ("/"))
+ return path;
+
+ string normalized_path = path.replace ("//", "/");
+
+ foreach (unowned string prefix in desktop_file_prefixes)
+ {
+ if (normalized_path.has_prefix (prefix))
+ {
+ string without_prefix = normalized_path.substring (prefix.length);
+
+ if (Path.DIR_SEPARATOR_S in without_prefix)
+ return without_prefix.replace (Path.DIR_SEPARATOR_S, "-");
+
+ return without_prefix;
+ }
+ }
+
+ return Path.get_basename (path);
+ }
+
+ /*
+ * Takes the basename of a .desktop and returns the Zeitgeist actor for it.
+ */
+ public static string? get_actor_for_desktop_file (string desktop_file,
+ out DesktopAppInfo dai = null)
+ {
+ init_desktop_id ();
+
+ if (Path.is_absolute (desktop_file))
+ {
+ dai = new DesktopAppInfo.from_filename (desktop_file);
+ }
+ else
+ {
+ dai = new DesktopAppInfo (desktop_file);
+ }
+
+ if (dai == null)
+ {
+ return null;
+ }
+
+ string desktop_id = dai.get_id () ?? extract_desktop_id (dai.get_filename ());
+ return "application://%s".printf (desktop_id);
+ }
+
+ /*
+ * Initialize the cache mapping application names (from GtkRecentManager)
+ * to Desktop IDs.
+ * */
+ private static void init_application_cache ()
+ {
+ if (unlikely (app_to_desktop_file == null))
+ app_to_desktop_file = new HashTable<string, string> (str_hash, str_equal);
+ }
+
+ /*
+ * Workaround for OpenOffice.org/LibreOffice.
+ * */
+ public static string? get_ooo_desktop_file_for_mimetype (string mimetype)
+ {
+ return find_desktop_file_for_app ("libreoffice", mimetype) ??
+ find_desktop_file_for_app ("ooffice", mimetype);
+ }
+
+ /*
+ * Takes an application name (from GtkRecentManager) and finds
+ * a .desktop file that launches the given application.
+ *
+ * It returns the complete path to the .desktop file.
+ */
+ public static string? find_desktop_file_for_app (string app_name,
+ string? mimetype = null)
+ {
+ init_application_cache ();
+
+ string hash_name = mimetype != null ?
+ "%s::%s".printf (app_name, mimetype) : app_name;
+ unowned string? in_cache = app_to_desktop_file.lookup (hash_name);
+ if (in_cache != null)
+ {
+ return in_cache;
+ }
+
+ string[] data_dirs = Environment.get_system_data_dirs ();
+ data_dirs += Environment.get_user_data_dir ();
+
+ foreach (unowned string dir in data_dirs)
+ {
+ var p = Path.build_filename (dir, "applications",
+ "%s.desktop".printf (app_name),
+ null);
+ var f = File.new_for_path (p);
+ if (f.query_exists (null))
+ {
+ app_to_desktop_file.insert (hash_name, p);
+ // FIXME: we're not checking mimetype here!
+ return p;
+ }
+ }
+
+ foreach (unowned string dir in data_dirs)
+ {
+ var p = Path.build_filename (dir, "applications", null);
+ var app_dir = File.new_for_path (p);
+ if (!app_dir.query_exists (null)) continue;
+
+ try
+ {
+ var enumerator =
+ app_dir.enumerate_children (FileAttribute.STANDARD_NAME, 0, null);
+ FileInfo fi = enumerator.next_file (null);
+ while (fi != null)
+ {
+ if (fi.get_name ().has_suffix (".desktop"))
+ {
+ var desktop_file = Path.build_filename (p, fi.get_name (), null);
+ var f = File.new_for_path (desktop_file);
+ try
+ {
+ string? contents = Utils.get_file_contents (f);
+ if (contents != null)
+ {
+ if ("Exec=%s".printf (app_name) in contents)
+ {
+ if (mimetype == null || mimetype in contents)
+ {
+ app_to_desktop_file.insert (hash_name, desktop_file);
+ return desktop_file;
+ }
+ }
+ }
+ }
+ catch (GLib.Error err)
+ {
+ warning ("%s", err.message);
+ }
+ }
+ fi = enumerator.next_file (null);
+ }
+
+ enumerator.close (null);
+ }
+ catch (GLib.Error err)
+ {
+ warning ("%s", err.message);
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/datahub/zeitgeist-datahub b/datahub/zeitgeist-datahub
new file mode 100755
index 00000000..44750d86
--- /dev/null
+++ b/datahub/zeitgeist-datahub
@@ -0,0 +1,228 @@
+#! /bin/bash
+
+# zeitgeist-datahub - temporary wrapper script for .libs/zeitgeist-datahub
+# Generated by libtool (GNU libtool) 2.4.2 Debian-2.4.2-1ubuntu2
+#
+# The zeitgeist-datahub program cannot be directly executed until all the libtool
+# libraries that it depends on are installed.
+#
+# This wrapper script should never be moved out of the build directory.
+# If it is, it will not operate correctly.
+
+# Sed substitution that helps us do robust quoting. It backslashifies
+# metacharacters that are still active within double-quoted strings.
+sed_quote_subst='s/\([`"$\\]\)/\\\1/g'
+
+# Be Bourne compatible
+if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then
+ emulate sh
+ NULLCMD=:
+ # Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which
+ # is contrary to our usage. Disable this feature.
+ alias -g '${1+"$@"}'='"$@"'
+ setopt NO_GLOB_SUBST
+else
+ case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac
+fi
+BIN_SH=xpg4; export BIN_SH # for Tru64
+DUALCASE=1; export DUALCASE # for MKS sh
+
+# The HP-UX ksh and POSIX shell print the target directory to stdout
+# if CDPATH is set.
+(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
+
+relink_command="(cd /home/manish/code/zeitgeist/datahub; { test -z \"\${LIBRARY_PATH+set}\" || unset LIBRARY_PATH || { LIBRARY_PATH=; export LIBRARY_PATH; }; }; { test -z \"\${COMPILER_PATH+set}\" || unset COMPILER_PATH || { COMPILER_PATH=; export COMPILER_PATH; }; }; { test -z \"\${GCC_EXEC_PREFIX+set}\" || unset GCC_EXEC_PREFIX || { GCC_EXEC_PREFIX=; export GCC_EXEC_PREFIX; }; }; { test -z \"\${LD_RUN_PATH+set}\" || unset LD_RUN_PATH || { LD_RUN_PATH=; export LD_RUN_PATH; }; }; { test -z \"\${LD_LIBRARY_PATH+set}\" || unset LD_LIBRARY_PATH || { LD_LIBRARY_PATH=; export LD_LIBRARY_PATH; }; }; PATH=/usr/lib/lightdm/lightdm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games; export PATH; gcc -g -O2 -o \$progdir/\$file configuration.o data-provider.o desktop-launch-listener.o downloads-directory-provider.o kde-recent-document-provider.o recent-manager-provider.o utils.o zeitgeist-datahub.o telepathy-observer.o -Wl,--export-dynamic ../libzeitgeist/.libs/libzeitgeist-2.0.so -lgtk-3 -lgdk-3 -latk-1.0 -lpangocairo-1.0 -lgdk_pixbuf-2.0 /usr/lib/i386-linux-gnu/libcairo-gobject.so -lpango-1.0 /usr/lib/i386-linux-gnu/libcairo.so -ljson-glib-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -ltelepathy-glib -pthread -Wl,-rpath -Wl,/home/manish/code/zeitgeist/libzeitgeist/.libs)"
+
+# This environment variable determines our operation mode.
+if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then
+ # install mode needs the following variables:
+ generated_by_libtool_version='2.4.2'
+ notinst_deplibs=' ../libzeitgeist/libzeitgeist-2.0.la'
+else
+ # When we are sourced in execute mode, $file and $ECHO are already set.
+ if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
+ file="$0"
+
+# A function that is used when there is no print builtin or printf.
+func_fallback_echo ()
+{
+ eval 'cat <<_LTECHO_EOF
+$1
+_LTECHO_EOF'
+}
+ ECHO="printf %s\\n"
+ fi
+
+# Very basic option parsing. These options are (a) specific to
+# the libtool wrapper, (b) are identical between the wrapper
+# /script/ and the wrapper /executable/ which is used only on
+# windows platforms, and (c) all begin with the string --lt-
+# (application programs are unlikely to have options which match
+# this pattern).
+#
+# There are only two supported options: --lt-debug and
+# --lt-dump-script. There is, deliberately, no --lt-help.
+#
+# The first argument to this parsing function should be the
+# script's ../libtool value, followed by no.
+lt_option_debug=
+func_parse_lt_options ()
+{
+ lt_script_arg0=$0
+ shift
+ for lt_opt
+ do
+ case "$lt_opt" in
+ --lt-debug) lt_option_debug=1 ;;
+ --lt-dump-script)
+ lt_dump_D=`$ECHO "X$lt_script_arg0" | /bin/sed -e 's/^X//' -e 's%/[^/]*$%%'`
+ test "X$lt_dump_D" = "X$lt_script_arg0" && lt_dump_D=.
+ lt_dump_F=`$ECHO "X$lt_script_arg0" | /bin/sed -e 's/^X//' -e 's%^.*/%%'`
+ cat "$lt_dump_D/$lt_dump_F"
+ exit 0
+ ;;
+ --lt-*)
+ $ECHO "Unrecognized --lt- option: '$lt_opt'" 1>&2
+ exit 1
+ ;;
+ esac
+ done
+
+ # Print the debug banner immediately:
+ if test -n "$lt_option_debug"; then
+ echo "zeitgeist-datahub:zeitgeist-datahub:${LINENO}: libtool wrapper (GNU libtool) 2.4.2 Debian-2.4.2-1ubuntu2" 1>&2
+ fi
+}
+
+# Used when --lt-debug. Prints its arguments to stdout
+# (redirection is the responsibility of the caller)
+func_lt_dump_args ()
+{
+ lt_dump_args_N=1;
+ for lt_arg
+ do
+ $ECHO "zeitgeist-datahub:zeitgeist-datahub:${LINENO}: newargv[$lt_dump_args_N]: $lt_arg"
+ lt_dump_args_N=`expr $lt_dump_args_N + 1`
+ done
+}
+
+# Core function for launching the target application
+func_exec_program_core ()
+{
+
+ if test -n "$lt_option_debug"; then
+ $ECHO "zeitgeist-datahub:zeitgeist-datahub:${LINENO}: newargv[0]: $progdir/$program" 1>&2
+ func_lt_dump_args ${1+"$@"} 1>&2
+ fi
+ exec "$progdir/$program" ${1+"$@"}
+
+ $ECHO "$0: cannot exec $program $*" 1>&2
+ exit 1
+}
+
+# A function to encapsulate launching the target application
+# Strips options in the --lt-* namespace from $@ and
+# launches target application with the remaining arguments.
+func_exec_program ()
+{
+ case " $* " in
+ *\ --lt-*)
+ for lt_wr_arg
+ do
+ case $lt_wr_arg in
+ --lt-*) ;;
+ *) set x "$@" "$lt_wr_arg"; shift;;
+ esac
+ shift
+ done ;;
+ esac
+ func_exec_program_core ${1+"$@"}
+}
+
+ # Parse options
+ func_parse_lt_options "$0" ${1+"$@"}
+
+ # Find the directory that this script lives in.
+ thisdir=`$ECHO "$file" | /bin/sed 's%/[^/]*$%%'`
+ test "x$thisdir" = "x$file" && thisdir=.
+
+ # Follow symbolic links until we get to the real thisdir.
+ file=`ls -ld "$file" | /bin/sed -n 's/.*-> //p'`
+ while test -n "$file"; do
+ destdir=`$ECHO "$file" | /bin/sed 's%/[^/]*$%%'`
+
+ # If there was a directory component, then change thisdir.
+ if test "x$destdir" != "x$file"; then
+ case "$destdir" in
+ [\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;;
+ *) thisdir="$thisdir/$destdir" ;;
+ esac
+ fi
+
+ file=`$ECHO "$file" | /bin/sed 's%^.*/%%'`
+ file=`ls -ld "$thisdir/$file" | /bin/sed -n 's/.*-> //p'`
+ done
+
+ # Usually 'no', except on cygwin/mingw when embedded into
+ # the cwrapper.
+ WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no
+ if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then
+ # special case for '.'
+ if test "$thisdir" = "."; then
+ thisdir=`pwd`
+ fi
+ # remove .libs from thisdir
+ case "$thisdir" in
+ *[\\/].libs ) thisdir=`$ECHO "$thisdir" | /bin/sed 's%[\\/][^\\/]*$%%'` ;;
+ .libs ) thisdir=. ;;
+ esac
+ fi
+
+ # Try to get the absolute directory name.
+ absdir=`cd "$thisdir" && pwd`
+ test -n "$absdir" && thisdir="$absdir"
+
+ program=lt-'zeitgeist-datahub'
+ progdir="$thisdir/.libs"
+
+ if test ! -f "$progdir/$program" ||
+ { file=`ls -1dt "$progdir/$program" "$progdir/../$program" 2>/dev/null | /bin/sed 1q`; \
+ test "X$file" != "X$progdir/$program"; }; then
+
+ file="$$-$program"
+
+ if test ! -d "$progdir"; then
+ mkdir "$progdir"
+ else
+ rm -f "$progdir/$file"
+ fi
+
+ # relink executable if necessary
+ if test -n "$relink_command"; then
+ if relink_command_output=`eval $relink_command 2>&1`; then :
+ else
+ printf %s\n "$relink_command_output" >&2
+ rm -f "$progdir/$file"
+ exit 1
+ fi
+ fi
+
+ mv -f "$progdir/$file" "$progdir/$program" 2>/dev/null ||
+ { rm -f "$progdir/$program";
+ mv -f "$progdir/$file" "$progdir/$program"; }
+ rm -f "$progdir/$file"
+ fi
+
+ if test -f "$progdir/$program"; then
+ if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
+ # Run the actual program with our arguments.
+ func_exec_program ${1+"$@"}
+ fi
+ else
+ # The program doesn't exist.
+ $ECHO "$0: error: \`$progdir/$program' does not exist" 1>&2
+ $ECHO "This script is just a wrapper for $program." 1>&2
+ $ECHO "See the libtool documentation for more information." 1>&2
+ exit 1
+ fi
+fi
diff --git a/datahub/zeitgeist-datahub.vala b/datahub/zeitgeist-datahub.vala
new file mode 100644
index 00000000..95e4fd56
--- /dev/null
+++ b/datahub/zeitgeist-datahub.vala
@@ -0,0 +1,291 @@
+/*
+ * Zeitgeist
+ *
+ * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by Michal Hruby <michal.mhr@gmail.com>
+ *
+ */
+
+using Zeitgeist;
+
+[DBus (name = "org.gnome.zeitgeist.datahub")]
+public interface DataHubService : Object
+{
+ public abstract string[] get_data_providers () throws IOError;
+}
+
+public class DataHub : Object, DataHubService
+{
+ private Zeitgeist.Log zg_log;
+ private Zeitgeist.DataSourceRegistry registry;
+ private MainLoop main_loop;
+ private List<DataProvider> providers;
+ private List<DataSource> sources_info; // list got from ZG's Registry
+ private GenericArray<Event> queued_events;
+ private uint idle_id = 0;
+
+ public int return_code { get; private set; default = 0; }
+
+ public DataHub ()
+ {
+ GLib.Object ();
+ }
+
+ construct
+ {
+ providers = new List<DataProvider> ();
+ sources_info = new List<DataSource> ();
+ queued_events = new GenericArray<Event> ();
+ main_loop = new MainLoop ();
+
+ zg_log = new Zeitgeist.Log ();
+ zg_log.notify["connected"].connect (() =>
+ {
+ if (!zg_log.is_connected)
+ {
+ debug ("Zeitgeist-daemon disappeared from the bus, exitting...");
+ quit ();
+ }
+ });
+
+ registry = new DataSourceRegistry ();
+ }
+
+ private void data_source_registered (DataSource ds)
+ {
+ unowned List<DataSource> iter = sources_info;
+ while (iter != null)
+ {
+ if (iter.data.unique_id == ds.unique_id)
+ {
+ break;
+ }
+ iter = iter.next;
+ }
+
+ if (iter != null)
+ {
+ iter.data = ds;
+ }
+ else
+ {
+ sources_info.prepend (ds);
+ }
+ }
+
+ private async void start_data_providers () throws Error
+ {
+ try
+ {
+ registry.source_registered.connect (data_source_registered);
+ var sources = yield registry.get_data_sources (null);
+ for (uint i=0; i<sources.length; i++)
+ {
+ sources_info.prepend (sources.get (i) as DataSource);
+ }
+ }
+ catch (GLib.Error err)
+ {
+ warning ("%s", err.message);
+ }
+ // TODO: load all datasources once we do them as modules
+ /*
+ foreach (var datasource in datasources)
+ {
+ providers.prepend (datasource.run ());
+ }
+ */
+ providers.prepend (new RecentManagerGtk (this));
+ providers.prepend (new RecentDocumentsKDE (this));
+
+#if ENABLE_TELEPATHY
+ providers.prepend (new TelepathyObserver (this));
+#endif
+
+ if (Config.DOWNLOADS_MONITOR_ENABLED)
+ providers.prepend (new DownloadsDirectoryMonitor (this));
+
+ if (GLibExtra.check_version (2, 28, 0))
+ {
+ providers.prepend (new DesktopLaunchListener (this));
+ }
+
+ foreach (unowned DataProvider prov in providers)
+ {
+ bool enabled = true;
+ // we need to get the timestamp before we register the data provider
+ int64 timestamp = 0;
+ foreach (var src in sources_info)
+ {
+ if (src.unique_id == prov.unique_id)
+ {
+ timestamp = src.timestamp;
+ break;
+ }
+ }
+
+ if (prov.register)
+ {
+ var ds = new DataSource.full (prov.unique_id,
+ prov.name,
+ prov.description,
+ new GenericArray<Event> ()); // FIXME: templates!
+ try
+ {
+ enabled = yield registry.register_data_source (ds, null);
+ }
+ catch (GLib.Error reg_err)
+ {
+ warning ("%s", reg_err.message);
+ }
+ }
+ prov.items_available.connect (this.items_available);
+ if (enabled)
+ {
+ prov.last_timestamp = timestamp;
+ prov.start ();
+ }
+ }
+ }
+
+ private void items_available (DataProvider prov, GenericArray<Event> events)
+ {
+ if (!prov.enabled) return;
+
+ events.foreach ((e) => { queued_events.add (e); });
+
+ if (queued_events.length > 0 && idle_id == 0)
+ {
+ idle_id = Idle.add (() =>
+ {
+ insert_events ();
+ idle_id = 0;
+ return false;
+ });
+ }
+ }
+
+ private void insert_events ()
+ {
+ debug ("Inserting %u events", queued_events.length);
+
+ batch_insert_events ();
+
+ queued_events = new GenericArray<Event> ();
+ }
+
+ protected async void batch_insert_events ()
+ {
+ // copy the events to GenericArray (with a ref on them)
+ GenericArray<Event> all_events = new GenericArray<Event> ();
+ queued_events.foreach ((e) => { all_events.add (e); });
+
+ while (all_events.length > 0)
+ {
+ uint elements_pushed = uint.min ((uint) all_events.length, 100);
+ GenericArray<Event> ptr_arr = new GenericArray<Event> ();
+ for (uint i=0; i<elements_pushed; i++) ptr_arr.add (all_events[i]);
+
+ try
+ {
+ yield zg_log.insert_events(ptr_arr, null);
+ }
+ catch (GLib.Error err)
+ {
+ warning ("Error during inserting events: %s", err.message);
+ }
+
+ all_events.remove_range (0, elements_pushed);
+ }
+ }
+
+ const string UNIQUE_NAME = "org.gnome.zeitgeist.datahub";
+ const string OBJECT_PATH = "/org/gnome/zeitgeist/datahub";
+
+ protected void run () throws Error
+ {
+ Bus.own_name (BusType.SESSION, UNIQUE_NAME, BusNameOwnerFlags.NONE,
+ (conn) => { conn.register_object (OBJECT_PATH, (DataHubService) this); },
+ () => { start_data_providers (); },
+ () =>
+ {
+ warning ("Unable to get name \"org.gnome.zeitgeist.datahub\"" +
+ " on the bus!");
+ this.return_code = 1;
+ this.quit ();
+ }
+ );
+
+ main_loop.run ();
+ }
+
+ protected void quit ()
+ {
+ // dispose all providers
+ providers = new List<DataProvider> ();
+ main_loop.quit ();
+ }
+
+ public string[] get_data_source_actors (bool only_enabled = true)
+ {
+ string[] actors = {};
+ foreach (unowned DataSource src in sources_info)
+ {
+ if (only_enabled && !src.enabled) continue;
+ var template_arr = src.event_templates;
+ if (template_arr != null)
+ {
+ for (uint i=0; i<template_arr.length; i++)
+ {
+ Zeitgeist.Event event_template =
+ template_arr.get (i) as Zeitgeist.Event;
+ string? actor = event_template.actor;
+
+ if (actor != null && actor != "")
+ {
+ actors += actor;
+ }
+ }
+ }
+ }
+
+ return actors;
+ }
+
+ public string[] get_data_providers () throws IOError
+ {
+ string[] arr = {};
+ foreach (var provider in providers)
+ {
+ arr += provider.unique_id;
+ }
+ return arr;
+ }
+
+ public static int main (string[] args)
+ {
+ Environment.set_prgname ("zeitgeist-datahub");
+ var hub = new DataHub ();
+ try {
+ hub.run ();
+ } catch (Error err) {
+ stdout.printf("Error running Zeitgeist Datahub %s".printf(err.message));
+ }
+
+ return hub.return_code;
+ }
+}