Export World of Warcraft Calendar To iCal
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 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();
}
}
?>

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!
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 ?
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.
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 ?
After some more testing – the login is not working – I can put any old password in and it still says login successful
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);
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
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
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!
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!
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?
I cannot log in successfully because I have ! and # in my password. When I change my password to alphanumeric it works fine. Great program.
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
Philip and Kumartheffar, make sure to have the cURL extension enabled in your PHP installation.
http://us2.php.net/manual/en/book.curl.php
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.
You can now report issues with this script at: http://github.com/ryonsherman/wowcal/issues
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.
The script requires PHP version 5. More information on this here: http://bit.ly/4G3Cdz
Greetings.
I am attempting to put together a script that will pull calendar data down from the armory at regular intervals and insert the data into a MySQL table.
I think that the code you’ve already written could be adapted to this task fairly easily but I have some questions.
First and foremost, it appears that you are having to define a specific date rage.. could the code be modified to simply retrieve ALL available calendar data and compare that to an existing data set (in the table I mentioned) and insert anything that is new?
It would not have to be particularly high performance, this is something I’d set on a cron job for once a week or every other day or somesuch.
Any help you can lend is greatly appreciated!
-Matt