├── README.md └── geochat.php /README.md: -------------------------------------------------------------------------------- 1 | # GeoChat Server 2 | 3 | This is a server for GeoChat applications, written for OpenStreetMap, but usable in other environments. 4 | 5 | ## Installation 6 | 7 | Just copy the `geochat.php` file anywhere accessible from the web and point the clients to it. It stores messages in a MySQL database: write user credentials and database name in the `geochat.php` and run it from a command line to initialize tables. 8 | 9 | ## Operation 10 | 11 | Add a task to crontab or other scheduling application to run the script for a command line at least once an hour. It will clean expired messages and user accounts. 12 | 13 | ## API 14 | 15 | The API is described in [the OSM wiki](http://wiki.openstreetmap.org/wiki/JOSM/Plugins/GeoChat/API). 16 | 17 | ## License 18 | 19 | This script was written by Ilya Zverev and licensed WTFPL. 20 | 21 | -------------------------------------------------------------------------------- /geochat.php: -------------------------------------------------------------------------------- 1 | connect_errno ) 9 | die('Cannot connect to database: ('.$db->connect_errno.') '.$db->connect_error); 10 | $db->set_charset('utf8'); 11 | 12 | if( PHP_SAPI == 'cli' ) { 13 | maintenance($db); 14 | exit; 15 | } 16 | 17 | header('Access-Control-Allow-Origin: *'); 18 | header('Content-type: application/json'); 19 | $action = req('action', ''); 20 | if( $action == 'get' || $action == 'post' || $action == 'register' ) { 21 | $lat = req('lat'); validate_num($lat, 'lat', true, -90.0, 90.0); 22 | $lon = req('lon'); validate_num($lon, 'lon', true, -180.0, 180.0); 23 | if( $action == 'get' ) 24 | get($db, $lat, $lon); 25 | elseif( $action == 'post' ) 26 | post($db, $lat, $lon); 27 | elseif( $action == 'register' ) 28 | register($db, $lat, $lon); 29 | } elseif( $action == 'logout' ) { 30 | logout($db); 31 | } elseif( $action == 'whoami' ) { 32 | $user_name = validate_user($db, req('uid')); 33 | print json_encode(array('name' => $user_name)); 34 | } elseif( $action == 'now' ) { 35 | $now = request_one($db, 'select now()'); 36 | print json_encode(array('date' => $now)); 37 | } elseif( $action == 'last' ) { 38 | get_last($db); 39 | } else { 40 | // header('Content-type: text/html; charset=utf-8'); 41 | // readfile('osmochat.html'); 42 | header('Location: http://wiki.openstreetmap.org/wiki/JOSM/Plugins/GeoChat/API'); 43 | } 44 | 45 | // Print error message and exit 46 | function error( $msg ) { 47 | print json_encode(array('error' => $msg)); 48 | exit; 49 | } 50 | 51 | // Check query parameter and return either it or the default value, or raise error if there's no default. 52 | function req( $param, $default = NULL ) { 53 | if( !isset($_REQUEST[$param]) || strlen($_REQUEST[$param]) == 0 ) { 54 | if( is_null($default) ) 55 | error("Missing required parameter \"$param\"."); 56 | else 57 | return $default; 58 | } 59 | return trim($_REQUEST[$param]); 60 | } 61 | 62 | // Validate float or integer number, and check for min/max. 63 | function validate_num( $f, $name, $float, $min = NULL, $max = NULL ) { 64 | if( !preg_match($float ? '/^-?\d+(?:\.\d+)?$/' : '/^-?\d+$/', $f) ) 65 | error("Parameter \"$name\" should be " 66 | .($float ? 'a floating-point number with a dot as a separator.' : 'an integer number')); 67 | if( (!is_null($min) && $f < $min) || (!is_null($max) && $f > $max) ) 68 | error("Parameter \"$name\" should be a number between $min and $max."); 69 | } 70 | 71 | // Request a single value from the database 72 | function request_one($db, $query) { 73 | $result = $db->query($query); 74 | if( !$result ) 75 | error('Database query failed: '.$db->error); 76 | if( $result->num_rows > 0 ) { 77 | $tmp = $result->fetch_row(); 78 | $ret = $tmp[0]; 79 | } else { 80 | $ret = null; 81 | } 82 | $result->free(); 83 | return $ret; 84 | } 85 | 86 | // Check that user exists and returns their name 87 | function validate_user( $db, $user_id ) { 88 | validate_num($user_id, 'uid', false); 89 | $user_name = request_one($db, 'select user_name from osmochat_users where user_id = '.$user_id); 90 | if( !$user_name ) 91 | error("No user with user_id $user_id found"); 92 | return $user_name; 93 | } 94 | 95 | // Returns where clause for offsets around a point with given radius in km 96 | function region_where_clause( $lat, $lon, $radius, $field ) { 97 | $basekm = 6371.0; 98 | $coslat = cos($lat * M_PI / 180.0); 99 | $dlat = ($radius / $basekm); 100 | $dlon = asin(sin($dlat) / $coslat) * 180.0 / M_PI; 101 | $dlat = $dlat * 180.0 / M_PI; 102 | $minlat = $lat - $dlat; 103 | $minlon = $lon - $dlon; 104 | $maxlat = $lat + $dlat; 105 | $maxlon = $lon + $dlon; 106 | $bbox = "ST_GeomFromText('POLYGON(($minlon $minlat, $minlon $maxlat, $maxlon $maxlat, $maxlon $minlat, $minlon $minlat))')"; 107 | return "MBRContains($bbox, $field)"; 108 | } 109 | 110 | // Prints all messages near the specified point 111 | // Also registers user coords and returns all nearby users 112 | function get( $db, $lat, $lon ) { 113 | $user_id = req('uid', 0); 114 | validate_user($db, $user_id); 115 | $last = req('last', 0); 116 | validate_num($last, 'last', false); 117 | $list = array(); 118 | 119 | $result = $db->query("update osmochat_users set last_time = NOW(), last_pos = POINT($lon, $lat) where user_id = $user_id"); 120 | if( !$result ) 121 | error('Failed to update user position: '.$db->error); 122 | 123 | $region = region_where_clause($lat, $lon, RADIUS, 'msgpos'); 124 | $query = "select *, ST_X(msgpos) as lon, ST_Y(msgpos) as lat, unix_timestamp(msgtime) as ts from osmochat where msgid > $last and ((recipient is null and $region) or recipient = $user_id or (recipient is not null and author = $user_id)) order by msgid desc limit 30"; 125 | $result = $db->query($query); 126 | if( !$result ) 127 | error('Database query for messages failed: '.$db->error); 128 | $msg = array(); 129 | $pmsg = array(); 130 | while( ($data = $result->fetch_assoc()) ) { 131 | $item = array(); 132 | $item['id'] = $data['msgid']; 133 | $item['lat'] = $data['lat']; 134 | $item['lon'] = $data['lon']; 135 | $item['time'] = $data['msgtime']; 136 | $item['timestamp'] = $data['ts']; 137 | $item['author'] = $data['user_name']; 138 | $item['message'] = $data['message']; 139 | $item['incoming'] = $data['author'] != $user_id; 140 | if( !is_null($data['recipient']) ) { 141 | $item['recipient'] = $data['recipient_name']; 142 | array_unshift($pmsg, $item); 143 | } else 144 | array_unshift($msg, $item); 145 | } 146 | $list['messages'] = $msg; 147 | $list['private'] = $pmsg; 148 | $result->free(); 149 | 150 | $region = region_where_clause($lat, $lon, RADIUS, 'last_pos'); 151 | $query = "select user_name, ST_X(last_pos) as lon, ST_Y(last_pos) as lat from osmochat_users where user_id != $user_id and $region limit 100"; 152 | $result = $db->query($query); 153 | if( !$result ) 154 | error('Database query for users failed: '.$db->error); 155 | $users = array(); 156 | while( ($data = $result->fetch_assoc()) ) { 157 | $item = array(); 158 | $item['user'] = $data['user_name']; 159 | $item['lat'] = $data['lat']; 160 | $item['lon'] = $data['lon']; 161 | $users[] = $item; 162 | } 163 | $list['users'] = $users; 164 | $result->free(); 165 | 166 | print json_encode($list); 167 | } 168 | 169 | // Returns last messages with little extra info (unusable for chatting) 170 | function get_last( $db ) { 171 | $last = req('last', 0); 172 | validate_num($last, 'last', false); 173 | $list = array(); 174 | 175 | $query = "select *, ST_X(msgpos) as lon, ST_Y(msgpos) as lat, unix_timestamp(msgtime) as ts from osmochat where msgid > $last and recipient is null order by msgid desc limit 20"; 176 | $result = $db->query($query); 177 | if( !$result ) 178 | error('Database query for messages failed: '.$db->error); 179 | while( ($data = $result->fetch_assoc()) ) { 180 | $item = array(); 181 | $item['id'] = $data['msgid']; 182 | $item['lat'] = $data['lat']; 183 | $item['lon'] = $data['lon']; 184 | $item['time'] = $data['msgtime']; 185 | $item['timestamp'] = $data['ts']; 186 | $item['author'] = $data['user_name']; 187 | $item['message'] = $data['message']; 188 | array_unshift($list, $item); 189 | } 190 | $result->free(); 191 | 192 | print json_encode($list); 193 | } 194 | 195 | // Adds a message to the database 196 | function post( $db, $lat, $lon ) { 197 | $message = $db->escape_string(req('message')); 198 | if( mb_strlen($message, 'UTF8') == 0 || mb_strlen($message, 'UTF8') > 1000 ) 199 | error('Incorrect message'); 200 | $user_id = req('uid'); 201 | $user_name = validate_user($db, $user_id); 202 | 203 | $to = req('to', ''); 204 | if( strlen($to) >= 2 ) { 205 | // This message is private 206 | $recipient = request_one($db, "select user_id from osmochat_users where user_name = '".$db->escape_string($to)."'"); 207 | if( !$recipient ) 208 | error("No user with the name '$to'"); 209 | $recname = "'".$db->escape_string($to)."'"; 210 | } else { 211 | $recipient = 'NULL'; 212 | $recname = 'NULL'; 213 | } 214 | 215 | $query = "insert into osmochat (msgtime, msgpos, user_name, author, recipient, recipient_name, message) values (now(), POINT($lon, $lat), '$user_name', $user_id, $recipient, $recname, '$message')"; 216 | $result = $db->query($query); 217 | if( !$result ) 218 | error('Failed to add message entry: '.$db->error); 219 | print json_encode(array('message' => 'Message was successfully added')); 220 | } 221 | 222 | // Register a user. Returns his user_id 223 | function register($db, $lat, $lon) { 224 | $token = req('token', ''); 225 | if( strlen($token) > 0 ) { 226 | if( !preg_match('/^[a-zA-Z0-9]{5,20}$/', $token) ) 227 | error('Incorrect token format'); 228 | $response_str = @file_get_contents("http://auth.osmz.ru/get?token=$token"); 229 | $response = $response_str === false ? array() : explode("\n", $response_str); 230 | if( count($response) < 4 ) 231 | error('Incorrect token'); 232 | $user_name = $db->escape_string(str_replace(' ','_', $response[1])); 233 | $force_user = true; 234 | } else { 235 | $user_name = $db->escape_string(req('name')); 236 | $force_user = false; 237 | } 238 | if( strpos($user_name, ' ') !== FALSE || mb_strlen($user_name, 'UTF8') < 2 || mb_strlen($user_name, 'UTF8') > 100 ) 239 | error('Incorrect user name'); 240 | $old_user_id = request_one($db, "select user_id from osmochat_users where user_name = '$user_name'"); 241 | if( $old_user_id ) { 242 | if( !$force_user ) 243 | error("User $user_name is already logged in, please choose another name"); 244 | else { 245 | // Log out that user 246 | $result = $db->query("delete from osmochat_users where user_id = $old_user_id"); 247 | if( !$result ) 248 | error('Database error: '.$db->error); 249 | } 250 | } 251 | $tries = 0; 252 | while( !isset($user_id) ) { 253 | $user_id = mt_rand(1, 2147483647); 254 | $res = request_one($db, "select user_id from osmochat_users where user_id = $user_id"); 255 | if( $res ) { 256 | unset($user_id); 257 | if( ++$tries >= 10 ) 258 | error("Could not invent user id after $tries tries"); 259 | } 260 | } 261 | $result = $db->query("insert into osmochat_users (user_id, user_name, last_time, last_pos) values($user_id, '$user_name', now(), POINT($lon, $lat))"); 262 | if( !$result ) 263 | error('Database error: '.$db->error); 264 | print json_encode(array('message' => 'The user has been registered', 'uid' => $user_id, 'name' => $user_name)); 265 | } 266 | 267 | // Log out a user by user_id 268 | function logout($db) { 269 | $user_id = req('uid'); 270 | validate_user($db, $user_id); 271 | $result = $db->query("delete from osmochat_users where user_id = $user_id"); 272 | if( !$result ) 273 | error('Database error: '.$db->error); 274 | print json_encode(array('message' => 'The user has been logged out')); 275 | } 276 | 277 | // Create the table if it does not exists 278 | // Delete all messages older than AGE 279 | function maintenance($db) { 280 | $res = $db->query("show tables like 'osmochat'"); 281 | if( $res->num_rows == 0 ) { 282 | print("Creating the tables: osmochat"); 283 | $query = <<query($query); 299 | if( !$result ) { 300 | print " - failed: ".$db->error."\n"; 301 | exit; 302 | } 303 | print ', osmochat_users'; 304 | $db->query('drop table if exists osmochat_users'); 305 | $query = <<query($query); 316 | if( !$result ) { 317 | print " - failed: ".$db->error."\n"; 318 | exit; 319 | } 320 | print " OK\n"; 321 | } else { 322 | print("Removing old messages..."); 323 | $query = 'delete from osmochat where msgtime < now() - interval '.AGE.' hour'; 324 | $result = $db->query($query); 325 | print(!$result ? 'Failed: '.$db->error."\n" : "OK\n"); 326 | 327 | print("Removing old users..."); 328 | $query = 'delete from osmochat_users where last_time < now() - interval '.USER_AGE.' hour'; 329 | $result = $db->query($query); 330 | print(!$result ? 'Failed: '.$db->error."\n" : "OK\n"); 331 | } 332 | } 333 | 334 | ?> 335 | --------------------------------------------------------------------------------