# Copyright (c) 2014 Eric Davis # This file is ~*slightly*~ heavily modified from simplynote.py. # Updated in 2018 to work with the new Simperium api. # -*- coding: utf-8 -*- """ simplenote.py ~~~~~~~~~~~~~~ Python library for accessing the Simplenote API :copyright: (c) 2011 by Daniel Schauenberg :license: MIT, see LICENSE for more details. """ import base64 import datetime import json import logging import time import urllib.parse import uuid import requests from requests.exceptions import ConnectionError, RequestException, HTTPError from simperium.core import Api, Auth # Application token provided for sncli. # Please do not abuse. APP_TOKEN = '26864ab5d6fd4a37b80343439f107350' # Simplenote app id on Simperium SIMPLENOTE_APP_ID = 'chalk-bump-f49' NOTE_FETCH_LENGTH = 100 class SimplenoteLoginFailed(Exception): pass class Simplenote(object): """ Class for interacting with the simplenote web service """ def __init__(self, username: str, password: str): """ object constructor """ self.auth = Auth(SIMPLENOTE_APP_ID, APP_TOKEN) self.api = None self.username = username self.password = password self.token = None self.status = 'offline' if not username or not password: logging.debug('Auth error: username or password not set') self.status = 'offline: username or password not set' return # attempt initial auth try: self.api = self.authenticate(self.username, self.password) except ConnectionError as e: logging.debug("Auth error: " + str(e)) self.status = 'offline: no connection' except HTTPError as e: logging.debug("Auth error: " + str(e)) self.status = 'offline: login failed; check username and password' except KeyError as e: logging.debug("Auth error: " + str(e)) self.status = 'offline: login failed; check username and password' except Exception as e: logging.debug("Auth error: " + str(e)) self.status = 'offline: unknown auth error; check log for details' def authenticate(self, user: str, password: str) -> Api: """ Method to get simplenote auth token Arguments: - user (string): simplenote email address - password (string): simplenote password Returns: Simplenote API instance """ token = self.auth.authorize(user, password) api = Api(SIMPLENOTE_APP_ID, token) self.status = "online" return api def get_note(self, noteid, version=None): """ method to get a specific note Arguments: - noteid (string): ID of the note to get - version (int): optional version of the note to get Returns: A tuple `(note, status)` - note (dict): note object - status (int): 0 on sucesss and -1 otherwise """ if self.api is None: return None, -1 try: note = self.api.note.get(noteid, version=version) if version is not None: note['version'] = version if note is None: return None, -1 note['key'] = noteid return note, 0 if note is not None else -1 except Exception as e: logging.debug(e) return None, -1 def update_note(self, note): """ function to update a specific note object, if the note object does not have a "key" field, a new note is created Arguments - note (dict): note object to update Returns: A tuple `(note, status)` - note (dict): note object (or error instance if failure) - status (int): 0 on sucesss and -1 otherwise """ # Note: all strings in notes stored as type str # - use s.encode('utf-8') when bytes type needed if self.api is None: return None, -1 try: # determine whether to create a new note or updated an existing one if 'key' not in note: # new note; build full note object to send to avoid 400 errors note = { 'tags': note['tags'], 'deleted': note['deleted'], 'content': note['content'], 'modificationDate': note['modificationDate'], 'creationDate': note['creationDate'], 'systemTags': note['systemTags'], 'shareURL': '', 'publishURL': '', } key, note = self.api.note.new(note, include_response=True) note['version'] = 1 else: key, note = self.api.note.set(note['key'], note, include_response=True) note['key'] = key except ConnectionError as e: self.status = 'offline, connection error' return e, -1 except RequestException as e: logging.debug('RESPONSE ERROR: ' + str(e)) self.status = 'error updating note, check log' return e, -1 except ValueError as e: return e, -1 return note, 0 def add_note(self, note): """wrapper function to add a note The function can be passed the note as a dict with the `content` property set, which is then directly send to the web service for creation. Alternatively, only the body as string can also be passed. In this case the parameter is used as `content` for the new note. Arguments: - note (dict or string): the note to add Returns: A tuple `(note, status)` - note (dict): the newly created note - status (int): 0 on sucesss and -1 otherwise """ if type(note) == str: return self.update_note({"content": note}) elif (type(note) == dict) and "content" in note: return self.update_note(note) else: return "No string or valid note.", -1 def _convert_index_to_note(cls, entry): """ Helper function to convert a note as returned in the api index method to how sncli expects it. """ note = entry['d'] note['key'] = entry['id'] note['version'] = entry['v'] return note def get_note_list(self, since=None, tags=[]): """ function to get the note list The function can be passed optional arguments to limit the date range of the list returned and/or limit the list to notes containing a certain tag. If omitted a list of all notes is returned. Arguments: - since=time.time() epoch stamp: only return notes modified since this date - tags=[] list of tags as string: return notes that have at least one of these tags Returns: A tuple `(notes, status)` - notes (list): A list of note objects with all properties set. - status (int): 0 on sucesss and -1 otherwise """ # initialize data status = 0 note_list = [] mark = None if self.api is None: return [], -1 while True: try: data = self.api.note.index(data=True, mark=mark, limit=NOTE_FETCH_LENGTH) note_list.extend(map(self._convert_index_to_note, data['index'])) if 'mark' not in data: break mark = data['mark'] except ConnectionError as e: self.status = 'offline, connection error' status = -1 break except RequestException as e: # if problem with network request/response status = -1 break except ValueError as e: # if invalid json data status = -1 break # Can only filter for tags at end, once all notes have been retrieved. #Below based on simplenote.vim, except we return deleted notes as well if (len(tags) > 0): note_list = [n for n in note_list if (len(set(n["tags"]).intersection(tags)) > 0)] if since is not None: note_list = [n for n in note_list if n['modificationDate'] > since] return note_list, status def trash_note(self, note_id): """ method to move a note to the trash Arguments: - note_id (string): key of the note to trash Returns: A tuple `(note, status)` - note (dict): the newly created note or an error message - status (int): 0 on sucesss and -1 otherwise """ # get note note, status = self.get_note(note_id) if (status == -1): return note, status # set deleted property note["deleted"] = True # update note return self.update_note(note) def delete_note(self, note_id): """ method to permanently delete a note Arguments: - note_id (string): key of the note to trash Returns: A tuple `(note, status)` - note (dict): an empty dict or an error message - status (int): 0 on sucesss and -1 otherwise """ # notes have to be trashed before deletion note, status = self.trash_note(note_id) if (status == -1): return note, status try: # self.api is obviously ok if self.trash_note worked self.api.note.delete(note_id) except ConnectionError as e: self.status = 'offline, connection error' return e, -1 except RequestException as e: return e, -1 return {}, 0