diff options
author | Rico Tzschichholz <ricotz@t-online.de> | 2013-03-15 17:38:16 -0700 |
---|---|---|
committer | Manish Sinha <manishsinha@ubuntu.com> | 2013-03-15 17:38:16 -0700 |
commit | 013e2f4e5ec27301d30bd8412f2374976f2f81ab (patch) | |
tree | 099ec130bc1b46a7141c4bc687ff903861693e6f | |
parent | eb1aac4898a6e645769dd3536369f26d37bf38d6 (diff) | |
download | zeitgeist-013e2f4e5ec27301d30bd8412f2374976f2f81ab.tar.gz |
Moved datahub inside the zeitgeist codebase
-rw-r--r-- | data/zeitgeist-datahub.desktop | 10 | ||||
-rw-r--r-- | data/zeitgeist-datahub.desktop.in | 10 | ||||
-rw-r--r-- | datahub/Makefile.am | 72 | ||||
-rw-r--r-- | datahub/configuration.vala | 30 | ||||
-rw-r--r-- | datahub/data-provider.vala | 46 | ||||
-rw-r--r-- | datahub/desktop-launch-listener.vala | 171 | ||||
-rw-r--r-- | datahub/downloads-directory-provider.vala | 150 | ||||
-rw-r--r-- | datahub/kde-recent-document-provider.vala | 273 | ||||
-rw-r--r-- | datahub/recent-manager-provider.vala | 233 | ||||
-rw-r--r-- | datahub/telepathy-observer.vala | 525 | ||||
-rw-r--r-- | datahub/utils.vala | 272 | ||||
-rwxr-xr-x | datahub/zeitgeist-datahub | 228 | ||||
-rw-r--r-- | datahub/zeitgeist-datahub.vala | 291 | ||||
-rw-r--r-- | doc/zeitgeist-datahub.1 | 50 |
14 files changed, 2361 insertions, 0 deletions
diff --git a/data/zeitgeist-datahub.desktop b/data/zeitgeist-datahub.desktop new file mode 100644 index 00000000..2f5ec510 --- /dev/null +++ b/data/zeitgeist-datahub.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +Name=Zeitgeist Datahub +Comment=Start the Zeitgeist Datahub for passive loggers +Exec=zeitgeist-datahub +Terminal=false +Type=Application +Categories= +GenericName= diff --git a/data/zeitgeist-datahub.desktop.in b/data/zeitgeist-datahub.desktop.in new file mode 100644 index 00000000..30b4aac5 --- /dev/null +++ b/data/zeitgeist-datahub.desktop.in @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Encoding=UTF-8 +_Name=Zeitgeist Datahub +_Comment=Start the Zeitgeist Datahub for passive loggers +Exec=zeitgeist-datahub +Terminal=false +Type=Application +Categories= +GenericName= 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; + } +} diff --git a/doc/zeitgeist-datahub.1 b/doc/zeitgeist-datahub.1 new file mode 100644 index 00000000..b4c3f356 --- /dev/null +++ b/doc/zeitgeist-datahub.1 @@ -0,0 +1,50 @@ +.TH ZEITGEIST\-DATAHUB 1 "May 20, 2009" "Zeitgeist" + +.SH NAME +zeitgeist\-datahub \- passive loggers for Zeitgeist + +.SH SYNOPSIS +\fBzeitgeist\-datahub\fP \fI[OPTION]\fP + +.SH DESCRIPTION +\fBzeitgeist\-datahub\fP is a daemon which centralizes all passive +data sources into a single process, and interfaces between said data +sources (also known as \fIloggers\fP) and \fBzeitgeist-daemon\fP (with +which it communicates via D-Bus). +.PP +\fBPassive\fP data sources are those which run in the background and +look for activities independently to the programs they monitor, whereas +Zeitgeist can also work with \fBactive\fP data sources which are those +in-build into other applications (either natively or through +extensions). + +.SH OPTIONS +The program follows the usual GNU command line syntax, with +options starting with two dashes (`--'). A summary of options is +included below. +.TP +.B Currently there are no options available. + +.SH SEE ALSO +\fBzeitgeist-daemon\fR + +.SH BUGS +Please report any bugs on https://bugs.launchpad.net/zeitgeist-datahub. + +.SH AUTHORS +Please see the AUTHORS file bundled with this application for +a complete list of contributors. + +.SH LICENSE +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. +.PP +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. +.PP +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/>. |