Ryon Sherman’s Blog

Export World of Warcraft Calendar To iCal

Posted in Programming by Ryon Sherman on 05/21/2009

Late to raids? Miss your favorite Battleground weekend? Now you can receive alerts from your favorite calendar application by importing the iCal file created by my World of Warcraft Calendar Export Tool.

WoWCal is a PHP script that will parse the JSON from the World of Warcraft Armory and create a basic iCal file.

You can specify:

  • Character
  • Realm
  • Calendar Type

Note: Since viewing a calendar on the Armory requires authentication, the full code is presented below for your examination. It seems an increasing number of WoW players have been hit by the plague of keyloggers and I’d rather not be filed into that category.

Download

Zip: http://github.com/ryonsherman/wowcal/zipball/master
MD5: 760e9d6d0361bf9643e49dfc40579752

Tarball: http://github.com/ryonsherman/wowcal/tarball/master
MD5: f86e5fffc7e31a6b02cc5083f9cfcf69

GitHub: http://github.com/ryonsherman/wowcal/
Git: git clone git://github.com/ryonsherman/wowcal.git

Requirements:
PHP 5 (Get with the times!)
PHP cURL extension

Screenshots

Gmail Agenda After Importing iCal

Gmail Agenda After Importing iCal

Gmail Event Detail

Gmail Event Detail

Help

user@host(~/WowCal): php wowcal.php -h

WoWCal 1.0, a World of Warcraft calendar export tool.
Usage: wowcal.php [OPTION]...

Mandatory arguments to long options are mandatory for short options too.

Startup:
	-V, --version			display the version of WoWCal and exit.
	-f, --file <file>		export to filename.
	-l, --logfile <file>		save log
	-v, --verbose			be verbose.

Battle.net:
	-u, --username <username>	account username.
	-p, --password
<password>	account password.

World of Warcraft Armory:
	-c, --character	<character>	character name.
	-r, --realm <realm>		realm name.

Calendar:
	-m,  --month		    	selected month. M-MM.
					* current month default.
	-y,  --year			selected year. YYYY.
					* current year default.
	-ut, --user-type		user calendar types. comma separated.
	     dungeon*
	     meeting*
	     other*
	     pvp*
	     raid*
	-wt, --world-type		world calendar types. comma separated.
	     bg				* indicates default.
	     darkmoon
	     holiday
	     holiday_weekly
	     player*
	     raid_lockout
	     raid_reset

Mail bug reports and suggestions to <ryon.sherman@gmail.com>

Example Usage

user@host(~/WowCal): php wowcal.php -u username -p password -c Character -r Realm -v -wt bg,raid_reset
May 20 22:10:48: Logging In...
May 20 22:10:49: Logged In.
May 20 22:10:49: Retreiving World Calendar: bg...
May 20 22:10:49: World Calendar Retreived: bg.
May 20 22:10:49: Merging 5 Events...
May 20 22:10:49: Events Merged.
May 20 22:10:49: Retreiving World Calendar: raid_reset...
May 20 22:10:50: World Calendar Retreived: raid_reset.
May 20 22:10:50: Merging 29 Events...
May 20 22:10:50: Events Merged.
May 20 22:10:50: 34 Events Found.
May 20 22:10:50: Creating iCal...
May 20 22:10:50: Writing Calendar to File (wowcal-Character-Realm.ical)...
May 20 22:10:50: Calendar Exported!

Source Code

<?php

define('SCRIPT_NAME', $_SERVER['argv'][0]);
define('SCRIPT_VERSION', '1.0.2');

define('URL_BASE_ARMORY', 'http://www.wowarmory.com/');
define('URL_BASE_LOGIN', 'https://us.battle.net/');

define('URL_CALENDAR_USER', 'vault/calendar/month-user.json');
define('URL_CALENDAR_WORLD', 'vault/calendar/month-world.json');
define('URL_CALENDAR_DETAIL', 'vault/calendar/detail.json');

define('URL_LOGIN', 'login/login.xml');

$WoWCal = new WoWCal;

class WoWCal_Event {

	private $calendar_type;

	public $description;

	public $summary;

	public $start;	

	public function __construct($event) {
		$this->calendar_type = $event->calendarType;
		$this->summary = $event->summary;
		$this->start = floor($event->start / 1000);
	}

}

class WoWCal_WorldEvent extends WoWCal_Event {

	public $end;

	public function __construct($event) {
		parent::__construct($event);

		$this->description = $event->description;
		$this->end = floor($event->end / 1000);
	}

}

class WoWCal_UserEvent extends WoWCal_Event {

	private $STATUSES = array(
		'signedUp' => 'Signed Up',
		'notSignedUp' => 'Not Signed Up',
		'confirmed' => 'Confirmed',
		'invited' => 'Invited',
		'available' => 'Available',
	);

	private $id;

	private $type;

	public $locked = false;

	public $owner;
	public $inviter;
	public $moderator = false;

	public $status;

	public function __construct($event, $detail = null) {
		parent::__construct($event);

		$this->id = $event->id;
		$this->type = $event->type;
		$this->locked = ($detail->locked) ? true : false;

		$this->description = $detail->description;

		$this->owner = $event->owner;
		$this->moderator = ($event->moderator) ? true : false;

		$this->inviter = @$event->inviter;
		$this->status = $this->STATUSES[$event->status];

		foreach($detail->invites as $invitee) {
			$this->invitees[] = new WoWCal_Invitee($invitee);
		}
	}

}

class WoWCal_Invitee {

	private $STATUSES = array(
		'signedUp' => 'Signed Up',
		'notSignedUp' => 'Not Signed Up',
		'confirmed' => 'Confirmed',
		'invited' => 'Invited',
		'available' => 'Available',
	);

	public $name;
	public $moderator = false;

	public $status;	

	public function __construct($invitee) {
		$this->name = $invitee->invitee;
		$this->moderator = ($invitee->moderator) ? true : false;

		$this->status = $this->STATUSES[$invitee->status];
	}

}

class WoWCal {

	private $REQUIRED_OPTIONS = array(
		'username',
		'password',
		'character',
		'realm',
	);

	private $CALENDAR_TYPES_WORLD = array(
		'player' => 'player',
		'holiday' => 'holiday',
		'bg' => 'bg',
		'darkmoon' => 'darkmoon',
		'raid_lockout' => 'raidLockout',
		'raid_reset' => 'raidReset',
		'holiday_weekly' => 'holidayWeekly',
	);
	private $CALENDAR_TYPES_USER = array(
		'raid',
		'dungeon',
		'pvp',
		'meeting',
		'other',
	);

	private $COOKIE;

	private $LOG;

	private $verbose = false;

	private $log_file;
	private $calendar_file;

	private $month;
	private $year;

	private $default_world_calendars = true;
	private $world_calendars = array(
		'player',
	);
	private $default_user_calendars = true;
	private $user_calendars = array(
		'dungeon',
		'meeting',
		'other',
		'pvp',
		'raid',
	);

	private $username;
	private $password;
	private $character;
	private $realm;

	public function __construct() {
		$this->month = date('n');
		$this->year = date('Y');

		$this->parse_options();	

		$this->log('WoWCal Started.');
		$this->login();		

		$user_events = array();
		if(!empty($this->user_calendars))
			$user_events = $this->get_user_calendar_events();
		$world_events = array();
		if(!empty($this->world_calendars))
			$world_events = $this->get_world_calendar_events();
		$events = array_merge($user_events, $world_events);

		$this->create_ical($events);
		$this->quit();
		$this->log('WoWCal Completed.');
	}

	private function parse_options() {
		if($_SERVER['argc'] <= 1) $this->print_usage();

		for($i = 1; $i < $_SERVER['argc']; $i++) {
			switch($_SERVER['argv'][$i]) {
				case '-u': case '--username': $this->username = $_SERVER['argv'][++$i]; break;
				case '-p': case '--password': $this->password = $_SERVER['argv'][++$i]; break;
				case '-c': case '--character': $this->character = $_SERVER['argv'][++$i]; break;
				case '-r': case '--realm': $this->realm = $_SERVER['argv'][++$i]; break;

				case '-f': case '--file':
					$this->calendar_file = $_SERVER['argv'][++$i];
				break;
				case '-l': case '--logfile':
					$this->log_file = $_SERVER['argv'][++$i];
				break;

				case '-m': case '--month': $this->month = intval($_SERVER['argv'][++$i]); break;
				case '-y': case '--year': $this->year = intval($_SERVER['argv'][++$i]); break;
				case '-ut': case '--user-type':
					$this->user_calendars = explode(',', $_SERVER['argv'][++$i]);
					$this->default_user_calendars = false;

					if($this->default_world_calendars)
						$this->world_calendars = array();
				break;
				case '-wt': case '--world-type':
					$this->world_calendars = explode(',', $_SERVER['argv'][++$i]);
					$this->default_world_calendars = false;

					if($this->default_user_calendars)
						$this->user_calendars = array();
				break;

				case '-v': case '--verbose': $this->verbose = true; break;				

				case '-V': case '--version':
					print SCRIPT_NAME." ".$this->SCRIPT_VERSION."\n";
					$this->quit();
				break;
				case '-h': case '--help': $this->print_usage(true); break;
				default: $this->print_usage(); break;
			}
		}

		foreach($this->REQUIRED_OPTIONS as $option) {
			if(!$this->$option) {
				print SCRIPT_NAME.": missing ".$option."\n";
				$this->print_usage();
			}
		}
	}

	private function login() {
		$this->log('Logging In...');

		$parameters = array(
			'accountName' => $this->username,
			'password' => $this->password,
			'ref' => URL_BASE_ARMORY.'index.xml',
			'app' => 'armory',
		);
		$response = $this->request(URL_BASE_LOGIN.URL_LOGIN, $parameters);		

		if(empty($response) or strstr($response, 'error.form.login')) {
			$this->log('Login Failed! Exiting.');
			$this->quit();
		} else $this->log('Login Successful!');
	}

	private function get_user_calendar_events() {
		$parameters = array(
			'month' => $this->month,
			'year' => $this->year,
		);
		$response = $this->request(URL_BASE_ARMORY.URL_CALENDAR_USER, $parameters);

		if(strstr($response, 'layout/maintenance.xsl')) {
			$this->log('Armory currently under maintenance...Please try again later');
			$this->quit();
		}

		$json = $this->json($response);

		$events = array();
		if(is_object($json)) {
			if($json->events) {
				foreach($this->user_calendars as $type) {
					$type = strtolower($type);

					if(in_array($type, $this->CALENDAR_TYPES_USER)) {
						$this->log('Retreiving User Calendar: '.$type.'...');

						$selected_events = array();
						foreach($json->events as $event) {
							if($event->type == $type)
								$selected_events[] = $event;
						}

						$this->log('Found '.count($selected_events).' Events...');

						foreach($selected_events as $event)
							$events[] = new WoWCal_UserEvent($event, $this->get_event_detail($event->id));

						$this->log('User Calendar Retreived.');
					} else $this->log('ALERT: Invalid user calendar type selected.');
				}
			} else $this->log('No Events Found.');
		} else $this->log('Invalid JSON received. Continuing.');

		return $events;
	}

	private function get_world_calendar_events() {
		$events = array();
		foreach($this->world_calendars as $type) {
			$type = strtolower($type);

			if(in_array($type, array_keys($this->CALENDAR_TYPES_WORLD))) {
				$this->log('Retreiving World Calendar: '.$type.'...');

				$parameters = array(
					'type' => $this->CALENDAR_TYPES_WORLD[$type],
					'month' => $this->month,
					'year' => $this->year,
				);
				$response = $this->request(URL_BASE_ARMORY.URL_CALENDAR_WORLD, $parameters);

				if(strstr($response, 'layout/maintenance.xsl')) {
					$this->log('Armory currently under maintenance...Please try again later');
					$this->quit();
				}

				$json = $this->json($response);

				if(is_object($json)) {
					if($json->events) {
						$this->log('Found '.count($json->events).' Events...');

						foreach($json->events as $event)
							$events[] = new WoWCal_WorldEvent($event);
					} else $this->log('No Events Found.');
				} else $this->log('Invalid JSON received. Continuing.');

				$this->log('World Calendar Retreived.');
			} else $this->log('ALERT: Invalid world calendar type selected.');
		}

		return $events;
	}

	private function get_event_detail($id) {
		$parameters = array(
			'e' => $id,
		);
		$response = $this->request(URL_BASE_ARMORY.URL_CALENDAR_DETAIL, $parameters);
		return $this->json($response);
	}

	private function create_ical($events = array()) {
		$this->log('Creating iCal ('.count($events).' events)...');

		$ical = "BEGIN:VCALENDAR\n";
		$ical .= "VERSION:2.0\n";
		$ical .= "PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\n";

		foreach($events as $event) {
			$ical .= "BEGIN:VEVENT\n";
			$ical .= "SUMMARY:".$event->summary."\n";
			$ical .= "DTSTART:".date('Ymd', $event->start)."T".date('His', $event->start)."\n";

			switch(get_class($event)) {
				case 'WoWCal_UserEvent':
					$ical .= "DTEND:".date('Ymd', $event->start)."T".date('His', $event->start)."\n";

					$description = $event->description.'\n\n';

					$description .= 'Creator: '.$event->owner.'\n';
					if($event->inviter)
						$description .= 'Inviter: '.$event->inviter.'\n';					

					if($event->locked)
						$description .= '\nTHIS EVENT IS LOCKED!\n';
					if($event->moderator)
						$description .= '\nYou are a moderator of this event.\n';
					$description .= '\n';					

					$description .= 'Your Status: '.$event->status.'\n';
					if($event->invitees) {
						$description .= 'Other\'s Status:\n';
						foreach($event->invitees as $invitee) {
							if($invitee->moderator)
								$description .= 'Moderator - ';
							$description .= $invitee->name.' - '.$invitee->status.'\n';
						}
					}					

					$ical .= 'DESCRIPTION:'.$description."\n";
				break;

				case 'WoWCal_WorldEvent':
					$ical .= "DTEND:".date('Ymd', $event->end)."T".date('His', $event->end)."\n";
					$ical .= 'DESCRIPTION:'.str_replace(array("\n", '  '), '\n', $event->description)."\n";
				break;

				default: break;
			}

			$ical .= "END:VEVENT\n";
		}

		$ical .= "END:VCALENDAR\n";

		$file = ($this->calendar_file) ? $this->calendar_file : 'wowcal-'.$this->character.'-'.$this->realm.'.ical';

		$this->log('Writing Calendar to File ('.$file.')...');
		file_put_contents($file, $ical);
		$this->log('Calendar Exported!');
	}

	private function json($string) {
		return json_decode(substr($string, 13, -2));
	}

	private function request($url, $parameters = array()) {
		if(!$this->COOKIE)
			$this->COOKIE = tempnam("/tmp", "CURLCOOKIE");

		$defaults = array(
			'cn' => $this->character,
			'r' => $this->realm,
		);
		if($parameters)
			$parameters = array_merge($defaults, $parameters);

		$params = null;
		foreach($parameters as $parameter => $value)
			$params .= $parameter.'='.urlencode($value).'&';
		$params = substr($params, 0, -1);

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url.'?'.$params);
		curl_setopt($ch, CURLOPT_HEADER, false);
		curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 Gecko/20070219 Firefox/2.0.0.2');

		curl_setopt($ch, CURLOPT_COOKIESESSION, true);
		curl_setopt($ch, CURLOPT_COOKIEJAR, $this->COOKIE);
		curl_setopt($ch, CURLOPT_COOKIEFILE, $this->COOKIE);		

		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

		curl_setopt($ch, CURLOPT_POST, true);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters);

		$response = curl_exec($ch);
		curl_close($ch);

		return $response;
	}

	private function log($message) {
		$message = date('M d H:i:s').": ".$message."\n";
		$this->LOG .= $message;

		if($this->verbose) echo $message;
	}

	private function quit() {
		if($this->log_file) {
			$this->log('Writing Log to File: '.$this->log_file);
			file_put_contents($this->log_file, $this->LOG);
		}
		exit();
	}

	private function print_usage($verbose = false) {
		print ($verbose) ?
"
WoWCal ".SCRIPT_VERSION.", a World of Warcraft calendar export tool.
Usage: ".SCRIPT_NAME." [OPTION]...

Mandatory arguments to long options are mandatory for short options too.

Startup:
	-V, --version			display the version of WoWCal and exit.
	-f, --file <file>		export to filename.
	-l, --logfile <file>		save log
	-v, --verbose			be verbose.

Battle.net:
	-u, --username <username>	account username.
	-p, --password
<password>	account password.

World of Warcraft Armory:
	-c, --character	<character>	character name.
	-r, --realm <realm>		realm name.

Calendar:
	-m,  --month		    	selected month. M-MM.
					* current month default.
	-y,  --year			selected year. YYYY.
					* current year default.
	-ut, --user-type		user calendar types. comma separated.
	     dungeon*
	     meeting*
	     other*
	     pvp*
	     raid*
	-wt, --world-type		world calendar types. comma separated.
	     bg				* indicates default.
	     darkmoon
	     holiday
	     holiday_weekly
	     player*
	     raid_lockout
	     raid_reset

Mail bug reports and suggestions to <ryon.sherman@gmail.com>
"
:
"Usage: ".SCRIPT_NAME." [OPTION]...

Try 'php ".SCRIPT_NAME." --help' for more options.
";

		$this->quit();
	}

}

?>
Tagged with: ,

18 Responses

Subscribe to comments with RSS.

  1. David Dashifen Kees said, on 05/22/2009 at 14:24

    Excellent work. I’ve been thinking about using cURL to try and log into the calendar features for some time. I might have to start from your code and see what I can do to get my guild’s calendar online in full. Thanks for your efforts and for the code!

  2. Owen said, on 06/10/2009 at 22:58

    Great idea – was just about to write this in perl ! Hoping to have raid signup summaries on our website.

    However I always get the error “Invalid JSON received. Continuing.”

    any ideas ?

  3. Ryon Sherman said, on 06/11/2009 at 01:09

    Hrm, well that error should only occur when the request receives invalid JSON from the Armory.

    Excluding sensitive information, what options are you passing the script? Unfortunately, I haven’t been able to recreate this so my only other idea is for you to post the response and json values.

    //snip...
    $response = $this->request(URL_BASE_ARMORY.URL_CALENDAR_USER, $post);
    $json = $this->json($response);

    // COPY
    echo $response;
    print_r($json);
    exit();
    // PASTE

    //...snip...
    if(is_object($json)) {
    //...snip...
    } else $this->log('Invalid JSON received. Continuing.');
    //...snip

    Sorry I’m not a very good tester! If it works on my work computer and at home…it’s good enough to go online. Hope this gets working for you.

  4. Owen said, on 06/11/2009 at 03:26

    Thanks for your quick reply – i have made the changes in both functions get_world_calendar_events() and get_user_calendar_events()

    Here are to command line entries tried and the respective output after changes.

    php wowcal.php -u XXXX -p YYYY -c Beefstapler -r Bloodscalp -ut raid,meeting,dungeon,other,pvp -v -l log.txt
    Jun 11 13:09:59: WoWCal Started.
    Jun 11 13:09:59: Logging In…
    Jun 11 13:10:00: Login Successful!

    
    

    php wowcal.php -u XXXX -p YYYY -c Beefstapler -r Bloodscalp -wt bg,holiday -v -l log.txt
    Jun 11 13:10:05: WoWCal Started.
    Jun 11 13:10:05: Logging In…
    Jun 11 13:10:06: Login Successful!
    Jun 11 13:10:06: Retreiving World Calendar: bg…

    
    

    php wowcal.php -u XXXX -p YYYY -c Beefstapler -r -ut raid,meeting,dungeon,other,pvp -v -l log.txt -m 6 -y 2009
    Jun 11 13:23:52: WoWCal Started.
    Jun 11 13:23:52: Logging In…
    Jun 11 13:23:53: Login Successful!

    
    

    So I am basically getting nothing back. Crap.
    I have updated all my php libs to the lastest versions, but no change.
    However, I was reading through the code and noticed that you refer to a battlenet account. I have not converted my account yet – mine is still a normal wow account.
    Any ideas ?

  5. Owen said, on 06/11/2009 at 03:48

    After some more testing – the login is not working – I can put any old password in and it still says login successful

  6. Ryon Sherman said, on 06/11/2009 at 15:57

    Found the problem! Apparently cURL operates a little differently in Linux.

    After looking at the verbose output of cURL I noticed Linux was performing a GET request while windows used POST. I tweaked the cURL options a bit and now it’s working fine for me.

    After some trial and error I got it to work by using a string of parameters passed in the URL and an array passed in the POSTFIELDS. This seems stupid so if anyone can figure this part out that would be great.

    curl_setopt($ch, CURLOPT_URL, $url.’?’.$params);
    ..snip…
    curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters);

  7. Owen said, on 06/11/2009 at 22:22

    Fantastic work Ryon. Works perfectly. Thank you.

    Hopefully I will get to some more perl on the weekend and see if I can add something other than just complaints.

    Cheers

  8. Owen said, on 06/11/2009 at 22:47

    Outlook Users:
    For this to calendar file to import into outlook you need to the following 2 steps

    1. remove the line “VERSION:2.0″
    2. rename the file to from *.ical to *.ics

    I believe outlook 2007 will accept the ical format, but 2003 and eariler don’t

    Cheers

  9. Ryon Sherman said, on 06/11/2009 at 23:36

    Thanks for that, you can also use “-f raid_calendar.ics” to output it as ics.

    Maybe I should have an option to exclude the “VERSION:2.0″ line for Outlook users. I really don’t know much about the iCal format.

    By the way, If you would like to help out you can find me at GitHub.

    Thanks for your input, it’s nice to know someone’s benefiting from my code!

  10. John said, on 06/17/2009 at 21:49

    Had a bit of a play, but I think the Armoury is killing some requests if they are made too fast. Playing about with a 10 second delay between request to the Armoury helped.

    Nice little script though!

  11. Philip said, on 06/26/2009 at 03:15

    I have been trying to get this to work but am running in to this error:
    PHP Fatal error: Call to undefined function curl_init() in C:\wowcal\wowcal.php on line 778
    I changed curl_setopt($ch, CURLOPT_URL, $url.’?’.$params);
    ..snip…
    curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters);
    as stated above and still no luck. Any ideas?

  12. Tyler said, on 06/27/2009 at 01:28

    I cannot log in successfully because I have ! and # in my password. When I change my password to alphanumeric it works fine. Great program.

  13. Kumartheffar said, on 06/27/2009 at 20:19

    Having trouble with logging in. I do not have a battle.net account and i am running on windows xp with php5.2 I keep getting the message

    Jun 27 15:13:45: WoWCal Started.
    Jun 27 15:13:45: Logging In…
    Fatal error: Call to undefined function curl_init() in C:\php\wowcal.php on line 778

    using the command

    C:\php>php wowcal.php -u kumartheffar -p -c kumarthefari -r kael’thas -v

  14. Ryon Sherman said, on 06/30/2009 at 13:15

    Philip and Kumartheffar, make sure to have the cURL extension enabled in your PHP installation.

    http://us2.php.net/manual/en/book.curl.php

  15. Ryon Sherman said, on 06/30/2009 at 14:23

    Tyler, the code has been updated to allow punctuation characters. Remember in Linux some of these (e.g. exclamation point) need to be escaped.

    Thanks for finding this bug.

  16. Ryon Sherman said, on 06/30/2009 at 14:24

    You can now report issues with this script at: http://github.com/ryonsherman/wowcal/issues

  17. Brandon Stewart said, on 08/27/2009 at 23:39

    I get the following error when I try to run the code:

    Parse error: parse error, expecting `T_OLD_FUNCTION’ or `T_FUNCTION’ or `T_VAR’ or `’}” in /Users/bstewart/Desktop/wowcal/wowcal.php on line 52

    I’m using Mac OS 10.4, which runs PHP on Apache through a BSD kernel. Hope that helps.

  18. Ryon Sherman said, on 09/05/2009 at 01:13

    The script requires PHP version 5. More information on this here: http://bit.ly/4G3Cdz


Leave a Reply