line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
|
2
|
|
|
|
|
|
|
package WWW::Workflowy; |
3
|
|
|
|
|
|
|
|
4
|
3
|
|
|
3
|
|
138239
|
use strict; |
|
3
|
|
|
|
|
7
|
|
|
3
|
|
|
|
|
131
|
|
5
|
3
|
|
|
3
|
|
18
|
use warnings; |
|
3
|
|
|
|
|
7
|
|
|
3
|
|
|
|
|
93
|
|
6
|
|
|
|
|
|
|
|
7
|
3
|
|
|
3
|
|
3810
|
use LWP; |
|
3
|
|
|
|
|
209525
|
|
|
3
|
|
|
|
|
92
|
|
8
|
3
|
|
|
3
|
|
28
|
use LWP::UserAgent; |
|
3
|
|
|
|
|
6
|
|
|
3
|
|
|
|
|
52
|
|
9
|
|
|
|
|
|
|
|
10
|
3
|
|
|
3
|
|
980
|
use Data::Dumper; |
|
3
|
|
|
|
|
8080
|
|
|
3
|
|
|
|
|
211
|
|
11
|
3
|
|
|
3
|
|
2612
|
use JSON::PP; |
|
3
|
|
|
|
|
29569
|
|
|
3
|
|
|
|
|
264
|
|
12
|
3
|
|
|
3
|
|
2919
|
use POSIX 'floor'; |
|
3
|
|
|
|
|
23054
|
|
|
3
|
|
|
|
|
24
|
|
13
|
3
|
|
|
3
|
|
3240
|
use Carp; |
|
3
|
|
|
|
|
7
|
|
|
3
|
|
|
|
|
346
|
|
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
our $VERSION = '0.6'; |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
# XXX need a public get_parent( $node ), and other traversal stuff. we have a _find_parent() (which uses the recursive find logic). |
18
|
|
|
|
|
|
|
# notes in /home/scott/projects/perl/workflowy_notes.txt |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
# use autobox::Closure::Attributes; # XXX hacked up local copy that permits lvalue assigns |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
=head1 NAME |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
WWW::Workflowy - Faked up API interface to the workflowy.com collaborative outlining webapp |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
=head1 SYNOPSIS |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
B |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
use WWW::Workflowy; |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
my $wf = WWW::Workflowy->new( |
33
|
|
|
|
|
|
|
# username => '', # optional: login credentials for private workflowies |
34
|
|
|
|
|
|
|
# password => '', |
35
|
|
|
|
|
|
|
url => 'https://workflowy.com/shared/b141ebc1-4c8d-b31a-e3e8-b9c6c633ca25/', |
36
|
|
|
|
|
|
|
# or instead of url: guid => 'b141ebc1-4c8d-b31a-e3e8-b9c6c633ca25', |
37
|
|
|
|
|
|
|
); |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
$node = $wf->dump; |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
$node = $wf->find( |
42
|
|
|
|
|
|
|
sub { |
43
|
|
|
|
|
|
|
my $node = shift; |
44
|
|
|
|
|
|
|
my @parent_nodes = @{ shift() }; |
45
|
|
|
|
|
|
|
return 1 if $node->{nm} eq 'The node you are looking for'; |
46
|
|
|
|
|
|
|
return 1 if $node->{id} eq 'Jxn637Zp-uA5O-Anw2-A4kq-4zqKx7WuJNBN'; |
47
|
|
|
|
|
|
|
}, |
48
|
|
|
|
|
|
|
); |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
$node_id = $wf->create( |
51
|
|
|
|
|
|
|
parent_id => 'Jxn637Zp-uA5O-Anw2-A4kq-4zqKx7WuJNBN', |
52
|
|
|
|
|
|
|
priority => 3, # which position in the list of items under the parent to insert this node |
53
|
|
|
|
|
|
|
text => "Don't forget to shave the yak", |
54
|
|
|
|
|
|
|
); |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
$node = $wf->edit( |
58
|
|
|
|
|
|
|
save_id => 'Jxn637Zp-uA5O-Anw2-A4kq-4zqKx7WuJNBN', |
59
|
|
|
|
|
|
|
text => "Think of an idea for a color for the bikeshed", |
60
|
|
|
|
|
|
|
); |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
$wf->delete( $node->{id} ); |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
sleep $wf->polling_interval; $wf->sync; |
65
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
$wf->fetch; |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
=head1 DESCRIPTION |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
All methods C on error. Trap errors with L, C, or similar to attempt to recover from them. |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
Each node has this structure: |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
{ |
75
|
|
|
|
|
|
|
'lm' => 1270, # time delta last modified; usually not too interesting |
76
|
|
|
|
|
|
|
'nm' => 'Test 2.1', # text |
77
|
|
|
|
|
|
|
'id' => '63c98305-cd96-2016-4c4f-a20f7384ad9c' # id |
78
|
|
|
|
|
|
|
} |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
It may also have a C<'ch'> containing an arrayref of additional nodes. |
81
|
|
|
|
|
|
|
To make things interesting, the root node does not have a C<'ch'> of nodes under it. |
82
|
|
|
|
|
|
|
Use the C method to avoid dealing with this special case. |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
The value from the C field is used as the value for C, C, or C |
85
|
|
|
|
|
|
|
in other calls. |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
=head2 new |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
Takes an optional C and C to access Workflowies that aren't shared. |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
Takes C resembling C or a L C such as C. |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
May also be initialized from a serialized copy of a previous C<$wf->outline>. See C for an example of that. |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
Returns a coderef. |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
=head2 dump |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
Produces an ASCII representation of the outline tree. |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
=head2 find |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
Recurses through the entire outline tree, calling the callback for each item. The callback is passed the node currently being examined and an |
104
|
|
|
|
|
|
|
arrayref of parents, top most parent first. |
105
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
=head2 edit |
107
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
Changes the text of a node. |
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
=head2 create |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
Created a new node. |
113
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
=head2 delete |
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
Deletes a node. |
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
=head2 move |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
No class method yet. The thing handles C commands sent down by the L server (when data was moved by another L client) but doesn't |
121
|
|
|
|
|
|
|
yet let you send that command to the server. |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
=head2 sync |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
C fetches changes other people have made to the current L outline and attempts to merge them into the local outline. |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
C, C, and C minipulate data locally but only queue it to later be sent to the L server. |
128
|
|
|
|
|
|
|
Executing a C causes pending operations to be sent. |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
B C returns B and does nothing if C<< $wf->polling_interval >> seconds have not yet passed |
131
|
|
|
|
|
|
|
since the last request to the F server. Calling C generally results in a request to the F server. |
132
|
|
|
|
|
|
|
To avoid C returning C and doing nothing, use this idiom: |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
sleep $wf->polling_interval; |
135
|
|
|
|
|
|
|
$wf->sync; |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
C<< $wf->last_poll_time >> contains a timestamp of the time that the last request was made. |
138
|
|
|
|
|
|
|
The value for C<< $wf->polling_interval >> may change in response to a request to the server. |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
=head2 fetch |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
Fetches the latest copy of the outline from the L server, blowing away any local changes made to it that haven't yet been pushed up. |
143
|
|
|
|
|
|
|
This happens automatically on C. |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
=head2 get_children |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
Takes a node id. Returns an arrayref of a node's children if it has children, or false otherwise. |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
=cut |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
package autobox::Closure::XAttributes::Methods; |
152
|
|
|
|
|
|
|
|
153
|
3
|
|
|
3
|
|
15
|
use base 'autobox'; |
|
3
|
|
|
|
|
6
|
|
|
3
|
|
|
|
|
2525
|
|
154
|
3
|
|
|
3
|
|
16478
|
use B; |
|
3
|
|
|
|
|
7
|
|
|
3
|
|
|
|
|
129
|
|
155
|
3
|
|
|
3
|
|
2163
|
use PadWalker; |
|
3
|
|
|
|
|
3674
|
|
|
3
|
|
|
|
|
960
|
|
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
sub AUTOLOAD :lvalue { |
158
|
27
|
|
|
27
|
|
21048
|
my $code = shift; |
159
|
27
|
|
|
|
|
269
|
(my $method = our $AUTOLOAD) =~ s/.*:://; |
160
|
27
|
50
|
|
|
|
97
|
return if $method eq 'DESTROY'; |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
# we want the scalar unless the method name already a sigil |
163
|
27
|
50
|
|
|
|
94
|
my $attr = $method =~ /^[\$\@\%\&\*]/ ? $method : '$' . $method; |
164
|
|
|
|
|
|
|
|
165
|
27
|
|
|
|
|
229
|
my $closed_over = PadWalker::closed_over($code); |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
# is there a method of that name in the package the coderef was created in? |
168
|
|
|
|
|
|
|
# if so, run it. |
169
|
|
|
|
|
|
|
# give methods priority over the variables we close over. |
170
|
|
|
|
|
|
|
# XXX this isn't lvalue friendly, but sdw can't figure out how to make it be and not piss off old perls. |
171
|
|
|
|
|
|
|
|
172
|
27
|
|
|
|
|
240
|
my $stash = B::svref_2object($code)->STASH->NAME; |
173
|
27
|
100
|
66
|
|
|
293
|
if( $stash and $stash->can($method) ) { |
174
|
|
|
|
|
|
|
# t/003-live-test.t .............. Can't modify non-lvalue subroutine call at lib/WWW/Workflowy.pm line 170. in perl 5.14.2 |
175
|
|
|
|
|
|
|
# goto apparently cheats lvalue detection; cheating detection is adequate for our purposes. |
176
|
|
|
|
|
|
|
# return $stash->can($method)->( $code, @_ ); |
177
|
9
|
|
|
|
|
31
|
@_ = ( $code, @_ ); goto &{ $stash->can($method) }; |
|
9
|
|
|
|
|
14
|
|
|
9
|
|
|
|
|
78
|
|
178
|
|
|
|
|
|
|
} |
179
|
|
|
|
|
|
|
|
180
|
18
|
50
|
|
|
|
51
|
exists $closed_over->{$attr} or Carp::croak "$code does not close over $attr"; |
181
|
|
|
|
|
|
|
|
182
|
18
|
|
|
|
|
35
|
my $ref = ref $closed_over->{$attr}; |
183
|
|
|
|
|
|
|
|
184
|
18
|
50
|
|
|
|
43
|
if (@_) { |
185
|
0
|
0
|
|
|
|
0
|
return @{ $closed_over->{$attr} } = @_ if $ref eq 'ARRAY'; |
|
0
|
|
|
|
|
0
|
|
186
|
0
|
0
|
|
|
|
0
|
return %{ $closed_over->{$attr} } = @_ if $ref eq 'HASH'; |
|
0
|
|
|
|
|
0
|
|
187
|
0
|
|
|
|
|
0
|
return ${ $closed_over->{$attr} } = shift; |
|
0
|
|
|
|
|
0
|
|
188
|
|
|
|
|
|
|
} |
189
|
|
|
|
|
|
|
|
190
|
18
|
50
|
33
|
|
|
79
|
$ref eq 'HASH' || $ref eq 'ARRAY' ? $closed_over->{$attr} : ${ $closed_over->{$attr} }; # lvalue friendly return |
|
18
|
|
|
|
|
10000377
|
|
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
} |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
# |
195
|
|
|
|
|
|
|
# |
196
|
|
|
|
|
|
|
# |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
package WWW::Workflowy; |
199
|
|
|
|
|
|
|
|
200
|
3
|
|
|
3
|
|
18
|
use autobox CODE => 'autobox::Closure::XAttributes::Methods'; # XXX temp since we can't 'use' it because it's inline |
|
3
|
|
|
|
|
6
|
|
|
3
|
|
|
|
|
15
|
|
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
sub import { |
203
|
3
|
|
|
3
|
|
26
|
my $class = shift; |
204
|
3
|
|
|
|
|
37
|
$class->autobox::import(CODE => 'autobox::Closure::XAttributes::Methods'); |
205
|
|
|
|
|
|
|
} |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
sub new { |
208
|
|
|
|
|
|
|
|
209
|
3
|
|
|
3
|
1
|
127
|
my $package = shift; |
210
|
3
|
|
|
|
|
12
|
my %args = @_; |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
# |
213
|
|
|
|
|
|
|
|
214
|
3
|
|
|
|
|
6
|
my $outline; |
215
|
|
|
|
|
|
|
my $client_id; |
216
|
0
|
|
|
|
|
0
|
my $date_joined; |
217
|
0
|
|
|
|
|
0
|
my $last_transaction_id; # transaction ids are much alrger than the lastModified/lm values; eg 106551357; comes from initialMostRecentOperationTransactionId then $result_json->{results}->[0]->{new_most_recent_operation_transaction_id} |
218
|
3
|
|
|
|
|
7
|
my $operations = []; # edits we've made but not yet posted |
219
|
3
|
|
|
|
|
7
|
my $polling_interval; # from $outline->{initialPollingIntervalInMs} and then ->{results}->[0]->{new_polling_interval_in_ms} |
220
|
|
|
|
|
|
|
my $last_poll_time; |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
# |
223
|
|
|
|
|
|
|
|
224
|
0
|
|
|
|
|
0
|
my $share_id; # stands in temporarily for shared_projectid for newer style URLs that look like https://workflowy.com/s/123abcdeABCDE |
225
|
|
|
|
|
|
|
|
226
|
3
|
50
|
33
|
|
|
78
|
if( $args{guid} and ! $args{url} ) { |
|
|
50
|
66
|
|
|
|
|
|
|
100
|
66
|
|
|
|
|
|
|
50
|
33
|
|
|
|
|
|
|
50
|
0
|
|
|
|
|
|
|
0
|
|
|
|
|
|
227
|
|
|
|
|
|
|
# https://workflowy.com/shared/b141ebc1-4c8d-b31a-e3e8-b9c6c633ca25/ |
228
|
0
|
|
|
|
|
0
|
$args{url} = "http://workflowy.com/shared/$args{guid}/"; |
229
|
|
|
|
|
|
|
} elsif( $args{url} and $args{url} =~ m{workflowy.com/s/\w+$} ) { |
230
|
0
|
|
|
|
|
0
|
($share_id) = $args{url} =~ m{workflowy.com/s/(\w+)$}; |
231
|
|
|
|
|
|
|
} elsif( ! $args{guid} and $args{url} ) { |
232
|
1
|
50
|
|
|
|
13
|
($args{guid}) = $args{url} =~ m{/shared/(.*?)/\w*$} or confess "workflowy url doesn't match pattern of ``.*/shared/.*/''"; |
233
|
|
|
|
|
|
|
} elsif( $args{guid} and $args{url} ) { |
234
|
0
|
|
|
|
|
0
|
confess "don't pass both guid and url parameters; pass one or the other"; |
235
|
|
|
|
|
|
|
} elsif( $args{outline} ) { |
236
|
|
|
|
|
|
|
# testing -- pass in an outline |
237
|
2
|
|
|
|
|
6
|
$outline = delete $args{outline}; |
238
|
2
|
50
|
|
|
|
9
|
$last_transaction_id = $outline->{initialMostRecentOperationTransactionId} or confess "no initialMostRecentOperationTransactionId in serialized outline"; |
239
|
2
|
|
|
|
|
5
|
$date_joined = $outline->{dateJoinedTimestampInSeconds}; # XXX probably have to compute clock skew (diff between time() and this value) and use that when computing $client_timestamp |
240
|
|
|
|
|
|
|
} elsif( $args{username} and $args{password} ) { |
241
|
|
|
|
|
|
|
# okay, they'll get their home node and we can pick out the id from there |
242
|
|
|
|
|
|
|
} else { |
243
|
0
|
|
|
|
|
0
|
confess "pass guid or url, or else username and password"; |
244
|
|
|
|
|
|
|
} |
245
|
|
|
|
|
|
|
|
246
|
3
|
|
|
|
|
8
|
my $workflowy_url = delete $args{url}; |
247
|
3
|
|
|
|
|
8
|
my $shared_projectid = delete $args{guid}; |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
# |
250
|
|
|
|
|
|
|
|
251
|
3
|
|
|
|
|
7
|
my $username = delete $args{username}; |
252
|
3
|
|
|
|
|
7
|
my $password = delete $args{password}; |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
# |
255
|
|
|
|
|
|
|
|
256
|
3
|
50
|
|
|
|
16
|
confess "unknown args to new(): " . join ', ', keys %args if keys %args; |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
# |
259
|
|
|
|
|
|
|
|
260
|
3
|
|
|
|
|
36
|
my $user_agent = LWP::UserAgent->new(agent => "Mozilla/5.0 (Windows NT 5.1; rv:5.0.1) Gecko/20100101 Firefox/5.0.1"); |
261
|
3
|
50
|
|
|
|
13266
|
$user_agent->cookie_jar or $user_agent->cookie_jar( { } ); |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
# login |
264
|
|
|
|
|
|
|
|
265
|
3
|
50
|
33
|
|
|
26924
|
if( $username and $password ) { |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
# get a cookie assigned to us |
268
|
|
|
|
|
|
|
|
269
|
0
|
|
|
|
|
0
|
my $r = HTTP::Request->new( GET => "https://workflowy.com/" ); |
270
|
0
|
|
|
|
|
0
|
my $response = $user_agent->request($r); |
271
|
|
|
|
|
|
|
# warn $response->as_string(); |
272
|
|
|
|
|
|
|
# warn "cookie jar: " . $user_agent->cookie_jar->as_string(); |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
# send login info |
275
|
|
|
|
|
|
|
|
276
|
0
|
|
|
|
|
0
|
my $referer = 'https://workflowy.com/'; |
277
|
|
|
|
|
|
|
|
278
|
0
|
|
|
|
|
0
|
try_login_again: |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
$r = HTTP::Request->new( POST => "https://workflowy.com/accounts/login" ); |
281
|
|
|
|
|
|
|
# $r->header( 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' ); |
282
|
0
|
|
|
|
|
0
|
$r->header( 'Content-Type' => 'application/x-www-form-urlencoded' ); |
283
|
0
|
|
|
|
|
0
|
$r->header( Referer => $referer ); |
284
|
0
|
|
|
|
|
0
|
$user_agent->cookie_jar->add_cookie_header( $r ); |
285
|
0
|
|
|
|
|
0
|
my $post = ''; |
286
|
0
|
|
|
|
|
0
|
$post .= 'username=' . _escape($username) . '&password=' . _escape($password); |
287
|
0
|
|
|
|
|
0
|
$r->content( $post ); |
288
|
|
|
|
|
|
|
# warn $r->as_string(); |
289
|
0
|
|
|
|
|
0
|
$response = $user_agent->request($r); |
290
|
0
|
0
|
|
|
|
0
|
if( $response->is_error ) { |
291
|
0
|
|
|
|
|
0
|
confess "error: " . $response->error_as_HTML; |
292
|
0
|
|
|
|
|
0
|
return; |
293
|
|
|
|
|
|
|
} |
294
|
|
|
|
|
|
|
# warn $response->as_string(); |
295
|
|
|
|
|
|
|
# warn "cookie jar: " . $user_agent->cookie_jar->as_string(); |
296
|
0
|
|
|
|
|
0
|
$referer = 'https://workflowy.com/accounts/login'; |
297
|
|
|
|
|
|
|
|
298
|
0
|
0
|
0
|
|
|
0
|
if( $response->code == 301 or $response->code == 302 ) { |
299
|
0
|
0
|
|
|
|
0
|
my $location = $response->header('Location') or die; |
300
|
|
|
|
|
|
|
# warn "location: $location"; # should send us to https://workflowy.com |
301
|
0
|
0
|
|
|
|
0
|
if( $location eq 'https://workflowy.com/accounts/login' ) { |
302
|
|
|
|
|
|
|
# warn "well, shit, trying to login again XXXX"; |
303
|
0
|
|
|
|
|
0
|
goto try_login_again; |
304
|
|
|
|
|
|
|
} |
305
|
0
|
|
|
|
|
0
|
$r = HTTP::Request->new( POST => $location ); |
306
|
0
|
|
|
|
|
0
|
$r->header( 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' ); |
307
|
0
|
|
|
|
|
0
|
$r->content( $post ); |
308
|
0
|
|
|
|
|
0
|
$r->header( Referer => 'https://workflowy.com/accounts/login' ); |
309
|
0
|
|
|
|
|
0
|
$user_agent->cookie_jar->add_cookie_header( $r ); |
310
|
|
|
|
|
|
|
# warn $r->as_string(); |
311
|
0
|
|
|
|
|
0
|
$response = $user_agent->request($r); |
312
|
|
|
|
|
|
|
# warn $response->as_string(); |
313
|
|
|
|
|
|
|
# warn "cookie jar: " . $user_agent->cookie_jar->as_string(); |
314
|
|
|
|
|
|
|
} |
315
|
|
|
|
|
|
|
}; |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
# |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
my $fetch_outline = sub { |
320
|
|
|
|
|
|
|
|
321
|
1
|
|
|
1
|
|
2
|
my $http_request; |
322
|
1
|
50
|
|
|
|
27
|
if( $shared_projectid) { |
|
|
0
|
|
|
|
|
|
323
|
|
|
|
|
|
|
# $http_request = HTTP::Request->new( GET => "https://workflowy.com/get_initialization_data?share_id=$shared_projectid&client_version=14" ); # do we have to start doing this instead? |
324
|
1
|
|
|
|
|
11
|
$http_request = HTTP::Request->new( GET => "https://workflowy.com/get_initialization_data?shared_projectid=$shared_projectid" ); |
325
|
|
|
|
|
|
|
} elsif($share_id) { |
326
|
0
|
|
|
|
|
0
|
$http_request = HTTP::Request->new( GET => "https://workflowy.com/get_initialization_data?share_id=$share_id&client_version=14" ); |
327
|
|
|
|
|
|
|
} else { |
328
|
0
|
|
|
|
|
0
|
$http_request = HTTP::Request->new( GET => "https://workflowy.com/get_initialization_data?client_version=14" ); |
329
|
|
|
|
|
|
|
} |
330
|
1
|
|
|
|
|
10918
|
$user_agent->cookie_jar->add_cookie_header( $http_request ); |
331
|
1
|
|
|
|
|
533
|
$http_request->header( Accept => 'application/json, text/javascript, */*; q=0.01' ); |
332
|
1
|
|
|
|
|
50
|
$http_request->header( 'Accept-Encoding' => 'gzip, deflate' ); |
333
|
1
|
|
|
|
|
45
|
$http_request->header( Referer => 'https://workflowy.com/' ); |
334
|
1
|
|
|
|
|
49
|
$http_request->header( 'X-Requested-With' => 'XMLHttpRequest' ); |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
# warn $http_request->as_string; |
337
|
|
|
|
|
|
|
|
338
|
1
|
|
|
|
|
67
|
my $response = $user_agent->request($http_request); |
339
|
1
|
50
|
|
|
|
1420078
|
if( $response->is_error ) { |
340
|
0
|
|
|
|
|
0
|
confess $http_request->uri ."\n" . $response->error_as_HTML ; |
341
|
|
|
|
|
|
|
} |
342
|
|
|
|
|
|
|
|
343
|
1
|
50
|
|
|
|
296
|
my $decoded_content = $response->decoded_content or die "no response content"; |
344
|
|
|
|
|
|
|
|
345
|
1
|
50
|
|
|
|
5087
|
my $response_json = decode_json $decoded_content or die "failed to decode response as JSON"; |
346
|
|
|
|
|
|
|
|
347
|
1
|
|
33
|
|
|
41315
|
$shared_projectid ||= $response_json->{projectTreeData}->{mainProjectTreeInfo}->{rootProject}->{id}; # happens with new style /s/123abcdefg style URLs that we don't know the shared_projectid until we etch it by the share_id |
348
|
|
|
|
|
|
|
|
349
|
1
|
50
|
|
|
|
8
|
$client_id = $response_json->{projectTreeData}->{clientId} or die "couldn't find clientId in project JSON"; |
350
|
|
|
|
|
|
|
|
351
|
1
|
|
|
|
|
4
|
$outline = $response_json->{projectTreeData}->{mainProjectTreeInfo}; |
352
|
|
|
|
|
|
|
|
353
|
1
|
50
|
|
|
|
6
|
$last_transaction_id = $outline->{initialMostRecentOperationTransactionId} or die "couldn't find initialMostRecentOperationTransactionId in project JSON"; |
354
|
|
|
|
|
|
|
|
355
|
1
|
50
|
|
|
|
6
|
$date_joined = $outline->{dateJoinedTimestampInSeconds} or die "couldn't find dateJoinedTimestampInSeconds in project JSON"; # XXX probably have to compute clock skew (diff between time() and this value) and use that when computing $client_timestamp |
356
|
1
|
50
|
|
|
|
6
|
$outline->{initialPollingIntervalInMs} or die "couldn't find initialPollingIntervalInMs in project JSON"; |
357
|
1
|
|
|
|
|
6
|
$polling_interval = $outline->{initialPollingIntervalInMs} / 1000; |
358
|
1
|
|
|
|
|
3
|
$last_poll_time = time; |
359
|
|
|
|
|
|
|
|
360
|
1
|
|
|
|
|
90
|
return $outline; |
361
|
|
|
|
|
|
|
|
362
|
3
|
|
|
|
|
33
|
}; |
363
|
|
|
|
|
|
|
|
364
|
3
|
100
|
|
|
|
36
|
$fetch_outline->() if ! $outline; |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
# |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
my $get_client_timestamp = sub { |
369
|
|
|
|
|
|
|
# adapted from this JS: |
370
|
|
|
|
|
|
|
# var a = datetime.getCurrentTimeInMS() / 1E3 - this.dateJoinedTimestampInSeconds; # / 1E3 should take it to seconds, I think |
371
|
|
|
|
|
|
|
# return Math.floor(a / 60) |
372
|
10
|
|
|
10
|
|
65
|
floor( ( time() - $date_joined ) / 60 ); |
373
|
3
|
|
|
|
|
23
|
}; |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
my $local_create_node = sub { |
376
|
5
|
|
|
5
|
|
16
|
my %args = @_; |
377
|
5
|
50
|
66
|
|
|
28
|
my $parent_id = $args{parent_id} || $args{parent_node}->{id} or confess; |
378
|
5
|
50
|
|
|
|
16
|
my $new_node = $args{new_node} or confess; |
379
|
5
|
50
|
|
|
|
6
|
my $priority = $args{priority}; defined $priority or confess; |
|
5
|
|
|
|
|
12
|
|
380
|
5
|
50
|
|
|
|
13
|
my( $parent_node, $children ) = _find_node( $outline, $parent_id ) or confess "couldn't find node for $parent_id in edit in create_node"; |
381
|
5
|
100
|
|
|
|
14
|
if( ! $children ) { |
382
|
1
|
50
|
|
|
|
3
|
if( $parent_id eq $shared_projectid ) { |
383
|
|
|
|
|
|
|
# root node |
384
|
0
|
|
0
|
|
|
0
|
$children = ( $outline->{rootProjectChildren} ||= [] ); |
385
|
|
|
|
|
|
|
} else { |
386
|
1
|
|
50
|
|
|
8
|
$children = ( $parent_node->{ch} ||= [] ); |
387
|
|
|
|
|
|
|
} |
388
|
|
|
|
|
|
|
} |
389
|
5
|
100
|
|
|
|
14
|
$priority = @$children if $priority > $#$children; |
390
|
5
|
|
|
|
|
8
|
splice @{ $children }, $priority, 0, $new_node; |
|
5
|
|
|
|
|
14
|
|
391
|
5
|
|
|
|
|
36
|
1; |
392
|
3
|
|
|
|
|
21
|
}; |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
my $local_edit_node = sub { |
395
|
4
|
|
|
4
|
|
15
|
my %args = @_; |
396
|
4
|
|
66
|
|
|
19
|
$args{node} ||= _find_node( $outline, $args{node_id} ); |
397
|
4
|
50
|
|
|
|
13
|
my $node = $args{node} or confess "no node or node_id passed to local_edit_node, or can't find the node: " . Data::Dumper::Dumper \%args; |
398
|
4
|
50
|
|
|
|
85
|
exists $args{text} or confess; |
399
|
4
|
|
|
|
|
11
|
$node->{nm} = $args{text}; |
400
|
4
|
|
|
|
|
8
|
$node->{lm} = $get_client_timestamp->(); |
401
|
4
|
|
|
|
|
17
|
1; |
402
|
3
|
|
|
|
|
15
|
}; |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
my $local_delete_node = sub { |
405
|
2
|
|
|
2
|
|
8
|
my %args = @_; |
406
|
2
|
|
|
|
|
4
|
my $node_id = $args{node_id}; |
407
|
2
|
50
|
33
|
|
|
13
|
$node_id = $args{node}->{id} if $args{node} and ! $node_id; |
408
|
2
|
50
|
|
|
|
7
|
$node_id or confess; |
409
|
2
|
|
|
|
|
7
|
my ( $parent_node, $node, $priority, $siblings ) = _find_parent($outline, $node_id ); |
410
|
2
|
|
|
|
|
14
|
_filter_out( $siblings, $node_id ); |
411
|
2
|
|
|
|
|
5
|
1; |
412
|
3
|
|
|
|
|
16
|
}; |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
my $local_move_node = sub { |
415
|
|
|
|
|
|
|
# XXX |
416
|
|
|
|
|
|
|
# executing ``move'' on data: $VAR1 = { |
417
|
|
|
|
|
|
|
# 'priority' => 0, |
418
|
|
|
|
|
|
|
# 'projectid' => 'acafae16-c8f0-b7a6-d44c-4672f68815da', |
419
|
|
|
|
|
|
|
# 'parentid' => 'c23ef558-3b78-cd2e-59df-7e578cded1e1' |
420
|
|
|
|
|
|
|
# }; |
421
|
1
|
|
|
1
|
|
3
|
my %args = @_; |
422
|
1
|
50
|
|
|
|
4
|
my $node_id = $args{node_id} or confess; |
423
|
1
|
50
|
|
|
|
4
|
my $parent_id = $args{parent_id} or confess; # new parent |
424
|
1
|
50
|
|
|
|
3
|
my $priority = $args{priority}; defined $priority or confess; |
|
1
|
|
|
|
|
3
|
|
425
|
|
|
|
|
|
|
|
426
|
1
|
50
|
|
|
|
8
|
my $node = _find_node( $outline, $node_id ) or confess "couldn't find node for $node_id in local_move_node"; |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
# remove it from where it was |
429
|
1
|
|
|
|
|
4
|
$local_delete_node->( node_id => $node_id ); |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
# insert it where it's going |
432
|
1
|
|
|
|
|
3
|
$local_create_node->( parent_id => $parent_id, new_node => $node, priority => $priority, ); |
433
|
|
|
|
|
|
|
|
434
|
3
|
|
|
|
|
18
|
}; |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
# |
437
|
|
|
|
|
|
|
|
438
|
|
|
|
|
|
|
my $update_outline = sub { |
439
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
# XXX currently only pushing changes up to workflowy, not merging in changes from workflowy; we have to re-fetch the outline to update our copy of it |
441
|
|
|
|
|
|
|
|
442
|
3
|
|
|
3
|
|
14
|
my %args = @_; |
443
|
|
|
|
|
|
|
|
444
|
3
|
|
|
|
|
7
|
my $cmd = delete $args{cmd}; |
445
|
3
|
|
|
|
|
7
|
my $node_id = delete $args{node_id}; |
446
|
3
|
|
|
|
|
5
|
my $text = delete $args{text}; |
447
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
# for cmd=create and cmd=move |
449
|
3
|
|
|
|
|
5
|
my $parent_id = delete $args{parent_id}; |
450
|
3
|
|
|
|
|
6
|
my $priority = delete $args{priority}; |
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
# for cmd=move |
453
|
3
|
|
|
|
|
5
|
my $previous_parent_id = delete $args{previous_parent_id}; |
454
|
3
|
|
|
|
|
5
|
my $previous_priority = delete $args{previous_priority}; |
455
|
|
|
|
|
|
|
|
456
|
3
|
50
|
|
|
|
10
|
confess "unknown args to update_outline: " . join ', ', keys %args if keys %args; |
457
|
|
|
|
|
|
|
|
458
|
3
|
|
|
|
|
9
|
my $client_timestamp = $get_client_timestamp->(); |
459
|
|
|
|
|
|
|
|
460
|
3
|
|
|
|
|
4
|
my $new_node_id; # set on cmd='create' # XXX should return the created/modified node |
461
|
|
|
|
|
|
|
|
462
|
3
|
100
|
|
|
|
21
|
if( $cmd eq 'edit' ) { |
|
|
50
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
463
|
|
|
|
|
|
|
|
464
|
1
|
50
|
|
|
|
4
|
my $node = _find_node( $outline, $node_id ) or confess "couldn't find node for $node_id in edit in update_outline"; |
465
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
# queue the changes to get pushed up to workflowy |
467
|
|
|
|
|
|
|
|
468
|
1
|
|
|
|
|
10
|
push @$operations, { |
469
|
|
|
|
|
|
|
type => 'edit', |
470
|
|
|
|
|
|
|
client_timestamp => $client_timestamp, |
471
|
|
|
|
|
|
|
data => { |
472
|
|
|
|
|
|
|
name => $text, |
473
|
|
|
|
|
|
|
projectid => $node_id, |
474
|
|
|
|
|
|
|
}, |
475
|
|
|
|
|
|
|
undo_data => { |
476
|
|
|
|
|
|
|
previous_last_modified => $node->{lm}, |
477
|
|
|
|
|
|
|
previous_name => $node->{nm}, |
478
|
|
|
|
|
|
|
}, |
479
|
|
|
|
|
|
|
}; |
480
|
|
|
|
|
|
|
|
481
|
1
|
|
|
|
|
46
|
$local_edit_node->( node => $node, text => $text ); |
482
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
} elsif( $cmd eq 'move' ) { |
484
|
|
|
|
|
|
|
|
485
|
0
|
0
|
|
|
|
0
|
my $node = _find_node( $outline, $node_id ) or confess "couldn't find node for $node_id in move in update_outline"; |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
# queue the changes to get pushed up to workflowy |
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
# [{"most_recent_operation_transaction_id":"186341078","operations":[{"type":"move","data":{"projectid":"1b3043b6-236d-dbd0-da7f-22c3cfcd1748","parentid":"0f60f496-5356-9040-72e6-2d3d5f94cd7c","priority":1},"client_timestamp":503732,"undo_data":{"previous_parentid":"0f60f496-5356-9040-72e6-2d3d5f94cd7c","previous_priority":4,"previous_last_modified":503353}}],"shared_projectid":"0af677e2-d205-22c8-bca6-2071a0e11ad7"}] |
490
|
|
|
|
|
|
|
|
491
|
0
|
|
|
|
|
0
|
push @$operations, { |
492
|
|
|
|
|
|
|
type => 'move', |
493
|
|
|
|
|
|
|
client_timestamp => $client_timestamp, |
494
|
|
|
|
|
|
|
data => { |
495
|
|
|
|
|
|
|
projectid => $node_id, |
496
|
|
|
|
|
|
|
parentid => $parent_id, |
497
|
|
|
|
|
|
|
priority => $priority, |
498
|
|
|
|
|
|
|
}, |
499
|
|
|
|
|
|
|
undo_data => { |
500
|
|
|
|
|
|
|
previous_parentid => $previous_parent_id, |
501
|
|
|
|
|
|
|
previous_priority => $previous_priority, |
502
|
|
|
|
|
|
|
}, |
503
|
|
|
|
|
|
|
}; |
504
|
|
|
|
|
|
|
|
505
|
0
|
|
|
|
|
0
|
$local_move_node->( node => $node, node_id => $node_id, parent_id => $parent_id, priority => $priority, ); |
506
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
} elsif( $cmd eq 'create' ) { |
508
|
|
|
|
|
|
|
|
509
|
|
|
|
|
|
|
# my ( $parent_node, $node, $priority, $siblings) = _find_parent( $outline, $parent_id ); |
510
|
|
|
|
|
|
|
# confess 'create cannot create additional root nodes' unless $parent_node; # really, we can't, not even if we wanted to! the array of siblings is faked up from the one root node ... but no, this can't happen, because the user is passing the parent's ID. we don't want the parent's parent. |
511
|
|
|
|
|
|
|
|
512
|
1
|
50
|
|
|
|
5
|
my( $parent, $children ) = _find_node( $outline, $parent_id ) or confess "couldn't find node for $node_id in edit in update_outline"; |
513
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
my $n_rand_chrs = sub { |
515
|
5
|
|
|
|
|
9
|
my $n = shift; |
516
|
5
|
|
|
|
|
68
|
join('', map { $_->[int rand scalar @$_] } (['a'..'z', 'A'..'Z', '0' .. '9']) x $n); |
|
32
|
|
|
|
|
104
|
|
517
|
1
|
|
|
|
|
5
|
}; |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
# 0da22641-65bf-9e96-70e7-dcc42c388cf3 |
520
|
1
|
|
|
|
|
5
|
$new_node_id = join '-', map $n_rand_chrs->($_), 8, 4, 4, 4, 12; |
521
|
|
|
|
|
|
|
|
522
|
1
|
|
|
|
|
17
|
push @$operations, { |
523
|
|
|
|
|
|
|
type => 'create', |
524
|
|
|
|
|
|
|
undo_data => {}, |
525
|
|
|
|
|
|
|
client_timestamp => $client_timestamp, |
526
|
|
|
|
|
|
|
data => { |
527
|
|
|
|
|
|
|
priority => $priority, |
528
|
|
|
|
|
|
|
projectid => $new_node_id, |
529
|
|
|
|
|
|
|
parentid => $parent_id, |
530
|
|
|
|
|
|
|
}, |
531
|
|
|
|
|
|
|
}, { |
532
|
|
|
|
|
|
|
type => 'edit', |
533
|
|
|
|
|
|
|
undo_data => { |
534
|
|
|
|
|
|
|
previous_last_modified => $client_timestamp, |
535
|
|
|
|
|
|
|
previous_name => '', |
536
|
|
|
|
|
|
|
}, |
537
|
|
|
|
|
|
|
client_timestamp => $client_timestamp, |
538
|
|
|
|
|
|
|
data => { |
539
|
|
|
|
|
|
|
name => $text, |
540
|
|
|
|
|
|
|
projectid => $new_node_id, |
541
|
|
|
|
|
|
|
}, |
542
|
|
|
|
|
|
|
}; |
543
|
|
|
|
|
|
|
|
544
|
1
|
|
|
|
|
4
|
my $new_node = { |
545
|
|
|
|
|
|
|
id => $new_node_id, |
546
|
|
|
|
|
|
|
nm => $text, |
547
|
|
|
|
|
|
|
lm => $client_timestamp, |
548
|
|
|
|
|
|
|
}; |
549
|
|
|
|
|
|
|
|
550
|
1
|
|
|
|
|
4
|
$local_create_node->( parent_node => $parent, new_node => $new_node, priority => $priority, ); |
551
|
|
|
|
|
|
|
|
552
|
|
|
|
|
|
|
} elsif( $cmd eq 'delete' ) { |
553
|
|
|
|
|
|
|
|
554
|
1
|
|
|
|
|
4
|
my ( $parent_node, $node, $priority, $siblings ) = _find_parent($outline, $node_id ); |
555
|
|
|
|
|
|
|
|
556
|
1
|
50
|
|
|
|
13
|
push @$operations, { |
557
|
|
|
|
|
|
|
undo_data => { |
558
|
|
|
|
|
|
|
priority => $priority, |
559
|
|
|
|
|
|
|
previous_last_modified => $node->{lm}, |
560
|
|
|
|
|
|
|
parentid => $parent_node ? $parent_node->{id} : 'None', |
561
|
|
|
|
|
|
|
}, |
562
|
|
|
|
|
|
|
client_timestamp => $client_timestamp, |
563
|
|
|
|
|
|
|
type => 'delete', |
564
|
|
|
|
|
|
|
data => { |
565
|
|
|
|
|
|
|
projectid => $node_id, # the node id of the node being deleted; not the actual shared_projectid |
566
|
|
|
|
|
|
|
}, |
567
|
|
|
|
|
|
|
}; |
568
|
|
|
|
|
|
|
|
569
|
1
|
|
|
|
|
5
|
$local_delete_node->( node_id => $node_id ); |
570
|
|
|
|
|
|
|
|
571
|
|
|
|
|
|
|
} |
572
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
# |
574
|
|
|
|
|
|
|
|
575
|
3
|
|
|
|
|
12
|
return $new_node_id; # set if cmd = 'create' |
576
|
|
|
|
|
|
|
|
577
|
3
|
|
|
|
|
36
|
}; |
578
|
|
|
|
|
|
|
|
579
|
|
|
|
|
|
|
# |
580
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
my $run_remote_operations = sub { |
582
|
3
|
|
|
3
|
|
5
|
my $run_ops = shift; |
583
|
3
|
50
|
|
|
|
11
|
$run_ops->{ops} or confess Data::Dumper::Dumper $run_ops; |
584
|
3
|
|
|
|
|
5
|
for my $op ( @{ $run_ops->{ops} } ) { |
|
3
|
|
|
|
|
5
|
|
585
|
|
|
|
|
|
|
|
586
|
7
|
|
|
|
|
10
|
my $type = $op->{type}; |
587
|
7
|
|
|
|
|
8
|
my $data = $op->{data}; |
588
|
|
|
|
|
|
|
|
589
|
7
|
100
|
|
|
|
21
|
if( $type eq 'create' ) { |
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
590
|
|
|
|
|
|
|
|
591
|
3
|
|
|
|
|
7
|
my $client_timestamp = $get_client_timestamp->(); |
592
|
|
|
|
|
|
|
|
593
|
3
|
|
|
|
|
23
|
my $new_node = { |
594
|
|
|
|
|
|
|
id => $data->{projectid}, |
595
|
|
|
|
|
|
|
nm => '', |
596
|
|
|
|
|
|
|
lm => $client_timestamp, |
597
|
|
|
|
|
|
|
}; |
598
|
|
|
|
|
|
|
|
599
|
3
|
|
|
|
|
5
|
my $parent_id = $data->{parentid}; |
600
|
3
|
50
|
|
|
|
7
|
$parent_id = $shared_projectid if $parent_id eq 'None'; |
601
|
|
|
|
|
|
|
|
602
|
3
|
|
|
|
|
8
|
$local_create_node->( parent_id => $parent_id, new_node => $new_node, priority => $data->{priority}, ); |
603
|
|
|
|
|
|
|
|
604
|
|
|
|
|
|
|
} elsif( $type eq 'edit' ) { |
605
|
|
|
|
|
|
|
|
606
|
3
|
|
|
|
|
9
|
$local_edit_node->( node_id => $data->{projectid}, text => $data->{name}, ); |
607
|
|
|
|
|
|
|
|
608
|
|
|
|
|
|
|
} elsif( $type eq 'delete' ) { |
609
|
|
|
|
|
|
|
|
610
|
0
|
|
|
|
|
0
|
$local_delete_node->( node_id => $data->{projectid}, ); |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
} elsif( $type eq 'move' ) { |
613
|
|
|
|
|
|
|
|
614
|
1
|
|
|
|
|
4
|
$local_move_node->( node_id => $data->{projectid}, parent_id => $data->{parentid}, priority => $data->{priority}, ); |
615
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
} |
617
|
|
|
|
|
|
|
} |
618
|
|
|
|
|
|
|
|
619
|
3
|
|
|
|
|
19
|
}; |
620
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
# |
622
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
my $sync_changes = sub { |
624
|
|
|
|
|
|
|
|
625
|
1
|
|
|
1
|
|
11
|
my $r = HTTP::Request->new( POST => "https://workflowy.com/push_and_poll" ); |
626
|
1
|
|
|
|
|
180
|
$user_agent->cookie_jar->add_cookie_header( $r ); |
627
|
|
|
|
|
|
|
|
628
|
1
|
|
|
|
|
334
|
$r->header( 'X-Requested-With' => 'XMLHttpRequest' ); |
629
|
1
|
|
|
|
|
71
|
$r->header( 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8' ); |
630
|
1
|
|
|
|
|
45
|
$r->header( 'Referer' => $workflowy_url ); |
631
|
|
|
|
|
|
|
|
632
|
1
|
50
|
|
|
|
49
|
$last_transaction_id or confess "no value in last_transaction_id in sync_changes"; |
633
|
|
|
|
|
|
|
|
634
|
1
|
|
|
|
|
8
|
my $push_poll_data = [{ |
635
|
|
|
|
|
|
|
most_recent_operation_transaction_id => $last_transaction_id, |
636
|
|
|
|
|
|
|
# shared_projectid => $shared_projectid, |
637
|
|
|
|
|
|
|
operations => $operations, |
638
|
|
|
|
|
|
|
}]; |
639
|
|
|
|
|
|
|
|
640
|
1
|
50
|
|
|
|
7
|
if( $share_id ) { |
641
|
0
|
|
|
|
|
0
|
$push_poll_data->[0]->{share_id} = $share_id; |
642
|
|
|
|
|
|
|
} else { |
643
|
1
|
|
|
|
|
5
|
$push_poll_data->[0]->{shared_projectid} = $shared_projectid; |
644
|
|
|
|
|
|
|
} |
645
|
|
|
|
|
|
|
|
646
|
1
|
|
|
|
|
3
|
my $post = ''; |
647
|
1
|
|
|
|
|
6
|
$post .= 'client_id=' . _escape($client_id); |
648
|
1
|
|
|
|
|
3
|
$post .= '&client_version=10'; |
649
|
1
|
|
|
|
|
17
|
$post .= '&push_poll_id=' . join('', map { $_->[int rand scalar @$_] } (['a'..'z', 'A'..'Z', '0' .. '9']) x 8); # XX guessing; seems to work though |
|
8
|
|
|
|
|
26
|
|
650
|
1
|
|
|
|
|
10
|
$post .= '&shared_projectid=' . $shared_projectid; |
651
|
1
|
|
|
|
|
7
|
$post .= '&push_poll_data=' . _escape( encode_json( $push_poll_data ) ); |
652
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
# warn "JSON sending: " . JSON::PP->new->pretty->encode( $push_poll_data ); |
654
|
|
|
|
|
|
|
|
655
|
1
|
|
|
|
|
1871
|
$r->content( $post ); |
656
|
|
|
|
|
|
|
|
657
|
1
|
|
|
|
|
50
|
my $response = $user_agent->request($r); |
658
|
1
|
50
|
|
|
|
610277
|
if( $response->is_error ) { |
659
|
0
|
|
|
|
|
0
|
confess "error: " . $response->error_as_HTML; |
660
|
0
|
|
|
|
|
0
|
return; |
661
|
|
|
|
|
|
|
} |
662
|
|
|
|
|
|
|
|
663
|
1
|
|
|
|
|
24
|
my $decoded_content = $response->decoded_content; |
664
|
|
|
|
|
|
|
|
665
|
1
|
|
|
|
|
113
|
my $result_json = decode_json $decoded_content; |
666
|
|
|
|
|
|
|
|
667
|
|
|
|
|
|
|
# "new_most_recent_operation_transaction_id": "106843573" |
668
|
|
|
|
|
|
|
# warn Data::Dumper::Dumper $result_json; |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
# warn JSON::PP->new->pretty->encode( $push_poll_data ); # <--- good for debugging |
671
|
|
|
|
|
|
|
# warn JSON::PP->new->pretty->encode( $result_json ); |
672
|
|
|
|
|
|
|
|
673
|
1
|
50
|
|
|
|
10728
|
$result_json->{results}->[0]->{error} and die "workflowy.com request failed with an error: ``$result_json->{results}->[0]->{error}''; response was: $decoded_content\npush_poll_data is: " . JSON::PP->new->pretty->encode( $push_poll_data ); |
674
|
|
|
|
|
|
|
|
675
|
1
|
50
|
|
|
|
9
|
$last_transaction_id = $result_json->{results}->[0]->{new_most_recent_operation_transaction_id} or confess "no new_most_recent_operation_transaction_id in sync changes\nresponse was: $decoded_content\npush_poll_data was: " . JSON::PP->new->pretty->encode( $push_poll_data ); |
676
|
|
|
|
|
|
|
|
677
|
1
|
|
50
|
|
|
8
|
$polling_interval = ( $result_json->{results}->[0]->{new_polling_interval_in_ms} || 1000 ) / 1000; # XXX this was probably just undef when we ignored an error before the checking above was added |
678
|
1
|
|
|
|
|
2
|
$last_poll_time = time; |
679
|
|
|
|
|
|
|
|
680
|
|
|
|
|
|
|
# |
681
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
# $results->[*]->{server_run_operation_transaction_json} is what we already did to our own copy of the outline; not sure if we should double check |
683
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
# XXX call fetch_outline if the server sent us any deltas; or else attempt to mirror those changes |
685
|
|
|
|
|
|
|
|
686
|
1
|
|
|
|
|
3
|
my $run_operations = $result_json->{results}->[0]->{concurrent_remote_operation_transactions}; |
687
|
1
|
|
|
|
|
3
|
for my $run_op ( @$run_operations ) { |
688
|
0
|
|
|
|
|
0
|
my $decoded_run_op = decode_json $run_op; |
689
|
0
|
|
|
|
|
0
|
$run_remote_operations->( $decoded_run_op ); |
690
|
|
|
|
|
|
|
} |
691
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
# |
693
|
|
|
|
|
|
|
|
694
|
1
|
|
|
|
|
57
|
$operations = []; |
695
|
|
|
|
|
|
|
|
696
|
3
|
|
|
|
|
27
|
}; |
697
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
# |
699
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
my $self = sub { |
701
|
|
|
|
|
|
|
|
702
|
4
|
|
|
4
|
|
8
|
my $action = shift; |
703
|
|
|
|
|
|
|
|
704
|
|
|
|
|
|
|
# important symbols |
705
|
|
|
|
|
|
|
|
706
|
4
|
50
|
|
|
|
107
|
$outline or confess "no outline"; # shouldn't happen |
707
|
4
|
50
|
|
|
|
13
|
$shared_projectid or confess "no shared_projectid"; # shouldn't happen |
708
|
|
|
|
|
|
|
|
709
|
4
|
100
|
|
|
|
14
|
if( $action eq 'edit' ) { |
710
|
1
|
|
|
|
|
6
|
my %args = @_; |
711
|
1
|
50
|
|
|
|
6
|
my $save_id = delete $args{save_id} or confess "pass a save_id parameter"; |
712
|
1
|
50
|
|
|
|
5
|
my $text = delete $args{text} or confess "pass a text parameter"; |
713
|
|
|
|
|
|
|
|
714
|
1
|
|
|
|
|
151
|
$update_outline->( |
715
|
|
|
|
|
|
|
cmd => 'edit', |
716
|
|
|
|
|
|
|
text => $text, |
717
|
|
|
|
|
|
|
node_id => $save_id, |
718
|
|
|
|
|
|
|
); |
719
|
|
|
|
|
|
|
|
720
|
1
|
|
|
|
|
5
|
return 1; |
721
|
|
|
|
|
|
|
} |
722
|
|
|
|
|
|
|
|
723
|
|
|
|
|
|
|
# [{"most_recent_operation_transaction_id":"186341078","operations":[{"type":"move","data":{"projectid":"1b3043b6-236d-dbd0-da7f-22c3cfcd1748","parentid":"0f60f496-5356-9040-72e6-2d3d5f94cd7c","priority":1},"client_timestamp":503732,"undo_data":{"previous_parentid":"0f60f496-5356-9040-72e6-2d3d5f94cd7c","previous_priority":4,"previous_last_modified":503353}}],"shared_projectid":"0af677e2-d205-22c8-bca6-2071a0e11ad7"}] |
724
|
|
|
|
|
|
|
|
725
|
3
|
50
|
|
|
|
9
|
if( $action eq 'move' ) { |
726
|
|
|
|
|
|
|
|
727
|
0
|
|
|
|
|
0
|
my %args = @_; |
728
|
0
|
0
|
|
|
|
0
|
my $node_id = delete $args{node_id} or confess "pass a node_id parameter"; |
729
|
0
|
0
|
|
|
|
0
|
my ( $parent_node, $node, $previous_priority, $siblings ) = _find_parent($outline, $node_id ) or confess "could not find node ``$node_id'' in ->move()"; |
730
|
0
|
|
0
|
|
|
0
|
my $parent_id = delete $args{parent_id} || $parent_node->{id}; # useful if the user only wants to update the priority |
731
|
0
|
|
0
|
|
|
0
|
my $priority = delete $args{priority} || 0; # useful if the user only wants to change the parent and is okay with it going to the head of the list |
732
|
|
|
|
|
|
|
|
733
|
0
|
|
|
|
|
0
|
return $update_outline->( |
734
|
|
|
|
|
|
|
cmd => 'move', |
735
|
|
|
|
|
|
|
node_id => $node_id, |
736
|
|
|
|
|
|
|
parent_id => $parent_id, # for cmd=create |
737
|
|
|
|
|
|
|
priority => $priority, # for cmd=create |
738
|
|
|
|
|
|
|
previous_parent_id => $parent_node->{id}, |
739
|
|
|
|
|
|
|
previous_priority => $previous_priority, |
740
|
|
|
|
|
|
|
); |
741
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
} |
743
|
|
|
|
|
|
|
|
744
|
3
|
100
|
|
|
|
9
|
if( $action eq 'create' ) { |
745
|
|
|
|
|
|
|
|
746
|
|
|
|
|
|
|
# $update_outline returns the id of the newly created node for cmd='create' |
747
|
|
|
|
|
|
|
|
748
|
1
|
|
|
|
|
6
|
my %args = @_; |
749
|
1
|
|
|
|
|
4
|
my $parent_id = delete $args{parent_id}; |
750
|
1
|
|
|
|
|
3
|
my $text = delete $args{text}; |
751
|
1
|
|
|
|
|
2
|
my $priority = delete $args{priority}; |
752
|
|
|
|
|
|
|
|
753
|
1
|
|
|
|
|
4
|
return $update_outline->( |
754
|
|
|
|
|
|
|
cmd => 'create', |
755
|
|
|
|
|
|
|
text => $text, |
756
|
|
|
|
|
|
|
parent_id => $parent_id, # for cmd=create |
757
|
|
|
|
|
|
|
priority => $priority, # for cmd=create |
758
|
|
|
|
|
|
|
); |
759
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
} |
761
|
|
|
|
|
|
|
|
762
|
2
|
100
|
|
|
|
8
|
if( $action eq 'delete' ) { |
763
|
1
|
|
|
|
|
3
|
my %args = @_; |
764
|
1
|
50
|
|
|
|
4
|
my $node_id = delete $args{node_id} or confess "pass a node_id parameter"; |
765
|
|
|
|
|
|
|
|
766
|
1
|
|
|
|
|
4
|
$update_outline->( |
767
|
|
|
|
|
|
|
cmd => 'delete', |
768
|
|
|
|
|
|
|
node_id => $node_id, |
769
|
|
|
|
|
|
|
); |
770
|
|
|
|
|
|
|
|
771
|
1
|
|
|
|
|
4
|
return 1; |
772
|
|
|
|
|
|
|
|
773
|
|
|
|
|
|
|
} |
774
|
|
|
|
|
|
|
|
775
|
1
|
50
|
|
|
|
4
|
if( $action eq 'sync' ) { |
776
|
|
|
|
|
|
|
|
777
|
1
|
50
|
|
|
|
7
|
if( ( time - $last_poll_time ) < $polling_interval ) { |
778
|
0
|
|
|
|
|
0
|
return; |
779
|
|
|
|
|
|
|
} |
780
|
|
|
|
|
|
|
|
781
|
1
|
|
|
|
|
6
|
$sync_changes->(); |
782
|
|
|
|
|
|
|
|
783
|
1
|
|
|
|
|
8
|
return 1; |
784
|
|
|
|
|
|
|
} |
785
|
|
|
|
|
|
|
|
786
|
0
|
0
|
0
|
|
|
0
|
if( $action eq 'fetch' or $action eq 'read' or $action eq 'get' ) { |
|
|
|
0
|
|
|
|
|
787
|
|
|
|
|
|
|
# XXX reconcile this with sync |
788
|
0
|
|
|
|
|
0
|
$fetch_outline->(); |
789
|
0
|
|
|
|
|
0
|
return 1; |
790
|
|
|
|
|
|
|
} |
791
|
|
|
|
|
|
|
|
792
|
3
|
|
|
|
|
41
|
}; |
793
|
|
|
|
|
|
|
} |
794
|
|
|
|
|
|
|
|
795
|
1
|
|
|
1
|
1
|
3
|
sub edit { my $self = shift; $self->( 'edit', @_ ); } |
|
1
|
|
|
|
|
4
|
|
796
|
0
|
|
|
0
|
1
|
0
|
sub move { my $self = shift; $self->( 'move', @_ ); } |
|
0
|
|
|
|
|
0
|
|
797
|
1
|
|
|
1
|
1
|
2
|
sub create { my $self = shift; $self->( 'create', @_ ); } |
|
1
|
|
|
|
|
4
|
|
798
|
1
|
50
|
|
1
|
1
|
3
|
sub delete { my $self = shift; my $node_id = ref($_[0]) ? $_[0]->{id} : $_[0]; $self->( 'delete', node_id => $node_id, ); } |
|
1
|
|
|
|
|
5
|
|
|
1
|
|
|
|
|
8
|
|
799
|
1
|
|
|
1
|
1
|
3
|
sub sync { my $self = shift; $self->( 'sync', @_ ); } |
|
1
|
|
|
|
|
4
|
|
800
|
0
|
|
|
0
|
1
|
0
|
sub fetch { my $self = shift; $self->( 'fetch', @_ ); } |
|
0
|
|
|
|
|
0
|
|
801
|
|
|
|
|
|
|
|
802
|
|
|
|
|
|
|
sub find { |
803
|
|
|
|
|
|
|
# external API takes $self |
804
|
5
|
|
|
5
|
1
|
7
|
my $self = shift; |
805
|
5
|
50
|
|
|
|
14
|
my $cb = shift or confess "pass a callback"; |
806
|
|
|
|
|
|
|
|
807
|
5
|
|
|
|
|
32
|
_find( $self->outline, $cb); |
808
|
|
|
|
|
|
|
} |
809
|
|
|
|
|
|
|
|
810
|
|
|
|
|
|
|
sub find_by_id { |
811
|
|
|
|
|
|
|
# external API takes $self |
812
|
0
|
|
|
0
|
0
|
0
|
my $self = shift; |
813
|
0
|
0
|
|
|
|
0
|
my $id = shift or confess "pass id"; |
814
|
0
|
|
|
0
|
|
0
|
_find( $self->outline, sub { $_[0]->{id} eq $id } ); |
|
0
|
|
|
|
|
0
|
|
815
|
|
|
|
|
|
|
} |
816
|
|
|
|
|
|
|
|
817
|
|
|
|
|
|
|
sub _find { |
818
|
22
|
|
|
22
|
|
29
|
my $outline = shift; |
819
|
22
|
50
|
|
|
|
69
|
my $cb = shift or confess "pass a callback"; |
820
|
|
|
|
|
|
|
|
821
|
|
|
|
|
|
|
# $outline->{rootProject} points to the root node; $outline->{rootProjectChlidren} has its children; this is wonky; normally, $node->{ch} has a nodes children |
822
|
|
|
|
|
|
|
# temporarily put rootProjectChildren under rootProject so we can recurse through this nicely |
823
|
|
|
|
|
|
|
|
824
|
22
|
|
|
|
|
66
|
local $outline->{rootProject}->{ch} = $outline->{rootProjectChildren}; |
825
|
22
|
|
|
|
|
95
|
my $fake_root = { lm => 0, nm => '', id => '0', ch => [ $outline->{rootProject} ], fake => 1, }; |
826
|
|
|
|
|
|
|
|
827
|
22
|
|
|
|
|
47
|
return _find_inner( $fake_root, $cb, ); |
828
|
|
|
|
|
|
|
} |
829
|
|
|
|
|
|
|
|
830
|
|
|
|
|
|
|
|
831
|
|
|
|
|
|
|
sub _find_inner { |
832
|
|
|
|
|
|
|
# there's no $self inside the coderef so stuff in there calls this directly |
833
|
70
|
|
|
70
|
|
93
|
my $node = shift; |
834
|
70
|
50
|
|
|
|
133
|
my $cb = shift or confess; |
835
|
70
|
|
100
|
|
|
185
|
my $stack = shift() || [ $node ]; |
836
|
70
|
|
|
|
|
67
|
my $position = 0; |
837
|
70
|
|
|
|
|
67
|
for my $child ( @{ $node->{ch} } ) { |
|
70
|
|
|
|
|
200
|
|
838
|
137
|
100
|
|
|
|
260
|
return $child if $cb->( $child, $stack, $position ); |
839
|
117
|
100
|
|
|
|
672
|
if( $child->{ch} ) { |
840
|
48
|
|
|
|
|
145
|
my $node = _find_inner( $child, $cb, [ @$stack, $child ], ); |
841
|
48
|
100
|
|
|
|
198
|
return $node if $node; |
842
|
|
|
|
|
|
|
} |
843
|
89
|
|
|
|
|
146
|
$position++; |
844
|
|
|
|
|
|
|
} |
845
|
|
|
|
|
|
|
} |
846
|
|
|
|
|
|
|
|
847
|
|
|
|
|
|
|
sub _find_node { |
848
|
11
|
|
|
11
|
|
12
|
my $outline = shift; |
849
|
11
|
|
|
|
|
13
|
my $node_id = shift; |
850
|
|
|
|
|
|
|
|
851
|
11
|
|
|
|
|
13
|
my $node; |
852
|
|
|
|
|
|
|
my $children; # since we temporarily attached the tree to the root node, $node->{ch} won't be valid if we return the root node |
853
|
|
|
|
|
|
|
|
854
|
|
|
|
|
|
|
_find( $outline, sub { |
855
|
42
|
|
|
42
|
|
38
|
my $child = shift; |
856
|
42
|
100
|
|
|
|
102
|
if( $child->{id} eq $node_id ) { |
857
|
11
|
|
|
|
|
11
|
$node = $child; |
858
|
11
|
|
|
|
|
17
|
$children = $node->{ch}; |
859
|
11
|
|
|
|
|
45
|
return 1; # stop looking |
860
|
|
|
|
|
|
|
} |
861
|
31
|
|
|
|
|
63
|
return 0; # keep looking |
862
|
11
|
|
|
|
|
56
|
} ); |
863
|
|
|
|
|
|
|
|
864
|
11
|
100
|
|
|
|
76
|
return wantarray ? ( $node, $children ) : $node; |
865
|
|
|
|
|
|
|
} |
866
|
|
|
|
|
|
|
|
867
|
|
|
|
|
|
|
sub _find_parent { |
868
|
6
|
|
|
6
|
|
11
|
my $outline = shift; # we want this |
869
|
6
|
|
|
|
|
8
|
my $node_id = shift; |
870
|
|
|
|
|
|
|
|
871
|
|
|
|
|
|
|
# return if $outline->{rootProject}->{id} eq $node_id; # not an error, just no parent; rootProject->id is the same as $shared_projectid; should be redundant |
872
|
|
|
|
|
|
|
|
873
|
|
|
|
|
|
|
# $outline->{rootProject} points to the root node; $outline->{rootProjectChlidren} has its children; this is wonky; normally, $node->{ch} has a nodes children |
874
|
|
|
|
|
|
|
# temporarily put rootProjectChildren under rootProject so we can recurse through this nicely |
875
|
|
|
|
|
|
|
|
876
|
|
|
|
|
|
|
# $outline->{rootProject}->{ch} = $outline->{rootProjectChildren}; # _find doesthis now |
877
|
|
|
|
|
|
|
|
878
|
6
|
|
|
|
|
10
|
my $parent_node; |
879
|
|
|
|
|
|
|
my $node; |
880
|
0
|
|
|
|
|
0
|
my $priority; |
881
|
0
|
|
|
|
|
0
|
my $parents_children; # since we temporarily attached the tree to the root node, $node->{ch} won't be valid if we return the root node |
882
|
|
|
|
|
|
|
|
883
|
|
|
|
|
|
|
_find( $outline, sub { |
884
|
26
|
|
|
26
|
|
26
|
my $child = shift; |
885
|
26
|
|
|
|
|
29
|
my @parent_nodes = @{ shift() }; |
|
26
|
|
|
|
|
53
|
|
886
|
26
|
100
|
|
|
|
67
|
if( $child->{id} eq $node_id ) { |
887
|
6
|
|
|
|
|
10
|
$node = $child; |
888
|
6
|
50
|
|
|
|
16
|
$parent_node = @parent_nodes ? $parent_nodes[-1] : undef; |
889
|
6
|
|
|
|
|
8
|
$priority = shift; |
890
|
6
|
|
|
|
|
11
|
$parents_children = $parent_node->{ch}; |
891
|
6
|
|
|
|
|
27
|
return 1; # stop looking |
892
|
|
|
|
|
|
|
} |
893
|
20
|
|
|
|
|
48
|
return 0; # keep looking |
894
|
6
|
|
|
|
|
35
|
} ); |
895
|
|
|
|
|
|
|
|
896
|
|
|
|
|
|
|
# delete $outline->{rootProject}->{ch}; # _find handles this now |
897
|
|
|
|
|
|
|
|
898
|
6
|
100
|
|
|
|
37
|
$parent_node = undef if $parent_node->{fake}; # don't return our faked up root node |
899
|
|
|
|
|
|
|
|
900
|
6
|
50
|
|
|
|
30
|
return wantarray ? ( $parent_node, $node, $priority, $parents_children ) : $parent_node; |
901
|
|
|
|
|
|
|
|
902
|
|
|
|
|
|
|
} |
903
|
|
|
|
|
|
|
|
904
|
|
|
|
|
|
|
sub get_children { |
905
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
906
|
0
|
0
|
|
|
|
0
|
my $node_id = shift or confess "pass a node id"; |
907
|
0
|
0
|
|
|
|
0
|
(undef, my $children) = _find_node( $self->outline, $node_id ) or confess; |
908
|
0
|
|
|
|
|
0
|
return $children; |
909
|
|
|
|
|
|
|
} |
910
|
|
|
|
|
|
|
|
911
|
|
|
|
|
|
|
sub _filter_out { |
912
|
2
|
50
|
|
2
|
|
9
|
my $arr = shift or confess; |
913
|
2
|
50
|
|
|
|
8
|
my $node_id = shift or confess; |
914
|
2
|
|
|
|
|
7
|
for my $i ( 0 .. $#$arr ) { |
915
|
5
|
100
|
|
|
|
16
|
if( $arr->[$i]->{id} eq $node_id ) { |
916
|
2
|
|
|
|
|
5
|
splice @$arr, $i, 1, (); |
917
|
2
|
|
|
|
|
5
|
return 1; |
918
|
|
|
|
|
|
|
} |
919
|
|
|
|
|
|
|
} |
920
|
|
|
|
|
|
|
} |
921
|
|
|
|
|
|
|
|
922
|
|
|
|
|
|
|
sub dump { |
923
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
924
|
|
|
|
|
|
|
|
925
|
0
|
|
|
|
|
0
|
my $output = ''; |
926
|
|
|
|
|
|
|
|
927
|
|
|
|
|
|
|
$self->find( sub { |
928
|
0
|
|
|
0
|
|
0
|
my $child = shift; |
929
|
0
|
|
|
|
|
0
|
my @parent_nodes = @{ shift() }; |
|
0
|
|
|
|
|
0
|
|
930
|
0
|
|
|
|
|
0
|
$output .= $child->{id} . ' '; |
931
|
0
|
|
|
|
|
0
|
$output .= ' ' x scalar @parent_nodes; |
932
|
0
|
|
|
|
|
0
|
$output .= $child->{nm} . "\n"; |
933
|
0
|
|
|
|
|
0
|
0; |
934
|
0
|
|
|
|
|
0
|
} ); |
935
|
|
|
|
|
|
|
|
936
|
0
|
|
|
|
|
0
|
return $output; |
937
|
|
|
|
|
|
|
|
938
|
|
|
|
|
|
|
} |
939
|
|
|
|
|
|
|
|
940
|
|
|
|
|
|
|
sub _escape { |
941
|
2
|
|
|
2
|
|
2045
|
my $arg = shift; |
942
|
2
|
|
|
|
|
15
|
$arg =~ s/([^a-zA-Z0-9_.-])/uc sprintf("%%%02x",ord($1))/eg; |
|
215
|
|
|
|
|
1915
|
|
943
|
2
|
|
|
|
|
21
|
$arg; |
944
|
|
|
|
|
|
|
} |
945
|
|
|
|
|
|
|
|
946
|
|
|
|
|
|
|
|
947
|
|
|
|
|
|
|
=head1 SEE ALSO |
948
|
|
|
|
|
|
|
|
949
|
|
|
|
|
|
|
=head1 BUGS |
950
|
|
|
|
|
|
|
|
951
|
|
|
|
|
|
|
Remote changes are not merged with forgiveness. For example, if you delete a node, someone else edits the node concurrently, and then you do a |
952
|
|
|
|
|
|
|
C operation, L will blow up when it can't find the node to edit. Forgiveness should be optional. |
953
|
|
|
|
|
|
|
|
954
|
|
|
|
|
|
|
L versions their protocol. This module targets C<10>. The protocol is not a documented API. This module will likely stop working without |
955
|
|
|
|
|
|
|
notice. This module does things like parse out JSON from JavaScript code using regexen. |
956
|
|
|
|
|
|
|
|
957
|
|
|
|
|
|
|
=head1 AUTHOR |
958
|
|
|
|
|
|
|
|
959
|
|
|
|
|
|
|
Scott Walters, Escott@slowass.netE |
960
|
|
|
|
|
|
|
|
961
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
962
|
|
|
|
|
|
|
|
963
|
|
|
|
|
|
|
Copyright (C) 2013 by Scott Walters |
964
|
|
|
|
|
|
|
|
965
|
|
|
|
|
|
|
This library is free software; you can redistribute it and/or modify |
966
|
|
|
|
|
|
|
it under the same terms as Perl itself, either Perl version 5.8.9 or, |
967
|
|
|
|
|
|
|
at your option, any later version of Perl 5 you may have available. |
968
|
|
|
|
|
|
|
|
969
|
|
|
|
|
|
|
=cut |
970
|
|
|
|
|
|
|
|
971
|
|
|
|
|
|
|
# |
972
|
|
|
|
|
|
|
# pasted in copy of my hacked up autobox::Attribute::Closures |
973
|
|
|
|
|
|
|
# |
974
|
|
|
|
|
|
|
|
975
|
|
|
|
|
|
|
|
976
|
|
|
|
|
|
|
|
977
|
|
|
|
|
|
|
1; |
978
|
|
|
|
|
|
|
|
979
|
|
|
|
|
|
|
__DATA__ |