1package AmphetaDesk::AmphetaDesk::MyChannels;
2###############################################################################
3# AmphetaDesk::AmphetaDesk                                           (c) 2000-2002 Disobey #
4# morbus@disobey.com                      http://www.disobey.com/amphetadesk/ #
5###############################################################################
6# ABOUT THIS PACKAGE:                                                         #
7#   This package is important - it carries all of the various functions that  #
8#   are used to create and interact with the user's subscription list. By     #
9#   default, it comes built in with a number of standard entries, but will    #
10#   create a user specific file when the time comes (much like Settings.pm).  #
11#                                                                             #
12# LIST OF ROUTINES BELOW:                                                     #
13#   add_url - adds a channel to myChannels.opml from passed url.              #
14#   check_for_duplicate_filename - checks for sub'd filenames existence.      #
15#   check_for_duplicate_url - checks for the existance of a sub'd url.        #
16#   count_my_channels - simply spits out the number of sub'd channels.        #
17#   create_channel_filename - creates a filename based off channel title.     #
18#   del_url - delete a single channel from the subscribed list.               #
19#   download_my_channels - read our myChannels.opml and get all channels.     #
20#   get_my_channels_data - get the value of the passed data from channel url. #
21#   get_my_sorted_channels - get arrays of various sorted channel options.    #
22#   import_my_channels - imports the OPML data into the current list.         #
23#   load_my_channels - reads in the myChannels.opml and returns the data.     #
24#   remove_old_channel_files - deletes old and dejected files.                #
25#   save_my_channels - saves the relevant channel subscriptions to a file.    #
26#   set_my_channels_data - set the value of the passed data for channel url.  #
27#   update_my_channel_data - takes a URL and new data and updates our OPML.   #
28###############################################################################
29#    "Get your hands off my damn remote, woman! Make me some pie! PIEEe!"     #
30###############################################################################
31
32use strict; $|++;
33use AmphetaDesk::AmphetaDesk::Settings;
34use AmphetaDesk::AmphetaDesk::Utilities;
35use AmphetaDesk::AmphetaDesk::WWW;
36use HTTP::Date qw/time2iso str2time/;
37use Time::Local;
38use File::Spec::Functions;
39use AmphetaDesk::XML::Simple;
40require Exporter;
41use vars qw( @ISA @EXPORT );
42@ISA = qw( Exporter );
43@EXPORT = qw( add_url count_my_channels del_url download_my_channels
44              get_my_channels_data get_my_sorted_channels import_my_channels
45              load_my_channels remove_old_channel_files save_my_channels
46              set_my_channels_data update_my_channel_data );
47
48# where we store our in
49# memory myChannels.opml.
50my %CHANNELS;
51
52###############################################################################
53# add_url - adds a channel to myChannels.opml from passed url.                #
54###############################################################################
55# USAGE:                                                                      #
56#    add_url( $url );                                                         #
57#                                                                             #
58# NOTES:                                                                      #
59#    This routine downloads the passed $url and creates an entry for          #
60#    myChannels.opml based on the data it sees within. After saving the file, #
61#    it saves the myChannels.opml list.                                       #
62#                                                                             #
63# RETURNS:                                                                    #
64#    1; there's no reason why this routine should fail.                       #
65###############################################################################
66
67sub add_url {
68
69   my ($incoming_urls) = @_;
70   return unless $incoming_urls;
71
72   # first thing we do is split the URL on any newlines
73   # or on ",http://". This allows us to accept mass
74   # POSTS of newlined URLs, as well as comma spliced lists.
75   my @urls = split(/\n|,http:\/\//, $incoming_urls);
76
77   # now, loop through each one.
78   foreach my $url (@urls) {
79
80      # add the http:// back if missing.
81      $url =~ s/^/http:\/\// unless $url =~ /^http:\/\//;
82
83      # just some happy blurbage for the log file.
84      note("Attempting to add a new channel from $url.");
85
86      # does the url already exists in our subscriptions?
87      if ( check_for_duplicate_url($url) ) {
88         note("AmphetaDesk has determined that you are already " .
89              "subscribed to '$CHANNELS{$url}{title}'.", 1, 1);
90         next; # ya boOog! subscribing to the subscribed. sheesh!
91      }
92
93      # download the channel data.
94      my $channel_raw_data = get($url);
95      unless ($channel_raw_data) {
96         note("AmphetaDesk could not connect to $url.", 1, 1); next;
97      }
98
99      # if this looks like a OPML file, load it in, and import all
100      # the various channels within (Radio/AmphetaDesk compatible).
101      # we check our return code for success (the import will fail
102      # if "xmlurl" attributes don't exist in the OPML.
103      if ($channel_raw_data =~ /<opml.*(version)?/i) {
104         my $success = import_my_channels($channel_raw_data);
105         note("AmphetaDesk has imported the channels from $url and " .
106              "will load them the next time you restart AmphetaDesk::AmphetaDesk.", 1, 1) if $success;
107         next; # oooh, super multiple import power! whoOO's house?
108      }
109
110      # and suck it in for parsing. module calling is bad.
111      my $channel_data = AmphetaDesk::AmphetaDesk::Channels::load_channel($channel_raw_data);
112      unless ($channel_data) {
113         note("AmphetaDesk could not parse $url.", 1, 1);
114         next; # Entities, doctype's, and amperstands, oh my!
115      }
116
117      # channel data holders.
118      my ($htmlurl, $title, $description);
119
120      # if this is RSS xml, then create a title and description.
121      if ( defined( $channel_data->{channel}->{title} ) ) {
122         $htmlurl = $channel_data->{channel}->{"link"};
123         $title = $channel_data->{channel}->{title};
124         $description = $channel_data->{channel}->{description}
125            if defined( $channel_data->{channel}->{description} );
126      }
127
128      # else, maybe it's scriptingNews...
129      elsif ( defined ( $channel_data->{header}->{channelTitle} ) ) {
130         $htmlurl = $channel_data->{header}->{channelLink};
131         $title = $channel_data->{header}->{channelTitle};
132         $description = $channel_data->{header}->{channelDescription}
133            if defined( $channel_data->{header}->{channelDescription} );
134      }
135
136      # else...
137      else { # we have no clue, so spit an error and return.
138         note("AmphetaDesk could not determine the format of $url.", 1, 1); next;
139      }
140
141      # sanitize our data before processing.
142      $title = strip_newlines_and_tabs($title);
143      $description = strip_newlines_and_tabs($description);
144      $title = $title || $url; $description = $description || "";
145
146      # now create a filename based off the title.
147      my $filename = create_channel_filename( $title );
148      my $filepath = catfile( get_setting("dir_data_channels"), $filename );
149
150      # and save the channel data, so we don't have to redownload it.
151      open(NEW_CHANNEL, ">$filepath") or
152         note("AmphetaDesk could not save the new channel data. " .
153              "Please report the following error to " . get_setting("app_email") . ": $!", 1, 1);
154      print NEW_CHANNEL $channel_raw_data; close(NEW_CHANNEL);
155
156      # now, add the new channel data to our %CHANNELS.
157      $CHANNELS{$url}{date_added}      = time2iso(time);
158      $CHANNELS{$url}{date_downloaded} = $CHANNELS{$url}{date_added};
159      $CHANNELS{$url}{description}     = $description;
160      $CHANNELS{$url}{title}           = $title;
161      $CHANNELS{$url}{filename}        = $filename;
162      $CHANNELS{$url}{htmlurl}         = $htmlurl;
163      $CHANNELS{$url}{xmlurl}          = $url;
164
165      # and save subscriptions to disk.
166      save_my_channels();
167
168      # and say hello to our log file.
169      note("AmphetaDesk has added '$title' successfully.", 1, 1);
170   }
171
172   return 1;
173
174}
175
176###############################################################################
177# check_for_duplicate_filename - checks for sub'd filenames existence.        #
178# check_for_duplicate_url - checks for the existance of a sub'd url.          #
179###############################################################################
180# USAGE:                                                                      #
181#    check_for_duplicate_filename;                                            #
182#    check_for_duplicate_url;                                                 #
183#                                                                             #
184# NOTES:                                                                      #
185#    These routines merely check to see if various filenames or URLs already  #
186#    exist in the currently subscribed channels. If so, we return happily.    #
187#                                                                             #
188# RETURNS:                                                                    #
189#    1; a matching filename or url was found.                                 #
190#    0; no matching filename or url was found.                                #
191###############################################################################
192
193sub check_for_duplicate_url { my ($url) = @_; foreach (keys %CHANNELS) { return 1 if $CHANNELS{$_}{xmlurl} eq $url; } return 0; }
194sub check_for_duplicate_filename { my ($filename) = @_; foreach (keys %CHANNELS) { return 1 if $CHANNELS{$_}{filename} eq $filename; } return 0; }
195
196###############################################################################
197# count_my_channels - simply spits out the number of sub'd channels.          #
198###############################################################################
199# USAGE:                                                                      #
200#    my $total_count = count_my_channels;                                     #
201#                                                                             #
202# NOTES:                                                                      #
203#    This routine counts the number of channels currently subscribed and      #
204#    returns the result. No muss, no fuss. Wheeeeee. My comments own j00.     #
205#                                                                             #
206# RETURNS:                                                                    #
207#    $count; the total number of subscribed channels.                         #
208###############################################################################
209
210sub count_my_channels {
211   my $total_count; # /me coughs like a sick dog. bah!
212   foreach ( values %CHANNELS ) { $total_count++; }
213   $total_count; # /me wanders about in a daze, moaning.
214}
215
216###############################################################################
217# create_channel_filename - creates a filename based off the channel title.   #
218###############################################################################
219# USAGE:                                                                      #
220#    create_channel_filename( $channel_title );                               #
221#                                                                             #
222# NOTES:                                                                      #
223#    This routine looks at $channel_title and creates a semi-unique filename  #
224#    based off it. It chalks on ".xml" to the end, and returns the creation.  #
225#                                                                             #
226# RETURNS:                                                                    #
227#    $filename; the semi-unique filename created.                             #
228###############################################################################
229
230sub create_channel_filename {
231
232   my ($channel_title) = @_;
233
234   # removing nonword characters,
235   # lowercase, chop at 20,
236   # and add extension.
237   $channel_title =~ s/\W//gi;
238   $channel_title = lc($channel_title);
239   $channel_title = substr($channel_title, 0, 20);
240
241   # check the %CHANNELS to see if the filename is already
242   # in use.  if so, chop the last four chars off the string,
243   # and append a number. fixes moreover.com title bug.
244   if ( check_for_duplicate_filename("$channel_title.xml") ) {
245      $channel_title = substr($channel_title, -4);
246      $channel_title .= int(rand 8999)+1000;
247   }
248
249   # my logfile is growing larger.
250   note("Channel filename has been created: $channel_title.xml.");
251
252   return "$channel_title.xml";
253
254}
255
256###############################################################################
257# del_url - delete a single channel from the subscribed list                  #
258###############################################################################
259# USAGE:                                                                      #
260#    del_url( $channel_url );                                                 #
261#                                                                             #
262# NOTES:                                                                      #
263#    This routine takes a look at the passed channel url and sees if there is #
264#    a matching one in our global %CHANNELS. If there is, then we delete that #
265#    one out of there, and resave our channels file. Multiple urls can be     #
266#    specified if they are seperated by ",http://" or a newline.              #
267#                                                                             #
268# RETURNS:                                                                    #
269#    1; there's no reason why this routine would fail.                        #
270###############################################################################
271
272sub del_url {
273
274   my ($incoming_urls) = @_;
275   return unless $incoming_urls;
276
277   # split the channel url on newline or http://.
278   my @urls = split(/\n|,http:\/\//, $incoming_urls);
279
280   # now, loop through each one.
281   foreach my $url (@urls) {
282
283      # add the http:// back if missing.
284      $url =~ s/^/http:\/\// unless $url =~ /^http:\/\//;
285
286      # go through each of the channel
287      # and remove from our $CHANNELS
288      # as well as the hard drive.
289      if (exists $CHANNELS{$url}) {
290         my $file_location = catfile(get_setting("dir_data_channels"), $CHANNELS{$url}{filename});
291         note("AmphetaDesk has removed '$CHANNELS{$url}{title}' successfully.", 1, 1);
292         delete $CHANNELS{$url}; unlink $file_location; # listening to chemical brothers.
293      } else { note("AmphetaDesk couldn't find $url. Did you already remove it?", 1, 1); }
294   }
295
296   # save the channels
297   save_my_channels();
298
299   return 1;
300
301}
302
303###############################################################################
304# download_my_channels - read our myChannels.opml and get all channels.       #
305###############################################################################
306# USAGE:                                                                      #
307#    download_my_channels();                                                  #
308#                                                                             #
309# NOTES:                                                                      #
310#    This routine takes a look at our loaded %CHANNELS and downloads each     #
311#    one to our local directory, assuming it meets a number of time-based     #
312#    prerequisities. It will sort the downloads by oldest modified channel.   #
313#                                                                             #
314# RETURNS:                                                                    #
315#    0; if the subscriptions_file could not be loaded.                        #
316#    1; all channels were mirrored successfully.                              #
317###############################################################################
318
319sub download_my_channels {
320
321   # sort and process each one. the channels are sorted in reverse order,
322   # based on the last time they were downloaded, so we loop through each ID.
323   my @sorted_channels = get_my_sorted_channels( "date_downloaded", "reversed_urls" );
324   foreach my $url (@sorted_channels) {
325
326      # if the downloaded date doesn't exist (because of an import, for example),
327      # set yesterday's date (which will force an update). check date_added too.
328      if (not $CHANNELS{$url}{date_downloaded} or $CHANNELS{$url}{date_downloaded} eq "") {
329         $CHANNELS{$url}{date_downloaded} = time2iso( time-86400 );
330      } $CHANNELS{$url}{date_added} = $CHANNELS{$url}{date_downloaded} unless $CHANNELS{$url}{date_added};
331
332      # if the file exists, then check the timestamps and see if we should download.
333      if ( -e catfile( get_setting("dir_data_channels"), $CHANNELS{$url}{filename} ) ) {
334
335         # figure out the epoch seconds on the last time the channel was downloaded.
336         my $channel_seconds = str2time($CHANNELS{$url}{date_downloaded});
337
338         # if the last time it was downloaded has been less
339         # than an hour ago, then we skip the blasted thing.
340         # via 'next'. if over an hour, we'll download it.
341         # over an hour ago, then we download the file.
342         if ((time - $channel_seconds) < 3600) { note("Skipping $CHANNELS{$url}{filename} - local copy is less than an hour old."); next; }
343         else { note("Checking $CHANNELS{$url}{filename} - local copy is more than an hour old.", 1); }
344      }
345
346      # else, the file doesn't exist, so let ourselves know.
347      else { note("Downloading $CHANNELS{$url}{filename} - local copy doesn't exist.", 1); }
348
349      # download and store the file.
350      my $result = mirror( $CHANNELS{$url}{xmlurl}, catfile( get_setting("dir_data_channels"), $CHANNELS{$url}{filename} ) );
351
352      # if we did indeed download something, then update our
353      # date_downloaded in our %CHANNELS. we'll resave later.
354      if ($result) { $CHANNELS{$url}{date_downloaded} =  time2iso(time); }
355   }
356
357   # make a pretty divider for our log file and window.
358   note("--------------------------------------------------------------------------------", 1);
359
360   # count 'em.
361   my $total_count = count_my_channels;
362   if ($total_count) {
363   note("There was a total of $total_count subscribed channels.");
364   note("We dance a drunken jig of epic proportions! " .
365           "<shake> <jiggle> <keRAASH!>...") if $total_count > 100;
366   }
367
368   # resave the channels list
369   # with the latest dates.
370   save_my_channels();
371
372   return 1;
373
374}
375
376###############################################################################
377# get_my_channels_data - get the value of the passed data from channel url.   #
378# set_my_channels_data - set the value of the passed data for channel url.    #
379###############################################################################
380# USAGE:                                                                      #
381#    my $answer = get_my_channels_data( $url, "app_os" );                     #
382#    set_setting( $url, "title", "Gamegrene.com" );                           #
383#                                                                             #
384# NOTES:                                                                      #
385#    Returns the value of the passed setting in $url, from $CHANNELS. If      #
386#    the passed setting doesn't exist, we return a hash ref of all settings.  #
387#    Sets the passed variable to the passed value for $url, in $CHANNELS      #
388#                                                                             #
389# RETURNS:                                                                    #
390#    $value; the value of the passed or set.                                  #
391#    undef; if the setting doesn't exist or isn't defined.                    #
392###############################################################################
393
394sub get_my_channels_data {
395   my ($url, $setting) = @_;
396   return undef unless $CHANNELS{$url};
397   return $CHANNELS{$url} unless $setting;
398   $CHANNELS{$url}{$setting} || undef;
399}
400
401sub set_my_channels_data {
402   my ($url, $setting, $value) = @_;
403   $CHANNELS{$url}{$setting} = $value;
404   $CHANNELS{$url}{$setting};
405}
406
407###############################################################################
408# get_my_sorted_channels - get arrays of various sorted channel options.      #
409###############################################################################
410# USAGE:                                                                      #
411#    # returns complete channel data, sorted by title.                        #
412#    my @array = get_my_sorted_channels("title", "data");                     #
413#                                                                             #
414#    # returns a list of arrays, sorted by last downloaded.                   #
415#    my @array = get_my_sorted_channels("date_downloaded", "reversed_urls");  #
416#                                                                             #
417# NOTES:                                                                      #
418#    This routine goes through our currently stored subscription list in      #
419#    %CHANNELS, and returns an array based on how the data should be sorted   #
420#    as well as how the data is returned. Sorting can be done on ANY          #
421#    attribute stored in %CHANNELS, and the returned array could be sorted    #
422#    normally or reversed, and could contain either URLs of the channels,     #
423#    or hashes of the data for the specific channel.                          #
424#                                                                             #
425# RETURNS:                                                                    #
426#    @array; the sorted array of channel ids.                                 #
427###############################################################################
428
429sub get_my_sorted_channels {
430
431   my ($sort_by, $return) = @_;
432
433   # prepare our various arrays.
434   my @urls = sort { uc( $CHANNELS{$a}{$sort_by} ) cmp uc ( $CHANNELS{$b}{$sort_by} ) } keys %CHANNELS;
435   my @urls_r = sort { uc( $CHANNELS{$b}{$sort_by} ) cmp uc ( $CHANNELS{$a}{$sort_by} ) } keys %CHANNELS;
436   my @data; foreach (@urls) { push(@data, $CHANNELS{$_}); }
437   my @data_r; foreach (@urls_r) { push(@data_r, $CHANNELS{$_}); }
438
439   # return the format requested.
440   return @urls if $return eq "urls"; return @urls_r if $return eq "reversed_urls";
441   return @data if $return eq "data"; return @data_r if $return eq "reversed_data";
442
443}
444
445###############################################################################
446# import_my_channels - imports the OPML data into the current list.           #
447# load_my_channels - reads in the myChannels.opml and returns the data.       #
448###############################################################################
449# USAGE:                                                                      #
450#    load_my_channels;                                                        #
451#    load_my_channels("/path/to/file");                                       #
452#    load_my_channels($string_with_opml);                                     #
453#    import_my_channels("/path/to/file");                                     #
454#    import_my_channels($string_with_opml);                                   #
455#                                                                             #
456# NOTES:                                                                      #
457#    This routine reads the passed info into %CHANNELS. If this file does not #
458#    exist, it will create one in memory based on the defaults. This fixes    #
459#    an earlier (pre v0.92) problem where upgrading became a hassle since     #
460#    the files could be overwritten. If any changes are made to the default   #
461#    channels, they are then saved to disk. The hash this routine creates is  #
462#    keyed to the channels xml (see the default hash layout below for an      #
463#    example of what it looks like).                                          #
464#                                                                             #
465#    This routine can accept a local file path, or a string with OPML in it.  #
466#                                                                             #
467# RETURNS:                                                                    #
468#    undef; if the import routine found no applicable XML URLs.               #
469#    1; this routine nearly always returns successfully.                      #
470###############################################################################
471
472sub import_my_channels { my ($o) = @_; my $rc = load_my_channels($o); save_my_channels(); return $rc; }
473sub load_my_channels { # import_my_channels is just a wrapper around this.
474
475   my ($opml) = @_;
476
477   # if we've got data from a file or a string, import.
478   if ($opml and ($opml =~ /\n|\f|\r|\t/ or -e $opml)) {
479
480      # if this isn't a string, open.
481      unless ($opml =~ /\n|\f|\r|\t/) {
482
483         # Radio Userland or similar importing. we load the file into
484         # a string first so that we can lowercase xmlUrl and htmlUrl.
485         open (OPML, $opml) or note("There was an error opening the OPML file. " .
486                                    "Please report the following error to " .
487                                    get_setting("app_email") . " : $!", 1);
488         local $/ = undef; $opml = <OPML>; close(OPML);
489      }
490
491      # does this OPML file have any xmlurl attributes?
492      unless ($opml =~ / xmlurl=/i) { note("AmphetaDesk could not find any " .
493                                            "XML URLs within the loaded OPML file.", 1, 1);
494                                            return undef; }
495
496      # our data is loaded in (either from the file, or as
497      # passed), so we do a innerUppers conversion to lowercase.
498      $opml =~ s/xmlUrl/xmlurl/g; $opml =~ s/htmlUrl/htmlurl/g;
499
500      # load the data into a temporary data structure.
501      note("Loading and parsing the channels from the OPML file.");
502      my $temp_data; eval { $temp_data = XMLin( $opml, keyattr=>{ outline => "+xmlurl" } ) };
503      if ($@) { $@ =~ s/[\r\n\f]//g; note("There was an error loading the OPML data. " .
504                                          "Please report the following error to " .
505                                          get_setting("app_email") . " : $@.", 1); }
506
507      # does the file only have one channel? if so, our structure
508      # doesn't contain an array (which we assume below in our foreach),
509      # so we've got to treat it specially here. ah well. silly code.
510      if ($temp_data->{body}->{outline}->{xmlurl}) {
511
512         # Radio Userland or similar importing.
513         unless ( $temp_data->{body}->{outline}->{filename} ) {
514            $temp_data->{body}->{outline}->{filename} =
515               create_channel_filename($temp_data->{body}->{outline}->{title});
516         } unless ( $temp_data->{body}->{outline}->{date_added} ) {
517           $temp_data->{body}->{outline}->{date_added} = time2iso( time ); }
518           # set the date_added iso. date downloaded will be set on next dl.
519
520         # and add the channel to our master structure, going
521         # through each hash entry and adding them manually.
522         # we do this so we don't clobber/delete existing attributes.
523         my $smaller_hashkey = $temp_data->{body}->{outline}->{xmlurl};
524         foreach my $key ( keys %{ $temp_data->{body}->{outline} } ) {
525            $CHANNELS{ $smaller_hashkey }->{$key} = $temp_data->{body}->{outline}->{$key};
526         } return 1; # we return early so we don't trip our foreach.
527      }
528
529      # take the temporary data structure and
530      # copy it into the right %CHANNELS structure.
531      foreach my $channel ( values %{ $temp_data->{body}->{outline} } ) {
532
533         # Radio Userland or similar importing.
534         unless ( $channel->{filename} ) {
535            $channel->{filename} = create_channel_filename($channel->{title});
536         } unless ( $channel->{date_added} ) { $channel->{date_added} = time2iso( time ); }
537
538         # and add the channel to our master structure, going
539         # through each hash entry and adding them manually.
540         # we do this so we don't clobber/delete existing attributes.
541         foreach my $key ( keys %{ $channel } ) {
542            $CHANNELS{ $channel->{xmlurl} }->{$key} = $channel->{$key};
543         }
544      }
545
546      return 1;
547   }
548
549   # if $opml wasn't passed,
550   # then load our defaults.
551   else {
552      note("User channels configuration doesn't exist. Using defaults.");
553      %CHANNELS = (
554        "http://www.disobey.com/amphetadesk/news.xml" => {
555          date_added      => "2002-03-01 00:00:00",
556          date_downloaded => "2002-03-01 00:00:00",
557          description     => "The latest news, updates, and press mentions of Disobey.com's AmphetaDesk::AmphetaDesk.",
558          filename        => "amphetadeskdocumenta.xml",
559          htmlurl         => "http://www.disobey.com/amphetadesk/",
560          title           => "Disobey.com's AmphetaDesk::AmphetaDesk - News and Updates",
561          xmlurl          => "http://www.disobey.com/amphetadesk/news.xml"
562        },
563        "http://www.freebsd.org/news/news.rdf" => {
564          date_added      => "2005-10-11 00:00:00",
565          date_downloaded => "2005-10-11 00:00:00",
566          description     => "News from the FreeBSD Project",
567          filename        => "freebsdprojectnews.xml",
568          htmlurl         => "http://www.FreeBSD.org/news/",
569          title           => "FreeBSD Project News",
570          xmlurl          => "http://www.freebsd.org/news/news.rdf"
571        },
572        "http://www.freshports.org/news.php" => {
573          date_added      => "2005-10-11 00:00:00",
574          date_downloaded => "2005-10-11 00:00:00",
575          description     => "he easiest place to find ports",
576          filename        => "freshportstheplacefo.xml",
577          htmlurl         => "http://www.FreshPorts.org/",
578          title           => "FreshPorts -- The Place For Ports",
579          xmlurl          => "http://www.freshports.org/news.php"
580        },
581        "http://www.oreillynet.com/feeds/author/?x-au=73&amp;x-mimetype=application/rdf+xm" => {
582          date_added      => "2005-10-11 00:00:00",
583          date_downloaded => "2005-10-11 00:00:00",
584          description     => "Dru Lavigne's O'Reilly Network Items: Weblogs, Articles, Hacks and Books",
585          filename        => "drulavigneoreillynet.xml",
586          htmlurl         => "http://www.oreillynet.com/pub/au/73",
587          title           => "Dru Lavigne, O'Reilly Network",
588          xmlurl          => "http://www.oreillynet.com/feeds/author/?x-au=73&amp;x-mimetype=application/rdf+xm"
589        },
590        "http://daily.daemonnews.org/ddn.rdf.php3" => {
591          date_added      => "2005-10-11 00:00:00",
592          date_downloaded => "2005-10-11 00:00:00",
593          description     => "Daemon News PHP News System",
594          filename        => "bsdnews.xml",
595          htmlurl         => "http://bsdnews.com",
596          title           => "BSD News",
597          xmlurl          => "http://daily.daemonnews.org/ddn.rdf.php3"
598        }
599      );
600
601      # now, save the subscriptions to disk.
602      save_my_channels();
603   }
604
605   return 1;
606
607}
608
609###############################################################################
610# remove_old_channel_files - deletes old and dejected files.                  #
611###############################################################################
612# USAGE:                                                                      #
613#    remove_old_channel_files;                                                #
614#                                                                             #
615# NOTES:                                                                      #
616#    A clean up routine run at the beginning of each Ampheta start that will  #
617#    remove any extra files that are no longer needed (channels, etc.). This  #
618#    really shouldn't be necessary anymore, but can occur with third party    #
619#    modification of the myChannels.opml without regard for deleting          #
620#    orphaned/stored channel data.                                            #
621#                                                                             #
622# RETURNS:                                                                    #
623#    0; if the channels directory could not be opened.                        #
624#    1; if everything happened successfully.                                  #
625###############################################################################
626
627sub remove_old_channel_files {
628
629   # say hello and load myChannels.opml.
630   note("Checking for orphaned channel files to delete...");
631
632   # open the directory and suck in the files.
633   opendir(CHANNELS , get_setting("dir_data_channels")) or return 0;
634   my @directory_files = grep !/^\.\.?$/, readdir(CHANNELS); closedir(CHANNELS);
635
636   # now go through each file and each
637   # subscription and make sure they match up.
638   foreach my $directory_file ( @directory_files ) {
639
640      # if the subscription matches the directory file,
641      # then it should be there and we last; out of it.
642      my $match; foreach my $channel_url ( keys %CHANNELS ) {
643         if ( $directory_file eq $CHANNELS{$channel_url}{filename} ) { $match++; last;  }
644      }
645
646      # if there was no match, however, it's a file
647      # we don't think should be there, so we delete it.
648      unless ( $match ) { note("Condemning \"$directory_file\" back to the grave.");
649         unlink( catfile(get_setting("dir_data_channels"), $directory_file) )
650            or note("AmphetaDesk couldn't delete $directory_file. Please " .
651                    "report the following error to " . get_setting("app_email") . ": $!");
652      }
653
654      $match = 0;
655   }
656
657   # make a pretty divider for our log file.
658   note("--------------------------------------------------------------------------------");
659
660   return 1;
661
662}
663
664###############################################################################
665# save_my_channels - saves the relevant channel subscriptions to a file.      #
666###############################################################################
667# USAGE:                                                                      #
668#    save_my_channels;                                                        #
669#    save_my_channels("/path/to/file");                                       #
670#                                                                             #
671# NOTES:                                                                      #
672#    This routine merely takes the current %CHANNELS, extracts the channel    #
673#    information that should be saved, and then dumps it to the user's        #
674#    data directory (or the passed file location). It will be read in on      #
675#    successive loads.                                                        #
676#                                                                             #
677# RETURNS:                                                                    #
678#    1; if the new channels were saved and activated.                         #
679#    0; if we couldn't save the settings to the passed file name.             #
680###############################################################################
681
682sub save_my_channels {
683
684   my ($passed_path) = @_;
685
686   # just some happy blurbage for the log file.
687   note("Creating a brand new channels file from the current subscriptions.");
688
689   # create an outputtable settings structure.
690   my $temp_data; # used for temporary storage.
691   foreach my $channel ( values %CHANNELS ) {
692      push @{ $temp_data->{body}->{outline} }, $channel;
693   }
694
695   # write out the file. we set noattr for "no attributes",
696   # as well as "opml" as the root name. xmldecl sets the xml
697   # declaration to version 1.0...
698   eval { XMLout( $temp_data, noattr => 0,
699                  rootname => 'opml',
700                  xmldecl => "<?xml version=\"1.0\"?>",
701                  outputfile => $passed_path || get_setting("files_myChannels"),
702                ) } or return 0;
703
704   # die if there are any errors.
705   if ($@) { $@ =~ s/[\r\n\f]//g; note("There was an error saving " .
706                                       $passed_path || get_setting("names_file_myChannels") .
707                                       ". Please report the following error to "
708                                       . get_setting("app_email") . ": $@.", 1); }
709
710   return 1;
711
712}
713
714###############################################################################
715# update_my_channel_data - takes a URL and new data and updates our OPML.     #
716###############################################################################
717# USAGE:                                                                      #
718#    update_my_channels_data($url, $parsed_data);                             #
719#                                                                             #
720# NOTES:                                                                      #
721#    This routine looks at the passed $data and uses it to update the OPML    #
722#    information for our passed $url. This routine is typically called with   #
723#    the data returned from a load_my_channel routine (which indicates a      #
724#    successful parse. Incidentally, this is called for every successful      #
725#    parse that we do, so that we'll always have the latest info.             #
726#                                                                             #
727# RETURNS:                                                                    #
728#    0; if the passed $url could not be found in our OPML.                    #
729#    1; if everything in the channel was updated successfully.                #
730###############################################################################
731
732sub update_my_channel_data {
733
734   my ($url, $data) = @_;
735
736   # we're not subscribed to this.
737   unless ($CHANNELS{$url}) {
738      note("AmphetaDesk couldn't update $url because " .
739           "we couldn't find a matching subscription. Please
740           report this error to " . get_setting("app_email"));
741           return 0;
742   }
743
744   # now update each relevant entry from our newly parsed data.
745   $CHANNELS{$url}{description} = $data->{channel}{description} if $data->{channel}{description};
746   $CHANNELS{$url}{htmlurl}     = $data->{channel}{"link"} if $data->{channel}{"link"};
747   $CHANNELS{$url}{title}       = $data->{channel}{title} if $data->{channel}{title};
748
749   # for email, we try going from best to worst.
750   $CHANNELS{$url}{email} = $data->{channel}{managingEditor} || $data->{channel}{webMaster} ||
751                            $data->{header}{webMaster} || $data->{header}{managingEditor} || "";
752
753   return 1;
754
755}
756
7571;
758