Happy 2018! A mixture of things have kept me preoccupied until now, including,
but not limited to, a hand injury stopping me from typing properly, a holiday
period, and of course, my favourite pastime of all, yak shaving (of both
varieties). This post is largely borne from my efforts in yak shaving of a
certain problem, motivated by a desire to get more organized in 2018 -
specifically, the problem of having a synchronized list of things to do.
More specifically, the problem is thus: I use a lot of devices. I have a
desktop computer, a laptop, a microserver, a tablet and a smartphone, and on
top of that, I also have a computer assigned to me in my office at uni. For a
to-do list to be useful, I have to be able to:
- Have the list available no matter which device I have close to hand;
- Be able to modify and delete entries from all these places; and
- Ensure that the entries are kept consistent across all devices.
This, by itself, could be done using some kind of manual synchronization method
(such as Git), but that would be both too much work and (strangely) too easy.
Something as boring as keeping files synchronized across multiple locations is a
job for a computer, not a person. Additionally, I disdain 'cloud' 'services'
with a fiery passion, as they're all basically surveillance capitalism,
whether you pay for them or not, and the inability to know or modify what
they're running also offends my inner engineer and perfectionist. Lastly, I
would much prefer a replication-based approach, because relying on an
Internet connection at all times is asking for trouble. That the solution
needed to both be open source and work from a command line where possible
should go absolutely without saying.
It turned out that all of these criteria can be met, and the result brings
with it a considerable number of additional helpful things. However, setting
everything up is not simple; you have to do it in an oddly specific order, and
the documentation for each of the pieces of puzzle seems strangely unaware of
each of the others, requiring no small amount of guesswork. Thus, this post
is meant to be a guide for anyone who wants to do this all themselves, and
benefit from the (arguably extremely cool) outcomes of all this without
having to go through the same frustrations I did.
The solution I found
The core of this whole bag of tricks is CalDAV - an open standard for
remote storage and access of calendars and contacts. As it is an open standard,
there are a whole bunch of implementations of both servers and clients for it,
which work on pretty much every platform you can think of. The main thing for
me, though, was that it can also synchronize to-do lists, even thought that's
not its primary purpose. The fact that I also get contacts and calendar
synchronization on top of that is just gravy.
CalDAV relies on a client-server model, and so to use it, you need a server. The
one I went with was Radicale, as it's written in Python (so no need to run
a disgusting slurry), is open source, and will happily run on a smaller device.
It also has authentication capabilities on a per-user basis, and lets each user
have multiple contact lists, calendars and to-do lists, so if I ever want to
share the server with others in a secure way, I can. From the point of view of
client programs, I basically only need two: one for my Linux-based machines, and
one for my Android devices. For the former, I decided to go with todoman, as
it's a simple command-line manager of to-do lists, while for the latter, I went
with OpenTasks mostly by process of elimination. Lastly, both clients need a
separate synchronizer program, which for me ended up being vdirsyncer for
todoman and DAVDroid for OpenTasks.
What you will need
Obviously, this is not easy to set up if you haven't self-hosted before, and
you're going to need certain basics in place. In particular, the following are
necessary:
- A static IP address (either via your ISP, a VPS, or something else)
- A machine to act as a server (any of the cheaper ARM-based SBCs will do)
- A domain name
- A web server set up to serve to the domain name you own (I use Nginx)
- A Let's Encrypt certificate for the domain name you own, with appropriate
configuration for your web server and auto-renewal
Setting up all of this is outside the scope of this particular article, although
I may write something for each of these things one day. If you already have a
self-hosted blog with HTTPS, you're all set. I will use Nginx as the web server
throughout this writeup, but if you use something else, it shouldn't be vastly
different.
What to do
For reasons that completely elude me, each of these tasks must be done in
exactly the order I'm presenting them in. If you don't do them in this order,
you'll either end up having to repeat work, or, in the worst case, your stack
won't work and you won't know why. I discovered this order through a lot of
trial and error, so spare yourself the pain and just follow it.
1. Set up Radicale
Installing Radicale is basically the same as setting up any Python package. You
can use pip
if that's how you want to do things, or you can check if your
distro packages it. Fortunately for me, Radicale was packaged for Arch Linux (in
the AUR, of course), so this part of the setup was fairly painless.
We also want to make sure that we have authentication support for users. This is
a good policy even if you don't plan to share, as it means that random drive-by
connections to your server won't make it off with your contacts or calendar. To
do this, you'll need a tool called htpasswd
, which is packaged with the
Apache server. If you don't have it (and I didn't, because I use Nginx), the
easiest thing to do is just to download it, although you can do without.
Additionally, you want to make sure you have Python's passlib
and bcrypt
modules available, so go ahead and install those too. The AUR package for
Radicale even helpfully tells you that they're optional dependencies.
If you are using a distro package, this next step might be optional, depending
on how your package is configured. In order to ensure good privilege
containment, Radicale should run as its own user, with its own directory,
without being able to write to arbitrary places. The AUR package already does
this for you, but if you installed it using pip
or through a different
package manager, you may have to do this step yourself. We'll assume that
Radicale's home directory is /home/radicale
, and its user is named
radicale
for the rest of this writeup.
We need to create a users
file which Radicale has access to, which we'll put
in /home/radicale/users
. You can create as many users as you want, but
you'll need at least one. The command in my case was:
[koz@banana ~]$ sudo -u radicale htpasswd -B -c /home/radicale/users koz
You'll be prompted to create a password for this new user, then confirm it.
Repeat this as many times as desired.
Next, you need to configure Radicale itself. There's really only two things we
need to do - specify what port Radicale will listen on, and tell it that we'll
be using our brand-new users
file for authentication. This is quite
straightforward: edit /etc/radicale/config
, and ensure it contains these
lines uncommented:
[server]
hosts = 127.0.0.1:5232
[auth]
type = htpasswd
htpasswd_filename = /home/radicale/users
htpasswd_encyption = bcrypt
Now, all of this bears some explanation before we continue. Radicale normally
runs only locally; it'll be the job of our friendly web server to forward it
external requests, with HTTPS authentication thanks to our Let's Encrypt
certificate. We're using bcrypt for password hashing (hence the -B
flag to
htpasswd
and the setting in the config
file) because that is what you
should do no really. With this, Radicale will nicely manage our contacts,
calendars and (most importantly for us) to-do lists, with authentication so that
our data doesn't get stomped on by randos.
At this stage, it's a good idea to test-start Radicale's server. You can do it
by invoking sudo -u radicale python radicale -f --debug
, then checking
localhost at port 5232. If you get a login page, success! If not, check the log
spew you're getting in your terminal to deduce what's up.
Once you have everything working, start the daemon, either manually or using
whatever init system you favour. The AUR package provides a systemd service
file, which works well enough, but needs a few edits to match your directory,
and also to provide it the --debug
flag.
2. Set up your web server to serve Radicale
Running your Radicale locally isn't going to get you very far on its own, so now
we need to set up your web server properly. Once again, the examples given here
use Nginx, so if you use some other web server, adjust appropriately.
In your Nginx config, you'll have an entry for your HTTPS server. In that
server
block, you want to add something that looks like this:
location /radicale/ {
proxy-pass http://localhost:5232/;
proxy-set-header X-Script-Name /radicale;
proxy-set-header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy-pass-header Authorization;
}
This will allow you to access your Radicale server at
https://your-domain.coffee/radicale/
. Restart your web server and check that
you can using an ordinary browser. If all went well, you should be able to get a
login page. Try using the credentials you created previously to log in - you
should be able to do so, but there won't be much there to look at yet.
3. Download and set up OpenTasks
I would suggest using F-Droid for this. Simply install the app, run it, and
give it appropriate permissions. You won't have anything useful there right now,
and thus, this step seems a bit silly, but as you'll see, this is essential.
4. Download and set up DAVDroid
Again, F-Droid is recommended. Once you've installed DAVDroid, you'll need to
point it at your now-accessible Radicale instance. You'll need to select 'Login
with URL and user name', and enter the link you tested with above, followed by
your credentials as generated in the first step.
If successful, you'll land on a screen asking you to create a user. Now, you're
likely a bit confused at this point, because you're not Xzibit, and you've
already created a user, but this is different. In the context of Radicale,
'users' are authentication groups to the server; in the context of DAVDroid,
'users' are individual instances of collections of contacts, calendars and to-do
lists in the same authentication group. Why these two terms were given such
similar names will always be beyond me. Additionally, DAVDroid will not inform
you what 'users' (from the DAVDroid point of view) exist on the 'user' (from the
Radicale point of view) that you've just logged into. It suggests using your
email address for the username, and I strongly suggest doing the same, as it'll
allow you to keep things nice and consistent if you have multiple Android
devices.
After this, you'll be on a screen with a CardDAV section and a CalDAV section.
The former is where you can create and store contact lists, while the latter is
where you can create and store calendars and to-do lists (or combinations of
both if you choose). For myself, I created one of each, but for the purposes of
this write-up, it's sufficient to just create a to-do list with an appropriate
name. Ensure that you synchronize it to the server before you continue. You may
also want to tell DAVDroid to only sync manually, to avoid draining your battery
and needlessly using mobile data when not appropriate.
5. Set up OpenTasks, again
You're probably wondering at the ordering, but unfortunately, it's a known bug
that installing DAVDroid first will break OpenTasks. This fact is hard to find
(in fact, I'm not managing to find out where I found out), but unfortunately,
them's the breaks. Add it to the list of things which make Android weird, I
guess.
Here, you just have to re-open OpenTasks, and delete the local task list which
you no longer need. Task lists are OpenTask's terminology for to-do lists; to
remove the local list, go to 'Displayed lists' in the corner menu and then
follow on from there. After you're done, you should only have the synchronized
task list you created earlier, which is empty.
6. Set up vdirsyncer
You can install vdirsyncer using either pip
or your package manager as you
prefer (assuming your distro bundles it). After this, we need to set up its
configuration file, which normally resides in ~/.config/vdirsyncer/config
.
The file should look something like this:
[general]
status_path = "~/.vdirsyncer/status/
[pair]
conflict_resolution = "b wins"
a = "todo_local"
b = "todo_dav"
collections = ["from a", "from b"]
metadata = ["color", "displayname"]
[storage todo_local]
type = filesystem
path = "~/documents/todo"
fileext = ".ics"
[storage todo_dav]
type = "caldav"
url = "https://your-domain.coffee"
username = "Auser"
password = "this is a secret"
This bears some explanation too. The way vdirsyncer works is by using 'pairs' of
local and remote documents. In this case, the local document is this machine's
replica of your to-do list (which is going to be stored in
~/documents/todo
), and the remote document is the replica living on your
server machine, managed by Radicale. You can have as many of these as you like,
but in our case, this will do. We do this after setting everything up for
DAVDroid because it's the easiest way to do this.
You then need to run vdirsyncer discover pair
, followed by vdirsyncer
sync
. This will transfer a directory with a name that looks like a hash from
your Radicale instance, which will go into ~/documents/todo
. Note what this
is - I will use deadbeef
as the name for typing simplicity. From this point
onward, every time you make any changes to your todo list that you want
synchronized, just run vdirsyncer sync
and they'll be matched. You can
automate this if you want, but I didn't bother.
7. Set up todoman
Like vdirsyncer, todoman is a Python program, so install it as appropriate. We
need to set it up so that it sends your to-dos to the folder we've set
vdirsyncer to synchronize. For that to work, todoman's configuration file should
look like:
[main]
path = ~/documents/todo/*
date_format = %Y-%m-%d
time_format = %H:%M
default_list = deadbeef
default_due = 1
The other settings are mostly self-explanatory if you're familiar with Python's
format strings. default_due
is the default number of hours from the point of
a new to-do item being created that it will be due - adjust as appropriate to
your needs.
With that, run todo
from your command line, which shouldn't produce any
output (as our to-do list is currently empty). To create a new item, run todo
new
from the command line, and use the TUI provided. Create a test item, and
check that you can see it with todo
.
8. Profit!
You are now ready to check synchronization. Try running vdirsyncer sync
, and
then when it completes, try refreshing in DAVDroid, then checking OpenTasks. If
your task appears there successfully - well done, you are well on the way to
being more productive. If not, work backwards and check that you've completed
all the steps along the way. You can delete the sample task if you want - you
can do this using todo delete x
, where x
is the number that appears next
to the task when you run todo
.
How to find out more
This has been a pretty 'quick-and-dirty' rundown of how to set all this up.
Obviously, your needs might be very different, and even if they're not, you
might at least want to know what your options are. Here is the relevant
documentation:
For DAVDroid and OpenTasks, there's not much out there, but they're fairly
self-explanatory as long as you install them in the right order.
Deficiencies
Despite this scheme being rather brilliant, there are a few missing pieces:
- todoman is a CLI client. If GUIs are more your thing, you can check out your
options here.
- No current way to set up recurring tasks. todoman devs are working on it,
but I am unsure if OpenTasks devs are.
These are not deal-breakers for me, but you may disagree. As always, think hard,
have fun, and happy to-do listing.