1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace communication_matrix; 18 19 use communication_matrix\local\command; 20 use core\http_client; 21 use DirectoryIterator; 22 use Exception; 23 use GuzzleHttp\Psr7\Response; 24 25 /** 26 * The abstract class for a versioned API client for Matrix. 27 * 28 * Matrix uses a versioned API, and a handshake occurs between the Client (Moodle) and server, to determine the APIs available. 29 * 30 * This client represents a version-less API client. 31 * Versions are implemented by combining the various features into a versionedclass. 32 * See v1p1 for example. 33 * 34 * @package communication_matrix 35 * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 abstract class matrix_client { 39 /** @var string $serverurl The URL of the home server */ 40 /** @var string $accesstoken The access token of the matrix server */ 41 42 /** @var http_client|null The client to use */ 43 protected static http_client|null $client = null; 44 45 /** 46 * Matrix events constructor to get the room id and refresh token usage if required. 47 * 48 * @param string $serverurl The URL of the API server 49 * @param string $accesstoken The admin access token 50 */ 51 protected function __construct( 52 protected string $serverurl, 53 protected string $accesstoken, 54 ) { 55 } 56 57 /** 58 * Return the versioned instance of the API. 59 * 60 * @param string $serverurl The URL of the API server 61 * @param string $accesstoken The admin access token to use 62 * @return matrix_client 63 */ 64 public static function instance( 65 string $serverurl, 66 string $accesstoken, 67 ): matrix_client { 68 // Fetch the list of supported API versions. 69 $clientversions = self::get_supported_versions(); 70 71 // Fetch the supported versions from the server. 72 $serversupports = self::query_server_supports($serverurl); 73 $serverversions = $serversupports->versions; 74 75 // Calculate the intersections and sort to determine the highest combined version. 76 $versions = array_intersect($clientversions, $serverversions); 77 if (count($versions) === 0) { 78 // No versions in common. 79 throw new \moodle_exception('No supported Matrix API versions found.'); 80 } 81 asort($versions); 82 $version = array_key_last($versions); 83 84 $classname = \communication_matrix\local\spec::class . '\\' . $version; 85 86 return new $classname( 87 $serverurl, 88 $accesstoken, 89 ); 90 } 91 92 /** 93 * Determine if the API supports a feature. 94 * 95 * If an Array is provided, this will return true if any of the specified features is implemented. 96 * 97 * @param string[]|string $feature The feature to check. This is in the form of a namespaced class. 98 * @return bool 99 */ 100 public function implements_feature(array|string $feature): bool { 101 if (is_array($feature)) { 102 foreach ($feature as $thisfeature) { 103 if ($this->implements_feature($thisfeature)) { 104 return true; 105 } 106 } 107 108 // None of the features are implemented in this API version. 109 return false; 110 } 111 112 return in_array($feature, $this->get_supported_features()); 113 } 114 115 /** 116 * Get a list of the features supported by this client. 117 * 118 * @return string[] 119 */ 120 public function get_supported_features(): array { 121 $features = []; 122 $class = static::class; 123 do { 124 $features = array_merge($features, class_uses($class)); 125 $class = get_parent_class($class); 126 } while ($class); 127 128 return $features; 129 } 130 131 /** 132 * Require that the API supports a feature. 133 * 134 * If an Array is provided, this is treated as a require any of the features. 135 * 136 * @param string[]|string $feature The feature to test 137 * @throws \moodle_exception 138 */ 139 public function require_feature(array|string $feature): void { 140 if (!$this->implements_feature($feature)) { 141 if (is_array($feature)) { 142 $features = implode(', ', $feature); 143 throw new \moodle_exception( 144 "None of the possible feature are implemented in this Matrix Client: '{$features}'" 145 ); 146 } 147 throw new \moodle_exception("The requested feature is not implemented in this Matrix Client: '{$feature}'"); 148 } 149 } 150 151 /** 152 * Require that the API supports a list of features. 153 * 154 * All features specified will be required. 155 * 156 * If an array is provided as one of the features, any of the items in the nested array will be required. 157 * 158 * @param string[]|array[] $features The list of features required 159 * 160 * Here is an example usage: 161 * <code> 162 * $matrixapi->require_features([ 163 * 164 * \communication_matrix\local\spec\features\create_room::class, 165 * [ 166 * \communication_matrix\local\spec\features\get_room_info_v1::class, 167 * \communication_matrix\local\spec\features\get_room_info_v2::class, 168 * ] 169 * ]) 170 * </code> 171 */ 172 public function require_features(array $features): void { 173 array_walk($features, [$this, 'require_feature']); 174 } 175 176 /** 177 * Get the URL of the server. 178 * 179 * @return string 180 */ 181 public function get_server_url(): string { 182 return $this->serverurl; 183 } 184 185 /** 186 * Query the supported versions, and any unstable features, from the server. 187 * 188 * Servers must implement the client versions API described here: 189 * - https://spec.matrix.org/latest/client-server-api/#get_matrixclientversions 190 * 191 * @param string $serverurl The server base 192 * @return \stdClass The list of supported versions and a list of enabled unstable features 193 */ 194 protected static function query_server_supports(string $serverurl): \stdClass { 195 // Attempt to return from the cache first. 196 $cache = \cache::make('communication_matrix', 'serverversions'); 197 $serverkey = sha1($serverurl); 198 if ($cache->get($serverkey)) { 199 return $cache->get($serverkey); 200 } 201 202 // Not in the cache - fetch and store in the cache. 203 $client = static::get_http_client(); 204 $response = $client->get("{$serverurl}/_matrix/client/versions"); 205 $supportsdata = json_decode( 206 json: $response->getBody(), 207 associative: false, 208 flags: JSON_THROW_ON_ERROR, 209 ); 210 211 $cache->set($serverkey, $supportsdata); 212 213 return $supportsdata; 214 } 215 216 /** 217 * Get the list of supported versions based on the available classes. 218 * 219 * @return array 220 */ 221 public static function get_supported_versions(): array { 222 $versions = []; 223 $iterator = new DirectoryIterator(__DIR__ . '/local/spec'); 224 foreach ($iterator as $fileinfo) { 225 if ($fileinfo->isDir()) { 226 continue; 227 } 228 229 // Get the classname from the filename. 230 $classname = substr($fileinfo->getFilename(), 0, -4); 231 232 if (!preg_match('/^v\d+p\d+$/', $classname)) { 233 // @codeCoverageIgnoreStart 234 // This file does not fit the format v[MAJOR]p[MINOR]]. 235 continue; 236 // @codeCoverageIgnoreEnd 237 } 238 239 $versions[$classname] = "v" . self::get_version_from_classname($classname); 240 } 241 242 return $versions; 243 } 244 245 /** 246 * Get the current token in use. 247 * 248 * @return string 249 */ 250 public function get_token(): string { 251 return $this->accesstoken; 252 } 253 254 /** 255 * Helper to fetch the HTTP Client for the instance. 256 * 257 * @return \core\http_client 258 */ 259 protected function get_client(): \core\http_client { 260 return static::get_http_client(); 261 } 262 263 /** 264 * Helper to fetch the HTTP Client. 265 * 266 * @return \core\http_client 267 */ 268 protected static function get_http_client(): \core\http_client { 269 if (static::$client !== null) { 270 return static::$client; 271 } 272 // @codeCoverageIgnoreStart 273 return new http_client(); 274 // @codeCoverageIgnoreEnd 275 } 276 277 /** 278 * Execute the specified command. 279 * 280 * @param command $command 281 * @return Response 282 */ 283 protected function execute( 284 command $command, 285 ): Response { 286 $client = $this->get_client(); 287 return $client->send( 288 $command, 289 $command->get_options(), 290 ); 291 } 292 293 /** 294 * Get the API version of the current instance. 295 * 296 * @return string 297 */ 298 public function get_version(): string { 299 $reflect = new \ReflectionClass(static::class); 300 $classname = $reflect->getShortName(); 301 return self::get_version_from_classname($classname); 302 } 303 304 /** 305 * Normalise an API version from a classname. 306 * 307 * @param string $classname The short classname, omitting any namespace or file extension 308 * @return string The normalised version 309 */ 310 protected static function get_version_from_classname(string $classname): string { 311 $classname = str_replace('v', '', $classname); 312 $classname = str_replace('p', '.', $classname); 313 return $classname; 314 } 315 316 /** 317 * Check if the API version is at least the specified version. 318 * 319 * @param string $minversion The minimum API version required 320 * @return bool 321 */ 322 public function meets_version(string $minversion): bool { 323 $thisversion = $this->get_version(); 324 return version_compare($thisversion, $minversion) >= 0; 325 } 326 327 /** 328 * Assert that the API version is at least the specified version. 329 * 330 * @param string $minversion The minimum API version required 331 * @throws Exception 332 */ 333 public function requires_version(string $minversion): void { 334 if ($this->meets_version($minversion)) { 335 return; 336 } 337 338 throw new \moodle_exception("Matrix API version {$minversion} or higher is required for this command."); 339 } 340 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body