Home About Eric Topics SourceGear

2013-03-15 12:00:00

Using Zumero from C#

I think it's safe to say that more RSS readers are being built this week than at any other point in history. I particularly enjoyed this image from Miguel de Icaza which showed up in my Twitter stream yesterday:

I hate being excluded from what all the cool kids are doing, so I'm going to build an RSS reader which (1) is designed for mobile devices, and (2) has offline support.

An RSS Reader built on SQLite/Zumero, Part 1

The basic design is to use one SQLite file to keep a list of all the feeds, plus one additional dbfile for each feed.

Here's the SQLite script to create the 'all_feeds' database:

.load zumero.dylib

.echo ON

BEGIN TRANSACTION;

CREATE VIRTUAL TABLE feeds USING zumero(
    feedid INTEGER PRIMARY KEY,
    url TEXT NOT NULL UNIQUE
    );

-- configure the permissions on this dbfile to allow 'anyone' to 
-- (1) pull the dbfile, and 
-- (2) add rows to the feeds table  
-- and nothing else.

SELECT zumero_define_acl_table('main');

INSERT INTO z_acl (scheme,who,tbl,op,result) VALUES (
    '',
    zumero_named_constant('acl_who_anyone'),
    '',
    '*',
    zumero_named_constant('acl_result_deny')
    );

INSERT INTO z_acl (scheme,who,tbl,op,result) VALUES (
    zumero_internal_auth_scheme('zumero_users_admin'),
    zumero_named_constant('acl_who_any_authenticated_user'),
    '',
    '*',
    zumero_named_constant('acl_result_allow')
    );

INSERT INTO z_acl (scheme,who,tbl,op,result) VALUES (
    '',
    zumero_named_constant('acl_who_anyone'),
    '',
    zumero_named_constant('acl_op_pull'),
    zumero_named_constant('acl_result_allow')
    );

INSERT INTO z_acl (scheme,who,tbl,op,result) VALUES (
    '',
    zumero_named_constant('acl_who_anyone'),
    'feeds',
    zumero_named_constant('acl_op_tbl_add_row'),
    zumero_named_constant('acl_result_allow')
    );

INSERT INTO feeds (url) VALUES ('http://feeds.hanselman.com/ScottHanselman');

COMMIT TRANSACTION;

SELECT zumero_sync(
    'main',
    'https://zinst393e9343b87.s.zumero.net',
    'all_feeds',
    zumero_internal_auth_scheme('zumero_users_admin'),
    'admin',
    'SECRETPASSWORD'
    );

Highlights:

To execute this script, I save it to a file called setup_dbfile_all_feeds.sql and then pipe it into the sqlite3 shell.

eric$ ./sqlite3 ./all_feeds.db < setup_dbfile_all_feeds.sql

Like I said, anyone can add feeds to this list. The server URL is actually the one shown in these examples. I'm going to open my local copy of "all_feeds" and add my own RSS feed URL to the list:

eric$ ./sqlite3 ./all_feeds.db 
SQLite version 3.7.11 2012-03-20 11:35:50
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .load zumero.dylib
sqlite> INSERT INTO feeds (url) VALUES ('https://ericsink.com/rss.xml');
sqlite> SELECT * FROM feeds;
1|http://feeds.hanselman.com/ScottHanselman
2|https://ericsink.com/rss.xml
sqlite> SELECT zumero_sync(
    'main',
    'https://zinst393e9343b87.s.zumero.net',
    'all_feeds'
    );
0;0;3584;0;448;0;1249;1264

Note that I didn't need to pass any authentication credentials to the zumero_sync() function.

So far, my RSS reader doesn't do anything. It's just a list of feeds. Granted, it's a really cool list, since it supports incremental sync, but still. I'm probably going to need my RSS reader to do something with, er, RSS. And for that, I'm going to need more than just SQL statements piped into the sqlite3 shell.

Here's my starting point, in C#:

using System;
using System.Collections.Generic;
using System.ServiceModel.Syndication;
using System.Xml;

using SQLite; // https://github.com/praeclarum/sqlite-net

namespace z
{
    class Program
    {
        // define a little class to represent rows of the feeds table

        public class feed_row
        {
            public string feedid { get; set; }
            public string url { get; set; }
        };

        public static void Main (string[] args)
        {
            // open the local SQLite db
            SQLiteConnection conn = new SQLiteConnection("all_feeds.db");

            // tell SQLite to allow load_extension()
            conn.EnableLoadExtension(1);

            // load the Zumero extension
            // this is the equivalent of 
            // ".load zumero.dylib" in the sqlite3 shell
            conn.ExecuteScalar("SELECT load_extension('zumero.dylib');");

            // iterate over all the rows in the feeds table
            var rows = conn.Query ("SELECT feedid, url FROM feeds;");
            foreach (feed_row q in rows)
            {
                Console.WriteLine(q.url);
                try
                {
                    XmlReader xr = new XmlTextReader(q.url);
                    SyndicationFeed f = SyndicationFeed.Load(xr);

                    // TODO store the items from this feed
                }
                catch (Exception e)
                {
                    // TODO failed trying to retrieve or parse this feed.
                    // TODO log the failure?
                    // TODO delete the feed row?
                    // TODO launch nethack?
                }
            }

            conn.Close();
        }
    }
}

In order to call the SQLite library from C#, I'm using SQLite.cs, by Frank Krueger. That wrapper doesn't have a way to call sqlite3_enable_load_extension(), so I added one. Here's the diff, which I plan to submit as a pull request:

eric$ diff orig_SQLite.cs SQLite.cs 
183a184,192
>         public void EnableLoadExtension(int onoff)
>         {
>             SQLite3.Result r = SQLite3.EnableLoadExtension(Handle, onoff);
>           if (r != SQLite3.Result.OK) {
>               string msg = SQLite3.GetErrmsg (Handle);
>               throw SQLiteException.New (r, msg);
>           }
>         }
> 
2645a2655,2659
>       [DllImport("sqlite3", 
>                 EntryPoint = "sqlite3_enable_load_extension", 
>                 CallingConvention=CallingConvention.Cdecl)]
>       public static extern Result EnableLoadExtension (IntPtr db, int onoff);
> 

I'm using System.ServiceModel.Syndication.SyndicationFeed to do the RSS parsing for me. I don't have any previous experience with that library, but so far, it just works.

The statement I used to compile this program is:

eric$ gmcs -reference:System.ServiceModel.Web.dll \
      -out:z.exe AssemblyInfo.cs SQLite.cs Main.cs
...
Compilation succeeded - 3 warning(s)

Since I'm running on Mac OS X (Lion), I had to bring my own SQLite to the party. On Mac OS, the sqlite3 library and shell are preinstalled with the OS, but they were compiled without support for dynamic extension loading. If you're on a Mac, it's easy to grab and build your own copies:

eric$ curl --silent --remote-name \
      http://www.sqlite.org/sqlite-amalgamation-3071502.zip

eric$ unzip sqlite-amalgamation-3071502.zip 
Archive:  sqlite-amalgamation-3071502.zip
   creating: sqlite-amalgamation-3071502/
  inflating: sqlite-amalgamation-3071502/sqlite3.h  
  inflating: sqlite-amalgamation-3071502/shell.c  
  inflating: sqlite-amalgamation-3071502/sqlite3ext.h  
  inflating: sqlite-amalgamation-3071502/sqlite3.c  

eric$ mv sqlite-amalgamation-3071502/* .

eric$ rmdir sqlite-amalgamation-3071502

eric$ clang -dynamiclib -arch i386 -arch x86_64 \
      -o libsqlite03071502.dylib sqlite3.c

eric$ clang -o sqlite3 -arch i386 -arch x86_64 sqlite3.c shell.c 

eric$ ./sqlite3 --version
3.7.15.2 2013-01-09 11:53:05 c0e09560d26f0a6456be9dd3447f5311eb4f238f

I also created a .config file because SQLite.cs does DllImport("sqlite3", and need to map that to the SQLite library we just built.

<configuration>
    <dllmap dll="sqlite3" target="libsqlite03071502.dylib" />
</configuration>

Running the program yields the expected output:

eric$ mono ./z.exe
https://ericsink.com/rss.xml
http://feeds.hanselman.com/ScottHanselman

So, after all that, this code STILL doesn't really do anything useful. It demonstrates that I can successfully use SQLite and Zumero from C#. And it retrieves and parses each feed. But I have not yet implemented any of the things that are supposed to happen next. Hopefully I'll make more progress in part 2.