Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

How I integrated the Kibela API into my local note-taking routine

I’m Kay and joined Money Forward as a new graduate in December 2020. Currently, I am a member of the Group that improves the architecture of Money Forward Business Company's Accounting Department.

NotePlan. This is the App that my work life revolves around since I started using it. It is a markdown note editor which has macOS Calendar integration. In a sidebar, you can see the current month with its days and the current day, which has the same layout as the day-view of the macOS Calendar. Further, when you click on the days of the month, you have your daily notes that are neatly accessible via the calendar and other shortcuts. Besides that, NotePlan has its folder structure for notes on the left sidebar. Organizing notes has never been easier, which is why I now use it every day non-stop since I found out about it.

Retrieved from NotePlan

Now, in MoneyForward we use Kibela, a note-sharing platform, which is accessed via the browser. For me, someone who rather doesn’t use a web app if there is a native app, it is rather appalling to use it for simple tasks like notes. Since there is no Kibela App - I would use NotePlan anyway if there were, to be honest - but a neat and easy-to-use Kibela API, I tried to integrate it into NotePlan. Usually, I would write such tools in Python, but this time, since I use Ruby at work, I wanted to see how it would be using Ruby for a change.

Let’s first take a look at the Kibela API.

This API is GraphQL based and has somewhat thorough documentation. Also, Kibela provides a GraphiQL Web API where one can test API calls beforehand - which I used heavily before deciding to turn it into a script. Two functions are needed: Create and Update. It is pretty simple to create a note, just send the needed variables: title, content, groupIds (which groups can see the note), and the folder in which the note should be placed. This returns a unique ID for it with which it can be accessed.

As for updating a note, sadly the dream of having a simple easy-to-use API ends. Somehow it is not enough to just tell the API the ID of the note that should be updated with the new contents. But rather you need to input the note before the changes as well. I say that again. To update a note you have to input the note before the changes as well as after the changes of the update! - At first, I was baffled as to why this is necessary. But actually, there is merit in providing the update functionality this way. You can compare it to git push. With git push you also cannot just apply your current state to something if the commit history is different. (Otherwise, you would do a git push -f, which should be avoided)

This makes things a bit more complex, but still manageable. So we just need to pull the note we want to update before we apply the changes. This is easily done with the ID of the note. For the basic flow it is something like: 1. Find the ID of the note you want to update 2. Pull the note via Kibela’s API (with the ID) 3. Make an update request to Kibela’s API (with the note before the update and after it)

With the API calls out of the way, how about accessing the files from NotePlan?

In that regard, NotePlan is really accomodating. For each note you view, there is a dropout menu with the option to show the note within the Finder. Once I followed that I was pleased to see that it doesn’t do any fancy stuff but rather neatly preserves the folder structure that is shown in the NotePlan sidebar. The left sidebar is basically a finder window with the NotePlan folder as the root folder. Since we need to set a folder in Kibela, the intuitive thing to do is to preserve NotePlan’s folder structure and carry it over to Kibela.

But when do we Create or Update our notes?

Of course, there are multiple ways to tackle that problem. One could say: “I just want to create/update my notes once, at the end of my day”. Might be a fit for a lot of people, but what if you forget it one day? Then again? And then you start not using it at all. I am one of those people. So to not have this happening, I thought, how about as soon as I change something, the tool should create/update the edited note on Kibela. Sounds pretty useful. But how do we implement it? Luckily there is a Gem called “Listen”, which does exactly that. Listen. Listen to file changes within a specific directory. It tells you if a file is newly created, modified, or deleted. Once any of the 3 events happen, the callback is executed and it tells you the path to the files that have been either added, modified, or removed. And it does all of that on a separate thread. This is exactly what we need. So once a file is added, we just need to read that file and make our API call with that. Easy. If a file is modified, we pull the prior file state via API using its ID and make the update API call. Wait, how do we know the ID? We receive it once we create the note on Kibela, but how do we save it, and where? There needs to be a mapping between the files and the respective Kibela ID. Of course, what comes to mind is something like another file to save that mapping as it needs to be persistent. Or a database, which is way too sophisticated for such a small tool. Also, if we would use something like mapping, there is another problem. What to do if a filename changes? The mapping is lost and we might duplicate the file on Kibela. It gets a bit sketchy but hang on, it’s useful.

We use macOS’ File Comment functionality!

In macOS when you right-click on any file, there is an option called “Get Info”. This opens another menu where all the information about the file is listed, name, metadata, size, access permissions, path, and a field called “Comments” which is an editable text field. The comments of a file are persistent! (Even if the file name changes) Also, if we use the comments section of the files, we don’t need to worry about creating and maintaining a fragile mapping. No database, just labeling. Sounds good. But how do we set a MacOS comment programmatically?

I like to promote the not well-known but useful AppleScript anywhere I can and I do so here as well. With AppleScript, you can do anything you can do in macOS with commands. Automating anything. For example, automatically connecting a Bluetooth device, without going to the Bluetooth menu and selecting it every time - a script I still use today after many years. In our case, we can use it to set and read the comments of a file. Also, AppleScript is very readable. Setting the comment for a file can be done like this:

tell application "Finder" to set comment of (POSIX file "path to file” as alias) to "comment text” as Unicode text

That’s it, a single line. Reading is similarly easy:

tell application "Finder"
        set s to (POSIX file "path to file” as alias)
        set c to comment of s
        return c
end tell

Now we know how NotePlan is handling its notes, creating and updating notes on Kibela, and handling the IDs from Kibela.

One more problem that needs to be solved is how to execute AppleScript from a Ruby script?

AppleScript is tightly integrated into macOS, which makes it possible to run it directly in the terminal with “osascript”. AppleScript is not widely adopted by a lot of people, so searching for anything can be quite tedious, so this saves us a lot of thinking about how to execute it from Ruby. We use the following method to execute AppleScript directly within Ruby:

def osascript(script)
  system 'osascript', *script.split(/\n/).map { |line| ['-e', line] }.flatten
end

We can just feed this method our AppleScripts as a multi-line string and it gets executed. The Ruby method “system” comes in handy for that. But there is one problem with that. It does not return the output of its commands. So if we want to read the comment (Kibela ID) of a file, we can’t directly access it when calling that method. That’s quite a bummer. The saving grace, AppleScript can execute shell commands. A sketchy solution to our problem is to create a temporary file where we store the Kibela ID, and after the script is executed we read that file and get the ID into our Ruby script. Not particularly nice to it that way - and that comes with a whole different kind of problems like race conditions - but it’s working for us here.

With that Kibela’s API is integrated into the basic note-taking of NotePlan. I say basic because we didn’t consider the attachment of media yet.

How to handle Attachments?

Kibela can handle attachments like pictures or pdf documents directly into the note. NotePlan can do that as well.

NotePlan is creating a folder for each file that has attachments and saves them there. Within the note itself, the attachment is linked with the path. Kibela is doing something similar when it comes to attachments. There is a separate API call for attachments, which also returns an ID.

So to be able to also store attachments we need to scan the files for links to them. The attachments themselves can be handled like the notes regarding their ID. But we don’t need to update them. Since if the attachment changes, there will be a new file. Once uploaded we attach the ID to the file as we would with notes as well, and if we want to call them we check if there is an ID there already or not. If there is one, then it is already on Kibela so we can just relate to it.

However, we need to find out where the attachments are linked within the note. So before updating or creating a note we need to scan it and check if there are any attachments. If we find an attachment, the macro to it needs to be changed to the format Kibela uses with the relation to the ID present on Kibela.

One more thing.

What if you collaborate with others and they make changes on Kibela?

We need to be able to “pull” the current state into our local notes as well. Otherwise, we can only upload and once a note is changed on kibela and we make local changes, they would just be overwritten.

How can we get the current state of a note from Kibela into NotePlan? Since it is not easily possible to add buttons or functionality into NotePlan, I added “Magic-Keywords”. Once a file is edited, it is checked if the file contains the word “pull-kibela”. If this word is found, the comment of the file (ID on Kibela) is read and the note is pulled via the API. Then, the local file is replaced with the pulled contents. It is not a 100% solution to the problem, since you need to know first if you need to pull the note before editing. But for now, it should suffice.

With that, I have integrated the Kibela API into the local note-taking with NotePlan!

If you want to give it a try for yourself or modify the code to be usable with a note-taking app of your choice feel free to fork or checkout the repository: https://github.com/kwallaschek/noteplan_kibela_integration

Happy Hacking!


マネーフォワードでは、エンジニアを募集しています。 / Money Forward is looking for an engineer. ご応募お待ちしています。 / We look forward to hearing from you.

【会社情報】 ■Wantedly株式会社マネーフォワード福岡開発拠点京都開発拠点

【SNS】 ■マネーフォワード公式noteTwitter - 【公式】マネーフォワードTwitter - Money Forward Developersconnpass - マネーフォワードYouTube - Money Forward Developers