1
<?php
2
#TODO: make sure that all classes with equals() have merge()
3
#TODO: threshold for multiple stories, and which point components are not displayed
4
#		i.e. over 10 recently listened to tracks, don't list em, just link to Last.fm
5
#TODO: move img urls to database
6
7
class Entry{
8
	var $canHaveMultiples = FALSE;
9
  
10
  public static $classpath = array();
11
12
  public static function add_classpath($class, $path) {
13
    self::$classpath[$path][] = $class;
14
  }
15
16
  public static function get_classpath($class) {
17
    foreach (self::$classpath as $path => $classes) {
18
      if (in_array($class, $classes)) {
19
        return $path;
20
      }
21
    }
22
  }
23
	
24
	function Entry($data) {
25
	  foreach ($data as $key => $value) {
26
	    $this->$key = htmlspecialchars($value, ENT_NOQUOTES, 'utf-8', FALSE);
27
	  }
28
    $possible_icons = array();
29
    $possible_icons[] = dirname(__FILE__) .'/'. strtolower( get_class($this) ) .'.png';
30
    $possible_icons[] = dirname(__FILE__) .'/favicon.png';
31
    foreach($possible_icons as $possible_icon) {
32
      if( is_file( $possible_icon ) ){
33
        $this->img = $possible_icon;
34
        break;
35
      }
36
    }
37
	}	
38
39
  /**
40
   * Turn URLs into links.
41
   */
42
  function linkify($text){
43
   return preg_replace('/((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:\/~\+#]*[\w\-\@?^=%&amp;\/~\+#])?)/i', '<a href="$1">$1</a>', $text); 
44
  }
45
46
	function timeSince(){
47
		$timeSince = time() - ($this->rec_updated);
48
49
		$units = array();
50
		$units['second']	= 1;
51
		$units['minute']	= 60;
52
		$units['hour']		= 60*60;
53
		$units['day']		= 60*60*24;
54
		$units['week']		= 60*60*24*7;
55
		$units['month']		= 60*60*24*30;
56
		$units['year']		= 60*60*24*365;
57
58
    # Override 'units ago' for hard date
59
    #TODO: make override dynamic depending on proximity to surrounding dates
60
    $override = $units['month'];
61
  
62
    if ($timeSince > $override) {
63
      $timeSinceString = date('\o\n M j Y \a\t g:i a', $this->rec_updated);
64
    } else {
65
      foreach($units as $unit=>$value){
66
        # Base case (0 seconds ago)
67
        if($timeSince == 0)
68
          $timeSinceString = 'just now';
69
        # Otherwise
70
        else if($timeSince < $value or $unit == 'year'){
71
          $timeSince = floor($timeSince/$prev_value);
72
          if($timeSince != 1)
73
            $prev_unit .= 's';
74
          $timeSinceString = "about $timeSince $prev_unit ago";
75
          break;
76
        }
77
        $prev_unit = $unit;
78
        $prev_value = $value;
79
      }
80
    }
81
		return "<span class='time-since'><a href='$this->link'>$timeSinceString</a></span>";
82
	}
83
84
	function canHaveMultiples(){
85
		return $this->canHaveMultiples;
86
	}
87
	
88
	function groupTogether($other){
89
    return get_class($this) == get_class($other);
90
	}
91
	
92
	function getIcon(){
93
    $class_name = get_class($this);
94
    $class_dir = dirname( Entry::get_classpath($class_name) );
95
    $module_icon_path = $class_dir . '/favicon.png';
96
97
    $img = WEBROOT . (file_exists($module_icon_path) ? str_replace(ABS_PATH, '', $module_icon_path) : DEFAULT_ENTRY_ICON);
98
		
99
    return '<img src="'.$img.'" alt="'.$this->feed_title.'" title="'.$this->feed_title.'" class="feed-icon"/>';
100
	}
101
102
	function getSingleTitle(){
103
		return "<a href='$this->link'>$this->title</a>";
104
	}
105
  function getMultipleTitle(){
106
    return $this->getSingleTitle();
107
  }
108
	
109
	function getNeutralTitle(){
110
		return $this->title;
111
	}	
112
  
113
  function getAuthor(){
114
    $author = $this->author;
115
116
    #TODO: move this to config file or something
117
    $author = str_replace('balleyne', 'Blaise', $author);
118
119
    return $author;
120
  }
121
	
122
	function getSingleDescription(){
123
		return $this->summary ? '<p>'.$this->summary.'</p>' : '';
124
	}
125
126
	/** Print out contents of this object. */
127
	function single(){
128
		#TODO: this is temporary, should move to XSLT soon
129
		
130
		$icon = $this->getIcon();
131
		$title = $this->getSingleTitle();
132
		$description = $this->getSingleDescription();
133
		$date = $this->timeSince();//date('r', $this->rec_created);
134
		
135
		return "$icon <span class='title'>$title</span> $date $description";
136
	}
137
138
	function multiple($other_entries, $more_threshold=5){
139
		$icon = $this->getIcon();
140
		$number = count($other_entries)+1;
141
		
142
		$list = '<ul><li>'.$this->getMultipleTitle().' '.$this->timeSince().'</li>';
143
		foreach($other_entries as $i=>$entry){
144
			if($i < $more_threshold){
145
				$list .= '<li>'.$entry->getMultipleTitle().' '
146
					.$entry->timeSince().'</li>';
147
			}else{
148
				$list .= '<li>and <a href="'.$this->feed_link.'">'
149
					.(count($other_entries)>=10 ? 'many' : '').
150
					' more...</a></li>';
151
				break;
152
			}
153
		}
154
		$list .= '</ul>';
155
		
156
		$date = $this->timeSince(); //calculates date for most recent entry
157
		
158
    return "$icon $this->feed_title: $list";
159
	}
160
		
161
162
}
163
164
165
/**
166
 * Abstract classes.
167
 **/
168
class Blog extends Entry{
169
170
  function getSingleDescription(){
171
     return htmlspecialchars_decode(parent::getSingleDescription());
172
  }
173
}
174
175
class Comments extends Entry {}
176
177
class MicroBlog extends Entry{
178
  function single($show_replies = FALSE) {
179
    if ($show_replies || substr($this->getNeutralTitle(), 0, 1) != '@') {
180
      return parent::single();
181
    } else {
182
      return FALSE;
183
    }
184
  }
185
186
  function format($message) {
187
    // Replace URLs with links
188
    $message = $this->linkify($message); 
189
190
    // Default to Twitter if multiple...
191
    $domain = isset($this->icons) && count($this->icons) > 1 ? Twitter::$static_domain : $this->domain;
192
    $tag_path = isset($this->icons) && count($this->icons) > 1 ? Twitter::$static_tag_path : $this->tag_path;
193
194
    // Link usernames
195
    $message = preg_replace('/( |\.|^)@([\w_-]+)/i', '$1@<a href="http://'.$domain.'/$2">$2</a>', $message);
196
197
    // Link hash tags
198
    $message = preg_replace('/( |^)#([\w_-]+)/i', '$1#<a href="http://'.$tag_path.'$2">$2</a>', $message);
199
200
    return $message;
201
  }
202
203
	#TODO: add link to post ?
204
	function getSingleTitle(){
205
    return $this->format( $this->getNeutralTitle() );
206
	}
207
	
208
	function getSingleDescription(){
209
		return "";
210
	}
211
	
212
	function equals($other){
213
    $x = trim($this->getNeutralTitle());
214
    $y = trim($other->getNeutralTitle());
215
216
		$basic_match = $x == $y;
217
218
    $punc_regex = '/[#:@.]/i';
219
    $x_stripped = preg_replace($punc_regex, '', $x);
220
    $y_stripped = preg_replace($punc_regex, '', $y);
221
    $without_punc = $x_stripped == $y_stripped;
222
223
//TODO: check without URLs?
224
    $url_regex = '/http:\/\/\S+( |$)/i'; // URL followed by space or end
225
    $x_url_stripped = preg_replace($url_regex, '', $x);
226
    $y_url_stripped = preg_replace($url_regex, '', $y);
227
    $without_urls = $x_url_stripped == $y_url_stripped;
228
229
    return $basic_match || $without_punc || $without_urls;
230
	}
231
232
	/**
233
	 * Return title with username stripped out.
234
	 */
235
	function getNeutralTitle(){
236
    // Everything up to the first space is the username
237
    $parts = explode(' ', $this->title, 2);
238
    return $parts[1];
239
	}
240
	
241
	function merge($other){
242
		if(!isset($this->icons)){
243
			$this->icons = array();
244
			$this->icons[$this->link] = $this->getIcon();
245
			$this->icons[$other->link] = $other->getIcon();
246
		} else {
247
			$this->icons[$other->link] = $other->getIcon();
248
		}
249
	}
250
	
251
	#TODO: find a better place for these links
252
	function getIcon(){
253
		if(empty($this->icons))
254
			$this->icons[$this->link] = parent::getIcon();
255
		
256
		$result = "";
257
		foreach($this->icons as $link=>$icon) {
258
      // This line is a hack to cover up for faulty logic (double nesting)
259
  		$result .= (!empty($icon) && !empty($link)) && strpos($icon, $link) === FALSE ? "<a href='$link'>$icon</a>" : $icon;
260
    }
261
		return $result;
262
	}
263
}
264
	
265
class SharedItems extends Entry{
266
	var $action = 'shared';
267
	var $items = 'items';
268
	var $canHaveMultiples = TRUE;
269
	var $multiple_sources = FALSE; //default
270
271
	function getSingleTitle(){
272
		#TODO: Is author consistent enough? Works for Delicious, but not TT-RSS or Google Reader...
273
		return $this->getAuthor()." $this->action " . parent::getSingleTitle();
274
	}
275
276
  function getMultipleTitle(){
277
    return str_replace($this->getAuthor()." $this->action ",'', $this->getSingleTitle());
278
  }
279
  function getMultipleBroadTitle($number){
280
    $elements = array();
281
    foreach (array($this->getAuthor(), $this->action, $number, $this->items) as $key=>$value) {
282
      if ($value != '') {
283
        $elements[] = $value;
284
      }
285
    }
286
		return implode($elements, ' ');
287
288
  }
289
	function multiple($other_entries, $more_threshold=5, $type='ul'){
290
		$icon = $this->getIcon();
291
		$number = count($other_entries)+1;
292
293
    switch($type) {
294
      case 'ul':
295
        $start = '<ul>';
296
        $before = '<li>';
297
        $after = '</li>';
298
        $end = '</ul>';
299
        $more = '';
300
        $showTimeSince = TRUE;
301
        break;
302
      default:
303
        $start = '<p>';
304
        $after = '';
305
        $before = '';
306
        $end = '</p>';
307
        $more = '<br/>';
308
        $every_how_many = 5;
309
        $every_text = '<br/>';
310
        $showTimeSince = FALSE;
311
    }
312
313
		#TODO: XSLT can handle this... maybe?
314
		$list = $start.$before.$this->getMultipleTitle().' '.($showTimeSince ? $this->timeSince() : '').$after;
315
		foreach($other_entries as $i=>$entry){
316
			if($i < $more_threshold){
317
        if (isset($every_how_many) && $i % $every_how_many == $every_how_many-1) {
318
          $list .= $every_text;
319
        }
320
				$list .= $before.$entry->getMultipleTitle().' '
321
					.($showTimeSince ? $entry->timeSince() : '').$after;
322
			}else{
323
				$list .= $more.$before.'and <a href="'.$this->feed_link.'">'
324
					.(count($other_entries)>=10 ? 'many' : '').
325
					' more...</a>'.$after;
326
				break;
327
			}
328
		}
329
		$list .= $end;
330
		
331
		$date = $this->timeSince(); //calculates date for most recent entry
332
		
333
		return "$icon ".$this->getMultipleBroadTitle($number).": $list";
334
	}
335
}
336
337
class AudioPlay extends SharedItems{
338
  var $img = 'http://blaise.ca/images/icons/music-notes.png';
339
  var $action = 'listened to';
340
	var $items = 'tracks';
341
	var $play_count = 1;
342
		
343
344
  // Test parent class to match on 'AudioPlay' instead of the particular service
345
	function groupTogether($other){
346
    return get_parent_class($this) == get_parent_class($other);
347
	}
348
349
  function getAuthor(){
350
    return 'Blaise'; #TODO: hardcoded author
351
  }
352
353
	function getSingleTitle(){
354
		$title = parent::getSingleTitle();
355
		if($this->play_count>1)
356
			$title = $this->getNeutralTitle() ." $this->play_count times";
357
		return $title;
358
	}
359
		
360
	function getMultipleTitle($force_neutral=FALSE){
361
		$title = ($force_neutral || $this->multiple_sources) ? $this->getNeutralTitle() : parent::getMultipleTitle();
362
		if($this->play_count > 1)
363
			$title .= " ($this->play_count times)";
364
		return $title;
365
	}
366
367
  // Take into account play count?
368
	function equals($other){
369
		return strtolower($other->getNeutralTitle()) == strtolower($this->getNeutralTitle());
370
	}
371
372
	function multiple($other_entries, $more_threshold=5){
373
		$number = count($other_entries)+1;
374
		
375
		$multinlist = $this->multiple_sources;
376
		$example = $this;
377
		if (!$multinlist) {
378
  		foreach ($other_entries as $entry) {
379
  		  if ($entry->multiple_sources) {
380
  		    $multinlist = TRUE;
381
  		    $example = $entry;
382
  		    break;
383
  		  }
384
  		}
385
  }
386
		
387
		
388
		#TODO: XSLT can handle this... maybe?
389
 		$list = '<ul><li>'
390
 		  .$this->getMultipleTitle($multinlist).' '
391
 		  .($multinlist ? $this->getIcon($multinlist) : '').' '
392
 		  .$this->timeSince().'</li>';
393
 		foreach($other_entries as $i=>$entry){
394
 			if($i < $more_threshold){
395
 				$list .= '<li>'.$entry->getMultipleTitle($multinlist).' '
396
 				  .($multinlist ? $entry->getIcon($multinlist) : '').' '
397
 					.$entry->timeSince().'</li>';
398
 			}else{
399
 				$list .= '<li>and <a href="'.$example->feed_link.'">'
400
 					.(count($other_entries)>=10 ? 'many' : '').
401
 					' more...</a></li>';
402
 				break;
403
 			}
404
 		}
405
 		$list .= '</ul>';
406
407
		$date = $this->timeSince(); //calculates date for most recent entry
408
		
409
410
		$icon = $example->getIcon();//$multinlist ? '<img src="http://blaise.ca/images/icons/music-notes.png"/>' : $example->getIcon();
411
		return "$icon ".$this->getAuthor()." $this->action $number $this->items: $list";
412
	}
413
414
	/**
415
	 * Return title with username stripped out.
416
	 */
417
	function getNeutralTitle(){
418
    return str_replace(' &#8211;', ':', $this->title);
419
	}
420
	
421
	function merge($other){
422
	  // If from same source, track was heard multiple times
423
	  if ($this->feed_type == $other->feed_type) {
424
  		$this->play_count++;
425
		}
426
		// Otherwise, this is a duplicate from multiple sources
427
		else{
428
		  $this->multiple_sources = TRUE;
429
  		if(!isset($this->icons)) {
430
  			$this->icons = array();
431
  			$this->icons[$this->link] = $this->getIcon();
432
  			$this->icons[$other->link] = $other->getIcon();
433
  		} else {
434
  			$this->icons[$other->link] = $other->getIcon();
435
  		}
436
    }
437
	}
438
	
439
	function getIcon($hyperlink=FALSE){
440
		if(empty($this->icons))
441
			$this->icons[$this->link] = parent::getIcon();
442
		
443
		$result = "";
444
		foreach($this->icons as $link=>$icon) {
445
		  if ($hyperlink) {
446
  			$result .= "<a href='$link'>$icon</a>";
447
			} else {
448
			  $result .= $icon;
449
			}
450
		}
451
		return $result;
452
	}
453
454
455
}
456
class Multimedia extends SharedItems{
457
	var $action = 'posted';
458
	var $canHaveMultiples = False;
459
}
460
461
class Favorites extends SharedItems{
462
	var $action = 'favourited';
463
}
464
465
class MicroBlogFavorites extends Favorites{
466
  function format($message){
467
    return MicroBlog::format($message);
468
  }
469
470
  function getSingleTitle(){
471
    $message = $this->format($this->title);
472
    $message = preg_replace('/^([\w]+)\: /i', '<a href="'.$this->link.'">$1</a>: ', $message);
473
    return $this->getAuthor() ." $this->action ". $message;
474
  }
475
476
	function getSingleDescription(){
477
		return '';
478
	}
479
480
  function getAuthor(){
481
    return 'Blaise'; #TODO: harcoded author
482
  }
483
484
}
485
486
#TODO: make this more interesting
487
class StatusUpdates extends MicroBlog{
488
	function getSingleTitle(){
489
		return $this->linkify($this->title);
490
	}
491
}