├── .bashrc ├── .taskrc ├── scripts ├── gcalendar_export.pl ├── gdrive_export.pl ├── gtasks_export.pl └── zohocrm_events.pl ├── tasks.kanban.html ├── tasks.timeline.html ├── tasks.weekly.txt └── tasks.xltx /.taskrc: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # task configuration file 3 | ################################################################################ 4 | 5 | data.location= /.g/_data/zactive/_pim/tasks 6 | 7 | ######################################## 8 | 9 | taskd.server= server.garybgenett.net:53589 10 | taskd.credentials= local/user/e52c1208-5c20-4935-aef2-84666d0167ff 11 | 12 | taskd.ca= /.g/_data/zactive/.home/.openssl/server-ca.garybgenett.net.crt 13 | taskd.certificate= /.g/_data/zactive/.home/.openssl/client.garybgenett.net.crt 14 | taskd.key= /.g/_data/zactive/.home/.openssl/client.garybgenett.net.key 15 | debug.tls= 1 16 | 17 | ######################################## 18 | 19 | defaultwidth= 160 20 | defaultheight= 0 21 | detection= 1 22 | xterm.title= 0 23 | 24 | ################################################################################ 25 | 26 | exit.on.missing.db= 1 27 | locking= 1 28 | #>>>verbose= 1 29 | debug= 0 30 | 31 | allow.empty.filter= 0 32 | confirmation= 1 33 | 34 | ######################################## 35 | 36 | gc= 0 37 | 38 | recurrence= 0 39 | recurrence.limit= 1 40 | 41 | indent.annotation= 2 42 | indent.report= 0 43 | 44 | abbreviation.minimum= 1 45 | bulk= 1 46 | 47 | search.case.sensitive= 0 48 | expressions= 1 49 | regex= 1 50 | 51 | list.all.projects= 1 52 | list.all.tags= 1 53 | print.empty.columns= 1 54 | 55 | journal.info= 1 56 | journal.time= 1 57 | journal.time.start.annotation= [track]:[begin] 58 | journal.time.stop.annotation= [track]:[end] 59 | 60 | ################################################################################ 61 | 62 | include /usr/share/task/rc/holidays.en-US.rc 63 | date.iso= 1 64 | 65 | weekstart= Monday 66 | due= 14 67 | 68 | dateformat= Y-M-D 69 | dateformat.report= Y-M-D 70 | dateformat.holiday= YMD 71 | 72 | dateformat.annotation= Y-M-D H:N:S 73 | dateformat.edit= Y-M-D H:N:S 74 | dateformat.info= Y-M-D H:N:S 75 | 76 | calendar.offset= 1 77 | calendar.offset.value= -1 78 | monthsperline= 4 79 | 80 | displayweeknumber= 1 81 | calendar.legend= 1 82 | calendar.holidays= full 83 | calendar.details= full 84 | calendar.details.report= mind 85 | 86 | ######################################## 87 | 88 | include /usr/share/task/rc/dark-16.theme 89 | color= 1 90 | 91 | rule.precedence.color= active,overdue,due.today,due,deleted,completed,recurring,pri.,scheduled,until,keyword.,project.,uda.,tag.,blocked,blocking,tagged 92 | rule.color.merge= 0 93 | 94 | color.debug= black on cyan 95 | color.warning= white on cyan 96 | color.error= magenta on cyan 97 | 98 | color.footnote= cyan 99 | color.header= cyan 100 | color.alternate= on black 101 | 102 | color.active= white on green 103 | color.overdue= yellow on red 104 | color.due.today= white on red 105 | color.due= black on red 106 | 107 | color.deleted= red on blue 108 | color.completed= green on blue 109 | color.recurring= red 110 | 111 | color.uda.priority.H= white on magenta 112 | color.uda.priority.M= black on magenta 113 | color.uda.priority.L= magenta 114 | color.scheduled= magenta 115 | color.until= magenta 116 | 117 | color.blocked= cyan 118 | color.blocking= yellow 119 | color.tagged= none 120 | 121 | color.calendar.weeknumber= cyan 122 | color.calendar.weekend= magenta 123 | 124 | color.calendar.overdue= yellow on red 125 | color.calendar.due.today= white on red 126 | color.calendar.due= black on red 127 | 128 | color.calendar.today= white on green 129 | color.calendar.holiday= black on yellow 130 | 131 | ################################################################################ 132 | 133 | default.command= view 134 | 135 | alias.cal= calendar 136 | alias.ll= zoom 137 | 138 | ######################################## 139 | 140 | uda.kind.label= Kind 141 | uda.kind.type= string 142 | uda.kind.values= notes,track, 143 | 144 | uda.area.label= Area 145 | uda.area.type= string 146 | uda.area.values= _gtd,computer,family,gear,health,money,people,self,travel,work,work-em,work.bz,work.cn,work.em,work.f5,work.gg,work.hs,work.jb,work.jl,work.me,work.tk,work.vn,writing, 147 | 148 | uda.priority.label= Priority 149 | uda.priority.type= string 150 | uda.priority.values= H,M,L, 151 | 152 | ######################################## 153 | 154 | #>>>context= k3 155 | 156 | # k1: holding mind rc.context=k1 157 | # k2: waiting read rc.context=k2 158 | # k3: working todo rc.context=k3 159 | # k4: complete read rc.context=k4 160 | # k5: archive read rc.context=k5 161 | 162 | context.k1.read= project.isnt:_gtd ( +OVERDUE or +TODAY or +DUE or +BLOCKED ) 163 | context.k2.read= project.isnt:_gtd ( +OVERDUE or +TODAY or +DUE or priority.any: ) area.isnt:computer tags.isnt:errand tags.isnt:home +BLOCKED 164 | context.k3.read= project.isnt:_gtd ( +OVERDUE or +TODAY or +DUE or priority.any: ) area.isnt:computer tags.isnt:errand tags.isnt:home +UNBLOCKED 165 | context.k4.read= project.isnt:_gtd ( status.isnt:pending end.after:today-1weeks ) 166 | context.k5.read= project.isnt:_gtd ( status.is:pending modified.after:today-1weeks ) 167 | 168 | ######################################## 169 | 170 | report.dump.description= Custom [Dump]: Shows all information 171 | report.dump.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,status,entry,wait,scheduled,until,modified,due,end,uuid,parent,imask,mask 172 | report.dump.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+STAT,+BORN,+WAIT,+HOLD,+REAP,+MOVE,+DEAD,+DIED,+UUID,+PUID,+I,+M 173 | report.dump.filter= #>>> 174 | report.dump.sort= entry+ 175 | 176 | report.fail.description= Custom [Fail]: Finds non-conforming tasks 177 | report.fail.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,status,entry,wait,scheduled,until,modified,due,end,uuid,parent,imask,mask 178 | report.fail.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+STAT,+BORN,+WAIT,+HOLD,+REAP,+MOVE,+DEAD,+DIED,+UUID,+PUID,+I,+M 179 | #>>>report.fail.filter= ((project:_gtd (kind.isnt:track recur.none: )) or (project:.gtd area.isnt:_gtd) or (project:.hpi area.isnt:_gtd) or ((project:_data or project:_journal or project:.someday) kind.isnt:notes) or ((project.isnt:.gtd project.isnt:.hpi project.isnt:_gtd project.isnt:_data) area:_gtd) or (project:.someday (status.isnt:pending status.isnt:deleted )) or (project.none: kind:notes) or (kind.any: tags.any: ) or (kind.none: tags.none: ) or (area.none: ) or (area.is:work) or (priority.any: or wait.any: or scheduled.any: or (until.any: recur.none: ) or (until.any: mask.none: )) or (kind.isnt:notes description.startswith:\[notes\]:) or (status:pending kind.none: +ANNOTATED) or +ORPHAN) 180 | #>>>report.fail.filter= ((project:_gtd (kind.isnt:track recur.none: )) or (project:.gtd area.isnt:_gtd) or (project:.hpi area.isnt:_gtd) or ((project:_data or project:_journal or project:.someday) kind.isnt:notes) or ((project.isnt:.gtd project.isnt:.hpi project.isnt:_gtd project.isnt:_data) area:_gtd) or (project:.someday (status.isnt:pending status.isnt:deleted )) or (project.none: kind:notes) or (kind.any: tags.any: ) or (kind.none: tags.none: ) or (area.none: ) or (area.is:work) or (priority.any: status.isnt:pending) or (wait.any: or scheduled.any: or (until.any: recur.none: ) or (until.any: mask.none: )) or (kind.isnt:notes description.startswith:\[notes\]:) or (status:pending kind.none: +ANNOTATED) or +ORPHAN) 181 | report.fail.filter= ((project:_gtd (kind.isnt:track recur.none: )) or (project:.gtd area.isnt:_gtd) or (project:.hpi area.isnt:_gtd) or ((project:_data or project:_journal or project:.someday) kind.isnt:notes) or ((project.isnt:.gtd project.isnt:.hpi project.isnt:_gtd project.isnt:_data) area:_gtd) or (project:.someday (status.isnt:pending status.isnt:deleted )) or (project.none: kind:notes) or (kind.any: tags.any: ) or (kind.none: tags.none: ) or (area.none: ) or (area.is:work) or (priority.any: status.isnt:pending) or (wait.any: or scheduled.any: or (until.any: recur.none: ) or (until.any: mask.none: )) or +ORPHAN) 182 | report.fail.sort= entry+ 183 | 184 | ######################################## 185 | 186 | report.data.description= Custom [Data]: All data entries 187 | report.data.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,status,entry,due,end,imask 188 | report.data.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+STAT,+BORN,+DEAD,+DIED,+I 189 | report.data.filter= (project:_data or project:_journal) 190 | report.data.sort= project+,kind-,description+,entry+ 191 | 192 | report.docs.description= Custom [Docs]: Active data and annotated entries 193 | #>>>report.docs.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,status,entry,due,end,imask 194 | #>>>report.docs.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+STAT,+BORN,+DEAD,+DIED,+I 195 | report.docs.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority 196 | report.docs.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P 197 | report.docs.filter= (status:pending ((project:_data or project:_journal) or (kind.isnt:track +ANNOTATED))) 198 | report.docs.sort= project+,kind-,description+,entry+ 199 | 200 | report.meta.description= Custom [Meta]: Metadata and recurring tasks 201 | report.meta.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,status,entry,due,end,imask 202 | report.meta.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+STAT,+BORN,+DEAD,+DIED,+I 203 | report.meta.filter= ((project.isnt:_data project.isnt:_journal (kind.any: or (recur.any: mask.any: ) or tags:.research or tags:.waiting)) or +ACTIVE) 204 | report.meta.sort= project+,kind-,description+,entry+ 205 | 206 | ######################################## 207 | 208 | report.mark.description= Custom [Mark]: Time tracking tasks 209 | report.mark.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority 210 | report.mark.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P 211 | report.mark.filter= ((status:pending kind:track) or +ACTIVE) 212 | report.mark.sort= project+,description+,entry+ 213 | 214 | report.mind.description= Custom [Mind]: Reminders for all tasks with due date 215 | #>>>report.mind.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,entry,due,imask 216 | report.mind.columns= id,depends,urgency.integer,start.age,due.remaining,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,entry,due,imask 217 | report.mind.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+BORN,+DEAD,+I 218 | report.mind.filter= ((status:pending due.any: ) or +ACTIVE) 219 | report.mind.sort= due+,priority-,urgency-,project+,description+,entry+ 220 | 221 | report.note.description= Custom [Note]: All active notes 222 | report.note.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority 223 | report.note.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P 224 | report.note.filter= ((status:pending kind:notes) or +ACTIVE) 225 | report.note.sort= project+,description+,entry+ 226 | 227 | ######################################## 228 | 229 | report.look.description= Custom [Look]: Filtered list view (Read) 230 | #>>>report.look.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,status,entry,due,end,imask 231 | #>>>report.look.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+STAT,+BORN,+DEAD,+DIED,+I 232 | report.look.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority 233 | report.look.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P 234 | report.look.filter= (status:pending (kind.any: or (+UNBLOCKED or +DUE or +TODAY or +OVERDUE))) 235 | report.look.sort= project+,kind-,depends-,description+,entry+ 236 | 237 | report.read.description= Custom [Read]: Global list view, unfiltered and sorted for review 238 | report.read.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,status,entry,due,end,imask 239 | report.read.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+STAT,+BORN,+DEAD,+DIED,+I 240 | report.read.filter= #>>> 241 | report.read.sort= project+,kind-,depends-,description+,entry+ 242 | 243 | report.skim.description= Custom [Skim]: List view (Read), condensed for reporting 244 | report.skim.columns= uuid.short,project,tags,priority,due,end,kind,description.truncated_count 245 | report.skim.labels= +UUID,PROJECT,TAGS,P,+DEAD,+DIED,KIND,DESCRIPTION 246 | report.skim.filter= #>>> 247 | report.skim.sort= project+,kind-,depends-,description+,entry+ 248 | 249 | report.sort.description= Custom [Sort]: List view, unfiltered and sorted by dates 250 | report.sort.columns= entry,modified,due,end,id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority,status,imask 251 | report.sort.labels= +BORN,+MOVE,+DEAD,+DIED,ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P,+STAT,+I 252 | report.sort.filter= #>>> 253 | report.sort.sort= end+,due+,modified+,entry+,description+ 254 | 255 | ######################################## 256 | 257 | report.todo.description= Custom [Todo]: Due and priority tasks 258 | report.todo.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority 259 | report.todo.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P 260 | report.todo.filter= ((status:pending (((project:.gtd kind.none: ) or (project:.hpi kind.none: ) or (kind.none: +ANNOTATED) or priority.any: ) +UNBLOCKED) or (+DUE or +TODAY or +OVERDUE)) or +ACTIVE) 261 | report.todo.sort= start-,due+,priority-,urgency-,project+,description+,entry+ 262 | 263 | report.view.description= Custom [View]: Default list view 264 | report.view.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority 265 | report.view.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P 266 | report.view.filter= ((status:pending project.isnt:_data project.isnt:_journal project.isnt:.someday kind.none: (recur.none: or +DUE or +TODAY or +OVERDUE) (+UNBLOCKED or +DUE or +TODAY or +OVERDUE)) or +ACTIVE) 267 | report.view.sort= start-,due+,priority-,urgency-,project+,description+,entry+ 268 | 269 | report.zoom.description= Custom [Zoom]: Default list view (View), condensed for reporting 270 | report.zoom.columns= uuid.short,project,tags,priority,due,end,kind,description.truncated_count 271 | report.zoom.labels= +UUID,PROJECT,TAGS,P,+DEAD,+DIED,KIND,DESCRIPTION 272 | report.zoom.filter= ((status:pending project.isnt:_data project.isnt:_journal project.isnt:.someday kind.none: (recur.none: or +DUE or +TODAY or +OVERDUE) (+UNBLOCKED or +DUE or +TODAY or +OVERDUE)) or +ACTIVE) 273 | report.zoom.sort= start-,due+,priority-,urgency-,project+,description+,entry+ 274 | 275 | #>>>report.next.sort= urgency-,due+,priority-,start-,project+ 276 | 277 | ################################################################################ 278 | 279 | report.agenda.description= Custom [Agenda]: Default list view (View), tag sorted alphabetically 280 | report.agenda.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority 281 | report.agenda.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P 282 | report.agenda.filter= (status:pending tags:agenda (recur.none: or +DUE or +TODAY or +OVERDUE) (+UNBLOCKED or +DUE or +TODAY or +OVERDUE)) 283 | report.agenda.sort= kind-,description+,due+,priority-,urgency-,project+,entry+ 284 | 285 | report.errand.description= Custom [Errand]: Default list view (View), tag sorted alphabetically 286 | report.errand.columns= id,depends,urgency.integer,start.age,due.age,description.truncated_count,project,kind,area,tags,entry.age,recur,priority 287 | report.errand.labels= ID,B,U,S,D,DESCRIPTION,PROJECT,KIND,AREA,TAGS,A,R,P 288 | report.errand.filter= (status:pending tags:errand (recur.none: or +DUE or +TODAY or +OVERDUE) (+UNBLOCKED or +DUE or +TODAY or +OVERDUE)) 289 | report.errand.sort= kind-,description+,due+,priority-,urgency-,project+,entry+ 290 | 291 | report.status.description= Custom [Status]: Default list view (View), status of work actions 292 | report.status.columns= uuid.short,project,tags,priority,due,end,kind,description.truncated_count 293 | report.status.labels= +UUID,PROJECT,TAGS,P,+DEAD,+DIED,KIND,DESCRIPTION 294 | report.status.filter= (status:pending area:work (recur.none: or +DUE or +TODAY or +OVERDUE) (+UNBLOCKED or +DUE or +TODAY or +OVERDUE)) 295 | report.status.sort= kind-,description+,due+,priority-,urgency-,project+,entry+ 296 | 297 | ################################################################################ 298 | # end of file 299 | ################################################################################ 300 | -------------------------------------------------------------------------------- /scripts/gcalendar_export.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | ################################################################################ 5 | # 5.18.2 https://www.perl.org 6 | # 1.730.0 http://search.cpan.org/~ether/WWW-Mechanize/lib/WWW/Mechanize.pm 7 | # 1.370.0 http://search.cpan.org/~phred/Archive-Zip/lib/Archive/Zip.pm 8 | # 0.230.0 http://search.cpan.org/~dagolden/File-Temp/lib/File/Temp.pm 9 | ################################################################################ 10 | 11 | use Carp qw(confess); 12 | #>>>$SIG{__WARN__} = \&confess; 13 | #>>>$SIG{__DIE__} = \&confess; 14 | 15 | use Data::Dumper; 16 | sub DUMPER { 17 | my $DUMP = shift; 18 | local $Data::Dumper::Purity = 1; 19 | print "<-- DUMPER " . ("-" x 30) . ">\n"; 20 | print Dumper(${DUMP}); 21 | print "<-- DUMPER " . ("-" x 30) . ">\n"; 22 | return(0); 23 | }; 24 | 25 | ######################################## 26 | 27 | use WWW::Mechanize; 28 | my $mech = WWW::Mechanize->new( 29 | "agent" => "Mozilla/5.0", 30 | "autocheck" => "1", 31 | "stack_depth" => "0", 32 | "onwarn" => \&mech_fail, 33 | "onerror" => \&mech_fail, 34 | ); 35 | sub mech_fail { 36 | &DUMPER($mech->response()); 37 | &confess(); 38 | }; 39 | 40 | use JSON::XS; 41 | my $json = JSON::XS->new(); 42 | 43 | use Archive::Zip qw(:ERROR_CODES); 44 | use File::Temp qw(tempfile); 45 | use File::Copy; 46 | 47 | ######################################## 48 | 49 | $| = "1"; 50 | 51 | ################################################################################ 52 | 53 | my $C_FILE = "calendar"; 54 | my $D_FILE = "drive"; 55 | my $EXTENSION = ".ics"; 56 | 57 | my $URL_WEB = "https://calendar.google.com"; 58 | my $URL_OAUTH_AUTH = "https://accounts.google.com/o/oauth2/auth"; 59 | my $URL_OAUTH_TOKEN = "https://accounts.google.com/o/oauth2/token"; 60 | my $URL_SCOPE = "https://www.googleapis.com/auth/calendar"; 61 | my $URL_API = "https://www.googleapis.com/calendar/v3"; 62 | 63 | #>>>my $REQ_PER_SEC = "1"; 64 | #>>>my $REQ_PER_SEC_SLEEP = "15"; 65 | my $REQ_PER_SEC = "3"; 66 | my $REQ_PER_SEC_SLEEP = "2"; 67 | 68 | ######################################## 69 | 70 | our $USERNAME; 71 | our $PASSWORD; 72 | our $CLIENTID; 73 | our $CLSECRET; 74 | our $REDIRECT; 75 | do("./.auth-calendar") || die(); 76 | 77 | our $CODE; 78 | our $REFRESH; 79 | our $ACCESS; 80 | do("./.token-calendar") || die(); 81 | 82 | ################################################################################ 83 | 84 | my $REQUEST_COUNT = "0"; 85 | 86 | sub EXIT { 87 | my $status = shift || "0"; 88 | print "\nRequests: ${REQUEST_COUNT}\n"; 89 | exit(${status}); 90 | }; 91 | 92 | ######################################## 93 | 94 | sub auth_login { 95 | my $mech_auth = shift; 96 | 97 | $mech_auth->get(${URL_WEB}); 98 | 99 | #>>> $mech_auth->get("https://accounts.google.com/ServiceLogin"); 100 | $mech_auth->form_id("gaia_loginform"); 101 | $mech_auth->field("Email", ${USERNAME}); 102 | $mech_auth->field("Passwd", ${PASSWORD}); 103 | $mech_auth->submit(); 104 | 105 | #>>> $mech_auth->get("https://accounts.google.com/AccountLoginInfo"); 106 | $mech_auth->form_id("gaia_loginform"); 107 | $mech_auth->field("Email", ${USERNAME}); 108 | $mech_auth->field("Passwd", ${PASSWORD}); 109 | $mech_auth->submit(); 110 | 111 | return(${mech_auth}); 112 | }; 113 | 114 | ######################################## 115 | 116 | sub refresh_tokens { 117 | if (!${CODE} || !${REFRESH}) { 118 | $mech = &auth_login(${mech}); 119 | 120 | $mech->get(${URL_OAUTH_AUTH} 121 | . "?client_id=${CLIENTID}" 122 | . "&redirect_uri=${REDIRECT}" 123 | . "&scope=${URL_SCOPE}" 124 | . "&response_type=code" 125 | ); 126 | $mech->submit_form( 127 | "form_id" => "connect-approve", 128 | "fields" => {"submit_access" => "true"}, 129 | ); 130 | $CODE = $mech->content(); 131 | $CODE =~ s|^.*post(${URL_OAUTH_TOKEN}, { 135 | "code" => ${CODE}, 136 | "client_id" => ${CLIENTID}, 137 | "client_secret" => ${CLSECRET}, 138 | "redirect_uri" => ${REDIRECT}, 139 | "grant_type" => "authorization_code", 140 | }); 141 | $REFRESH = decode_json($mech->content()); 142 | $REFRESH = $REFRESH->{"refresh_token"}; 143 | 144 | open(OUTPUT, ">", ".token") || die(); 145 | print OUTPUT "our \$CODE = '${CODE}';\n"; 146 | print OUTPUT "our \$REFRESH = '${REFRESH}';\n"; 147 | close(OUTPUT) || die(); 148 | }; 149 | 150 | $mech->post(${URL_OAUTH_TOKEN}, { 151 | "refresh_token" => ${REFRESH}, 152 | "client_id" => ${CLIENTID}, 153 | "client_secret" => ${CLSECRET}, 154 | "grant_type" => "refresh_token", 155 | }); 156 | $ACCESS = decode_json($mech->content()); 157 | $ACCESS = $ACCESS->{"access_token"}; 158 | 159 | print "CODE: ${CODE}\n"; 160 | print "REFRESH: ${REFRESH}\n"; 161 | print "ACCESS: ${ACCESS}\n"; 162 | print "\n"; 163 | 164 | $mech->add_header("Authorization" => "Bearer ${ACCESS}"); 165 | 166 | return(0); 167 | }; 168 | 169 | ################################################################################ 170 | 171 | sub get_calendar { 172 | my $name = shift; 173 | my $id = shift; 174 | 175 | $name = "${C_FILE}-${name}${EXTENSION}"; 176 | print "${name} :: ${id}\n"; 177 | 178 | my($TEMPFILE, $tempfile) = tempfile(".${C_FILE}.XXXX", "UNLINK" => "1"); 179 | 180 | my $zip = Archive::Zip->new(); 181 | 182 | if ((${REQUEST_COUNT} % ${REQ_PER_SEC}) == 0) { 183 | sleep(${REQ_PER_SEC_SLEEP}); 184 | }; 185 | #>>> $mech->get("https://calendar.google.com/calendar/exporticalzip?cexp=${id}") && $REQUEST_COUNT++; 186 | $mech->get("https://apidata.googleusercontent.com/caldav/v2/${id}/events") && $REQUEST_COUNT++; 187 | #>>> 188 | $mech->save_content($tempfile, binmode => ":raw:utf8"); 189 | #>>> &DUMPER(${mech}); 190 | 191 | #>>> if ($zip->read($tempfile) != AZ_OK) { die(); }; 192 | #>>>#>>> &DUMPER($zip); 193 | #>>> my @files = $zip->memberNames(); 194 | #>>> my $files = \@files; 195 | #>>>#>>> &DUMPER($files); 196 | #>>> foreach my $file (@{$files}) { 197 | #>>> print "\t${tempfile} -> ${file} -> ${name}\n"; 198 | #>>> if ($zip->extractMember(${file}) != AZ_OK) { die(); }; 199 | #>>> move(${file}, ${name}); 200 | #>>> }; 201 | move($tempfile, ${name}); 202 | #>>> 203 | 204 | close(${TEMPFILE}) || die(); 205 | 206 | return(0); 207 | }; 208 | 209 | ######################################## 210 | 211 | sub get_drive { 212 | my $name = shift; 213 | my $id = shift; 214 | 215 | $name = "${D_FILE}-${name}"; 216 | print "${name} :: ${id}\n"; 217 | 218 | my($TEMPFILE, $tempfile) = tempfile(".${D_FILE}.XXXX", "UNLINK" => "1"); 219 | 220 | if ((${REQUEST_COUNT} % ${REQ_PER_SEC}) == 0) { 221 | sleep(${REQ_PER_SEC_SLEEP}); 222 | }; 223 | $mech->get("https://docs.google.com/uc?export=download&id=${id}") && $REQUEST_COUNT++; 224 | $mech->save_content($tempfile); 225 | #>>> &DUMPER(${mech}); 226 | 227 | print "\t${tempfile} -> ${name}\n"; 228 | move($tempfile, ${name}); 229 | 230 | close(${TEMPFILE}) || die(); 231 | 232 | return(0); 233 | }; 234 | 235 | ################################################################################ 236 | 237 | if (@{ARGV}) { 238 | &refresh_tokens(); 239 | 240 | foreach my $calendar (@{ARGV}) { 241 | my($type, $data) = split(/[|]/, ${calendar}); 242 | my($name, $id) = split(/[:]/, ${data}); 243 | if (${type} eq "c") { 244 | &get_calendar(${name}, ${id}); 245 | } 246 | elsif (${type} eq "d") { 247 | &get_drive(${name}, ${id}); 248 | } 249 | else { 250 | die("INVALID TYPE [${calendar}]!"); 251 | }; 252 | }; 253 | }; 254 | 255 | ######################################## 256 | 257 | &EXIT(0); 258 | 259 | ################################################################################ 260 | # end of file 261 | ################################################################################ 262 | -------------------------------------------------------------------------------- /scripts/gdrive_export.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | ################################################################################ 5 | # https://developers.google.com/accounts/docs/OAuth2InstalledApp 6 | # https://developers.google.com/google-apps/tasks/v1/reference 7 | ################################################################################ 8 | # 5.18.2 https://www.perl.org 9 | # 1.730.0 http://search.cpan.org/~ether/WWW-Mechanize/lib/WWW/Mechanize.pm 10 | # 6.50.0 http://search.cpan.org/~gaas/libwww-perl/lib/LWP/UserAgent.pm 11 | # 6.30.0 http://search.cpan.org/~gaas/HTTP-Message/lib/HTTP/Request.pm 12 | # 2.272.20 http://search.cpan.org/~makamaka/JSON-PP/lib/JSON/PP.pm 13 | # 0.230.0 http://search.cpan.org/~dagolden/File-Temp-0.2304/lib/File/Temp.pm 14 | ################################################################################ 15 | 16 | use Carp qw(confess); 17 | #>>>$SIG{__WARN__} = \&confess; 18 | #>>>$SIG{__DIE__} = \&confess; 19 | 20 | use Data::Dumper; 21 | sub DUMPER { 22 | my $DUMP = shift; 23 | local $Data::Dumper::Purity = 1; 24 | print "<-- DUMPER " . ("-" x 30) . ">\n"; 25 | print Dumper(${DUMP}); 26 | print "<-- DUMPER " . ("-" x 30) . ">\n"; 27 | return(0); 28 | }; 29 | 30 | ######################################## 31 | 32 | use WWW::Mechanize; 33 | my $mech = WWW::Mechanize->new( 34 | "agent" => "Mozilla/5.0", 35 | "autocheck" => "1", 36 | "stack_depth" => "0", 37 | "onwarn" => \&mech_fail, 38 | "onerror" => \&mech_fail, 39 | ); 40 | sub mech_fail { 41 | &DUMPER($mech->response()); 42 | &confess(); 43 | }; 44 | 45 | use HTTP::Request; 46 | use JSON::XS; 47 | 48 | ######################################## 49 | 50 | $| = "1"; 51 | 52 | ################################################################################ 53 | 54 | my $FILE = "drive"; 55 | 56 | my $URL_WEB = "https://mail.google.com/tasks/canvas"; 57 | my $URL_OAUTH_AUTH = "https://accounts.google.com/o/oauth2/auth"; 58 | my $URL_OAUTH_TOKEN = "https://accounts.google.com/o/oauth2/token"; 59 | my $URL_SCOPE = "https://www.googleapis.com/auth/drive"; 60 | my $URL_API_UP = "https://www.googleapis.com/upload/drive/v3"; 61 | my $URL_API = "https://www.googleapis.com/drive/v3"; 62 | 63 | my $REQ_PER_SEC = "3"; 64 | my $REQ_PER_SEC_SLEEP = "2"; 65 | 66 | ######################################## 67 | 68 | our $USERNAME; 69 | our $PASSWORD; 70 | our $CLIENTID; 71 | our $CLSECRET; 72 | our $REDIRECT; 73 | do("./.auth-${FILE}") || die(); 74 | 75 | our $CODE; 76 | our $REFRESH; 77 | our $ACCESS; 78 | do("./.token-${FILE}") || die($!); 79 | 80 | ################################################################################ 81 | 82 | my $API_REQUEST_COUNT = "0"; 83 | 84 | sub EXIT { 85 | my $status = shift || "0"; 86 | print "\nAPI Requests: ${API_REQUEST_COUNT}\n"; 87 | exit(${status}); 88 | }; 89 | 90 | ######################################## 91 | 92 | sub auth_login { 93 | my $mech_auth = shift; 94 | 95 | $mech_auth->get(${URL_WEB}); 96 | 97 | #>>> $mech_auth->get("https://accounts.google.com/ServiceLogin"); 98 | $mech_auth->form_id("gaia_loginform"); 99 | $mech_auth->field("Email", ${USERNAME}); 100 | $mech_auth->field("Passwd", ${PASSWORD}); 101 | $mech_auth->submit(); 102 | 103 | #>>> $mech_auth->get("https://accounts.google.com/AccountLoginInfo"); 104 | $mech_auth->form_id("gaia_loginform"); 105 | $mech_auth->field("Email", ${USERNAME}); 106 | $mech_auth->field("Passwd", ${PASSWORD}); 107 | $mech_auth->submit(); 108 | 109 | return(${mech_auth}); 110 | }; 111 | 112 | ######################################## 113 | 114 | sub refresh_tokens { 115 | if (!${CODE} || !${REFRESH}) { 116 | $mech = &auth_login(${mech}); 117 | 118 | $mech->get(${URL_OAUTH_AUTH} 119 | . "?client_id=${CLIENTID}" 120 | . "&redirect_uri=${REDIRECT}" 121 | . "&scope=${URL_SCOPE}" 122 | . "&response_type=code" 123 | ); 124 | $mech->submit_form( 125 | "form_id" => "connect-approve", 126 | "fields" => {"submit_access" => "true"}, 127 | ); 128 | $CODE = $mech->content(); 129 | $CODE =~ s|^.*post(${URL_OAUTH_TOKEN}, { 133 | "code" => ${CODE}, 134 | "client_id" => ${CLIENTID}, 135 | "client_secret" => ${CLSECRET}, 136 | "redirect_uri" => ${REDIRECT}, 137 | "grant_type" => "authorization_code", 138 | }); 139 | $REFRESH = decode_json($mech->content()); 140 | $REFRESH = $REFRESH->{"refresh_token"}; 141 | 142 | open(OUTPUT, ">", ".token-${FILE}") || die(); 143 | print OUTPUT "our \$CODE = '${CODE}';\n"; 144 | print OUTPUT "our \$REFRESH = '${REFRESH}';\n"; 145 | close(OUTPUT) || die(); 146 | }; 147 | 148 | $mech->post(${URL_OAUTH_TOKEN}, { 149 | "refresh_token" => ${REFRESH}, 150 | "client_id" => ${CLIENTID}, 151 | "client_secret" => ${CLSECRET}, 152 | "grant_type" => "refresh_token", 153 | }); 154 | $ACCESS = decode_json($mech->content()); 155 | $ACCESS = $ACCESS->{"access_token"}; 156 | 157 | print "CODE: ${CODE}\n"; 158 | print "REFRESH: ${REFRESH}\n"; 159 | print "ACCESS: ${ACCESS}\n"; 160 | 161 | $mech->add_header("Authorization" => "Bearer ${ACCESS}"); 162 | 163 | return(0); 164 | }; 165 | 166 | ######################################## 167 | 168 | sub api_req_per_sec { 169 | $API_REQUEST_COUNT++; 170 | if ((${API_REQUEST_COUNT} % ${REQ_PER_SEC}) == 0) { 171 | sleep(${REQ_PER_SEC_SLEEP}); 172 | }; 173 | return(); 174 | }; 175 | 176 | ################################################################################ 177 | 178 | sub api_upload { 179 | my $file = shift; 180 | my $id = shift; 181 | 182 | open(INPUT, "<", ${file}) || die(); 183 | my $input = do { local $/; }; 184 | close(INPUT) || die(); 185 | 186 | $mech->request(HTTP::Request->new( 187 | "PATCH", "${URL_API_UP}/files/${id}?uploadType=media", [], ${input}, 188 | )) && api_req_per_sec(); 189 | 190 | print "${file} ("; 191 | { use bytes; print length(${input}); }; 192 | print ") -> ${id}\n"; 193 | 194 | return(0); 195 | }; 196 | 197 | ######################################## 198 | 199 | sub api_download { 200 | my $file = shift; 201 | my $id = shift; 202 | 203 | $mech->get("${URL_API}/files/${id}?alt=media") && api_req_per_sec(); 204 | my $output = $mech->content(); 205 | 206 | open(OUTPUT, ">", ${file}) || die(); 207 | print OUTPUT ${output}; 208 | close(OUTPUT) || die(); 209 | 210 | print "${id} -> ${file} ("; 211 | { use bytes; print length(${output}); }; 212 | print ")\n"; 213 | 214 | return(0); 215 | }; 216 | 217 | ################################################################################ 218 | 219 | if (@{ARGV}) { 220 | &refresh_tokens(); 221 | print "\n"; 222 | 223 | my $upload = "0"; 224 | my $num = "0"; 225 | while ($ARGV[${num}]) { 226 | if ($ARGV[${num}] eq "upload") { 227 | $upload = "1"; 228 | splice(@{ARGV}, ${num}, 1); 229 | ${num}--; 230 | }; 231 | ${num}++; 232 | }; 233 | 234 | foreach my $drive (@{ARGV}) { 235 | my($file, $id) = split(/[:]/, ${drive}); 236 | if (${upload}) { 237 | &api_upload(${file}, ${id}); 238 | } else { 239 | &api_download(${file}, ${id}); 240 | }; 241 | }; 242 | }; 243 | 244 | ######################################## 245 | 246 | &EXIT(0); 247 | 248 | ################################################################################ 249 | # end of file 250 | ################################################################################ 251 | -------------------------------------------------------------------------------- /scripts/gtasks_export.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | ################################################################################ 5 | # https://developers.google.com/accounts/docs/OAuth2InstalledApp 6 | # https://developers.google.com/google-apps/tasks/v1/reference 7 | ################################################################################ 8 | # 5.18.2 https://www.perl.org 9 | # 1.730.0 http://search.cpan.org/~ether/WWW-Mechanize/lib/WWW/Mechanize.pm 10 | # 6.50.0 http://search.cpan.org/~gaas/libwww-perl/lib/LWP/UserAgent.pm 11 | # 6.30.0 http://search.cpan.org/~gaas/HTTP-Message/lib/HTTP/Request.pm 12 | # 2.272.20 http://search.cpan.org/~makamaka/JSON-PP/lib/JSON/PP.pm 13 | # 0.230.0 http://search.cpan.org/~dagolden/File-Temp-0.2304/lib/File/Temp.pm 14 | ################################################################################ 15 | 16 | use Carp qw(confess); 17 | #>>>$SIG{__WARN__} = \&confess; 18 | #>>>$SIG{__DIE__} = \&confess; 19 | 20 | use Data::Dumper; 21 | sub DUMPER { 22 | my $DUMP = shift; 23 | local $Data::Dumper::Purity = 1; 24 | print "<-- DUMPER " . ("-" x 30) . ">\n"; 25 | print Dumper(${DUMP}); 26 | print "<-- DUMPER " . ("-" x 30) . ">\n"; 27 | return(0); 28 | }; 29 | 30 | ######################################## 31 | 32 | use WWW::Mechanize; 33 | my $mech = WWW::Mechanize->new( 34 | "agent" => "Mozilla/5.0", 35 | "autocheck" => "1", 36 | "stack_depth" => "0", 37 | "onwarn" => \&mech_fail, 38 | "onerror" => \&mech_fail, 39 | ); 40 | sub mech_fail { 41 | &DUMPER($mech->response()); 42 | &confess(); 43 | }; 44 | #>>> ${web}, ${PURGE_LIST}, ${URL_WEB_POST} and ${purge_list} only exist for the hackery in &{purge_lists}! 45 | #my $web = WWW::Mechanize->new( 46 | # "agent" => "Mozilla/5.0", 47 | # "autocheck" => "1", 48 | # "stack_depth" => "0", 49 | # "onwarn" => \&web_fail, 50 | # "onerror" => \&web_fail, 51 | #); 52 | #sub web_fail { 53 | # &DUMPER($web->response()); 54 | # &confess(); 55 | #}; 56 | #>>> 57 | 58 | use HTTP::Request; 59 | use JSON::XS; 60 | my $json = JSON::XS->new(); 61 | 62 | use File::Temp qw(tempfile); 63 | use MIME::Base64; 64 | 65 | use POSIX qw(strftime); 66 | 67 | ######################################## 68 | 69 | $| = "1"; 70 | 71 | ################################################################################ 72 | 73 | my $FILE = "tasks"; 74 | my $DEFAULT_LIST = "0.GTD"; 75 | my $PROJECT_LIST = "0.Projects"; 76 | #>>> ${web}, ${PURGE_LIST}, ${URL_WEB_POST} and ${purge_list} only exist for the hackery in &{purge_lists}! 77 | #my $PURGE_LIST = "PURGE"; 78 | #>>> 79 | 80 | my $PROJ_LINK_NORMAL = "*"; 81 | my $PROJ_LINK_OPEN = "="; 82 | my $PROJ_LINK_CLOSED = "x"; 83 | my $PROJ_LINK_SEPARATE = ": "; 84 | 85 | my $NOTES_APPEND = "[...]"; 86 | my $NOTES_LENGTH = ((2**13) - length(${NOTES_APPEND})); 87 | 88 | my $INDENT = " "; 89 | 90 | my $URL_WEB = "https://mail.google.com/tasks/canvas"; 91 | my $URL_OAUTH_AUTH = "https://accounts.google.com/o/oauth2/auth"; 92 | my $URL_OAUTH_TOKEN = "https://accounts.google.com/o/oauth2/token"; 93 | my $URL_SCOPE = "https://www.googleapis.com/auth/tasks"; 94 | my $URL_API = "https://www.googleapis.com/tasks/v1"; 95 | #>>> ${web}, ${PURGE_LIST}, ${URL_WEB_POST} and ${purge_list} only exist for the hackery in &{purge_lists}! 96 | #my $URL_WEB_POST = "https://mail.google.com/tasks/r/d"; 97 | #>>> 98 | 99 | my $REQ_PER_SEC = "3"; 100 | my $REQ_PER_SEC_SLEEP = "2"; 101 | 102 | ######################################## 103 | 104 | my $SEARCH_FIELDS = [ 105 | "title", 106 | "due", 107 | "notes", 108 | ]; 109 | 110 | my $MANAGE_LINKS_ALL = "0"; 111 | my $MLINK_SRC = "PARENTS"; 112 | my $MLINK_DST = "CHILDREN"; 113 | 114 | my $MANAGE_CRUFT_ALL = "1"; 115 | 116 | my $EXPORT_JSON = "1"; 117 | my $EXPORT_CSV = "1"; 118 | my $EXPORT_TXT = "1"; 119 | 120 | my $JSON_FIELDS = [ 121 | "kind", 122 | "id", 123 | "etag", 124 | "title", 125 | "updated", 126 | "selfLink", 127 | "parent", 128 | "position", 129 | "notes", 130 | "status", 131 | "due", 132 | "completed", 133 | "deleted", 134 | "hidden", 135 | "links", 136 | 137 | "nextPageToken", 138 | "items", 139 | ]; 140 | 141 | my $CSV_FIELDS = [ 142 | "title", 143 | 144 | "due", 145 | "status", 146 | "completed", 147 | "deleted", 148 | "hidden", 149 | "notes", 150 | 151 | "kind", 152 | "id", 153 | "etag", 154 | "selfLink", 155 | "updated", 156 | 157 | "parent", 158 | "position", 159 | "links", 160 | ]; 161 | 162 | my $HIDE_COMPLETED = "0"; 163 | my $HIDE_DELETED = "0"; 164 | 165 | my $CAT_TEXT = "0"; 166 | 167 | ######################################## 168 | 169 | #>>> JSON Methods 170 | 171 | $json->allow_blessed(0); 172 | $json->allow_nonref(0); 173 | $json->allow_unknown(0); 174 | $json->convert_blessed(0); 175 | $json->relaxed(0); 176 | 177 | $json->ascii(1); 178 | $json->latin1(0); 179 | $json->utf8(0); 180 | 181 | $json->canonical(0); 182 | $json->pretty(0); 183 | $json->shrink(0); 184 | 185 | $json->indent(1); 186 | $json->space_after(1); 187 | $json->space_before(0); 188 | 189 | #>>> JSON::PP Methods 190 | 191 | #> $json->loose(0); 192 | #> 193 | #> $json->escape_slash(0); 194 | #> $json->indent_length(1); 195 | #> 196 | #> $json->sort_by(sub { 197 | #> my $order = {}; 198 | #> #>>> http://learn.perl.org/faq/perlfaq4.html#How-do-I-merge-two-hashes- 199 | #> @{ $order }{@{ $JSON_FIELDS }} = 0..$#{ $JSON_FIELDS }; 200 | #> if (exists($order->{$JSON::PP::a}) && exists($order->{$JSON::PP::b})) { 201 | #> $order->{$JSON::PP::a} <=> $order->{$JSON::PP::b}; 202 | #> } else { 203 | #> $JSON::PP::a cmp $JSON::PP::b; 204 | #> }; 205 | #> }); 206 | 207 | ######################################## 208 | 209 | our $USERNAME; 210 | our $PASSWORD; 211 | our $CLIENTID; 212 | our $CLSECRET; 213 | our $REDIRECT; 214 | do("./.auth") || die(); 215 | 216 | our $CODE; 217 | our $REFRESH; 218 | our $ACCESS; 219 | do("./.token") || die(); 220 | 221 | ################################################################################ 222 | 223 | my $API_ERROR = "GTASKS_EXPORT_ERROR"; 224 | my $API_PAGES = "GTASKS_EXPORT_PAGES"; 225 | 226 | my $API_REQUEST_COUNT = "0"; 227 | 228 | sub EXIT { 229 | my $status = shift || "0"; 230 | print "\nAPI Requests: ${API_REQUEST_COUNT}\n"; 231 | exit(${status}); 232 | }; 233 | 234 | ######################################## 235 | 236 | sub auth_login { 237 | my $mech_auth = shift; 238 | 239 | $mech_auth->get(${URL_WEB}); 240 | 241 | #>>> $mech_auth->get("https://accounts.google.com/ServiceLogin"); 242 | $mech_auth->form_id("gaia_loginform"); 243 | $mech_auth->field("Email", ${USERNAME}); 244 | $mech_auth->field("Passwd", ${PASSWORD}); 245 | $mech_auth->submit(); 246 | 247 | #>>> $mech_auth->get("https://accounts.google.com/AccountLoginInfo"); 248 | $mech_auth->form_id("gaia_loginform"); 249 | $mech_auth->field("Email", ${USERNAME}); 250 | $mech_auth->field("Passwd", ${PASSWORD}); 251 | $mech_auth->submit(); 252 | 253 | return(${mech_auth}); 254 | }; 255 | 256 | ######################################## 257 | 258 | sub refresh_tokens { 259 | if (!${CODE} || !${REFRESH}) { 260 | $mech = &auth_login(${mech}); 261 | 262 | $mech->get(${URL_OAUTH_AUTH} 263 | . "?client_id=${CLIENTID}" 264 | . "&redirect_uri=${REDIRECT}" 265 | . "&scope=${URL_SCOPE}" 266 | . "&response_type=code" 267 | ); 268 | $mech->submit_form( 269 | "form_id" => "connect-approve", 270 | "fields" => {"submit_access" => "true"}, 271 | ); 272 | $CODE = $mech->content(); 273 | $CODE =~ s|^.*post(${URL_OAUTH_TOKEN}, { 277 | "code" => ${CODE}, 278 | "client_id" => ${CLIENTID}, 279 | "client_secret" => ${CLSECRET}, 280 | "redirect_uri" => ${REDIRECT}, 281 | "grant_type" => "authorization_code", 282 | }); 283 | $REFRESH = decode_json($mech->content()); 284 | $REFRESH = $REFRESH->{"refresh_token"}; 285 | 286 | open(OUTPUT, ">", ".token") || die(); 287 | print OUTPUT "our \$CODE = '${CODE}';\n"; 288 | print OUTPUT "our \$REFRESH = '${REFRESH}';\n"; 289 | close(OUTPUT) || die(); 290 | }; 291 | 292 | $mech->post(${URL_OAUTH_TOKEN}, { 293 | "refresh_token" => ${REFRESH}, 294 | "client_id" => ${CLIENTID}, 295 | "client_secret" => ${CLSECRET}, 296 | "grant_type" => "refresh_token", 297 | }); 298 | $ACCESS = decode_json($mech->content()); 299 | $ACCESS = $ACCESS->{"access_token"}; 300 | 301 | print "CODE: ${CODE}\n"; 302 | print "REFRESH: ${REFRESH}\n"; 303 | print "ACCESS: ${ACCESS}\n"; 304 | 305 | $mech->add_header("Authorization" => "Bearer ${ACCESS}"); 306 | 307 | return(0); 308 | }; 309 | 310 | ######################################## 311 | 312 | sub api_req_per_sec { 313 | $API_REQUEST_COUNT++; 314 | if ((${API_REQUEST_COUNT} % ${REQ_PER_SEC}) == 0) { 315 | sleep(${REQ_PER_SEC_SLEEP}); 316 | }; 317 | return(); 318 | }; 319 | 320 | ################################################################################ 321 | 322 | sub api_get { 323 | my $url = shift; 324 | my $fields = shift; 325 | my $object; 326 | my $page; 327 | 328 | #>>> BUG IN GOOGLE TASKS API! 329 | #>>> http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=2837 330 | #>>> SHOULD BE ABLE TO REQUEST AN ARBITRARY AMOUNT 331 | $url .= "?maxResults=100"; 332 | 333 | if (defined(${fields})) { 334 | foreach my $field (keys(%{$fields})) { 335 | $url .= "&" . ${field} . "=" . $fields->{$field}; 336 | }; 337 | }; 338 | 339 | do { 340 | $mech->get("${url}" 341 | . (defined(${page}) ? "&pageToken=${page}" : "") 342 | ) && api_req_per_sec(); 343 | my $out = decode_json($mech->content()); 344 | 345 | #>>> http://www.perlmonks.org/?node_id=995613 346 | foreach my $key (keys(%{$out})) { 347 | if (exists($object->{$key}) && $object->{$key} ne $out->{$key}) { 348 | if (ref($object->{$key}) eq "ARRAY") { 349 | push(@{$object->{$key}}, @{$out->{$key}}); 350 | } else { 351 | push(@{$object->{$API_ERROR}}, [ ${key}, $object->{$key}, $out->{$key} ]); 352 | }; 353 | } else { 354 | $object->{$key} = $out->{$key}; 355 | }; 356 | } 357 | 358 | $page = $out->{"nextPageToken"}; 359 | delete($out->{"nextPageToken"}); 360 | delete($object->{"nextPageToken"}); 361 | $object->{$API_PAGES}++; 362 | } 363 | until (!defined(${page})); 364 | 365 | return(${object}); 366 | }; 367 | 368 | ######################################## 369 | 370 | sub api_method { 371 | my $method = shift; 372 | my $object = shift; 373 | my $fields = shift; 374 | my $selflink = $object->{"selfLink"}; 375 | if ($fields->{"parent"}) { 376 | $selflink .= "?"; 377 | $selflink .= "parent=" . ($fields->{"parent"} || ""); 378 | }; 379 | if ($fields->{"previous"}) { 380 | if (!$fields->{"parent"}) { $selflink .= "?"; 381 | } else { $selflink .= "&"; 382 | }; 383 | $selflink .= "previous=" . ($fields->{"previous"} || ""); 384 | }; 385 | if ($fields->{"notes"}) { 386 | $fields->{"notes"} = substr($fields->{"notes"} || "", 0, ${NOTES_LENGTH}) . ${NOTES_APPEND}; 387 | }; 388 | $mech->request(HTTP::Request->new( 389 | ${method}, ${selflink}, ["Content-Type", "application/json"], encode_json(${fields}), 390 | )) && api_req_per_sec(); 391 | if (${method} eq "DELETE") { 392 | return(0); 393 | } else { 394 | return(decode_json($mech->content())); 395 | }; 396 | }; 397 | 398 | ################################################################################ 399 | 400 | sub api_create_list { 401 | my $fields = shift; 402 | my $object = {}; 403 | #>>> THIS IS A HACK! 404 | $object->{"selfLink"} = "${URL_API}/users/\@me/lists"; 405 | #>>> 406 | my $output = &api_method("POST", ${object}, ${fields}); 407 | return(${output}); 408 | }; 409 | 410 | ######################################## 411 | 412 | sub api_fetch_lists { 413 | my $output = &api_get("${URL_API}/users/\@me/lists"); 414 | return(${output}); 415 | }; 416 | 417 | ######################################## 418 | 419 | sub api_create_task { 420 | my $listid = shift; 421 | my $fields = shift; 422 | my $object = {}; 423 | #>>> THIS IS A HACK! 424 | $object->{"selfLink"} = "${URL_API}/lists/${listid}/tasks"; 425 | #>>> 426 | my $output = &api_method("POST", ${object}, ${fields}); 427 | return(${output}); 428 | }; 429 | 430 | ######################################## 431 | 432 | sub api_fetch_tasks { 433 | my $listid = shift; 434 | my $output = &api_get("${URL_API}/lists/${listid}/tasks", { 435 | "showCompleted" => "true", 436 | "showDeleted" => "true", 437 | "showHidden" => "true", 438 | }); 439 | return(${output}); 440 | }; 441 | 442 | ################################################################################ 443 | 444 | sub taskwarrior_export { 445 | my $title = shift || "Export"; 446 | my $tasks = shift || ""; 447 | my $search = shift || ""; 448 | my $links = []; 449 | my $previous = undef; 450 | my $filter; 451 | my $fields; 452 | my $created; 453 | my $listid; 454 | my $output; 455 | 456 | print "\n${title}: "; 457 | 458 | if (!${tasks}) { 459 | $tasks = qx(task show rc.defaultwidth=0 "default.command" 2>&1); $tasks =~ m/^default[.]command[ ](.*)$/gm; $tasks = (${1} || ""); 460 | }; 461 | $filter = qx(task show rc.defaultwidth=0 "report.${tasks}.filter" 2>&1); $filter =~ m/^report[.]${tasks}[.]filter[ ](.*)$/gm; $filter = (${1} || ""); 462 | $fields = qx(task show rc.defaultwidth=0 "report.${tasks}.sort" 2>&1); $fields =~ m/^report[.]${tasks}[.]sort[ ](.*)$/gm; $fields = (${1} || ""); 463 | $tasks = qx(task export "${filter}" "${search}"); 464 | $tasks =~ s/\n//g; 465 | $tasks = decode_json(${tasks}); 466 | 467 | $output = &api_fetch_lists(); 468 | 469 | foreach my $tasklist (@{$output->{"items"}}) { 470 | if ($tasklist->{"title"} eq ${title}) { 471 | $created = "1"; 472 | $listid = $tasklist->{"id"}; 473 | 474 | $output = &api_fetch_tasks($tasklist->{"id"}); 475 | 476 | foreach my $task (@{$output->{"items"}}) { 477 | push(@{$links}, ${task}); 478 | }; 479 | 480 | last(); 481 | }; 482 | }; 483 | if (!${created}) { 484 | $output = &api_create_list({ 485 | "title" => ${title}, 486 | }); 487 | $listid = $output->{"id"}; 488 | }; 489 | 490 | #>>> BUG IN PERL! 491 | #>>> http://www.perlmonks.org/?node_id=490213 492 | my @array = @{$tasks}; 493 | my @yarra = split(/,/, ${fields}); 494 | foreach my $task (sort({ 495 | my $result = "0"; 496 | my %pri = ("H" => 3, "M" => 2, "L" => 1); 497 | foreach my $field_source (@{yarra}) { 498 | my $field = ${field_source}; 499 | my $order = ""; 500 | if ($field =~ m/[+-]$/) { 501 | $field =~ s/([+-])$//g; 502 | $order = ${1}; 503 | }; 504 | if (${field} eq "priority") { 505 | my $aa = ($a->{$field} || ""); 506 | my $bb = ($b->{$field} || ""); 507 | if (${order} eq "-") { 508 | $result = ($pri{$bb} || "0") <=> ($pri{$aa} || "0"); 509 | } else { 510 | $result = ($pri{$aa} || "0") <=> ($pri{$bb} || "0"); 511 | }; 512 | } elsif ( 513 | (${field} eq "id") || 514 | (${field} eq "imask") || 515 | (${field} eq "urgency") 516 | ) { 517 | if (${order} eq "-") { 518 | $result = ($b->{$field} || "0") <=> ($a->{$field} || "0"); 519 | } else { 520 | $result = ($a->{$field} || "0") <=> ($b->{$field} || "0"); 521 | }; 522 | } elsif ( 523 | (${field} eq "due") || 524 | (${field} eq "end") || 525 | (${field} eq "entry") || 526 | (${field} eq "modified") || 527 | (${field} eq "scheduled") || 528 | (${field} eq "start") || 529 | (${field} eq "until") || 530 | (${field} eq "wait") 531 | ) { 532 | if (${order} eq "-") { 533 | $result = ($b->{$field} || "0") cmp ($a->{$field} || "0"); 534 | } else { 535 | $result = ($a->{$field} || "9999") cmp ($b->{$field} || "9999"); 536 | }; 537 | } else { 538 | if (${order} eq "-") { 539 | $result = ($b->{$field} || "") cmp ($a->{$field} || ""); 540 | } else { 541 | $result = ($a->{$field} || "") cmp ($b->{$field} || ""); 542 | }; 543 | }; 544 | if (${result} != 0) { 545 | last(); 546 | }; 547 | }; 548 | return(${result}); 549 | } @{array})) { 550 | #>>> 551 | if ($task->{"status"} eq "deleted") { 552 | $task->{"deleted"} = "true"; 553 | }; 554 | $task->{"status"} = "needsAction"; 555 | $task->{"notes"} = ""; 556 | if (defined($task->{"due"})) { 557 | $task->{"due"} =~ s/^([0-9]{4})([0-9]{2})([0-9]{2})[T]([0-9]{2})([0-9]{2})([0-9]{2})[Z]$/$1-$2-$3T$4:$5:$6Z/; 558 | }; 559 | if (defined($task->{"end"})) { 560 | $task->{"end"} =~ s/^([0-9]{4})([0-9]{2})([0-9]{2})[T]([0-9]{2})([0-9]{2})([0-9]{2})[Z]$/$1-$2-$3T$4:$5:$6Z/; 561 | $task->{"status"} = "completed"; 562 | }; 563 | if (defined($task->{"annotations"})) { 564 | foreach my $annotation (@{$task->{"annotations"}}) { 565 | if ($annotation->{"description"} =~ /^[[]notes[]][:]/) { 566 | my $notes = $annotation->{"description"}; 567 | $notes =~ s/^[[]notes[]][:]//g; 568 | $task->{"notes"} = decode_base64(${notes}); 569 | }; 570 | }; 571 | }; 572 | my $task_title = $task->{"description"}; 573 | if (defined($task->{"project"})) { $task_title = "[" . $task->{"project"} . "] " . ${task_title} ; }; 574 | if (defined($task->{"priority"})) { $task_title = "<" . $task->{"priority"} . "> " . ${task_title} ; }; 575 | if (defined($task->{"tags"})) { $task_title .= " @" . join(" @", @{$task->{"tags"}}) ; }; 576 | if (defined($task->{"uuid"})) { $task_title .= " [" . substr($task->{"uuid"}, 0, 8) . "]" ; }; 577 | my $blob = { 578 | "title" => ${task_title}, 579 | "status" => $task->{"status"}, 580 | "due" => $task->{"due"}, 581 | "completed" => $task->{"end"}, 582 | "deleted" => $task->{"deleted"}, 583 | "notes" => $task->{"notes"}, 584 | "parent" => undef, 585 | "previous" => ${previous}, 586 | }; 587 | if (@{$links}) { 588 | $output = &api_method("PATCH", shift(@{$links}), ${blob}); 589 | $previous = $output->{"id"}; 590 | print "="; 591 | } else { 592 | $output = &api_create_task(${listid}, ${blob}); 593 | $previous = $output->{"id"}; 594 | print "+"; 595 | }; 596 | }; 597 | 598 | while (@{$links}) { 599 | $output = &api_method("PATCH", shift(@{$links}), { 600 | "title" => "0", 601 | "status" => "needsAction", 602 | "due" => undef, 603 | "completed" => undef, 604 | "deleted" => JSON::XS::true, 605 | "notes" => "", 606 | "parent" => undef, 607 | "previous" => ${previous}, 608 | }); 609 | $previous = $output->{"id"}; 610 | print "-"; 611 | }; 612 | 613 | print "\n"; 614 | 615 | return(0); 616 | }; 617 | 618 | ######################################## 619 | 620 | sub taskwarrior_import { 621 | my $title = shift; 622 | my $created; 623 | my $output; 624 | my $taskid; 625 | my $uuid; 626 | 627 | print "\n${title}:\n"; 628 | 629 | $output = &api_fetch_lists(); 630 | 631 | foreach my $tasklist (@{$output->{"items"}}) { 632 | if ($tasklist->{"title"} eq ${title}) { 633 | $created = "1"; 634 | 635 | $output = &api_fetch_tasks($tasklist->{"id"}); 636 | 637 | foreach my $task (@{$output->{"items"}}) { 638 | if (!$task->{"completed"} && !$task->{"deleted"}) { 639 | print "\n"; 640 | print $task->{"title"} . "\n"; 641 | 642 | alarm 10; 643 | chomp($output = qx(task $task->{"title"} 2>&1)); 644 | my $status = $?; 645 | alarm 0; 646 | 647 | if (${status} != 0 ) { 648 | $output = "FAILED COMMAND!\n" . ${output}; 649 | print STDERR ${output} . "\n"; 650 | 651 | &api_method("PATCH", ${task}, { 652 | "notes" => ${output}, 653 | }); 654 | } else { 655 | $taskid = ${output}; 656 | $taskid =~ m/task[ ]([0-9a-f-]+)/; 657 | $taskid = $1; 658 | 659 | if (!${taskid}) { 660 | $output = "UNKNOWN OUTPUT!\n" . ${output}; 661 | print STDERR ${output} . "\n"; 662 | 663 | &api_method("PATCH", ${task}, { 664 | "notes" => ${output}, 665 | }); 666 | } else { 667 | print ${output} . "\n"; 668 | 669 | chomp($uuid = qx(task uuids ${taskid})); 670 | chomp($taskid = qx(task export ${uuid})); 671 | $uuid = $task->{"updated"} . "\n" . ${uuid} . "\n" . ${taskid}; 672 | print ${uuid} . "\n"; 673 | 674 | &api_method("PATCH", ${task}, { 675 | "status" => "completed", 676 | "completed" => strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()), 677 | "notes" => ${uuid}, 678 | }); 679 | }; 680 | }; 681 | }; 682 | }; 683 | 684 | last(); 685 | }; 686 | }; 687 | 688 | if (!${created}) { 689 | print STDERR "\n"; 690 | print STDERR "DOES NOT EXIST!\n"; 691 | &EXIT(1); 692 | }; 693 | 694 | return(0); 695 | }; 696 | 697 | ######################################## 698 | 699 | sub search_regex { 700 | my $regex = shift; 701 | my $output; 702 | 703 | print "\n"; 704 | 705 | $output = &api_fetch_lists(); 706 | 707 | #>>> BUG IN PERL! 708 | #>>> http://www.perlmonks.org/?node_id=490213 709 | my @array = @{$output->{"items"}}; 710 | foreach my $tasklist (sort({$a->{"title"} cmp $b->{"title"}} @{array})) { 711 | #>>> 712 | if ($tasklist->{"title"} ne ${DEFAULT_LIST}) { 713 | printf("%-10.10s %-50.50s %s\n", (("-" x 9) . ">"), $tasklist->{"id"}, $tasklist->{"title"} || "-"); 714 | 715 | $output = &api_fetch_tasks($tasklist->{"id"}); 716 | 717 | foreach my $task (@{$output->{"items"}}) { 718 | my $match; 719 | foreach my $field (@{$SEARCH_FIELDS}) { 720 | if ( 721 | !$task->{"completed"} && !$task->{"deleted"} && 722 | $task->{$field} && $task->{$field} =~ m|${regex}|gm 723 | ) { 724 | push(@{$match}, ${field}); 725 | }; 726 | }; 727 | if (${match}) { 728 | print "\t" . $task->{"title"} . "\n"; 729 | foreach my $field (@{$match}) { 730 | if (${field} eq "title") { 731 | print "\t\t<" . ${field} . ">\n"; 732 | next(); 733 | }; 734 | my $test = $task->{$field}; 735 | my $link = "\\s*(?:MATCH|[${PROJ_LINK_NORMAL}${PROJ_LINK_OPEN}${PROJ_LINK_CLOSED}][ ])?"; 736 | while (${test} =~ m|^${link}(.*${regex}.*)$|gm) { 737 | print "\t\t<" . ${field} . ">\t" . $1 . "\n"; 738 | }; 739 | }; 740 | }; 741 | }; 742 | }; 743 | }; 744 | 745 | return(0); 746 | }; 747 | 748 | ######################################## 749 | 750 | sub manage_links { 751 | my $links = {}; 752 | my $output; 753 | 754 | $output = &api_fetch_lists(); 755 | 756 | #>>> BUG IN PERL! 757 | #>>> http://www.perlmonks.org/?node_id=490213 758 | my @array = @{$output->{"items"}}; 759 | foreach my $tasklist (sort({$a->{"title"} cmp $b->{"title"}} @{array})) { 760 | #>>> 761 | if ($tasklist->{"title"} ne ${DEFAULT_LIST}) { 762 | my $out = &manage_links_list($tasklist->{"id"}); 763 | if ($tasklist->{"title"} eq ${PROJECT_LIST}) { 764 | #>>> http://learn.perl.org/faq/perlfaq4.html#How-do-I-merge-two-hashes- 765 | @{ $links->{$MLINK_SRC} }{keys(%{ $out->{$MLINK_SRC} })} = values(%{ $out->{$MLINK_SRC} }); 766 | } else { 767 | #>>> http://learn.perl.org/faq/perlfaq4.html#How-do-I-merge-two-hashes- 768 | @{ $links->{$MLINK_DST} }{keys(%{ $out->{$MLINK_DST} })} = values(%{ $out->{$MLINK_DST} }); 769 | }; 770 | }; 771 | }; 772 | 773 | foreach my $key (sort({$a cmp $b} keys(%{ $links->{$MLINK_SRC} }))) { 774 | foreach my $val (@{$links->{$MLINK_SRC}->{ $key }}) { 775 | my $match = 0; 776 | foreach my $cmp (@{$links->{$MLINK_DST}->{ $key }}) { 777 | if ($val eq $cmp) { 778 | $match = 1; 779 | }; 780 | }; 781 | if (!${match}) { 782 | push(@{$links->{"NONE_$MLINK_DST"}->{ $key }}, $val); 783 | }; 784 | }; 785 | }; 786 | foreach my $key (sort({$a cmp $b} keys(%{ $links->{$MLINK_DST} }))) { 787 | foreach my $val (@{$links->{$MLINK_DST}->{ $key }}) { 788 | my $match = 0; 789 | foreach my $cmp (@{$links->{$MLINK_SRC}->{ $key }}) { 790 | if ($val eq $cmp) { 791 | $match = 1; 792 | }; 793 | }; 794 | if (!${match}) { 795 | push(@{$links->{"NONE_$MLINK_SRC"}->{ $key }}, $val); 796 | }; 797 | }; 798 | }; 799 | 800 | print "\n"; 801 | print "NO ${MLINK_DST}\n"; 802 | foreach my $key (sort({$a cmp $b} keys(%{ $links->{"NONE_$MLINK_DST"} }))) { 803 | print "\t${key}\n"; 804 | foreach my $val (@{$links->{"NONE_$MLINK_DST"}->{ $key }}) { 805 | print "\t\t${val}\n"; 806 | }; 807 | }; 808 | print "NO ${MLINK_SRC}\n"; 809 | foreach my $key (sort({$a cmp $b} keys(%{ $links->{"NONE_$MLINK_SRC"} }))) { 810 | foreach my $val (@{$links->{"NONE_$MLINK_SRC"}->{ $key }}) { 811 | print "\t${key}${PROJ_LINK_SEPARATE}${val}\n"; 812 | }; 813 | }; 814 | 815 | return(0); 816 | }; 817 | 818 | ######################################## 819 | 820 | sub manage_links_list { 821 | my $listid = shift; 822 | my $output; 823 | 824 | $output = &api_fetch_tasks(${listid}); 825 | 826 | foreach my $task (@{$output->{"items"}}) { 827 | while ($task->{"notes"} && $task->{"notes"} =~ m|^\s*([${PROJ_LINK_OPEN}${PROJ_LINK_CLOSED}])[ ](.+)$|gm) { 828 | if (${MANAGE_LINKS_ALL} || (!$task->{"completed"} && $1 ne ${PROJ_LINK_CLOSED})) { 829 | push(@{$output->{$MLINK_SRC}->{ $task->{"title"} }}, $2); 830 | }; 831 | }; 832 | while ($task->{"title"} && $task->{"title"} =~ m|^(.+)${PROJ_LINK_SEPARATE}(.+)$|gm) { 833 | if (${MANAGE_LINKS_ALL} || !$task->{"completed"}) { 834 | push(@{$output->{$MLINK_DST}->{ $1 }}, $2); 835 | }; 836 | }; 837 | }; 838 | 839 | return(${output}); 840 | }; 841 | 842 | ######################################## 843 | 844 | sub manage_cruft { 845 | my $output; 846 | 847 | print "\n"; 848 | 849 | $output = &api_fetch_lists(); 850 | 851 | #>>> BUG IN PERL! 852 | #>>> http://www.perlmonks.org/?node_id=490213 853 | my @array = @{$output->{"items"}}; 854 | foreach my $tasklist (sort({$a->{"title"} cmp $b->{"title"}} @{array})) { 855 | #>>> 856 | if (${MANAGE_CRUFT_ALL} || $tasklist->{"title"} eq ${DEFAULT_LIST}) { 857 | printf("%-10.10s %-50.50s %s\n", (("-" x 9) . ">"), $tasklist->{"id"}, $tasklist->{"title"} || "-"); 858 | &manage_cruft_list($tasklist->{"id"}); 859 | }; 860 | }; 861 | 862 | return(0); 863 | }; 864 | 865 | ######################################## 866 | 867 | sub manage_cruft_list { 868 | my $listid = shift; 869 | my $output; 870 | 871 | $output = &api_fetch_tasks(${listid}); 872 | 873 | foreach my $task (@{$output->{"items"}}) { 874 | #>>> BUG IN GOOGLE TASKS API! 875 | #>>> http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=2888 876 | #>>> SHOULD JUST BE ABLE TO MOVE THEM TO A "PURGE" LIST FOR MANUAL DELETION 877 | if ($task->{"title"} =~ "\n") { 878 | $task->{"title"} =~ s/\n//g; 879 | printf("%-10.10s %-50.50s %s\n", "rescuing:", $task->{"id"}, $task->{"title"} || "-"); 880 | &api_method("PATCH", ${task}, { 881 | "title" => $task->{"title"}, 882 | }); 883 | }; 884 | 885 | if ( $task->{"title"} ne "0" && 886 | !$task->{"title"} && 887 | !$task->{"notes"} && 888 | !$task->{"due"} 889 | ) { 890 | printf("%-10.10s %-50.50s %s\n", "clearing:", $task->{"id"}, $task->{"title"} || "-"); 891 | &api_method("PATCH", ${task}, { 892 | "title" => "0", 893 | "status" => "needsAction", 894 | "completed" => undef, 895 | "deleted" => "0", 896 | }); 897 | }; 898 | 899 | if (( !$task->{"title"} && ( 900 | $task->{"notes"} || 901 | $task->{"due"} ) 902 | ) || ( 903 | $task->{"deleted"} 904 | )) { 905 | printf("%-10.10s %-50.50s %s\n", "reviving:", $task->{"id"}, $task->{"title"} || "-"); 906 | &api_method("PATCH", ${task}, { 907 | "title" => "[" . sprintf("%.3d", int(rand(10**3))) . "]:[" . $task->{"title"} . "]", 908 | "status" => "needsAction", 909 | "completed" => undef, 910 | "deleted" => "0", 911 | }); 912 | }; 913 | #>>> 914 | }; 915 | 916 | return(0); 917 | }; 918 | 919 | ######################################## 920 | 921 | sub purge_lists { 922 | my $skips = shift; 923 | my $keeps = shift; 924 | my $skip; 925 | my $keep; 926 | my $output; 927 | 928 | if ($skips) { $skips = [ split(",", ${skips}) ]; } else { $skips = []; }; 929 | if ($keeps) { $keeps = [ split(",", ${keeps}) ]; } else { $keeps = []; }; 930 | 931 | print "\n"; 932 | 933 | #>>> ${web}, ${PURGE_LIST}, ${URL_WEB_POST} and ${purge_list} only exist for the hackery in &{purge_lists}! 934 | # $output = &api_create_list({ 935 | # "title" => ${PURGE_LIST}, 936 | # }); 937 | # my $purge_list = $output->{"selfLink"}; 938 | # $web = &auth_login(${web}); 939 | #>>> 940 | $output = &api_fetch_lists(); 941 | 942 | #>>> BUG IN PERL! 943 | #>>> http://www.perlmonks.org/?node_id=490213 944 | my @array = @{$output->{"items"}}; 945 | foreach my $tasklist (sort({$a->{"title"} cmp $b->{"title"}} @{array})) { 946 | #>>> 947 | $skip = "0"; 948 | $keep = "0"; 949 | 950 | foreach my $list (@{$skips}) { 951 | if ($tasklist->{"title"} eq ${list}) { 952 | $skip = "1"; 953 | last(); 954 | }; 955 | }; 956 | foreach my $list (@{$keeps}) { 957 | if ($tasklist->{"title"} eq ${list}) { 958 | $keep = "1"; 959 | last(); 960 | }; 961 | }; 962 | 963 | #>>> ${web}, ${PURGE_LIST}, ${URL_WEB_POST} and ${purge_list} only exist for the hackery in &{purge_lists}! 964 | # if ($tasklist->{"selfLink"} eq ${purge_list}) { 965 | # print $tasklist->{"title"} . ": ignored"; 966 | # } 967 | # elsif ($skip) { 968 | if ($skip) { 969 | #>>> 970 | print $tasklist->{"title"} . ": skipped"; 971 | } 972 | elsif ($keep) { 973 | print $tasklist->{"title"} . ": keeping"; 974 | #>>> MISSING FEATURE IN GOOGLE TASKS! 975 | #>>> https://productforums.google.com/d/msg/gmail/yKbRwhi6hYU/48XhvtZ5LTIJ 976 | #>>> https://productforums.google.com/d/msg/gmail/yKbRwhi6hYU/2QXGlAfwQx4J 977 | $output = &api_fetch_tasks($tasklist->{"id"}); 978 | foreach my $task (@{$output->{"items"}}) { 979 | &api_method("PATCH", ${task}, { 980 | #>>> "title" => " ", 981 | "title" => "0", 982 | #>>> "status" => "needsAction", 983 | "status" => "completed", 984 | "due" => undef, 985 | #>>> "completed" => undef, 986 | "completed" => strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()), 987 | "deleted" => undef, 988 | "notes" => "", 989 | }); 990 | #>>> &api_method("DELETE", ${task}); 991 | print "."; 992 | }; 993 | #>>> ${web}, ${PURGE_LIST}, ${URL_WEB_POST} and ${purge_list} only exist for the hackery in &{purge_lists}! 994 | #>>> keeping for posterity... this crap didn't work either (essentially the same effect)... 995 | # $web->get(${URL_WEB}); 996 | # $output = $web->content(); 997 | # $output =~ s|^.+.+$||gms; 999 | # $output = decode_json(${output}); 1000 | # 1001 | # #>>> https://stackoverflow.com/questions/4214917/android-problem-to-access-google-tasks-with-oauth#4771907 1002 | # $web->add_header("AT" => "1"); 1003 | # 1004 | # my $target; 1005 | # my $id; 1006 | # 1007 | # foreach $output (@{ $output->{"t"}{"lists"} }) { 1008 | # #>>> technically, ${PURGE_LIST} is not accurate here, but what the hell... this is a total hack already... 1009 | # if ($output->{"name"} eq ${PURGE_LIST}) { 1010 | # $target = $output->{"id"}; 1011 | # }; 1012 | # if ($output->{"name"} eq $tasklist->{"title"}) { 1013 | # $id = $output->{"id"}; 1014 | # }; 1015 | # }; 1016 | # 1017 | # $web->post(${URL_WEB_POST}, { 1018 | # "r" => encode_json({ 1019 | # "action_list" => [{ 1020 | # "action_type" => "get_all", 1021 | # "action_id" => "0", 1022 | # "list_id" => ${id}, 1023 | # "get_deleted" => JSON::XS::true, 1024 | # }], 1025 | # "client_version" => "0", 1026 | # }), 1027 | # }); 1028 | # $output = decode_json($web->content()); 1029 | # 1030 | # foreach $output (@{ $output->{"tasks"} }) { 1031 | # $web->post(${URL_WEB_POST}, { 1032 | # "r" => encode_json({ 1033 | # "action_list" => [{ 1034 | # "action_type" => "move", 1035 | # "action_id" => "0", 1036 | # "id" => $output->{"id"}, 1037 | # "source_list" => ${id}, 1038 | # "dest_list" => ${target}, 1039 | # "dest_parent" => ${target}, 1040 | # }], 1041 | # "current_list_id" => ${id}, 1042 | # "client_version" => "0", 1043 | # "last_sync_point" => "0", 1044 | # }), 1045 | # }); 1046 | # #>>> clear instead of move, just for fun... 1047 | # #$web->post(${URL_WEB_POST}, { 1048 | # # "r" => encode_json({ 1049 | # # "action_list" => [{ 1050 | # # "action_type" => "update", 1051 | # # "action_id" => "0", 1052 | # # "id" => $output->{"id"}, 1053 | # # "entity_delta" => { "task_date" => "", }, 1054 | # # }], 1055 | # # "current_list_id" => ${id}, 1056 | # # "client_version" => "0", 1057 | # # "last_sync_point" => "0", 1058 | # # }), 1059 | # #}); 1060 | # #$web->post(${URL_WEB_POST}, { 1061 | # # "r" => encode_json({ 1062 | # # "action_list" => [{ 1063 | # # "action_type" => "update", 1064 | # # "action_id" => "0", 1065 | # # "id" => $output->{"id"}, 1066 | # # "entity_delta" => { "notes" => "", }, 1067 | # # }], 1068 | # # "current_list_id" => ${id}, 1069 | # # "client_version" => "0", 1070 | # # "last_sync_point" => "0", 1071 | # # }), 1072 | # #}); 1073 | # #>>> 1074 | # print ","; 1075 | # }; 1076 | #>>> 1077 | } 1078 | else { 1079 | print $tasklist->{"title"} . ": purged"; 1080 | &api_method("DELETE", ${tasklist}); 1081 | }; 1082 | 1083 | print "\n"; 1084 | }; 1085 | 1086 | #>>> ${web}, ${PURGE_LIST}, ${URL_WEB_POST} and ${purge_list} only exist for the hackery in &{purge_lists}! 1087 | # &api_method("DELETE", ${purge_list}); 1088 | #>>> 1089 | return(0); 1090 | }; 1091 | 1092 | ####################################### 1093 | 1094 | sub edit_notes { 1095 | my $argv_list = shift; 1096 | my $argv_name = shift; 1097 | my $object; 1098 | my $output; 1099 | 1100 | if (${argv_list} eq "0") { 1101 | $argv_list = ${PROJECT_LIST}; 1102 | }; 1103 | 1104 | $output = &api_fetch_lists(); 1105 | 1106 | foreach my $tasklist (@{$output->{"items"}}) { 1107 | if ($tasklist->{"title"} eq ${argv_list}) { 1108 | $output = &api_fetch_tasks($tasklist->{"id"}); 1109 | 1110 | foreach my $task (@{$output->{"items"}}) { 1111 | if ($task->{"title"} eq ${argv_name}) { 1112 | $object = ${task}; 1113 | last(); 1114 | }; 1115 | }; 1116 | 1117 | last(); 1118 | }; 1119 | }; 1120 | 1121 | if (!${object}) { 1122 | print STDERR "\n"; 1123 | print STDERR "DOES NOT EXIST!\n"; 1124 | &EXIT(1); 1125 | } else { 1126 | $output = &api_get($object->{"selfLink"}); 1127 | $output = &edit_notes_text($output->{"notes"}); 1128 | 1129 | if ($output) { 1130 | &refresh_tokens(); 1131 | &api_method("PATCH", ${object}, { 1132 | "notes" => ${output}, 1133 | }); 1134 | }; 1135 | }; 1136 | 1137 | return(0); 1138 | }; 1139 | 1140 | ######################################## 1141 | 1142 | sub edit_notes_text { 1143 | my $notes = shift; 1144 | 1145 | $notes =~ s|^(${INDENT}+)|("\t" x (length($1) / 2))|egm; 1146 | 1147 | my($TEMPFILE, $tempfile) = tempfile(".${FILE}.XXXX", "UNLINK" => "0"); 1148 | print ${TEMPFILE} ${notes}; 1149 | close(${TEMPFILE}) || die(); 1150 | 1151 | system("${ENV{EDITOR}} ${tempfile}"); 1152 | 1153 | open(${TEMPFILE}, "<", "${tempfile}") || die(); 1154 | $notes = do { local $/; <$TEMPFILE> }; 1155 | close(${TEMPFILE}) || die(); 1156 | 1157 | $notes =~ s|^(\t+)|(${INDENT} x (length($1) * 2))|egm; 1158 | $notes =~ s/\n+$//; 1159 | 1160 | return(${notes}); 1161 | }; 1162 | 1163 | ######################################## 1164 | 1165 | sub export_files { 1166 | my $output; 1167 | 1168 | (${EXPORT_JSON}) && (open(JSON, ">", "${FILE}.json") || die()); 1169 | (${EXPORT_CSV}) && (open(CSV, ">", "${FILE}.csv") || die()); 1170 | (${EXPORT_TXT}) && (open(TXT, ">", "${FILE}.txt") || die()); 1171 | 1172 | if (${EXPORT_CSV}) { 1173 | print CSV "\"indent\","; 1174 | foreach my $field (@{$CSV_FIELDS}) { 1175 | print CSV "\"${field}\","; 1176 | }; 1177 | print CSV "\n"; 1178 | }; 1179 | 1180 | $output = &api_fetch_lists(); 1181 | 1182 | if (${EXPORT_JSON}) { 1183 | print JSON ("#" x 5) . "[ LISTS ]" . ("#" x 5) . "\n\n"; 1184 | print JSON $json->encode(${output}); 1185 | print JSON "\n"; 1186 | }; 1187 | 1188 | #>>> BUG IN PERL! 1189 | #>>> http://www.perlmonks.org/?node_id=490213 1190 | my @array = @{$output->{"items"}}; 1191 | foreach my $tasklist (sort({$a->{"title"} cmp $b->{"title"}} @{array})) { 1192 | #>>> 1193 | $output = &api_fetch_tasks($tasklist->{"id"}); 1194 | 1195 | $tasklist->{"title"} .= " (" . ($#{$output->{"items"}} + 1) . ")"; 1196 | 1197 | if (${EXPORT_JSON}) { 1198 | print JSON ("#" x 5) . "[ " . $tasklist->{"title"} . " ]" . ("#" x 5) . "\n\n"; 1199 | print JSON $json->encode(${output}); 1200 | print JSON "\n"; 1201 | }; 1202 | 1203 | &export_files_item(${tasklist}, "-", "-"); 1204 | &export_files_list(${output}); 1205 | 1206 | print TXT "\n"; 1207 | }; 1208 | 1209 | (${EXPORT_JSON}) && print JSON ("#" x 5) . "[ END OF FILE ]" . ("#" x 5) . "\n"; 1210 | (${EXPORT_TXT}) && print TXT ("=" x 5) . "[ END OF FILE ]" . ("=" x 5) . "\n"; 1211 | 1212 | (${EXPORT_JSON}) && (close(JSON) || die()); 1213 | (${EXPORT_CSV}) && (close(CSV) || die()); 1214 | (${EXPORT_TXT}) && (close(TXT) || die()); 1215 | 1216 | if (${EXPORT_TXT} && ${CAT_TEXT}) { 1217 | open(TXT, "<", "${FILE}.txt") || die(); 1218 | print "\n"; 1219 | print ; 1220 | close(TXT) || die(); 1221 | }; 1222 | 1223 | return(0); 1224 | }; 1225 | 1226 | ######################################## 1227 | 1228 | sub export_files_list { 1229 | my $list = shift; 1230 | my $tree = {}; 1231 | 1232 | foreach my $task (@{$list->{"items"}}) { 1233 | (${HIDE_COMPLETED} && $task->{"completed"} ) && (next()); 1234 | (${HIDE_DELETED} && $task->{"deleted"} ) && (next()); 1235 | if (!exists($task->{"parent"})) { 1236 | $tree->{$task->{"id"}} = { 1237 | "node" => ${task}, 1238 | "pos" => $task->{"position"}, 1239 | }; 1240 | } else { 1241 | $tree->{$task->{"parent"}}{"sub"}{$task->{"id"}} = { 1242 | "node" => ${task}, 1243 | "pos" => $task->{"position"}, 1244 | }; 1245 | }; 1246 | }; 1247 | 1248 | &export_files_list_tree(${tree}, ${tree}, "0"); 1249 | 1250 | return(0); 1251 | }; 1252 | 1253 | ######################################## 1254 | 1255 | sub export_files_list_tree { 1256 | my $root_tree = shift; 1257 | my $tree = shift; 1258 | my $indent = shift; 1259 | my $key; 1260 | 1261 | foreach $key (keys(%{$tree})) { 1262 | if (!exists($tree->{$key}->{"pos"})) { 1263 | $tree->{$key}->{"pos"} = ""; 1264 | }; 1265 | }; 1266 | 1267 | foreach $key (sort({$tree->{$a}{"pos"} cmp $tree->{$b}{"pos"}} keys(%{$tree}))) { 1268 | if ($tree->{$key}->{"pos"}) { 1269 | &export_files_item($tree->{$key}{"node"}, ${indent}, ""); 1270 | if (exists($root_tree->{$key}{"sub"})) { 1271 | &export_files_list_tree(${root_tree}, $root_tree->{$key}->{"sub"}, (${indent} + 1)); 1272 | }; 1273 | }; 1274 | }; 1275 | 1276 | return(0); 1277 | }; 1278 | 1279 | ######################################## 1280 | 1281 | sub export_files_item { 1282 | my $task = shift; 1283 | my $indent = shift; 1284 | my $empty = shift; 1285 | 1286 | if (${EXPORT_CSV}) { 1287 | print CSV "\"${indent}\","; 1288 | foreach my $field (@{$CSV_FIELDS}) { 1289 | if(exists($task->{$field})) { 1290 | my $output = $task->{$field}; 1291 | $output =~ s/"/""/g; 1292 | print CSV "\"${output}\","; 1293 | } else { 1294 | print CSV "\"${empty}\","; 1295 | }; 1296 | }; 1297 | print CSV "\n"; 1298 | }; 1299 | 1300 | if (${EXPORT_TXT}) { 1301 | if (${indent} !~ /\d+/) { 1302 | print TXT ("=" x 5) . "[ " . $task->{"title"} . " ]" . ("=" x 5) . "\n"; 1303 | } else { 1304 | print TXT ("\t" x (${indent} + 1)); 1305 | my $note = ("\t" x (${indent} + 2)) . ("-" x 5); 1306 | my $tabs = ("\t" x (${indent} + 3)); 1307 | 1308 | if ($task->{"completed"}) { 1309 | print TXT "x"; 1310 | } elsif ($task->{"deleted"}) { 1311 | print TXT ">"; 1312 | } else { 1313 | print TXT "*"; 1314 | }; 1315 | 1316 | foreach my $field (qw/ 1317 | completed 1318 | due 1319 | title 1320 | notes 1321 | /) { 1322 | if(exists($task->{$field})) { 1323 | my $output = $task->{$field}; 1324 | if (${field} eq "due") { 1325 | $output =~ s/T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9A-Z]{4}$//g; 1326 | }; 1327 | if (${field} eq "notes") { 1328 | $output =~ s|^(${INDENT}+)|("\t" x (length($1) / 2))|egm; 1329 | $output =~ s/^/${tabs}/gm; 1330 | $output =~ s/^/\n${note}\n/; 1331 | }; 1332 | if (${field} ne "notes") { 1333 | print TXT " "; 1334 | }; 1335 | print TXT "${output}"; 1336 | }; 1337 | }; 1338 | 1339 | print TXT "\n"; 1340 | }; 1341 | }; 1342 | 1343 | return(0); 1344 | }; 1345 | 1346 | ################################################################################ 1347 | 1348 | if (@{ARGV}) { 1349 | if (${ARGV[0]} eq "twexport" || ${ARGV[0]} eq "taskwarrior") { 1350 | shift; 1351 | &refresh_tokens(); 1352 | &taskwarrior_export(@{ARGV}); 1353 | } 1354 | elsif (${ARGV[0]} eq "twimport") { 1355 | shift; 1356 | &refresh_tokens(); 1357 | &taskwarrior_import(@{ARGV}); 1358 | } 1359 | elsif (${ARGV[0]} eq "search") { 1360 | shift; 1361 | &refresh_tokens(); 1362 | &search_regex(@{ARGV}); 1363 | } 1364 | elsif (${ARGV[0]} eq "links") { 1365 | shift; 1366 | &refresh_tokens(); 1367 | &manage_links(@{ARGV}); 1368 | } 1369 | elsif (${ARGV[0]} eq "cruft") { 1370 | shift; 1371 | &refresh_tokens(); 1372 | &manage_cruft(@{ARGV}); 1373 | } 1374 | elsif (${ARGV[0]} eq "purge") { 1375 | shift; 1376 | &refresh_tokens(); 1377 | &purge_lists(@{ARGV}); 1378 | } 1379 | elsif (defined(${ARGV[0]}) && defined(${ARGV[1]})) { 1380 | &refresh_tokens(); 1381 | &edit_notes(@{ARGV}); 1382 | } 1383 | else { 1384 | print STDERR "\n"; 1385 | print STDERR "INVALID ARGUMENTS!\n"; 1386 | &EXIT(1); 1387 | }; 1388 | } 1389 | 1390 | ######################################## 1391 | 1392 | else { 1393 | &refresh_tokens(); 1394 | &export_files(@{ARGV}); 1395 | }; 1396 | 1397 | ######################################## 1398 | 1399 | &EXIT(0); 1400 | 1401 | ################################################################################ 1402 | # end of file 1403 | ################################################################################ 1404 | -------------------------------------------------------------------------------- /scripts/zohocrm_events.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | ################################################################################ 5 | 6 | use Carp qw(confess); 7 | #>>>$SIG{__WARN__} = \&confess; 8 | #>>>$SIG{__DIE__} = \&confess; 9 | 10 | use Data::Dumper; 11 | sub DUMPER { 12 | my $DUMP = shift(); 13 | local $Data::Dumper::Purity = 1; 14 | &printer(2, "<-- DUMPER " . ("-" x 30) . ">\n"); 15 | &printer(2, Dumper(${DUMP})); 16 | &printer(2, "<-- DUMPER " . ("-" x 30) . ">\n"); 17 | return(0); 18 | }; 19 | 20 | ######################################## 21 | 22 | use WWW::Mechanize; 23 | my $mech = WWW::Mechanize->new( 24 | "agent" => "Mozilla/5.0", 25 | "autocheck" => "1", 26 | "stack_depth" => "0", 27 | "onwarn" => \&mech_fail, 28 | "onerror" => \&mech_fail, 29 | ); 30 | sub mech_fail { 31 | &DUMPER($mech->response()); 32 | &confess(); 33 | }; 34 | 35 | use JSON::XS; 36 | my $json = JSON::XS->new(); 37 | $json->ascii(1); 38 | $json->canonical(1); 39 | $json->pretty(1); 40 | 41 | use POSIX qw(strftime); 42 | use Time::Local qw(timegm timelocal); 43 | 44 | ######################################## 45 | 46 | $| = "1"; 47 | 48 | ################################################################################ 49 | 50 | my $URL_AUTH = "https://accounts.zoho.com/apiauthtoken/nb/create"; 51 | sub URL_FETCH { "https://crm.zoho.com/crm/private/json/" . shift() . "/getRecords"; }; 52 | sub URL_LINK { "https://crm.zoho.com/crm/EntityInfo.do?module=" . shift() . "&id=" . shift(); }; 53 | 54 | my $URL_SCOPE = "crmapi"; 55 | my $API_SCOPE = "ZohoCRM/${URL_SCOPE}"; 56 | 57 | my $APP_NAME = "ZohoCRM Export"; 58 | my $THOROUGH = "0"; # MANUAL TOGGLE: DOES NOT SEEM TO WORK (SKIPS MULTIPLE ENTRIES) 59 | 60 | ######################################## 61 | 62 | my $AUTH_CRED = ".zoho-auth"; 63 | my $AUTH_TOKEN = ".zoho-token"; 64 | 65 | my $LEGEND_NAME = "Marker: Legend"; 66 | my $LEGEND_FILE = ".zoho.reports"; 67 | my $LEGEND_IMP = "1"; 68 | 69 | my $TODAY_NAME = "Marker: Today"; 70 | my $TODAY_EXP = "zoho.today.md"; 71 | my $TODAY_IMP = "zoho.today.out.md"; 72 | my $TODAY_TMP = "zoho.today.tmp.md"; 73 | 74 | my $FIND_NOTES = "0"; # MANUAL TOGGLE: HUGE DRAIN ON API REQUESTS (ONLY USE ONCE OR TWICE A DAY) 75 | my $NOTES_FILE = "zoho/_Note.csv"; 76 | 77 | my $JSON_BASE = "zoho-export"; 78 | my $CSV_FILE = "zoho-data.csv"; 79 | my $ALL_FILE = "zoho.all.md"; 80 | my $OUT_FILE = "zoho.md"; 81 | 82 | my $START_DATE = "2016-10-24"; if ($ARGV[0] && $ARGV[0] =~ m/^[0-9]{4}[-][0-9]{2}[-][0-9]{2}$/) { $START_DATE = shift(); }; 83 | my $CSV_DATE = "2017-01-02"; 84 | my $SORT_COLUMN = "Modified DateTime"; 85 | my $SORT_ORDER = "asc"; 86 | my $MAX_RECORDS = "200"; 87 | 88 | my $NULL_CNAME = "\[0 NULL\]"; 89 | my $NULL_ENAME = "New Event"; 90 | my $NAME_DIV = " "; 91 | my $DSC_IMPORT = "IMPORTED"; 92 | my $DSC_EXP_BD = "CANCELLED"; my $DSC_EXP_BD_I = "X"; 93 | my $DSC_EXP_GD = "CANCEL"; my $DSC_EXP_GD_I = "!"; 94 | my $DSC_FLAG = "WORK"; 95 | my $NON_ASCII = "###"; 96 | my $NON_ASCII_M = "[^[:ascii:]]"; 97 | #>>>my $NON_ASCII_S = "(\r|[[:space:]]\n|\n\n\n)"; 98 | my $NON_ASCII_S = "(\r|\n\n\n)"; 99 | my $CLOSED_MARK = "[\$]"; 100 | 101 | my $MARK_REGEX = "^([\$A-Z:-]+)[:][ ]"; 102 | my $SPLIT_CHAR = "[\|]"; 103 | my $A_BEG_CHAR = "[\[]"; 104 | my $A_END_CHAR = "[\]]"; 105 | 106 | my $SEC_IN_DAY = 60 * 60 * 24; 107 | my $AGING_DAYS = 28 * 5; 108 | 109 | ######################################## 110 | 111 | my $LEVEL_1 = "#" x 3; 112 | my $LEVEL_2 = "#" x 4; 113 | my $HEAD_MARKER = "#"; 114 | 115 | my $S_UID = "%-19.19s"; 116 | my $S_DATE = "%-19.19s"; 117 | my $S_DATE_ONLY = "%-10.10s"; 118 | 119 | ######################################## 120 | 121 | #WORK: generic search/replace 122 | # * create global matching hash: module => [ field list ] 123 | # * module name is arg/switch matched against matching hash 124 | # * second arg/switch is field matched againtst matching hash 125 | # * title is default? can't be 126 | # * exit if either not matched? nope, arg will just fail or not 127 | # * only pull module? nope, be lazy but thorough 128 | # * don't reset files? nope, same as above 129 | # * exit immediately? nope, like above 130 | # * final arg/switch is exact "s//" regex, fail if none 131 | # * create "new" variable hash, based on matching hash, loop over field array and regex hash item using field token, post all "new" fields also using loop 132 | # * test with TEST -> TSET, event location, lead desc, task title 133 | #WORK 134 | 135 | my $TYPES = [ "Leads", "Tasks", "Events" ]; 136 | 137 | my $LID = "LEADID"; 138 | my $SRC = "Lead Source"; 139 | my $STS = "Lead Status"; 140 | my $FNM = "First Name"; 141 | my $LNM = "Last Name"; 142 | my $CMP = "Company"; 143 | 144 | my $RID = "RELATEDTOID"; 145 | 146 | my $TID = "ACTIVITYID"; 147 | my $DUE = "Due Date"; 148 | my $CLT = "Closed Time"; 149 | my $TST = "Status"; 150 | my $PRI = "Priority"; 151 | 152 | my $UID = "UID"; 153 | my $MOD = "Modified Time"; 154 | my $BEG = "Start DateTime"; 155 | my $END = "End DateTime"; 156 | my $REL = "Related To"; 157 | my $SUB = "Subject"; 158 | my $LOC = "Venue"; 159 | my $DSC = "Description"; 160 | 161 | ######################################## 162 | 163 | our $USERNAME; 164 | our $PASSWORD; 165 | do("./${AUTH_CRED}") || die(); 166 | 167 | our $APITOKEN; 168 | do("./${AUTH_TOKEN}") || die(); 169 | 170 | ################################################################################ 171 | 172 | my $API_REQUEST_COUNT = "0"; 173 | 174 | ######################################## 175 | 176 | if (!${APITOKEN}) { 177 | $mech->get(${URL_AUTH} 178 | . "?SCOPE=${API_SCOPE}" 179 | . "&DISPLAY_NAME=${APP_NAME}" 180 | . "&EMAIL_ID=${USERNAME}" 181 | . "&PASSWORD=${PASSWORD}" 182 | ); 183 | $APITOKEN = $mech->content(); 184 | $APITOKEN =~ s/^.+AUTHTOKEN[=](.+)\n.+$/$1/gms; 185 | 186 | open(OUTPUT, ">", ${AUTH_TOKEN}) || die(); 187 | print OUTPUT "our \$APITOKEN = '${APITOKEN}';\n"; 188 | close(OUTPUT) || die(); 189 | }; 190 | 191 | ######################################## 192 | 193 | open(ALL_FILE, ">", ${ALL_FILE}) || die(); 194 | open(OUT_FILE, ">", ${OUT_FILE}) || die(); 195 | open(TODAY_TMP, ">", ${TODAY_TMP}) || die(); 196 | 197 | sub printer { 198 | my $output = shift() || ""; 199 | 200 | my $stderr = "0"; 201 | 202 | if (${output} =~ m/^[0123]$/) { 203 | $stderr = ${output}; 204 | $output = ""; 205 | }; 206 | $output .= join("", @{_}); 207 | 208 | if (${stderr} == 3) { 209 | print TODAY_TMP ${output}; 210 | } 211 | elsif (${stderr} == 2) { 212 | print ALL_FILE ${output}; 213 | print STDERR ${output}; 214 | } 215 | elsif (${stderr}) { 216 | print ALL_FILE ${output}; 217 | } 218 | else { 219 | print ALL_FILE ${output}; 220 | print OUT_FILE ${output}; 221 | }; 222 | }; 223 | 224 | sub printer_test { 225 | &DUMPER("DUMPER"); 226 | 227 | &printer(2, "output\n"); 228 | &printer(1, "stderr\n"); 229 | &printer(0, "stdout\n"); 230 | &printer("no_std\n"); 231 | 232 | &printer(2, "output", "multiple", "arguments", "\n"); 233 | &printer(1, "stderr", "multiple", "arguments", "\n"); 234 | &printer(0, "stdout", "multiple", "arguments", "\n"); 235 | &printer("no_std", "multiple", "arguments", "\n"); 236 | }; 237 | #>>>&printer_test(); 238 | 239 | ######################################## 240 | 241 | &printer(2, "${LEVEL_1} Processing Log\n"); 242 | #>>>&printer(2, "\n"); 243 | #>>>&printer(2, "\tTOKEN: ${APITOKEN}\n"); 244 | 245 | ################################################################################ 246 | 247 | my $z = { 248 | "leads" => {}, 249 | "tasks" => {}, 250 | "events" => {}, 251 | }; 252 | my $leads = $z->{"leads"}; 253 | my $tasks = $z->{"tasks"}; 254 | my $events = $z->{"events"}; 255 | 256 | my $closed_list = {}; 257 | my $cancel_bd_list = {}; 258 | my $cancel_gd_list = {}; 259 | my $related_list = {}; 260 | my $empty_events = []; 261 | my $orphaned_tasks = {}; 262 | 263 | my $fail_exit = "0"; 264 | 265 | ######################################## 266 | 267 | sub fetch_entries { 268 | my $type = shift() || "Events"; 269 | 270 | my $last_mod = ${START_DATE}; 271 | my $index_no = "1"; 272 | my $index_to = "1"; 273 | my $records = "0"; 274 | my $fetches = {}; 275 | my $output; 276 | my $found; 277 | 278 | &printer(2, "\n\tFetching ${type}..."); 279 | 280 | while (1) { 281 | if ($last_mod =~ m/^[0-9]{4}[-][0-9]{2}[-][0-9]{2}$/) { 282 | $last_mod .= " 00:00:00"; 283 | }; 284 | if (${THOROUGH}) { 285 | $last_mod =~ s/^([0-9]{4}[-][0-9]{2}[-][0-9]{2}).*$/${1} 00:00:00/g; 286 | $index_no = "1"; 287 | }; 288 | $index_to = (${index_no} + (${MAX_RECORDS} -1)); 289 | 290 | &printer(2, "\n\tProcessing: ${last_mod} (${index_no} to ${index_to})... "); 291 | 292 | $mech->get(&URL_FETCH(${type}) 293 | . "?scope=${URL_SCOPE}" 294 | . "&authtoken=${APITOKEN}" 295 | . (${THOROUGH} ? "&lastModifiedTime=${last_mod}" : "") 296 | . "&sortColumnString=${SORT_COLUMN}" 297 | . "&sortOrderString=${SORT_ORDER}" 298 | . "&fromIndex=${index_no}" 299 | . "&toIndex=${index_to}" 300 | ) && $API_REQUEST_COUNT++; 301 | #WORK 302 | #sleep(2); 303 | #WORK 304 | $output = decode_json($mech->content()); 305 | 306 | if ( $output->{"response"}{"nodata"} ) { 307 | &printer(2, "\n\tNo Data!"); 308 | last(); 309 | }; 310 | 311 | my($uid, $new); 312 | if (ref( $output->{"response"}{"result"}{$type}{"row"} ) eq "ARRAY") { 313 | $found = $#{ $output->{"response"}{"result"}{$type}{"row"} }; 314 | foreach my $event (@{ $output->{"response"}{"result"}{$type}{"row"} }) { 315 | (${last_mod}, ${uid}, ${new}) = &parse_entry( $event->{"FL"} ); 316 | $fetches->{$uid} = ${new}; 317 | }; 318 | } else { 319 | $found = $#{ $output->{"response"}{"result"}{$type}{"row"}{"FL"} }; 320 | (${last_mod}, ${uid}, ${new}) = &parse_entry( $output->{"response"}{"result"}{$type}{"row"}{"FL"} ); 321 | $fetches->{$uid} = ${new}; 322 | }; 323 | 324 | $records += ++${found}; 325 | &printer(2, "${found} records found (${records} total)."); 326 | 327 | if (${found} < ${MAX_RECORDS}) { 328 | &printer(2, "\n\tCompleted!"); 329 | last(); 330 | }; 331 | 332 | $index_no += ${MAX_RECORDS}; 333 | }; 334 | 335 | &printer(2, "\n"); 336 | 337 | &printer(2, "\tTotal ${type}: " . scalar(keys(%{$fetches})) . "\n"); 338 | 339 | return(%{$fetches}); 340 | }; 341 | 342 | ######################################## 343 | 344 | sub parse_entry { 345 | my $event = shift(); 346 | 347 | my $new = {}; 348 | my $uid; 349 | my $mod; 350 | 351 | foreach my $value (@{$event}) { 352 | if ($value->{"val"} eq ${LID} || $value->{"val"} eq ${TID} || $value->{"val"} eq ${UID}) { 353 | $uid = $value->{"content"}; 354 | }; 355 | if ($value->{"val"} eq ${MOD}) { 356 | $mod = $value->{"content"}; 357 | }; 358 | 359 | $new->{ $value->{"val"} } = $value->{"content"}; 360 | }; 361 | 362 | return(${mod}, ${uid}, ${new}); 363 | } 364 | 365 | ######################################## 366 | 367 | sub update_legend { 368 | &update_file(${LEGEND_NAME}, ${LEGEND_FILE}, ${LEGEND_IMP}); 369 | 370 | return(0); 371 | }; 372 | 373 | ######################################## 374 | 375 | sub update_today { 376 | if (-f ${TODAY_IMP}) { 377 | &update_file(${TODAY_NAME}, ${TODAY_IMP}, "1"); 378 | unlink(${TODAY_IMP}) || die(); 379 | } else { 380 | &update_file(${TODAY_NAME}, ${TODAY_EXP}); 381 | }; 382 | 383 | return(0); 384 | }; 385 | 386 | ######################################## 387 | 388 | sub update_file { 389 | my $title = shift(); 390 | my $file = shift(); 391 | my $import = shift(); 392 | 393 | my $uid = ""; 394 | my $output = ""; 395 | my $input; 396 | 397 | &printer(2, "\n"); 398 | &printer(2, "\tMatching '${title}'...\n"); 399 | 400 | if (-f ${file}) { 401 | open(FILE, "<", ${file}) || die(); 402 | if (${import}) { 403 | $output = (stat(${file}))[9]; 404 | $output = localtime(${output}) . "\n\n"; 405 | }; 406 | $output .= do { local $/; }; 407 | # make the input for "&print_events()" and "foreach my $search (@{ARGV})" pretty 408 | $output =~ s/^["]([${HEAD_MARKER}])[ ]([^"]*)["]$/${1} ${2}/gm; 409 | $output =~ s/^["][^|]*[|][^|]*[|]([^|]+)[|](.+)["]$/[${1}] ${2}/gm; 410 | $output =~ s/[ ]${A_BEG_CHAR}(.*)${A_END_CHAR}$/ {${1}}/gm; 411 | close(FILE) || die(); 412 | }; 413 | 414 | foreach my $event (sort(keys(%{$events}))) { 415 | if ($events->{$event}{$SUB} eq ${title}) { 416 | $uid = $events->{$event}{$UID}; 417 | 418 | &printer(2, "\t\t[" . $events->{$event}{$MOD} . "]\n"); 419 | &printer(2, "\t\t\t[${uid}](" . &URL_LINK("Events", ${uid}) . ")\n"); 420 | }; 421 | }; 422 | 423 | if (!${uid}) { 424 | &printer(2, "\tDID NOT FIND ANY MATCHES!\n"); 425 | return(0); 426 | }; 427 | 428 | if (!${import}) { 429 | &printer(2, "\tExporting: ${uid}\n"); 430 | } else { 431 | &printer(2, "\tImporting: ${uid}\n"); 432 | }; 433 | 434 | my $url_get = &URL_FETCH("Events"); 435 | $url_get =~ s/getRecords/getRecordById/g; 436 | $url_get =~ s/json/xml/g; 437 | 438 | $mech->get(${url_get} 439 | . "?scope=${URL_SCOPE}" 440 | . "&authtoken=${APITOKEN}" 441 | . "&id=${uid}" 442 | ) && $API_REQUEST_COUNT++; 443 | 444 | if ($mech->content() =~ m/[<]error[>]/) { 445 | &printer(2, "\nGET[" . $mech->content() . "]\n"); 446 | &printer(2, "\n"); 447 | die(); 448 | }; 449 | 450 | $input = $mech->content(); 451 | $input =~ s|^.*.*$||gms; 453 | $input .= "\n"; 454 | 455 | if (${output} ne ${input}) { 456 | if (!${import}) { 457 | open(FILE, ">", ${file}) || die(); 458 | print FILE ${input}; 459 | close(FILE) || die(); 460 | } else { 461 | my $url_post = &URL_FETCH("Events"); 462 | $url_post =~ s/getRecords/updateRecords/g; 463 | $url_post =~ s/json/xml/g; 464 | 465 | my $post_data = ""; 466 | $post_data .= ''; 467 | $post_data .= ''; 468 | $post_data .= '' . ${uid} . ''; 469 | $post_data .= ''; 470 | $post_data .= ''; 471 | $post_data .= ''; 472 | $post_data .= ''; 473 | 474 | $mech->post(${url_post}, { 475 | "scope" => ${URL_SCOPE}, 476 | "authtoken" => ${APITOKEN}, 477 | "newFormat" => 1, 478 | "id" => ${uid}, 479 | "xmlData" => ${post_data}, 480 | }) && $API_REQUEST_COUNT++; 481 | 482 | if ($mech->content() =~ m/[<]error[>]/) { 483 | &printer(2, "\nPOST[" . $mech->content() . "]\n"); 484 | &printer(2, "\n"); 485 | die(); 486 | }; 487 | }; 488 | 489 | &printer(2, "\tCompleted!\n"); 490 | } else { 491 | &printer(2, "\tSkipped!\n"); 492 | }; 493 | 494 | return(0); 495 | }; 496 | 497 | ######################################## 498 | 499 | sub find_notes_entries { 500 | my $notes = {}; 501 | 502 | &printer(2, "\n"); 503 | &printer(2, "\tNotes Search"); 504 | 505 | my $url_get = &URL_FETCH("Notes"); 506 | $url_get =~ s/getRecords/getRelatedRecords/g; 507 | $url_get =~ s/json/xml/g; 508 | 509 | foreach my $lead (sort(keys(%{$leads}))) { 510 | &printer(2, "."); 511 | 512 | $mech->get(${url_get} 513 | . "?scope=${URL_SCOPE}" 514 | . "&authtoken=${APITOKEN}" 515 | . "&id=${lead}" 516 | . "&parentModule=Leads" 517 | ) && $API_REQUEST_COUNT++; 518 | 519 | if ($mech->content() =~ m/[<]error[>]/) { 520 | &printer(2, "\nGET[" . $mech->content() . "]\n"); 521 | &printer(2, "\n"); 522 | die(); 523 | }; 524 | 525 | my $result = $mech->content(); 526 | 527 | if (${result} !~ m||) { 528 | while (${result} =~ m|(.+?)|gms) { 529 | my $note = ${1}; 530 | my $created = ${note}; 531 | my $content = ${note}; 532 | $created =~ s|^.*.*$||gms; 534 | $content =~ s|^.*.*$||gms; 536 | 537 | $notes->{$lead}{$created} = ${content}; 538 | }; 539 | }; 540 | }; 541 | 542 | &printer(2, "\n"); 543 | 544 | if (%{$notes}) { 545 | &printer(2, "\n"); 546 | &printer(2, "\tNotes Entries:\n"); 547 | $fail_exit = "1"; 548 | 549 | foreach my $lead (sort(keys(%{$notes}))) { 550 | my $subject = ($leads->{$lead}{$FNM} || "") . ${NAME_DIV} . ($leads->{$lead}{$LNM} || ""); 551 | $subject = "[${subject}](" . &URL_LINK("Leads", $leads->{$lead}{$LID}) . ")"; 552 | &printer(2, "\t\t${subject}\n"); 553 | 554 | foreach my $note (sort(keys(%{$notes->{$lead}}))) { 555 | my $content = $notes->{$lead}{$note}; 556 | 557 | $content =~ s|\n|\n\t\t\t|gms; 558 | $content =~ s|\t\t\t$||gms; 559 | 560 | &printer(2, "\t\t\tNOTES ENTRY (${note}):\n"); 561 | &printer(2, "\t\t\t${content}\n"); 562 | }; 563 | }; 564 | }; 565 | 566 | return(0); 567 | }; 568 | 569 | ######################################## 570 | 571 | sub check_recycle_bin { 572 | my $recycled = {}; 573 | my $index_no = "1"; 574 | my $index_to = "1"; 575 | my $output; 576 | my $found; 577 | 578 | #WORK 579 | #my $date = &strftime("%Y-%m-%d", localtime(time())); 580 | #open(API_FAIL, ">/tmp/zoho_api-broken_recycle_bin_calls-${date}.txt") || die(); 581 | #local $Data::Dumper::Purity = 1; 582 | #WORK 583 | foreach my $type (@{$TYPES}) { 584 | while (1) { 585 | 586 | $index_to = (${index_no} + (${MAX_RECORDS} -1)); 587 | 588 | my $url_get = &URL_FETCH(${type}); 589 | $url_get =~ s/getRecords/getDeletedRecordIds/g; 590 | $url_get =~ s/json/xml/g; 591 | 592 | $mech->get(${url_get} 593 | . "?scope=${URL_SCOPE}" 594 | . "&authtoken=${APITOKEN}" 595 | . "&fromIndex=${index_no}" 596 | . "&toIndex=${index_to}" 597 | ) && $API_REQUEST_COUNT++; 598 | #WORK 599 | #print API_FAIL ${url_get} 600 | # . "?scope=${URL_SCOPE}" 601 | # . "&authtoken=${APITOKEN}" 602 | # . "&fromIndex=${index_no}" 603 | # . "&toIndex=${index_to}" 604 | # . "\n" 605 | # . $mech->content() 606 | #. "\n"; 607 | #WORK 608 | 609 | if ($mech->content() =~ m/[<]error[>]/) { 610 | &printer(2, "\nGET[" . $mech->content() . "]\n"); 611 | &printer(2, "\n"); 612 | die(); 613 | }; 614 | 615 | $output = $mech->content(); 616 | $output =~ s|^.*||gms; 617 | $output =~ s|.*$||gms; 618 | 619 | $found = "0"; 620 | foreach my $item (split(",", ${output})) { 621 | $recycled->{$type}{$item}++; 622 | $found++; 623 | }; 624 | if (${found} < ${MAX_RECORDS}) { 625 | last(); 626 | }; 627 | 628 | $index_no += ${MAX_RECORDS}; 629 | }; 630 | }; 631 | 632 | if (%{$recycled}) { 633 | &printer(2, "\n"); 634 | &printer(2, "\tRecycle Bin:\n"); 635 | #WORK $fail_exit = "1"; 636 | 637 | foreach my $type (@{$TYPES}) { 638 | &printer(2, "\t\t${type}: " . scalar(keys(%{ $recycled->{$type} })) . "\n"); 639 | }; 640 | }; 641 | 642 | #WORK 643 | #WORK print API_FAIL Dumper($recycled); 644 | #foreach my $type (@{$TYPES}) { 645 | # my $url_get = &URL_FETCH(${type}); 646 | # $url_get =~ s/getRecords/getRecordById/g; 647 | # $url_get =~ s/json/xml/g; 648 | # foreach my $record (sort(keys(%{ $recycled->{$type} }))) { 649 | # $mech->get(${url_get} 650 | # . "?scope=${URL_SCOPE}" 651 | # . "&authtoken=${APITOKEN}" 652 | # . "&id=${record}" 653 | # ) && $API_REQUEST_COUNT++; 654 | # print API_FAIL ${url_get} 655 | # . "?scope=${URL_SCOPE}" 656 | # . "&authtoken=${APITOKEN}" 657 | # . "&id=${record}" 658 | # . "\n" 659 | # . $mech->content() 660 | # . "\n"; 661 | # }; 662 | #}; 663 | #close(API_FAIL) || die(); 664 | #WORK 665 | return(0); 666 | }; 667 | 668 | ######################################## 669 | 670 | sub unlink_null_events { 671 | my $null_linked = []; 672 | 673 | foreach my $event (sort(keys(%{$events}))) { 674 | if ( 675 | ($events->{$event}{$RID}) && 676 | ($leads->{ $events->{$event}{$RID} }{$CMP} eq ${NULL_CNAME}) 677 | ) { 678 | push(@{$null_linked}, ${event}); 679 | }; 680 | }; 681 | 682 | if (@{$null_linked}) { 683 | &printer(2, "\n"); 684 | &printer(2, "\tNull Linked:\n"); 685 | $fail_exit = "1"; 686 | 687 | foreach my $event (@{$null_linked}) { 688 | &printer(2, "\t\t[" . $events->{$event}{$SUB} . "](" . &URL_LINK("Events", $events->{$event}{$UID}) . ")\n"); 689 | 690 | my $url_post = &URL_FETCH("Events"); 691 | $url_post =~ s/getRecords/updateRecords/g; 692 | $url_post =~ s/json/xml/g; 693 | 694 | my $post_data = ""; 695 | $post_data .= ''; 696 | $post_data .= ''; 697 | $post_data .= '' . ${event} . ''; 698 | $post_data .= '{$event}{$SUB} . ']]>'; 699 | $post_data .= ''; 700 | $post_data .= ''; 701 | $post_data .= ''; 702 | $post_data .= ''; 703 | 704 | $mech->post(${url_post}, { 705 | "scope" => ${URL_SCOPE}, 706 | "authtoken" => ${APITOKEN}, 707 | "newFormat" => 1, 708 | "id" => ${event}, 709 | "xmlData" => ${post_data}, 710 | }) && $API_REQUEST_COUNT++; 711 | 712 | if ($mech->content() =~ m/[<]error[>]/) { 713 | &printer(2, "\nPOST[" . $mech->content() . "]\n"); 714 | &printer(2, "\n"); 715 | die(); 716 | }; 717 | }; 718 | }; 719 | 720 | return(0); 721 | }; 722 | 723 | ######################################## 724 | 725 | sub print_leads { 726 | my $report = shift() || ""; 727 | 728 | my $stderr = "1"; 729 | my $err_date_list = {}; 730 | my $err_dates = {}; 731 | my $entries = "0"; 732 | 733 | if (${report} eq "CSV") { 734 | print CSV "\"Date\",\"Day\",\"New\",\"Changed\",\"Closed\",\"Cancelled\",\"${SRC}\",\"${STS}\",\"${FNM}${NAME_DIV}${LNM}\",\n"; 735 | print CSV "\"${CSV_DATE}\",\"Mon\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\n"; 736 | } 737 | elsif (${report} eq "Aging") { 738 | $stderr = "0"; 739 | 740 | &printer("\n"); 741 | &printer("${LEVEL_2} QC Aging\n"); 742 | &printer("\n"); 743 | 744 | &printer("| ${MOD} | Modified Overdue | Last Note | QC Overdue | ${REL} | ${FNM}${NAME_DIV}${LNM}\n"); 745 | &printer("|:---|:---|:---|:---|:---|:---|\n"); 746 | } 747 | else { 748 | if ( 749 | (${report} ne "Cancelled?") && 750 | (${report} ne "Broken") && 751 | (${report} ne "Graveyard") 752 | ) { 753 | $report = "All"; 754 | }; 755 | 756 | if (${report} eq "Cancelled?") { 757 | $stderr = "0"; 758 | 759 | &printer(${stderr}, "\n"); 760 | &printer(${stderr}, "${LEVEL_2} ${report}\n"); 761 | &printer(${stderr}, "\n"); 762 | } else { 763 | &printer(${stderr}, "\n"); 764 | &printer(${stderr}, "${LEVEL_2} ${report} Leads\n"); 765 | &printer(${stderr}, "\n"); 766 | }; 767 | 768 | if (${report} eq "Broken") { 769 | &printer(${stderr}, "| ${SRC} | ${STS} | ${REL} | ${FNM}${NAME_DIV}${LNM} | ${DSC}\n"); 770 | &printer(${stderr}, "|:---|:---|:---|:---|:---|\n"); 771 | } else { 772 | &printer(${stderr}, "| ${SRC} | ${STS} | ${REL} | ${FNM}${NAME_DIV}${LNM}\n"); 773 | &printer(${stderr}, "|:---|:---|:---|:---|\n"); 774 | }; 775 | }; 776 | 777 | foreach my $lead (sort({ 778 | ((${report} eq "Aging") && 779 | (($leads->{$a}{$MOD} || "") cmp ($leads->{$b}{$MOD} || "")) 780 | ) || 781 | ((${report} eq "Cancelled?") && 782 | (($cancel_bd_list->{$a} || $cancel_gd_list->{$a} || "") cmp ($cancel_bd_list->{$b} || $cancel_gd_list->{$b} || "")) 783 | ) || 784 | (($leads->{$a}{$FNM} || "") cmp ($leads->{$b}{$FNM} || "")) || 785 | (($leads->{$a}{$LNM} || "") cmp ($leads->{$b}{$LNM} || "")) || 786 | (($leads->{$a}{$MOD} || "") cmp ($leads->{$b}{$MOD} || "")) 787 | } keys(%{$leads}))) { 788 | my $source = ($leads->{$lead}{$SRC} || ""); 789 | my $status = ($leads->{$lead}{$STS} || ""); 790 | 791 | my $related = ($related_list->{ $leads->{$lead}{$LID} } || ""); 792 | my $subject = ($leads->{$lead}{$FNM} || "") . ${NAME_DIV} . ($leads->{$lead}{$LNM} || ""); 793 | my $details = ($leads->{$lead}{$DSC} || ""); 794 | $subject = "[${subject}](" . &URL_LINK("Leads", $leads->{$lead}{$LID}) . ")"; 795 | $details = "[${details}]"; $details =~ s/\n+/\]\[/g; 796 | $details =~ s/${NON_ASCII_M}/${NON_ASCII}/gms; 797 | $details =~ s/${NON_ASCII_S}/${NON_ASCII}/gms; 798 | my $modified = $leads->{$lead}{$MOD} || ""; 799 | 800 | if (${report} eq "CSV") { 801 | if ($leads->{$lead}{$DSC}) { 802 | my $matches = []; 803 | my $new = "1"; 804 | my $mod = ""; 805 | my $mod_date = ${modified}; 806 | $mod_date =~ m/^([0-9]{4})[-]([0-9]{2})[-]([0-9]{2})[ ]([0-9]{2})[:]([0-9]{2})[:]([0-9]{2})$/; 807 | $mod_date = &timegm(${6},${5},${4},${3},(${2}-1),${1}); 808 | $mod_date = &strftime("%Y-%m-%d", localtime(${mod_date})); 809 | my $subject_csv = ${subject}; 810 | $subject_csv =~ s/\"/\'/g; 811 | while ($leads->{$lead}{$DSC} =~ m/^([0-9][0-9-]+[,]?[ ]?[A-Za-z]*)$/gm) { 812 | if (${1}) { 813 | my $match = ${1}; 814 | if (${match} =~ m/^([0-9]{4}[-][0-9]{2}[-][0-9]{2})(.*)$/gm) { 815 | push(@{$matches}, ${match}); 816 | } else { 817 | push(@{ $err_dates->{"NULL"}{$match} }, ${subject}); 818 | }; 819 | }; 820 | }; 821 | my $num = "0"; 822 | foreach my $match (@{$matches}) { 823 | ${match} =~ m/^([0-9]{4}[-][0-9]{2}[-][0-9]{2})(.*)$/gm; 824 | my $date = ${1}; 825 | my $day = ${2}; 826 | $day =~ s/^[,][ ]//g; 827 | 828 | if ( 829 | (${date} eq ${mod_date}) || 830 | (${num} eq $#{$matches}) 831 | ) { 832 | $mod = "1"; 833 | }; 834 | print CSV "\"${date}\",\"${day}\",\"${new}\",\"${mod}\",\"\",\"\",\"${source}\",\"${status}\",\"${subject_csv}\",\n"; 835 | $new = ""; 836 | 837 | if (!${day}) { 838 | $day = "NULL"; 839 | }; 840 | push(@{ $err_date_list->{$date}{$day} }, ${subject}); 841 | 842 | ${num}++; 843 | }; 844 | if (!${mod}) { 845 | print CSV "\"${mod_date}\",\"[MOD]\",\"\",\"1\",\"\",\"\",\"${source}\",\"${status}\",\"${subject_csv}\",\n"; 846 | }; 847 | }; 848 | } 849 | elsif (${report} eq "Aging") { 850 | if ( 851 | ($leads->{$lead}{$STS}) && ( 852 | ($leads->{$lead}{$STS} eq "Closed Won") || 853 | ($leads->{$lead}{$STS} eq "Demo") 854 | ) && 855 | (!$cancel_bd_list->{$lead}) 856 | ) { 857 | my $mod_days = $modified; 858 | my $last_log = ""; 859 | my $overdue = "-1"; 860 | my $logs = {}; 861 | while ($leads->{$lead}{$DSC} =~ m/^([0-9]{4}[-][0-9]{2}[-][0-9]{2})(.*)$/gm) { 862 | if (${1}) { 863 | my $date = ${1}; 864 | $logs->{$date}++; 865 | }; 866 | }; 867 | foreach my $date (sort(keys(%{$logs}))) { 868 | $last_log = ${date}; 869 | $overdue = ${date}; 870 | }; 871 | if ($mod_days =~ m/^([0-9]{4})[-]([0-9]{2})[-]([0-9]{2}).*$/) { 872 | $mod_days = &timelocal(0,0,0,${3},(${2}-1),${1}); 873 | $mod_days = (time() - (${AGING_DAYS} * ${SEC_IN_DAY})) - ${mod_days}; 874 | $mod_days = int(${mod_days} / ${SEC_IN_DAY}); 875 | }; 876 | if ($overdue =~ m/^([0-9]{4})[-]([0-9]{2})[-]([0-9]{2}).*$/) { 877 | $overdue = &timelocal(0,0,0,${3},(${2}-1),${1}); 878 | $overdue = (time() - (${AGING_DAYS} * ${SEC_IN_DAY})) - ${overdue}; 879 | $overdue = int(${overdue} / ${SEC_IN_DAY}); 880 | }; 881 | if ( 882 | (${mod_days} >= 0) || 883 | (${overdue} >= 0) || 884 | (!${last_log}) 885 | ) { 886 | ${mod_days} .= " days"; 887 | ${overdue} .= " days"; 888 | 889 | &printer(${stderr}, "| ${modified} | ${mod_days} | ${last_log} | ${overdue} | ${related} | ${subject}\n"); 890 | 891 | $entries++; 892 | }; 893 | }; 894 | } 895 | elsif (${report} eq "Cancelled?") { 896 | if ($cancel_bd_list->{$lead} || $cancel_gd_list->{$lead}) { 897 | my $cancel = ($cancel_bd_list->{$lead} || $cancel_gd_list->{$lead}); 898 | $cancel =~ s/^[^[]*[[]([^]:]*)[]:].*$/$1/g; 899 | my $subject_csv = ${subject}; 900 | $subject_csv =~ s/\"/\'/g; 901 | 902 | print CSV "\"${cancel}\",\"[CNL]\",\"\",\"\",\"\",\"1\",\"${source}\",\"${status}\",\"${subject_csv}\",\n"; 903 | 904 | $subject = ($cancel_bd_list->{$lead} || $cancel_gd_list->{$lead}) . " " . ${subject}; 905 | 906 | &printer(${stderr}, "| ${source} | ${status} | ${related} | ${subject}\n"); 907 | 908 | $entries++; 909 | }; 910 | } 911 | elsif (${report} eq "Broken") { 912 | if (( 913 | (!$leads->{$lead}{$SRC}) || 914 | (!$leads->{$lead}{$STS}) 915 | ) || ( 916 | ($leads->{$lead}{$STS} eq "Demo") && ( 917 | ($leads->{$lead}{$SRC} ne "Referral") || 918 | ($leads->{$lead}{$DSC} !~ m/${DSC_IMPORT}/) 919 | ) 920 | ) || ( 921 | (!$related_list->{ $leads->{$lead}{$LID} }) && ( 922 | ($leads->{$lead}{$STS} ne "Demo") && 923 | ($leads->{$lead}{$STS} ne "Initial Call") && 924 | ($leads->{$lead}{$STS} ne "Not Interested") 925 | ) 926 | ) || ( 927 | ($related_list->{ $leads->{$lead}{$LID} }) && (( 928 | ($related_list->{ $leads->{$lead}{$LID} } > 2) 929 | ) || ( 930 | ($related_list->{ $leads->{$lead}{$LID} } > 1) && 931 | ($leads->{$lead}{$STS} ne "Closed Won") 932 | ) || ( 933 | ($related_list->{ $leads->{$lead}{$LID} } > 0) && 934 | ($leads->{$lead}{$CMP} eq ${NULL_CNAME}) 935 | )) 936 | ) || ( 937 | (!$leads->{$lead}{$FNM}) || 938 | ($leads->{$lead}{$FNM} eq $leads->{$lead}{$LNM}) || 939 | ($leads->{$lead}{$FNM} eq $leads->{$lead}{$CMP}) || 940 | ($leads->{$lead}{$LNM} ne $leads->{$lead}{$CMP}) 941 | ) || ( 942 | ($leads->{$lead}{$DSC}) && 943 | ($leads->{$lead}{$DSC} =~ m/(${DSC_FLAG}|${NON_ASCII_M}|${NON_ASCII_S})/gms) 944 | )) { 945 | $details = ($leads->{$lead}{$DSC} || ""); 946 | my $matching = ""; 947 | #>>> while ($details =~ m/^(.*(${DSC_FLAG}|${NON_ASCII_M}|${NON_ASCII_S}).*)$/gms) { 948 | while ($details =~ m/^(.*(${DSC_FLAG}|${NON_ASCII_M}|${NON_ASCII_S}).*)$/gm) { 949 | $matching .= "[${1}]"; 950 | }; 951 | $matching =~ s/${NON_ASCII_M}/${NON_ASCII}/gms; 952 | $matching =~ s/${NON_ASCII_S}/${NON_ASCII}/gms; 953 | $details = ${matching}; 954 | 955 | if (!${details}) { 956 | $details = "."; 957 | }; 958 | &printer(${stderr}, "| ${source} | ${status} | ${related} | ${subject} | ${details}\n"); 959 | 960 | $entries++; 961 | }; 962 | } 963 | elsif (${report} eq "Graveyard") { 964 | if (($leads->{$lead}{$STS}) && ($leads->{$lead}{$STS} eq "Not Interested")) { 965 | &printer(${stderr}, "| ${source} | ${status} | ${related} | ${subject}\n"); 966 | 967 | $entries++; 968 | }; 969 | } 970 | else { 971 | &printer(${stderr}, "| ${source} | ${status} | ${related} | ${subject}\n"); 972 | 973 | $entries++; 974 | }; 975 | }; 976 | 977 | if (${report} eq "CSV") { 978 | if ((%{$err_date_list}) || (%{$err_dates})) { 979 | foreach my $date (sort(keys(%{$err_date_list}))) { 980 | my $date_list = []; 981 | @{$date_list} = sort(keys(%{ $err_date_list->{$date} })); 982 | 983 | my $is_day = ${date}; 984 | $is_day =~ m/^([0-9]{4})[-]([0-9]{2})[-]([0-9]{2})$/; 985 | $is_day = &timelocal(0,0,0,${3},(${2}-1),${1}); 986 | $is_day = &strftime("%a", localtime(${is_day})); 987 | 988 | if ( 989 | (defined($err_date_list->{$date}{"NULL"})) || 990 | (!defined($err_date_list->{$date}{$is_day})) || 991 | ($#{$date_list} >= 1) 992 | ) { 993 | my $entry = "${date}, ${is_day}"; 994 | foreach my $day (sort(@{$date_list})) { 995 | foreach my $item (sort(@{ $err_date_list->{$date}{$day} })) { 996 | push(@{ $err_dates->{$entry}{$day} }, ${item}); 997 | }; 998 | }; 999 | }; 1000 | }; 1001 | 1002 | if (%{$err_dates}) { 1003 | &printer(2, "\n"); 1004 | &printer(2, "\tIncorrect Dates:\n"); 1005 | $fail_exit = "1"; 1006 | 1007 | foreach my $date (sort(keys(%{$err_dates}))) { 1008 | &printer(2, "\t\t[${date}]\n"); 1009 | foreach my $day (sort(keys(%{$err_dates->{$date}}))) { 1010 | &printer(2, "\t\t\t[${day}]\n"); 1011 | foreach my $item (sort(@{$err_dates->{$date}{$day}})) { 1012 | &printer(2, "\t\t\t\t${item}\n"); 1013 | }; 1014 | }; 1015 | }; 1016 | }; 1017 | }; 1018 | } else { 1019 | if (!${entries}) { 1020 | &printer(${stderr}, "|\n"); 1021 | }; 1022 | &printer(${stderr}, "\nEntries: ${entries}\n"); 1023 | }; 1024 | 1025 | return(0); 1026 | }; 1027 | 1028 | ######################################## 1029 | 1030 | sub print_tasks { 1031 | my $report = shift() || ""; 1032 | 1033 | my $entries = "0"; 1034 | 1035 | &printer(1, "\n"); 1036 | if (!${report}) { 1037 | &printer(1, "${LEVEL_2} Open Tasks\n"); 1038 | } else { 1039 | &printer(1, "${LEVEL_2} ${report} Tasks\n"); 1040 | }; 1041 | &printer(1, "\n"); 1042 | 1043 | &printer(1, "| ${DUE} | ${CLT} | ${TST} | ${PRI} | ${REL} | ${SUB}\n"); 1044 | &printer(1, "|:---|:---|:---|:---|:---|:---|\n"); 1045 | 1046 | foreach my $task (sort({ 1047 | (($tasks->{$a}{$DUE} || "") cmp ($tasks->{$b}{$DUE} || "")) || 1048 | (($tasks->{$a}{$CLT} || "") cmp ($tasks->{$b}{$CLT} || "")) || 1049 | (($tasks->{$a}{$REL} || "") cmp ($tasks->{$b}{$REL} || "")) || 1050 | (($tasks->{$a}{$SUB} || "") cmp ($tasks->{$b}{$SUB} || "")) 1051 | } keys(%{$tasks}))) { 1052 | my $related = ($tasks->{$task}{$REL} || ""); 1053 | my $subject = ($tasks->{$task}{$SUB} || ""); 1054 | if ($tasks->{$task}{$REL} && $tasks->{$task}{$RID}) { $related = "[${related}](" . &URL_LINK("Leads", $tasks->{$task}{$RID}) . ")"; }; 1055 | if ($tasks->{$task}{$SUB} && $tasks->{$task}{$TID}) { $subject = "[${subject}](" . &URL_LINK("Tasks", $tasks->{$task}{$TID}) . ")"; }; 1056 | 1057 | if (( 1058 | (!${report}) && 1059 | ($tasks->{$task}{$TST} eq "Not Started") 1060 | ) || ( 1061 | (${report} eq "Broken") && ( 1062 | (!$tasks->{$task}{$DUE}) || 1063 | ((!$tasks->{$task}{$TST}) || ( 1064 | ($tasks->{$task}{$TST} ne "Not Started") && 1065 | ($tasks->{$task}{$TST} ne "Deferred") && 1066 | ($tasks->{$task}{$TST} ne "Completed") 1067 | )) || 1068 | ($tasks->{$task}{$PRI} ne "High") || 1069 | ((!$tasks->{$task}{$REL}) || ( 1070 | ($tasks->{$task}{$TST} eq "Not Started") && 1071 | (!$related_list->{ $tasks->{$task}{$RID} }) 1072 | )) || 1073 | ($tasks->{$task}{$SUB} =~ m/${DSC_FLAG}/) || 1074 | ($tasks->{$task}{$DSC}) 1075 | ) 1076 | ) || ( 1077 | (${report}) && 1078 | ($tasks->{$task}{$TST} eq ${report}) 1079 | )) { 1080 | &printer(1, "| " . ($tasks->{$task}{$DUE} || "")); 1081 | &printer(1, " | " . ($tasks->{$task}{$CLT} ? sprintf("${S_DATE_ONLY}", $tasks->{$task}{$CLT}) : "")); 1082 | &printer(1, " | " . ($tasks->{$task}{$TST} || "")); 1083 | &printer(1, " | " . ($tasks->{$task}{$PRI} || "")); 1084 | &printer(1, " | ${related}"); 1085 | &printer(1, " | ${subject}"); 1086 | &printer(1, "\n"); 1087 | 1088 | $entries++; 1089 | }; 1090 | }; 1091 | 1092 | if (!${entries}) { 1093 | &printer(1, "|\n"); 1094 | }; 1095 | &printer(1, "\nEntries: ${entries}\n"); 1096 | 1097 | return(0); 1098 | }; 1099 | 1100 | ######################################## 1101 | 1102 | sub print_events { 1103 | my $find = shift() || "."; 1104 | my $keep = shift() || [ $UID, $MOD, $BEG, $END, $SRC, $STS, $REL, $SUB, $DSC, ]; 1105 | my $list = shift() || []; 1106 | 1107 | my $stderr = "1"; 1108 | my $case = ""; 1109 | my $label = ""; 1110 | my $report = ""; 1111 | my $fields = {}; 1112 | my $count_list = []; 1113 | my $entries = "0"; 1114 | my $output = ""; 1115 | 1116 | if (${find} =~ /${SPLIT_CHAR}/) { 1117 | # from "foreach my $search (@{ARGV})" and in "&update_file()" it is made pretty 1118 | ($stderr, $case, $find, $label) = split(/${SPLIT_CHAR}/, ${find}); 1119 | }; 1120 | if (!${label}) { 1121 | if ($find eq ".") { 1122 | $label = "All Events"; 1123 | } else { 1124 | $label = ${find}; 1125 | }; 1126 | }; 1127 | 1128 | if (${find} eq "Closed!") { 1129 | $report = ${find}; 1130 | $label = ${find}; 1131 | $find = "."; 1132 | $stderr = ""; 1133 | } 1134 | elsif ( 1135 | (${find} eq "Broken") || 1136 | (${find} eq "Active") 1137 | ) { 1138 | $report = ${find}; 1139 | $label = ${find} . " Events"; 1140 | $find = "."; 1141 | }; 1142 | 1143 | &printer(${stderr}, "\n"); 1144 | &printer(${stderr}, "${LEVEL_2} ${label}\n"); 1145 | &printer(${stderr}, "\n"); 1146 | 1147 | foreach my $field (@{$keep}) { 1148 | $fields->{$field} = ${field}; 1149 | }; 1150 | &print_event_fields(${stderr}, "1", ${keep}, ${fields}); 1151 | 1152 | foreach my $event (sort({ 1153 | (($events->{$a}{$BEG} || "") cmp ($events->{$b}{$BEG} || "")) || 1154 | (($events->{$a}{$END} || "") cmp ($events->{$b}{$END} || "")) || 1155 | (($events->{$a}{$SUB} || "") cmp ($events->{$b}{$SUB} || "")) 1156 | } keys(%{$events}))) { 1157 | if (( 1158 | (!${report}) && ( 1159 | (($events->{$event}{$BEG} ge ${START_DATE}) && ($events->{$event}{$SUB} =~ m/${find}/i)) && 1160 | ((!${case}) || ($events->{$event}{$SUB} =~ m/${find}/)) 1161 | ) 1162 | ) || ( 1163 | (${report} eq "Closed!") && ( 1164 | (($events->{$event}{$RID}) && ($closed_list->{ $events->{$event}{$RID} })) && 1165 | ($events->{$event}{$SUB} =~ m/${CLOSED_MARK}/) 1166 | ) 1167 | ) || ( 1168 | (${report} eq "Broken") && (( 1169 | (($events->{$event}{$SUB}) && ($events->{$event}{$SUB} =~ m/${DSC_FLAG}/)) || 1170 | (($events->{$event}{$LOC}) && ($events->{$event}{$LOC} =~ m/${DSC_FLAG}/)) || 1171 | (($events->{$event}{$DSC}) && ($events->{$event}{$DSC} =~ m/${DSC_FLAG}/)) 1172 | ) || ( 1173 | ($events->{$event}{$DSC}) && ( 1174 | ($events->{$event}{$SUB} ne ${LEGEND_NAME}) && 1175 | ($events->{$event}{$SUB} ne ${TODAY_NAME}) 1176 | ) 1177 | )) 1178 | ) || ( 1179 | (${report} eq "Active") && ( 1180 | ($events->{$event}{$RID}) && 1181 | ($events->{$event}{$SUB} !~ m/${CLOSED_MARK}/) 1182 | ) 1183 | )) { 1184 | if (${report} eq "Closed!") { 1185 | my $source = ($leads->{ $events->{$event}{$RID} }{$SRC} || ""); 1186 | my $status = ($leads->{ $events->{$event}{$RID} }{$STS} || ""); 1187 | my $subject = ($leads->{ $events->{$event}{$RID} }{$FNM} || "") . ${NAME_DIV} . ($leads->{ $events->{$event}{$RID} }{$LNM} || ""); 1188 | $subject = "[${subject}](" . &URL_LINK("Leads", $leads->{ $events->{$event}{$RID} }{$LID}) . ")"; 1189 | my $subject_csv = ${subject}; 1190 | $subject_csv =~ s/\"/\'/g; 1191 | 1192 | print CSV "\"$events->{$event}{$BEG}\",\"[CLS]\",\"\",\"\",\"1\",\"\",\"${source}\",\"${status}\",\"${subject_csv}\",\n"; 1193 | }; 1194 | 1195 | &print_event_fields(${stderr}, "", ${keep}, $events->{$event}); 1196 | 1197 | push(@{$count_list}, ${event}); 1198 | $entries++; 1199 | }; 1200 | }; 1201 | 1202 | if (!${entries}) { 1203 | $output .= "|\n"; 1204 | }; 1205 | $output .= "\nEntries: ${entries}\n"; 1206 | &printer(${stderr}, ${output}); 1207 | 1208 | &count_events(${stderr}, ${count_list}, ${list}); 1209 | 1210 | if (!${report}) { 1211 | &today_tmp(${find}, ${count_list}, ${list}); 1212 | }; 1213 | 1214 | return(0); 1215 | }; 1216 | 1217 | ######################################## 1218 | 1219 | sub print_event_fields { 1220 | my $stderr = shift() || ""; 1221 | my $header = shift() || ""; 1222 | my $keep = shift() || []; 1223 | my $vals = shift() || {}; 1224 | 1225 | my $rsource = ($vals->{$SRC} || ""); 1226 | my $rstatus = ($vals->{$STS} || ""); 1227 | my $related = ($vals->{$REL} || ""); 1228 | my $subject = ($vals->{$SUB} || ""); 1229 | my $details = ($vals->{$DSC} || ""); 1230 | my $output = ""; 1231 | 1232 | if (!${header}) { 1233 | if ($vals->{$REL} && $vals->{$RID} && $leads->{ $vals->{$RID} }{$SRC}) { $rsource = $leads->{ $vals->{$RID} }{$SRC}; }; 1234 | if ($vals->{$REL} && $vals->{$RID} && $leads->{ $vals->{$RID} }{$STS}) { $rstatus = $leads->{ $vals->{$RID} }{$STS}; }; 1235 | if ($vals->{$REL} && $vals->{$RID}) { $related = "[${related}](" . &URL_LINK("Leads", $vals->{$RID}) . ")"; }; 1236 | if ($vals->{$SUB} && $vals->{$UID}) { $subject = "[${subject}](" . &URL_LINK("Events", $vals->{$UID}) . ")"; }; 1237 | if ($vals->{$LOC}) { $subject = "[${subject}][$vals->{$LOC}]"; }; 1238 | if ($vals->{$DSC}) { $subject = "**${subject}**"; }; 1239 | if ($vals->{$DSC}) { $details = "[${details}]"; $details =~ s/\n+/\]\[/g; }; 1240 | $details =~ s/${NON_ASCII_M}/${NON_ASCII}/gms; 1241 | $details =~ s/${NON_ASCII_S}/${NON_ASCII}/gms; 1242 | }; 1243 | 1244 | foreach my $val (@{$keep}) { 1245 | my $value = ""; 1246 | if ($vals->{$val}) { 1247 | $value = $vals->{$val}; 1248 | }; 1249 | 1250 | if (${val} eq $UID) { $value = sprintf("${S_UID}", ${value}); }; 1251 | if (${val} eq $MOD) { $value = sprintf("${S_DATE}", ${value}); }; 1252 | if (${val} eq $BEG) { $value = sprintf("${S_DATE}", ${value}); }; 1253 | if (${val} eq $END) { $value = sprintf("${S_DATE}", ${value}); }; 1254 | if (${val} eq $SRC) { $value = ${rsource}; }; 1255 | if (${val} eq $STS) { $value = ${rstatus}; }; 1256 | if (${val} eq $REL) { $value = ${related}; }; 1257 | if (${val} eq $SUB) { $value = ${subject}; }; 1258 | if (${val} eq $DSC) { $value = ${details}; }; 1259 | 1260 | if ((${val} eq $REL) && (defined($vals->{$RID}))) { 1261 | if ($cancel_bd_list->{ $vals->{$RID} } || $cancel_gd_list->{ $vals->{$RID} }) { 1262 | $value = ($cancel_bd_list->{ $vals->{$RID} } || $cancel_gd_list->{ $vals->{$RID} }) . " " . ${value}; 1263 | }; 1264 | }; 1265 | 1266 | $output .= "| ${value} "; 1267 | }; 1268 | 1269 | $output =~ s/\s*$//g; 1270 | $output .= "\n"; 1271 | 1272 | if (${header}) { 1273 | foreach my $val (@{$keep}) { 1274 | $output .= "|:---"; 1275 | }; 1276 | $output .= "|\n"; 1277 | }; 1278 | 1279 | &printer(${stderr}, ${output}); 1280 | 1281 | return(0); 1282 | }; 1283 | 1284 | ######################################## 1285 | 1286 | sub count_events { 1287 | my $stderr = shift() || ""; 1288 | my $count_list = shift() || []; 1289 | my $list = shift() || []; 1290 | 1291 | my $count = {}; 1292 | my $entries = "0"; 1293 | 1294 | if (@{$count_list} && @{$list}) { 1295 | &printer(${stderr}, "\n"); 1296 | &printer(${stderr}, "| Match | Count |\n"); 1297 | &printer(${stderr}, "|:---|:---|\n"); 1298 | 1299 | foreach my $item (@{$list}) { 1300 | $count->{$item} = "0"; 1301 | }; 1302 | $count->{"DUPL"} = []; 1303 | $count->{"NULL"} = []; 1304 | 1305 | foreach my $event (@{$count_list}) { 1306 | my $matched = "0"; 1307 | 1308 | foreach my $item (@{$list}) { 1309 | if ($events->{$event}{$SUB} =~ m/${item}/) { 1310 | $count->{$item}++; 1311 | ${matched}++; 1312 | ${entries}++; 1313 | }; 1314 | }; 1315 | 1316 | if (!${matched}) { 1317 | push(@{ $count->{"NULL"} }, ${event}); 1318 | } 1319 | elsif (${matched} > 1) { 1320 | push(@{ $count->{"DUPL"} }, ${event}); 1321 | }; 1322 | }; 1323 | 1324 | foreach my $item (@{$list}) { 1325 | &printer(${stderr}, "| ${item} | $count->{$item}\n"); 1326 | }; 1327 | 1328 | foreach my $error ("DUPL", "NULL") { 1329 | if (@{ $count->{$error} }) { 1330 | &printer(${stderr}, "| *(" 1331 | . (($error eq "DUPL") ? "Duplicate" : "") 1332 | . (($error eq "NULL") ? "Unmatched" : "") 1333 | . ")* | *(" . scalar(@{ $count->{$error} }) . ")*" 1334 | ); 1335 | 1336 | foreach my $event (@{ $count->{$error} }) { 1337 | &printer(${stderr}, " [[" . $events->{$event}{$SUB} . "](" . &URL_LINK("Events", $events->{$event}{$UID}) . ")]"); 1338 | }; 1339 | &printer(${stderr}, "\n"); 1340 | }; 1341 | }; 1342 | 1343 | &printer(${stderr}, "\nEntries: ${entries}"); 1344 | if (@{ $count->{"DUPL"} } || @{ $count->{"NULL"} }) { 1345 | &printer(${stderr}, " (" . ((${entries} - scalar(@{ $count->{"DUPL"} })) + scalar(@{ $count->{"NULL"} })) . ")"); 1346 | }; 1347 | &printer(${stderr}, "\n"); 1348 | }; 1349 | 1350 | return(0); 1351 | }; 1352 | 1353 | ######################################## 1354 | 1355 | sub today_tmp { 1356 | my $title = shift() || ""; 1357 | my $count_list = shift() || []; 1358 | my $list = shift() || []; 1359 | 1360 | my $stderr = "3"; 1361 | my $current = []; 1362 | my $output = ""; 1363 | my $line; 1364 | 1365 | if (-f ${TODAY_IMP}) { 1366 | open(FILE, "<", ${TODAY_IMP}) || die(); 1367 | @{$current} = split("\n", do { local $/; }); 1368 | close(FILE) || die(); 1369 | } 1370 | elsif (-f ${TODAY_EXP}) { 1371 | open(FILE, "<", ${TODAY_EXP}) || die(); 1372 | @{$current} = split("\n", do { local $/; }); 1373 | close(FILE) || die(); 1374 | }; 1375 | 1376 | if (@{$count_list} && @{$list}) { 1377 | foreach my $item (@{$list}) { 1378 | $output .= "${LEVEL_2} [${title}|${item}]\n\n"; 1379 | 1380 | foreach my $event (@{$count_list}) { 1381 | if ($events->{$event}{$SUB} =~ m/${item}/) { 1382 | $line = &today_tmp_format(${event}, "[${title}|${item}]"); 1383 | 1384 | my $match = "0"; 1385 | foreach my $test (@{$current}) { 1386 | if (${test} =~ m/\Q${line}\E/) { 1387 | $match = "1"; 1388 | }; 1389 | }; 1390 | if (!${match}) { 1391 | $output .= "${line}\n"; 1392 | }; 1393 | }; 1394 | }; 1395 | 1396 | $output .= "\n"; 1397 | }; 1398 | }; 1399 | 1400 | &printer(${stderr}, ${output}); 1401 | 1402 | return(0); 1403 | }; 1404 | 1405 | ######################################## 1406 | 1407 | sub today_tmp_format { 1408 | my $event = shift() || ""; 1409 | my $mark = shift() || "[${DSC_FLAG}]"; 1410 | my $line; 1411 | 1412 | my $comp = $events->{$event}{$SUB}; 1413 | my $name = ""; 1414 | 1415 | if ($events->{$event}{$RID}) { 1416 | $comp =~ s/^${MARK_REGEX}//; 1417 | if (${1}) { $mark = "[${1}]"; }; 1418 | if ($leads->{ $events->{$event}{$RID} }{$CMP}) { $comp = $leads->{ $events->{$event}{$RID} }{$CMP}; }; 1419 | if ($leads->{ $events->{$event}{$RID} }{$FNM}) { $name = $leads->{ $events->{$event}{$RID} }{$FNM}; }; 1420 | }; 1421 | 1422 | $line = " * ${mark} ${comp} {${name}}"; 1423 | $line .= " {"; 1424 | $line .= (($events->{$event}{$BEG}) ? $events->{$event}{$BEG} : ""); 1425 | foreach my $task (sort(keys(%{$tasks}))) { 1426 | if ( 1427 | (($tasks->{$task}{$RID}) && ($events->{$event}{$RID})) && 1428 | ($tasks->{$task}{$RID} eq $events->{$event}{$RID}) && 1429 | ($tasks->{$task}{$TST} eq "Not Started") 1430 | ) { 1431 | $line .= "|" . $tasks->{$task}{$SUB}; 1432 | $orphaned_tasks->{$task}++; 1433 | }; 1434 | }; 1435 | $line .= "}"; 1436 | $line .= (($events->{$event}{$LOC}) ? (" -- " . $events->{$event}{$LOC}) : ""); 1437 | 1438 | return(${line}); 1439 | }; 1440 | 1441 | ######################################## 1442 | 1443 | sub today_tmp_reverse { 1444 | my $stderr = "3"; 1445 | my $current = []; 1446 | my $duplicates = {}; 1447 | 1448 | if (-f ${TODAY_IMP}) { 1449 | open(FILE, "<", ${TODAY_IMP}) || die(); 1450 | @{$current} = split("\n", do { local $/; }); 1451 | close(FILE) || die(); 1452 | } 1453 | elsif (-f ${TODAY_EXP}) { 1454 | open(FILE, "<", ${TODAY_EXP}) || die(); 1455 | @{$current} = split("\n", do { local $/; }); 1456 | close(FILE) || die(); 1457 | }; 1458 | 1459 | my $lines = []; 1460 | foreach my $event (sort(keys(%{$events}))) { 1461 | push(@{$lines}, &today_tmp_format($events->{$event}{$UID})); 1462 | }; 1463 | 1464 | &printer(${stderr}, "${LEVEL_2} [${DSC_FLAG}]\n\n"); 1465 | 1466 | foreach my $task (sort(keys(%{$tasks}))) { 1467 | if (( 1468 | (!$tasks->{$task}{$RID}) || 1469 | (!$orphaned_tasks->{$task}) 1470 | ) && ( 1471 | ($tasks->{$task}{$TST} eq "Not Started") 1472 | )) { 1473 | my $related = ($tasks->{$task}{$REL} || ""); 1474 | my $subject = ($tasks->{$task}{$SUB} || ""); 1475 | if ($tasks->{$task}{$REL} && $tasks->{$task}{$RID}) { $related = "[${related}](" . &URL_LINK("Leads", $tasks->{$task}{$RID}) . ")"; }; 1476 | if ($tasks->{$task}{$SUB} && $tasks->{$task}{$TID}) { $subject = "[${subject}](" . &URL_LINK("Tasks", $tasks->{$task}{$TID}) . ")"; }; 1477 | &printer(${stderr}, "\t${subject} -> ${related}\n"); 1478 | }; 1479 | }; 1480 | 1481 | &printer(${stderr}, "\n${LEVEL_2} [${DSC_FLAG}]\n\n"); 1482 | 1483 | foreach my $test (@{$current}) { 1484 | my $match = "0"; 1485 | foreach my $line (@{$lines}) { 1486 | if (${test} =~ m/\Q${line}\E/) { 1487 | $match = "1"; 1488 | }; 1489 | }; 1490 | if (!${match}) { 1491 | &printer(${stderr}, "\t${test}\n"); 1492 | }; 1493 | $duplicates->{$test}++; 1494 | }; 1495 | 1496 | &printer(${stderr}, "\n${LEVEL_2} [${DSC_FLAG}]\n\n"); 1497 | 1498 | foreach my $item (sort(keys(%{$duplicates}))) { 1499 | if ( 1500 | (${item}) && 1501 | ($duplicates->{$item} > 1) 1502 | ) { 1503 | &printer(${stderr}, "\t${item}\n"); 1504 | }; 1505 | }; 1506 | 1507 | return(0); 1508 | }; 1509 | 1510 | ######################################## 1511 | 1512 | sub print_notes { 1513 | my $notes = {}; 1514 | my $entries = "0"; 1515 | my $updated; 1516 | 1517 | open(CSV, "<", ${NOTES_FILE}) || die(); 1518 | $updated = (stat(${NOTES_FILE}))[9]; 1519 | $updated = localtime(${updated}); 1520 | while () { 1521 | my $num = "1"; 1522 | while (${_} =~ m/zcrm[_]([0-9]+)/gm) { 1523 | if (${num} == 3) { 1524 | $notes->{$1}++; 1525 | ${entries}++; 1526 | }; 1527 | ${num}++; 1528 | }; 1529 | }; 1530 | close(CSV) || die(); 1531 | 1532 | &printer(1, "\n"); 1533 | &printer(1, "${LEVEL_2} Exported Notes\n"); 1534 | &printer(1, "\n"); 1535 | &printer(1, "[[Recycle Bin]](https://crm.zoho.com/crm/ShowSetup.do?tab=data&subTab=recyclebin)"); 1536 | &printer(1, "${NAME_DIV}"); 1537 | &printer(1, "[[Export Link]](https://crm.zoho.com/crm/ShowSetup.do?tab=data&subTab=export): ${updated}"); 1538 | &printer(1, "\n\n"); 1539 | &printer(1, "| Notes Count | ${FNM}${NAME_DIV}${LNM} |\n"); 1540 | &printer(1, "|:---|:---|\n"); 1541 | 1542 | foreach my $note (sort(keys(%{$notes}))) { 1543 | my $subject = ($leads->{$note}{$FNM} || "") . ${NAME_DIV} . ($leads->{$note}{$LNM} || ""); 1544 | $subject = "[${subject}](" . &URL_LINK("Leads", $leads->{$note}{$LID}) . ")"; 1545 | &printer(1, "| " . $notes->{$note} . " | " . ${subject} . "\n"); 1546 | }; 1547 | 1548 | if (!${entries}) { 1549 | &printer(1, "|\n"); 1550 | }; 1551 | &printer(1, "\nEntries: " . ${entries} . " (" . scalar(keys(%{$notes})) . " Leads)\n"); 1552 | 1553 | return(0); 1554 | }; 1555 | 1556 | ################################################################################ 1557 | 1558 | foreach my $type (@{$TYPES}) { 1559 | my $var = lc(${type}); 1560 | 1561 | %{ $z->{$var} } = &fetch_entries(${type}); 1562 | 1563 | open(JSON, ">", ${JSON_BASE} . "." . ${var} . ".json") || die(); 1564 | print JSON $json->encode($z->{$var}); 1565 | close(JSON) || die(); 1566 | }; 1567 | 1568 | if ( 1569 | (%{$leads}) || 1570 | (%{$events}) 1571 | ) { 1572 | open(CSV, ">", ${CSV_FILE}) || die(); 1573 | close(CSV) || die(); 1574 | }; 1575 | 1576 | ######################################## 1577 | 1578 | foreach my $lead (keys(%{$leads})) { 1579 | if ($leads->{$lead}{$STS} && $leads->{$lead}{$STS} eq "Closed Won") { 1580 | $closed_list->{$lead}++; 1581 | }; 1582 | foreach my $lead (keys(%{$leads})) { 1583 | while ($leads->{$lead}{$DSC} =~ m/(${DSC_EXP_BD}|${DSC_EXP_GD})[:]?(.*)$/gm) { 1584 | if (${1} eq ${DSC_EXP_BD}) { $cancel_bd_list->{$lead} = "**[${2}][${DSC_EXP_BD_I}]**"; }; 1585 | if (${1} eq ${DSC_EXP_GD}) { $cancel_gd_list->{$lead} = "**[${2}][${DSC_EXP_GD_I}]**"; }; 1586 | }; 1587 | }; 1588 | }; 1589 | 1590 | foreach my $event (keys(%{$events})) { 1591 | if ($events->{$event}{$RID}) { 1592 | $related_list->{ $events->{$event}{$RID} }++; 1593 | }; 1594 | 1595 | if ($events->{$event}{$SUB} eq ${NULL_ENAME}) { 1596 | push(@{$empty_events}, "[" . $events->{$event}{$BEG} . "](" . &URL_LINK("Events", $events->{$event}{$UID}) . ")"); 1597 | }; 1598 | }; 1599 | 1600 | ######################################## 1601 | 1602 | if (%{$events}) { 1603 | &update_legend(); 1604 | &update_today(); 1605 | }; 1606 | 1607 | if ( 1608 | (%{$leads}) && 1609 | (${FIND_NOTES}) 1610 | ) { 1611 | &find_notes_entries(); 1612 | }; 1613 | 1614 | if (%{$leads}) { 1615 | open(CSV, ">>", ${CSV_FILE}) || die(); 1616 | &print_leads("CSV"); 1617 | close(CSV) || die(); 1618 | }; 1619 | 1620 | if (@{$empty_events}) { 1621 | &printer(2, "\n"); 1622 | &printer(2, "\tEmpty Events:\n"); 1623 | $fail_exit = "1"; 1624 | 1625 | foreach my $entry (sort(@{$empty_events})) { 1626 | &printer(2, "\t\t${entry}\n"); 1627 | }; 1628 | }; 1629 | 1630 | &check_recycle_bin(); 1631 | 1632 | if (%{$events}) { 1633 | &unlink_null_events(); 1634 | }; 1635 | 1636 | ######################################## 1637 | 1638 | &printer(2, "\n"); 1639 | &printer(2, "\tTotal Requests: ${API_REQUEST_COUNT}\n"); 1640 | 1641 | if (${fail_exit}) { 1642 | exit(${fail_exit}); 1643 | }; 1644 | 1645 | ################################################################################ 1646 | 1647 | &printer(1, "\n"); 1648 | &printer("${LEVEL_1} Core Reports\n"); 1649 | 1650 | if (%{$events}) { 1651 | open(CSV, ">>", ${CSV_FILE}) || die(); 1652 | &print_events("Closed!", [ $BEG, $SRC, $STS, $REL, $SUB, ]); 1653 | &printer("\n"); 1654 | &printer("Closed: " . scalar(keys(%{$closed_list})) . "\n"); 1655 | close(CSV) || die(); 1656 | }; 1657 | 1658 | ######################################## 1659 | 1660 | if (%{$leads}) { 1661 | open(CSV, ">>", ${CSV_FILE}) || die(); 1662 | &print_leads("Cancelled?"); 1663 | close(CSV) || die(); 1664 | &print_leads("Broken"); 1665 | #>>> &print_leads(); 1666 | &print_leads("Graveyard"); 1667 | &print_leads("Aging"); 1668 | }; 1669 | 1670 | ######################################## 1671 | 1672 | if (%{$tasks}) { 1673 | &print_tasks("Broken"); 1674 | &print_tasks(); 1675 | &print_tasks("Deferred"); 1676 | &print_tasks("Completed"); 1677 | }; 1678 | 1679 | ######################################## 1680 | 1681 | if (%{$events}) { 1682 | &print_events("Broken", [ $BEG, $STS, $REL, $SUB, ]); 1683 | #>>> &print_events(); 1684 | 1685 | my $counts = []; 1686 | my $opt_num = "0"; 1687 | foreach my $opt (@{ARGV}) { 1688 | if (${opt} =~ m/[#][ ]Active[ ]${A_BEG_CHAR}(.*)${A_END_CHAR}$/) { 1689 | @{$counts} = split(/${SPLIT_CHAR}/, ${1}); 1690 | splice(@{ARGV}, ${opt_num}, 1); 1691 | }; 1692 | ${opt_num}++; 1693 | }; 1694 | &print_events("Active", [ $BEG, $STS, $REL, $SUB, ], ${counts}); 1695 | }; 1696 | 1697 | ######################################## 1698 | 1699 | if (-f ${NOTES_FILE}) { 1700 | &print_notes(); 1701 | }; 1702 | 1703 | ######################################## 1704 | 1705 | if (%{$events}) { 1706 | # input for "&print_events()" and in "&update_file()" it is made pretty 1707 | foreach my $search (@{ARGV}) { 1708 | if (${search} =~ m/^[${HEAD_MARKER}][ ]/) { 1709 | $search =~ s/^[${HEAD_MARKER}][ ]//g; 1710 | &printer("\n"); 1711 | &printer("${LEVEL_1} ${search}\n"); 1712 | } else { 1713 | my $counts = []; 1714 | if (${search} =~ m/[ ]${A_BEG_CHAR}(.*)${A_END_CHAR}$/) { 1715 | $search =~ s/[ ]${A_BEG_CHAR}(.*)${A_END_CHAR}$//; 1716 | @{$counts} = split(/${SPLIT_CHAR}/, ${1}); 1717 | }; 1718 | &print_events(${search}, [ $BEG, $STS, $REL, $SUB, ], ${counts}); 1719 | }; 1720 | }; 1721 | 1722 | &today_tmp_reverse(); 1723 | }; 1724 | 1725 | ################################################################################ 1726 | 1727 | close(ALL_FILE) || die(); 1728 | close(OUT_FILE) || die(); 1729 | close(TODAY_TMP) || die(); 1730 | 1731 | exit(0); 1732 | 1733 | ################################################################################ 1734 | # end of file 1735 | ################################################################################ 1736 | -------------------------------------------------------------------------------- /tasks.kanban.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | Taskwarrior: Kanban Board 13 | 14 |
15 |
16 | 17 | 30 | 31 | 35 | 42 | 43 | 44 | Data 45 | Board 46 |
47 |
48 | 49 | 51 | 52 | 53 | 55 | 69 | 70 | 71 | 72 | 74 | 87 | 113 | 114 | 227 | 228 |
229 | 234 | 258 | -------------------------------------------------------------------------------- /tasks.timeline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Taskwarrior: SIMILE Timeline Widget 4 | 5 | 6 | 7 | 8 | 9 | 10 | 66 | 72 | 73 | 74 | 75 | 78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /tasks.weekly.txt: -------------------------------------------------------------------------------- 1 | ###[ Weekly Review Steps & Commands ]### 2 | 3 | #[ Markdown Source ] 4 | #]#### Weekly Review 5 | #] 6 | #] * [System](#system) 7 | #] * Taskwarrior ('.view') 8 | #] * Reminders (`calendar`, `mind`) 9 | #] * Past, for missed 10 | #] * Future, for upcoming 11 | #] * Todos (`todo`) 12 | #] * Overview (`fail`, `data`, `meta status:pending`) 13 | #] * Recurring (`read status:recurring`, `+task-recur status:recurring`, `read status.isnt:recurring mask.any:`, `+task-recur status.isnt:recurring mask.any:`) 14 | #] * Projects (`projects`) 15 | #] * Active (`projects status:pending`) 16 | #] * Iterate (`+for FILE in $(task status:pending _unique project); do task read project.is:${FILE} -PARENT -CHILD; task-depends -r project.is:${FILE} -PARENT -CHILD; done`) 17 | #] * Tasks (`view status:pending project.none:; task-depends -r status:pending project.none:`) 18 | #] * UDAs (`udas`) 19 | #] * Kinds (`+for FILE in $(task status:pending _unique kind); do task read status:pending kind.is:${FILE}; done`) 20 | #] * Areas (`+for FILE in $(task status:pending _unique area); do task tags status:pending area.is:${FILE}; task read status:pending area.is:${FILE}; done`) 21 | #] * Tags (`tags`) 22 | #] * Active (`tags status:pending`) 23 | #] * Iterate (`+for FILE in $(task status:pending _unique tags); do task read status:pending tags.is:${FILE}; done`) 24 | #] * Report (`.repo`) 25 | 26 | #[ _task_parse_cmd :: impersonate_command ] 27 | #|calendar 28 | #|mind 29 | #|todo 30 | #|fail 31 | #|data 32 | #|meta status:pending 33 | #|read status:recurring 34 | #|+task-recur status:recurring 35 | #|read status.isnt:recurring mask.any: 36 | #|+task-recur status.isnt:recurring mask.any: 37 | #|projects 38 | #|projects status:pending 39 | #|+for FILE in $(task status:pending _unique project); do task read project.is:${FILE} -PARENT -CHILD; task-depends -r project.is:${FILE} -PARENT -CHILD; done 40 | #|view status:pending project.none:; task-depends -r status:pending project.none: 41 | #|udas 42 | #|+for FILE in $(task status:pending _unique kind); do task read status:pending kind.is:${FILE}; done 43 | #|+for FILE in $(task status:pending _unique area); do task tags status:pending area.is:${FILE}; task read status:pending area.is:${FILE}; done 44 | #|tags 45 | #|tags status:pending 46 | #|+for FILE in $(task status:pending _unique tags); do task read status:pending tags.is:${FILE}; done 47 | #|.repo 48 | 49 | #[ _task_parse_cmd_bash :: bash] 50 | task calendar; 51 | task mind; 52 | task todo; 53 | task fail; 54 | task data; 55 | task meta status:pending; 56 | task read status:recurring; 57 | eval ${MARKER}; IMPERSONATE_NAME=task .bashrc task-recur status:recurring; 58 | task read status.isnt:recurring mask.any:; 59 | eval ${MARKER}; IMPERSONATE_NAME=task .bashrc task-recur status.isnt:recurring mask.any:; 60 | task projects; 61 | task projects status:pending; 62 | for FILE in $(task status:pending _unique project); do task read project.is:${FILE} -PARENT -CHILD; eval ${MARKER}; IMPERSONATE_NAME=task .bashrc task-depends -r project.is:${FILE} -PARENT -CHILD; done; 63 | task view status:pending project.none:; eval ${MARKER}; IMPERSONATE_NAME=task .bashrc task-depends -r status:pending project.none:; 64 | task udas; 65 | for FILE in $(task status:pending _unique kind); do task read status:pending kind.is:${FILE}; done; 66 | for FILE in $(task status:pending _unique area); do task tags status:pending area.is:${FILE}; task read status:pending area.is:${FILE}; done; 67 | task tags; 68 | task tags status:pending; 69 | for FILE in $(task status:pending _unique tags); do task read status:pending tags.is:${FILE}; done; 70 | eval ${MARKER}; IMPERSONATE_NAME=task .bashrc impersonate_command repo; 71 | 72 | #[ Markdown Source ] 73 | #]#### Weekly Report 74 | #] 75 | #] * Taskwarrior ('.repo') 76 | #] * Integrity (`diagnostics`) 77 | #] * Projects (`summary`) 78 | #] * History 79 | #] * Numerical (`rc.defaultwidth=120 history.monthly`) 80 | #] * Graphical (`rc.defaultwidth=120 ghistory.monthly`) 81 | #] * Trending 82 | #] * Weekly (`rc.defaultwidth=120 rc.defaultheight=40 burndown.weekly`) 83 | #] * Daily (`rc.defaultwidth=120 rc.defaultheight=40 burndown.daily`) 84 | #] * Activity 85 | #] * Custom (`+SINCE="$(date --date="@$(calc $(date +%s)-$(calc 60*60*24*7))" --iso=s)"; task sort rc.color.completed=green rc.color.deleted=red \( \( end.after:${SINCE} \) or \( modified.after:${SINCE} kind.any: \) \)`) 86 | #] * Default (`timesheet 2`) 87 | #] * Statistics (`stats`) 88 | 89 | #[ _task_parse_cmd :: impersonate_command ] 90 | #|diagnostics 91 | #|summary 92 | #|rc.defaultwidth=120 history.monthly 93 | #|rc.defaultwidth=120 ghistory.monthly 94 | #|rc.defaultwidth=120 rc.defaultheight=40 burndown.weekly 95 | #|rc.defaultwidth=120 rc.defaultheight=40 burndown.daily 96 | #|+SINCE="$(date --date="@$(calc $(date +%s)-$(calc 60*60*24*7))" --iso=s)"; task sort rc.color.completed=green rc.color.deleted=red \( \( end.after:${SINCE} \) or \( modified.after:${SINCE} kind.any: \) \) 97 | #|timesheet 2 98 | #|stats 99 | 100 | #[ _task_parse_cmd_bash :: bash] 101 | task diagnostics; 102 | task summary; 103 | task rc.defaultwidth=120 history.monthly; 104 | task rc.defaultwidth=120 ghistory.monthly; 105 | task rc.defaultwidth=120 rc.defaultheight=40 burndown.weekly; 106 | task rc.defaultwidth=120 rc.defaultheight=40 burndown.daily; 107 | SINCE="$(date --date="@$(calc $(date +%s)-$(calc 60*60*24*7))" --iso=s)"; task sort rc.color.completed=green rc.color.deleted=red \( \( end.after:${SINCE} \) or \( modified.after:${SINCE} kind.any: \) \); 108 | task timesheet 2; 109 | task stats; 110 | -------------------------------------------------------------------------------- /tasks.xltx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garybgenett/tasks/e63c290d86fa47b5a1f8a89cd86b5a6854e14fd2/tasks.xltx --------------------------------------------------------------------------------