fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2010-08-23 10:25 am

[dw-free] trust groups for xml-rpc protocol

[commit: http://hg.dwscoalition.org/dw-free/rev/74a45a47496a]

http://bugs.dwscoalition.org/show_bug.cgi?id=2451

Added gettrustgroups, getcircle, editcircle to the protocol.

Patch by [personal profile] catness.

Files modified:
  • cgi-bin/ljprotocol.pl
  • t/protocol.t
--------------------------------------------------------------------------------
diff -r 2b2eefda745d -r 74a45a47496a cgi-bin/ljprotocol.pl
--- a/cgi-bin/ljprotocol.pl	Mon Aug 23 13:00:23 2010 +0800
+++ b/cgi-bin/ljprotocol.pl	Mon Aug 23 18:25:43 2010 +0800
@@ -179,7 +179,10 @@ sub do_request
 
     if ($method eq "login")            { return login(@args);            }
     if ($method eq "getfriendgroups")  { return getfriendgroups(@args);  }
+    if ($method eq "gettrustgroups")   { return gettrustgroups(@args);   }
     if ($method eq "getfriends")       { return getfriends(@args);       }
+    if ($method eq "getcircle")        { return getcircle(@args);        }
+    if ($method eq "editcircle")       { return editcircle(@args);       }
     if ($method eq "friendof")         { return friendof(@args);         }
     if ($method eq "checkfriends")     { return checkfriends(@args);     }
     if ($method eq "getdaycounts")     { return getdaycounts(@args);     }
@@ -651,17 +654,23 @@ sub login
     return $res;
 }
 
+#deprecated
 sub getfriendgroups
 {
-    my ($req, $err, $flags) = @_;
-    return undef unless authenticate($req, $err, $flags);
-    my $u = $flags->{'u'};
-    my $res = {};
-    $res->{'friendgroups'} = list_friendgroups($u);
-    return fail($err, 502, "Error loading friend groups") unless $res->{'friendgroups'};
-    if ($req->{'ver'} >= 1) {
-        foreach (@{$res->{'friendgroups'} || []}) {
-            LJ::text_out(\$_->{'name'});
+    return fail( $_[1], 504 );
+}
+
+sub gettrustgroups
+{
+    my ( $req, $err, $flags ) = @_;
+    return undef unless authenticate( $req, $err, $flags );
+    my $u = $flags->{u};
+    my $res = {};
+    $res->{trustgroups} = list_trustgroups( $u );
+    return fail( $err, 502, "Error loading trust groups" ) unless $res->{trustgroups};
+    if ( $req->{ver} >= 1 ) {
+        foreach ( @{$res->{trustgroups} || []} ) {
+            LJ::text_out( \$_->{name} );
         }
     }
     return $res;
@@ -714,6 +723,77 @@ sub getfriends
     });
     if ($req->{'ver'} >= 1) {
         foreach(@{$res->{'friends'}}) { LJ::text_out(\$_->{'fullname'}) };
+    }
+    return $res;
+}
+
+sub getcircle
+{
+    my ( $req,  $err,  $flags ) = @_;
+    return undef unless authenticate( $req,  $err,  $flags );
+    my $u = $flags->{u};
+    my $res = {};
+    my $limit = $LJ::MAX_WT_EDGES_LOAD;
+    $limit = $req->{limit} 
+      if defined $req->{limit} && $req->{limit} < $limit;
+
+    if ( $req->{includetrustgroups} ) {
+      $res->{trustgroups} = list_trustgroups( $u );
+      return fail( $err,  502,  "Error loading trust groups" ) unless $res->{trustgroups};
+      if ( $req->{ver} >= 1 ) {
+        LJ::text_out( \$_->{name} )
+            foreach ( @{$res->{trustgroups} || []} );
+      }
+    }
+    if ( $req->{includecontentfilters} ) {
+      $res->{contentfilters} = list_contentfilters( $u );
+      return fail( $err, 502, "Error loading content filters" ) unless $res->{contentfilters};
+      if ( $req->{ver} >= 1 ) {
+        LJ::text_out( \$_->{name} )
+            foreach ( @{$res->{contentfilters} || []} );
+      }
+    }
+    if ( $req->{includewatchedusers} ) {
+      $res->{watchedusers} = list_users( $u, 
+                                         limit => $limit,
+                                         watched => 1,
+                                         includebdays => $req->{includebdays},
+                                       );
+      if ( $req->{ver} >= 1 ) {
+        LJ::text_out( \$_->{fullname} )
+            foreach ( @{$res->{watchedusers} || []} );
+      }
+    }
+    if ( $req->{includewatchedby} ) {
+      $res->{watchedbys} = list_users( $u, 
+                                       limit => $limit,
+                                       watchedby => 1,
+                                     );
+      if ( $req->{ver} >= 1 ) {
+        LJ::text_out( \$_->{fullname} ) 
+            foreach ( @{$res->{watchedbys} || []} );
+      }
+    }
+    if ( $req->{includetrustedusers} ) {
+      $res->{trustedusers} = list_users( $u, 
+                                         limit => $limit,
+                                         trusted => 1,
+                                         includebdays => $req->{includebdays},
+                                       );
+      if ($req->{ver} >= 1) {
+        LJ::text_out(\$_->{fullname})
+            foreach (@{$res->{trustedusers} || []});
+      }
+    }
+    if ( $req->{includetrustedby} ) {
+      $res->{trustedbys} = list_users( $u, 
+                                       limit => $limit,
+                                       trustedby => 1,
+                                     );
+      if ( $req->{ver} >= 1 ) {
+        LJ::text_out( \$_->{fullname} )
+            foreach ( @{$res->{trustedbys} || []} );
+      }
     }
     return $res;
 }
@@ -2412,6 +2492,144 @@ sub editfriendgroups {
     return fail( $_[1], 504 );
 }
 
+sub editcircle 
+{
+    my ( $req, $err, $flags ) = @_;
+    return undef unless authenticate( $req, $err, $flags );
+  
+    my $u = $flags->{u};
+    my $res = {};
+
+    if ( ref $req->{settrustgroups} eq 'HASH' ) {
+      while ( my ( $bit, $group ) = each %{$req->{settrustgroups}} ) {
+        my $name = $group->{name};
+        my $sortorder = $group->{sort};
+        my $public = $group->{public};
+        my %params = ( id => $bit,
+                       groupname => $name,
+                       _force_create => 1
+                     );
+      
+        $params{sortorder} = $sortorder if defined $sortorder;
+        $params{is_public} = $public if defined $public;
+        $u->edit_trust_group( %params );
+      }
+    }
+  
+    if ( ref $req->{deletetrustgroups} eq 'ARRAY' ) {
+      foreach my $bit ( @{$req->{deletetrustgroups}} ) {
+        $u->delete_trust_group( id => $bit );
+      }
+    }
+
+    if ( ref $req->{setcontentfilters} eq 'HASH' ) {
+      while ( my ( $bit, $group ) = each %{$req->{setcontentfilters} } ) {
+        my $name = $group->{name};
+        my $public = $group->{public};
+        my $sortorder = $group->{sort};
+        my $cf = $u->content_filters( id => $bit );
+        if ( $cf ) {
+          $cf->name( $name )
+            if $name && $name ne $cf->name;
+          $cf->public( $public )
+            if ( defined $public ) && $public ne $cf->public;
+          $cf->sortorder( $sortorder )
+            if ( defined $sortorder ) && $sortorder ne $cf->sortorder;
+        } else {
+          my $fid = $u->create_content_filter( name => $name, public => $public, sortorder => $sortorder );
+          my $added = { 
+                       id => $fid,
+                       name => $name,
+                      };
+          push @{$res->{addedcontentfilters}}, $added;
+        }
+      }
+    }
+  
+    if ( ref $req->{deletecontentfilters} eq 'ARRAY' ) {
+      foreach my $bit ( @{$req->{deletecontentfilters}} ) {
+        $u->delete_content_filter( id => $bit );
+      }
+    }
+
+    if ( ref $req->{add} eq 'ARRAY' ) {
+      foreach my $row ( @{$req->{add}} ) {
+        my $other_user = LJ::load_user( $row->{username} );
+        return fail( $err, 203 ) unless $other_user; 
+        my $other_userid = $other_user->{userid};
+
+        if ( defined ( $row->{groupmask} ) ) {
+          $u->add_edge( $other_userid, trust => {
+                                                 mask => $row->{groupmask},
+                                                 nonotify => 1,
+                                                } );
+        } else {
+          if ( $row->{edge} & 1 ) {
+            $u->add_edge ( $other_userid, trust => { 
+                                                    nonotify => $u->trusts ( $other_userid ) ? 1 : 0,
+                                                   } );
+          } else {
+            $u->remove_edge ( $other_userid, trust => { 
+                                                       nonotify => $u->trusts ( $other_userid ) ? 0 : 1,
+                                                      } );         
+          }
+          if ( $row->{edge} & 2 ) {
+            my $fg = $row->{fgcolor} || "#000000";
+            my $bg = $row->{bgcolor} || "#FFFFFF";
+            $u->add_edge ( $other_userid, watch => {
+                                                    fgcolor => LJ::color_todb( $fg ),
+                                                    bgcolor => LJ::color_todb( $bg ),
+                                                    nonotify => $u->watches ( $other_userid ) ? 1 : 0,
+                                                   } );
+          } else {
+            $u->remove_edge ( $other_userid, watch => { 
+                                                       nonotify => $u->watches ( $other_userid ) ? 0 : 1,
+                                                      } );
+          }
+          if ( $row->{edge} ) {
+            my $myid = $u->userid;
+            my $added = { 
+                         username => $other_user->{user},
+                         fullname => $other_user->{name},
+                         trusted => $u->trusts ( $other_userid ), 
+                         trustedby => $other_user->trusts ( $myid ),
+                         watched => $u->watches ( $other_userid ),
+                         watchedby => $other_user->watches ( $myid )
+                        };
+            push @{$res->{added}}, $added;
+          }
+        }
+      }
+    }
+
+    # if ( ref $req->{delete} eq 'ARRAY' ) {
+    #   foreach my $row ( @{$req->{delete}} ) {
+    #     not implemented yet - maybe unnecessary
+    #   }
+    # }
+
+    if ( ref $req->{addtocontentfilters} eq 'ARRAY' ) {
+      foreach my $row ( @{$req->{addtocontentfilters}} ) {
+        my $other_user = LJ::load_user( $row->{username} );
+        return fail( $err, 203 ) unless $other_user; 
+        my $other_userid = $other_user->{userid};
+        my $cf = $u->content_filters( id => $row->{id} );
+        $cf->add_row( userid => $other_userid ) if $cf;
+      }
+    }
+
+    if ( ref $req->{deletefromcontentfilters} eq 'ARRAY' ) {
+      foreach my $row ( @{$req->{deletefromcontentfilters}} ) {
+        my $other_user = LJ::load_user( $row->{username} ); 
+        return fail( $err, 203 ) unless $other_user; 
+        my $other_userid = $other_user->{userid};
+        my $cf = $u->content_filters( id => $row->{id} );
+        $cf->delete_row( $other_userid ) if $cf;
+      }
+    }
+    return $res;
+}
+
 sub sessionexpire {
     my ($req, $err, $flags) = @_;
     return undef unless authenticate($req, $err, $flags);
@@ -2544,6 +2762,74 @@ sub list_friends
         last if @$res == $limitnum;
     }
     return $res;
+}
+
+sub list_users
+{
+    my ($u, %opts) = @_;
+
+    my %hide;
+    my $list = LJ::load_rel_user( $u, 'B' );
+    $hide{$_} = 1 foreach @{$list||[]};
+
+
+    my $friendof = $opts{trustedby} || $opts{watchedby};
+    my ( $filter,  @userids );
+    if ( $friendof ) {
+      @userids = $opts{trustedby} ? $u->trusted_by_userids : $u->watched_by_userids;
+    } else {
+      $filter = $opts{trusted} ? $u->trust_list : $u->watch_list;
+      @userids = keys %{$filter};
+    }
+  
+    my $limitnum = $opts{limit} + 0;
+    my @res;
+
+    my $us = LJ::load_userids( @userids );
+    while ( my( $userid, $u ) = each %$us ) {
+      next unless LJ::isu( $u );
+      next if $friendof && ! $u->is_visible;
+      next if $hide{$userid};
+    
+      my $r = {
+               username => $u->user,
+               fullname => $u->display_name
+              };
+
+      if ( $u->identity ) {
+        my $i = $u->identity;
+        $r->{identity_type} = $i->pretty_type;
+        $r->{identity_value} = $i->value;
+        $r->{identity_display} = $u->display_name;
+      }
+    
+      if ( $opts{includebdays} ) {
+        $r->{birthday} = $u->bday_string;
+      }
+    
+      unless ( $friendof ) {
+        $r->{fgcolor} = LJ::color_fromdb( $filter->{$userid}->{fgcolor} );
+        $r->{bgcolor} = LJ::color_fromdb( $filter->{$userid}->{bgcolor} );
+        $r->{groupmask} = $filter->{$userid}->{groupmask};
+      }
+    
+      $r->{type} = {
+                    C => 'community',
+                    Y => 'syndicated',
+                    I => 'identity',
+                   }->{$u->journaltype} unless $u->is_person;
+    
+      $r->{status} = {
+                      D => 'deleted',
+                      S => 'suspended',
+                      X => 'purged',
+                     }->{$u->statusvis} unless $u->is_visible;
+    
+      push @res, $r;
+      # won't happen for zero limit (which means no limit)
+      last if scalar @res == $limitnum;
+    }
+    return \@res;
 }
 
 sub syncitems
@@ -2686,13 +2972,13 @@ sub list_friendgroups
 
 #    warn "ljprotocol.pl: list_friendgroups called.\n";
     return [];
-
-# TODO(mark): this needs updating to determine if we should send trust groups?
-#             answer is yes, but we also need to move this to list_trustgroups
-#             so clients don't think those are friend groups.
-
-    # get the groups for this user, return undef if error
-    my $groups = LJ::get_friend_group($u);
+}
+
+sub list_trustgroups
+{
+    my $u = shift;
+
+    my $groups = $u->trust_groups;
     return undef unless $groups;
 
     # we got all of the groups, so put them into an arrayref sorted by the
@@ -2705,6 +2991,23 @@ sub list_friendgroups
               values %$groups;
 
     return \@res;
+}
+
+sub list_contentfilters
+{
+    my $u = shift;
+    my @filters = $u->content_filters;
+    return [] unless @filters;
+
+    my @res = map { { id => $_->{id},         name => $_->{name},
+                      public => $_->{public}, sortorder => $_->{sortorder}, 
+                      data => join ( ' ', 
+                                    map { my $uid = $_; 
+                                          LJ::load_userid( $uid )->user } 
+                                    ( keys %{$u->content_filters (id => $_->id )->data} ) ) } }
+       @filters;          
+    
+    return \@res;   
 }
 
 sub list_usejournals {
@@ -2980,6 +3283,9 @@ sub do_request
     if ($req->{'mode'} eq "getfriendgroups") {
         return getfriendgroups($req, $res, $flags);
     }
+    if ($req->{'mode'} eq "gettrustgroups") {
+        return gettrustgroups($req, $res, $flags);
+    }
     if ($req->{'mode'} eq "getfriends") {
         return getfriends($req, $res, $flags);
     }
@@ -3155,6 +3461,26 @@ sub getfriendgroups
     }
     $res->{'success'} = "OK";
     populate_friend_groups($res, $rs->{'friendgroups'});
+
+    return 1;
+}
+
+## flat wrapper
+sub gettrustgroups
+{
+    my ($req, $res, $flags) = @_;
+
+    my $err = 0;
+    my $rq = upgrade_request($req);
+
+    my $rs = LJ::Protocol::do_request('gettrustgroups', $rq, \$err, $flags);
+    unless ($rs) {
+        $res->{success} = "FAIL";
+        $res->{errmsg} = LJ::Protocol::error_message($err);
+        return 0;
+    }
+    $res->{success} = "OK";
+    populate_groups($res, 'tr', $rs->{trustgroups});
 
     return 1;
 }
@@ -3675,6 +4001,22 @@ sub populate_friend_groups
     $res->{'frgrp_maxnum'} = $maxnum;
 }
 
+## given a $res hashref and trust group (arrayref), flattens it
+sub populate_groups
+{
+    my ($res, $pfx, $fr) = @_;
+
+    my $maxnum = 0;
+    foreach my $fg ( @$fr ) {
+        my $num = $fg->{id};
+        $res->{"${pfx}_${num}_name"} = $fg->{name};
+        $res->{"${pfx}_${num}_sortorder"} = $fg->{sortorder};
+        $res->{"${pfx}_${num}_public"} = 1 if $fg->{public};
+        $maxnum = $num if ($num > $maxnum);
+    }
+    $res->{"${pfx}_maxnum"} = $maxnum;
+}
+
 ## given a menu tree, flattens it into $res hashref
 sub populate_web_menu
 {
diff -r 2b2eefda745d -r 74a45a47496a t/protocol.t
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/t/protocol.t	Mon Aug 23 18:25:43 2010 +0800
@@ -0,0 +1,475 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use Test::More;
+plan tests => 191;
+
+use lib "$ENV{LJHOME}/cgi-bin";
+require 'ljlib.pl';
+require 'ljprotocol.pl';
+
+use LJ::Test qw( temp_user temp_comm );
+
+my $u = temp_user();
+my $watched = temp_user();
+my $trusted = temp_user();
+my $watchedtrusted = temp_user();
+my $comm = temp_comm();
+
+my $watcher = temp_user();
+my $truster = temp_user();
+my $watchertruster = temp_user();
+
+my @watched = ( $watched, $watchedtrusted, $comm );
+my @trusted = ( $trusted, $watchedtrusted );
+my @watchedby = ( $watcher, $watchertruster );
+my @trustedby = ( $truster, $watchertruster );
+
+$u->add_edge( $_, watch => { nonotify => 1 } ) foreach @watched;
+$u->add_edge( $_, trust => { nonotify => 1 } ) foreach @trusted;
+$_->add_edge( $u, watch => { nonotify => 1 } ) foreach @watchedby;
+$_->add_edge( $u, trust => { nonotify => 1 } ) foreach @trustedby;
+
+my $err = 0;
+my $res = {};
+
+my $do_request = sub {
+    my ( $mode, %request_args ) = @_;
+
+    my $err = 0;
+    my $req = \%request_args;
+
+    my $res = LJ::Protocol::do_request( $mode, $req, \$err, { noauth => 1 } );
+
+    return ( $res, $err );
+};
+
+
+my $check_err = sub {
+    my ( $responsecode, $expectedcode, $testmsg ) = @_;
+
+    is( $responsecode, $expectedcode,
+        "$testmsg Protocol error ($err) = " . LJ::Protocol::error_message( $err ) );
+};
+
+my $success = sub {
+    my ( $responsecode, $testmsg ) = @_;
+
+    is( $responsecode, 0, "$testmsg (success)" );
+};
+
+note( "getfriendgroups" );
+{
+    ( $res, $err ) = $do_request->( "getfriendgroups" );
+    $check_err->( $err, 504, "'getfriendgroups' is deprecated." );
+    is( $res, undef, "No response expected." );
+
+
+    ( $res, $err ) = $do_request->( "getfriendgroups", username => $u->user );
+    $check_err->( $err, 504, "'getfriendgroups' is deprecated." );
+    is( $res, undef, "No response expected." );
+}
+
+note( "gettrustgroups" );
+{
+    # test arguments:
+    #   username
+
+    ( $res, $err ) = $do_request->( "gettrustgroups" );
+    $check_err->( $err, 200, "'gettrustgroups' needs a user." );
+    is( $res, undef, "No response expected." );
+
+
+    ( $res, $err ) = $do_request->( "gettrustgroups", username => $u->user );
+    $success->( $err, "'gettrustgroups' for user." );
+    ok( ref $res->{trustgroups} eq "ARRAY" && scalar @{$res->{trustgroups}} == 0, 
+        "Empty trust groups list." );
+};
+
+note( "getcircle" );
+{
+    # test arguments:
+    #   username
+    #   limit
+    #   includetrustgroups
+    #   includecontentfilters
+    #   includewatchedusers
+    #   includewatchedby
+    #   includetrustedusers
+
+    ( $res, $err ) = $do_request->( "getcircle" );
+     $check_err->( $err, 200, "'getcircle' needs a user." );
+    is( $res, undef, "No response expected." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user );
+    $success->( $err, "'getcircle' for user." );
+    is( scalar keys %$res, 0, "Empty circle; no arguments provided to select the subset of the circle." );
+
+    my %circle_args = (
+        # request hash key  => response hash key, users
+        includewatchedby    => [ "watchedbys", \@watchedby ],
+        includetrustedby    => [ "trustedbys", \@trustedby ],
+        includewatchedusers => [ "watchedusers", \@watched ],
+        includetrustedusers => [ "trustedusers", \@trusted ],
+    );
+    while( my ( $include, $val ) = each %circle_args ) {
+        ( $res, $err ) = $do_request->( "getcircle", username => $u->user, $include => 1 );
+        $success->( $err, "'getcircle' => $include" );
+        is( scalar keys %$res, 1, "One key: " . (keys %$res)[0] );
+        is( ref $res->{$val->[0]}, "ARRAY", "Returned an arrayref of this user's $include." );
+
+        my @cached_users = @{$val->[1]};
+        my @response_users = @{$res->{$val->[0]}};
+        is ( scalar @response_users, scalar @cached_users, "Matched the number of users who are watching/trusting." );
+
+        # check both ways that the users we added are the users we got back
+        my %cached_users = map { $_->user => 0 } @cached_users;
+        foreach my $user ( @response_users ) {
+            ok( ++$cached_users{$user->{username}}, "User from response is expected to be there." );
+        }
+        ok( $cached_users{$_}, "User appeared in the response." ) foreach keys %cached_users;
+    }
+
+    # set a limit, and check against that
+    my $old_limit = $LJ::MAX_WT_EDGES_LOAD;
+    $LJ::MAX_WT_EDGES_LOAD = 2;
+    while( my ( $include, $val ) = each %circle_args ) {
+        my @cached_users = @{$val->[1]};
+        my $limit = scalar @cached_users > $LJ::MAX_WT_EDGES_LOAD
+            ? $LJ::MAX_WT_EDGES_LOAD
+            : scalar @cached_users;
+
+        ( $res, $err ) = $do_request->( "getcircle", username => $u->user, $include => 1,
+            limit => $LJ::MAX_WT_EDGES_LOAD + 1 );
+        $success->( $err, "'getcircle' => $include" );
+
+        # check that the users we got back are from the users we added
+        # don't check the other way, since we are over limit
+        my @response_users = @{$res->{$val->[0]}};
+        is ( scalar @response_users, $limit, "Check that the number of users who can be fetched is limited." );
+        my %cached_users = map { $_->user => 0 } @cached_users;
+        foreach my $user ( @response_users ) {
+            ok( ++$cached_users{$user->{username}}, "User from response is expected to be there." );
+        }
+    }
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includetrustgroups => 1 );
+    $success->( $err, "'getcircle' => includetrustgroups" );
+    is( scalar keys %$res, 1, "One key: " . (keys %$res)[0] );
+    ok( ref $res->{trustgroups} eq "ARRAY" && scalar @{$res->{trustgroups}} == 0, 
+        "Empty trust groups list." );
+
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    $success->( $err, "'getcircle' => includecontentfilters" );
+    is( scalar keys %$res, 1, "One key: " . (keys %$res)[0] );
+    ok( ref $res->{contentfilters} eq "ARRAY" && scalar @{$res->{contentfilters}} == 0, "Empty list of content filters for this user." );
+
+    $LJ::MAX_WT_EDGES_LOAD = $old_limit;
+}
+
+note( "editcircle" );
+{
+    # test arguments:
+    #     settrustgroups
+    #     deletetrustgroups
+    #     setcontentfilters
+    #     deletecontentfilters
+    #     add
+    #     addtocontentfilters
+    #     deletefromcontentfilters
+
+    ( $res, $err ) = $do_request->( "editcircle", settrustgroups => 1 );
+    $check_err->( $err, 200, "'editcircle' needs a user." );
+    is( $res, undef, "No response expected." );
+
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, settrustgroups => 1 );
+    $success->( $err, "No valid action provided for editcircle; ignore." );
+    is( scalar keys %$res, 0, "No action taken." );
+
+
+    my %trustgroups = (
+        1 => {
+            name => "first",
+            sort => 1,
+            public => 0
+        },
+        5 => {
+            name => "incomplete"
+        },
+        10 => {
+            # no name?
+        }
+    );
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, settrustgroups => \%trustgroups );
+    $success->( $err, "Set trust groups." );
+    is( scalar keys %$res, 0, "No response expected." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includetrustgroups => 1 );
+    is( scalar @{$res->{trustgroups}}, scalar keys %trustgroups, "Number of trust groups match." );
+    foreach my $trustgroup ( @{$res->{trustgroups}} ) {
+        my $id = $trustgroup->{id};
+        my $orig_trustgroup = $trustgroups{$id};
+
+        is( $trustgroup->{name}, $orig_trustgroup->{name} || "", "Trustgroup name matches." );
+        is( $trustgroup->{public}, $orig_trustgroup->{public} || 0, "Trustgroup public setting matches." );
+        is( $trustgroup->{sortorder}, $orig_trustgroup->{sort} || 50, "Trustgroup sortorder matches." );
+    }
+
+
+    # then edit one trust group, and add another
+    my $edited = {
+        name => "hasname",
+        sortorder => 20,
+        public => 1,
+    };
+    $trustgroups{10} = $edited;
+
+    my $new = {
+        name => "new",
+    };
+    $trustgroups{20} = $new;
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, settrustgroups => { 10 => $edited, 20 => $new } );
+    $success->( $err, "Edited trust groups; those not mentioned should not be affected." );
+    is( scalar keys %$res, 0, "No response expected." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includetrustgroups => 1 );
+    is( scalar @{$res->{trustgroups}}, scalar keys %trustgroups, "Number of trust groups match." );
+    foreach my $trustgroup ( @{$res->{trustgroups}} ) {
+        my $id = $trustgroup->{id};
+        my $orig_trustgroup = $trustgroups{$id};
+
+        is( $trustgroup->{name}, $orig_trustgroup->{name} || "", "Trustgroup name matches." );
+        is( $trustgroup->{public}, $orig_trustgroup->{public} || 0, "Trustgroup public setting matches." );
+        is( $trustgroup->{sortorder}, $orig_trustgroup->{sort} || 50, "Trustgroup sortorder matches." );
+    }
+
+
+    # then delete some trust groups
+    delete $trustgroups{5};
+    delete $trustgroups{10};
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, deletetrustgroups => [ 10, 5 ] );
+    $success->( $err, "Deleted a trust group." );
+    is( scalar keys %$res, 0, "No response expected." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includetrustgroups => 1 );
+    is( scalar @{$res->{trustgroups}}, scalar keys %trustgroups, "Number of trust groups match." );
+    foreach my $trustgroup ( @{$res->{trustgroups}} ) {
+        my $id = $trustgroup->{id};
+        my $orig_trustgroup = $trustgroups{$id};
+
+        is( $trustgroup->{name}, $orig_trustgroup->{name} || "", "Trustgroup name matches." );
+        is( $trustgroup->{public}, $orig_trustgroup->{public} || 0, "Trustgroup public setting matches." );
+        is( $trustgroup->{sortorder}, $orig_trustgroup->{sort} || 50, "Trustgroup sortorder matches." );
+    }
+
+
+    # now add / edit some users' status in your circle
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, add => [ { username => "invalidusername" } ] );
+    $check_err->( $err, 203, "Tried to edit invalid user." );
+    is( scalar keys %$res, 0, "No response expected." );
+
+    # let's make our watch/trust mutual
+    # ... but not yet
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, add => [ { username => $watchertruster->user } ] );
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includewatchedusers => 1, includetrustedusers => 1 );
+
+    is( scalar @{$res->{watchedusers}}, scalar @watched, "Number of watched users did not change." );
+    is( scalar @{$res->{trustedusers}}, scalar @trusted, "Number of trusted users did not change." );
+
+    # add with trust group
+    is( $u->trustmask( $watchertruster ), 0, "Currently not trusted." );
+    ok( ! $u->trusts( $watchertruster ), "Currently not trusted." );
+    ok( ! $u->watches( $watchertruster ), "Currently not watched." );
+    ok( $watchertruster->trusts( $u ), "Trusted by." );
+    ok( $watchertruster->watches( $u ), "Watched by." );
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, add => [ { username => $watchertruster->user, groupmask => 201 } ] );
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includewatchedusers => 1, includetrustedusers => 1 );
+    is( scalar @{$res->{watchedusers}}, scalar @watched, "Number of watched users did not change." );
+    is( scalar @{$res->{trustedusers}}, scalar @trusted + 1, "Trusted this user." );
+    is( $u->trustmask( $watchertruster ), 201, "Trusted, with trust group that matches." );
+    ok( $u->trusts( $watchertruster ), "Currently trusted." );
+    ok( ! $u->watches( $watchertruster ), "Currently not watched." );
+    ok( $watchertruster->trusts( $u ), "Trusted by." );
+    ok( $watchertruster->watches( $u ), "Watched by." );
+
+
+    # add and remove
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, add => [ { username => $watchertruster->user, edge => 0b00 } ] );
+    is( $u->trustmask( $watchertruster ), 0, "Not trusted." );
+    ok( ! $u->trusts( $watchertruster ), "Currently not trusted." );
+    ok( ! $u->watches( $watchertruster ), "Currently not watched." );
+    ok( $watchertruster->trusts( $u ), "Trusted by." );
+    ok( $watchertruster->watches( $u ), "Watched by." );
+
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, add => [ { username => $watchertruster->user, edge => 0b01 } ] );
+    is( $u->trustmask( $watchertruster ), 1, "Just trust." );
+    ok( $u->trusts( $watchertruster ), "Currently trusted." );
+    ok( ! $u->watches( $watchertruster ), "Currently not watched." );
+    ok( $watchertruster->trusts( $u ), "Trusted by." );
+    ok( $watchertruster->watches( $u ), "Watched by." );
+
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, add => [ { username => $watchertruster->user, edge => 0b10 } ] );
+    is( $u->trustmask( $watchertruster ), 0, "Not trusted." );
+    ok( ! $u->trusts( $watchertruster ), "Currently not trusted." );
+    ok( $u->watches( $watchertruster ), "Currently watched." );
+    ok( $watchertruster->trusts( $u ), "Trusted by." );
+    ok( $watchertruster->watches( $u ), "Watched by." );
+
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, add => [ { username => $watchertruster->user, edge => 0b11 } ] );
+    is( $u->trustmask( $watchertruster ), 1, "Just trust." );
+    ok( $u->trusts( $watchertruster ), "Currently trusted." );
+    ok( $u->watches( $watchertruster ), "Currently watched." );
+    ok( $watchertruster->trusts( $u ), "Trusted by." );
+    ok( $watchertruster->watches( $u ), "Watched by." );
+
+
+    # edit again with groupmask, after already having created a trust mask
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, add => [ { username => $watchertruster->user, groupmask => 201 } ] );
+    is( $u->trustmask( $watchertruster ), 201, "Trusted, with trust group that matches." );
+    ok( $u->trusts( $watchertruster ), "Currently trusted." );
+    ok( $u->watches( $watchertruster ), "Currently watched." );
+    ok( $watchertruster->trusts( $u ), "Trusted by." );
+    ok( $watchertruster->watches( $u ), "Watched by." );
+
+
+    # now to 
+    my %contentfilters = (
+        1 => {
+            name => "first",
+            sort => 1,
+            public => 0
+        },
+        2 => {
+            name => "incomplete"
+        },
+    );
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, setcontentfilters => \%contentfilters );
+    $success->( $err, "Set content filters." );
+    is( scalar keys %$res, 1, "Response contains only the newly added content filters." );
+    is( scalar @{$res->{addedcontentfilters}}, scalar keys %contentfilters, "Got back the newly-added content filters." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    is( scalar @{$res->{contentfilters}}, scalar keys %contentfilters, "Number of content filters match." );
+    foreach my $filter ( @{$res->{contentfilters}} ) {
+        my $id = $filter->{id};
+        my $orig_filter = $contentfilters{$id};
+
+        is( $filter->{name}, $orig_filter->{name} || "", "Filter name matches." );
+        is( $filter->{public}, $orig_filter->{public} || 0, "Filter public setting matches." );
+        is( $filter->{sortorder}, $orig_filter->{sort} || 0, "Filter sortorder matches." );
+    }
+
+    # then edit one filter, and add another
+    $edited = {
+        name => "not so incomplete",
+        sortorder => 20,
+        public => 1,
+    };
+    $contentfilters{2} = $edited;
+
+    $new = {
+        name => "new",
+    };
+    $contentfilters{3} = $new;
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, setcontentfilters => { 2 => $edited, 3 => $new } );
+    $success->( $err, "Edited content filters; those not mentioned should not be affected." );
+    is( scalar keys %$res, 1, "Response contains only the newly added content filters." );
+    is( scalar @{$res->{addedcontentfilters}}, 1, "Got back the newly-added content filter." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    is( scalar @{$res->{contentfilters}}, scalar keys %contentfilters, "Number of content filters match." );
+    foreach my $filter ( @{$res->{contentfilters}} ) {
+        my $id = $filter->{id};
+        my $orig_filter = $contentfilters{$id};
+
+        is( $filter->{name}, $orig_filter->{name} || "", "Filter name matches." );
+        is( $filter->{public}, $orig_filter->{public} || 0, "Filter public setting matches." );
+        is( $filter->{sortorder}, $orig_filter->{sort} || 0, "Filter sortorder matches." );
+    }
+
+
+    # then delete some content filters
+    delete $contentfilters{1};
+    delete $contentfilters{3};
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, deletecontentfilters => [ 1, 3 ] );
+    $success->( $err, "Deleted content filters." );
+    is( scalar keys %$res, 0, "No response expected." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    is( scalar @{$res->{contentfilters}}, scalar keys %contentfilters, "Number of content filters match." );
+    foreach my $filter ( @{$res->{contentfilters}} ) {
+        my $id = $filter->{id};
+        my $orig_filter = $contentfilters{$id};
+
+        is( $filter->{name}, $orig_filter->{name} || "", "Filter name matches." );
+        is( $filter->{public}, $orig_filter->{public} || 0, "Filter public setting matches." );
+        is( $filter->{sortorder}, $orig_filter->{sort} || 0, "Filter sortorder matches." );
+    }
+
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    ok( $res->{contentfilters}->[0]->{data} eq "", "No data for the content filter." );
+
+
+    # added non-existent user
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, addtocontentfilters => [ {
+        username => "invalid_user",
+        id => 2
+    } ] );
+    $check_err->( $err, 203, "Tried to add an invalid user to a content filter." );
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    ok( $res->{contentfilters}->[0]->{data} eq "", "No data for the content filter." );
+
+
+    # added a valid user
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, addtocontentfilters => [ {
+        username => $watched->user,
+        id => 2
+    } ] );
+    $success->( $err, "Added a user to a content filter." );
+    is( scalar keys %$res, 0, "No response expected." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    ok( $res->{contentfilters}->[0]->{data} ne "", "Some data in the content filter." );
+
+
+    # tried to remove a non-existent user
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, deletefromcontentfilters => [ {
+        username => "invalid_user",
+        id => 2
+    } ] );
+    $check_err->( $err, 203, "Tried to remove an invalid user from a content filter." );
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    ok( $res->{contentfilters}->[0]->{data} ne "", "Some data in the content filter." );
+
+
+    # tried to remove a valid user, but one not in the content filter
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, deletefromcontentfilters => [ {
+        username => $watchedtrusted->user,
+        id => 2
+    } ] );
+    $success->( $err, "Tried to remove a user who was not in the content filter." );
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    ok( $res->{contentfilters}->[0]->{data} ne "", "Some data in the content filter." );
+
+
+    ( $res, $err ) = $do_request->( "editcircle", username => $u->user, deletefromcontentfilters => [ {
+        username => $watched->user,
+        id => 2
+    } ] );
+    $success->( $err, "Removed a user from a content filter." );
+    is( scalar keys %$res, 0, "No response expected." );
+
+    ( $res, $err ) = $do_request->( "getcircle", username => $u->user, includecontentfilters => 1 );
+    ok( $res->{contentfilters}->[0]->{data} eq "", "No more data in the content filter." );
+}
--------------------------------------------------------------------------------