gnome-pass-search-provider/gnome-pass-search-provider.py
Matthias Vogelgesang 119a5c42f0 Implement fuzzy search and ranking
The regex based method to find key matches does not work very well for deeper
hierarchies and partial matching of the hierarchy levels. This approach uses the
SequenceMatcher from the difflib module to compute a score for an entry and rank
them accordingly.

This change also filters out all hidden file system entries to avoid traversing
and evaluating Git repositories which are commonly used to store pass storages.
2017-09-04 11:23:52 +02:00

173 lines
5.8 KiB
Python
Executable File

#!/usr/bin/env python3
# This file is a part of gnome-pass-search-provider.
#
# gnome-pass-search-provider is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gnome-pass-search-providers 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with search-pass. If not, see
# <http://www.gnu.org/licenses/>.
# Copyright (C) 2017 Jonathan Lestrelin
# Author: Jonathan Lestrelin <jonathan.lestrelin@gmail.com>
# This project was based on gnome-shell-search-github-repositories
# Copyright (C) 2012 Red Hat, Inc.
# Author: Ralph Bean <rbean@redhat.com>
# which itself was based on fedmsg-notify
# Copyright (C) 2012 Red Hat, Inc.
# Author: Luke Macken <lmacken@redhat.com>
from os import getenv
from os import walk
from os.path import expanduser, join as path_join
import re
import difflib
import subprocess
import dbus
import dbus.glib
import dbus.service
from gi.repository import GLib
# Convenience shorthand for declaring dbus interface methods.
# s.b.n. -> search_bus_name
search_bus_name = 'org.gnome.Shell.SearchProvider2'
sbn = dict(dbus_interface=search_bus_name)
class SearchPassService(dbus.service.Object):
""" The pass search daemon.
This service is started through DBus activation by calling the
:meth:`Enable` method, and stopped with :meth:`Disable`.
"""
bus_name = 'org.gnome.Pass.SearchProvider'
_object_path = '/' + bus_name.replace('.', '/')
def __init__(self):
self.session_bus = dbus.SessionBus()
bus_name = dbus.service.BusName(self.bus_name, bus=self.session_bus)
dbus.service.Object.__init__(self, bus_name, self._object_path)
self.password_store = getenv('PASSWORD_STORE_DIR') or \
expanduser('~/.password-store')
@dbus.service.method(in_signature='sasu', **sbn)
def ActivateResult(self, id, terms, timestamp):
self.send_password_to_clipboard(id)
@dbus.service.method(in_signature='as', out_signature='as', **sbn)
def GetInitialResultSet(self, terms):
return self.get_result_set(terms)
@dbus.service.method(in_signature='as', out_signature='aa{sv}', **sbn)
def GetResultMetas(self, ids):
return [dict(id=id, name=id,) for id in ids]
@dbus.service.method(in_signature='asas', out_signature='as', **sbn)
def GetSubsearchResultSet(self, previous_results, new_terms):
return self.get_result_set(new_terms)
@dbus.service.method(in_signature='asu', terms='as', timestamp='u', **sbn)
def LaunchSearch(self, terms, timestamp):
pass
def get_result_set(self, terms):
name = ' '.join(terms)
matcher = difflib.SequenceMatcher(b=name, autojunk=False)
matches = []
for root, dirs, files in walk(self.password_store):
dir_path = root[len(self.password_store) + 1:]
if dir_path.startswith('.'):
continue
for filename in files:
path = path_join(dir_path, filename)
matcher.set_seq1(path[:-4])
score = matcher.ratio()
if score >= 0.5:
matches.append((score, path[:-4]))
return [x[1] for x in sorted(matches, key=lambda x: x[0], reverse=True)]
def send_password_to_gpaste(self, name):
pass_cmd = subprocess.run(
['pass', 'show', name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
password = re.sub(b'\n$', b'', pass_cmd.stdout)
error = re.sub(b'\n$', b'', pass_cmd.stderr)
if not pass_cmd.returncode:
self.session_bus.get_object(
'org.gnome.GPaste.Daemon',
'/org/gnome/GPaste'
).AddPassword(
name,
password,
dbus_interface='org.gnome.GPaste1'
)
self.notify('Password {} copied to clipboard.'.format(name))
else:
self.notify('Failed to copy password', body=error, error=True)
def send_password_to_native_clipboard(self, name):
pass_cmd = subprocess.run(
['pass', 'show', '-c', name],
stderr=subprocess.PIPE
)
error = re.sub(b'\n$', b'', pass_cmd.stderr)
if pass_cmd.returncode:
self.notify('Failed to copy password', body=error, error=True)
else:
self.notify('Password {} copied to clipboard.'.format(name))
def send_password_to_clipboard(self, name):
try:
self.send_password_to_gpaste(name)
except dbus.DBusException:
# We couldn't join GPaste over D-Bus,
# use pass native clipboard copy
self.send_password_to_native_clipboard(name)
def notify(self, message, body='', error=False):
try:
self.session_bus.get_object(
'org.freedesktop.Notifications',
'/org/freedesktop/Notifications'
).Notify(
'Pass',
0,
'',
message,
body,
'',
{'transient': False if error else True},
0 if error else 3000,
dbus_interface='org.freedesktop.Notifications'
)
except dbus.DBusException as err:
print('Got error {} while trying to display message {}.'.format(
err, message))
def main():
SearchPassService()
GLib.MainLoop().run()
if __name__ == '__main__':
main()