├── .gitignore ├── lib ├── Stats.php ├── Log.php ├── ServerBan.php ├── Rpc.php ├── NameBan.php ├── Channel.php ├── Server.php ├── ServerBanException.php ├── Message.php ├── Spamfilter.php ├── User.php └── Connection.php ├── composer.json ├── LICENSE ├── README.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | -------------------------------------------------------------------------------- /lib/Stats.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Get basic statistical information: user counts, channel counts, etc. 20 | * 21 | * @return stdClass|array|bool 22 | */ 23 | public function get(int $object_detail_level=1): stdClass|array|bool 24 | { 25 | return $this->connection->query('stats.get', [ 26 | 'object_detail_level' => $object_detail_level, 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unrealircd/unrealircd-rpc", 3 | "version": "0.1.1", 4 | "description": "JSON-RPC interface to UnrealIRCd", 5 | "type": "library", 6 | "require": { 7 | "php": "^8.0|^8.1", 8 | "textalk/websocket": "^1.5" 9 | }, 10 | "license": "MIT", 11 | "autoload": { 12 | "psr-4": { 13 | "UnrealIRCd\\": "lib/" 14 | } 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Bram Matthys", 19 | "email": "syzop@vulnscan.org" 20 | }, 21 | { 22 | "name": "Denver Freeburn", 23 | "email": "sketch@sketchni.uk" 24 | }, 25 | { 26 | "name": "Valware", 27 | "email": "v.a.pond@outlook.com" 28 | } 29 | ], 30 | "support": { 31 | "irc": "ircs://irc.unrealircd.org/unreal-webpanel" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 UnrealIRCd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/Log.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Subscribe to log events. 20 | * Any previous subscriptions are overwritten (lost). 21 | * 22 | * @return stdClass|array|bool 23 | */ 24 | public function subscribe(array $sources): stdClass|array|bool 25 | { 26 | return $this->connection->query('log.subscribe', [ 27 | 'sources' => $sources, 28 | ]); 29 | } 30 | 31 | /** 32 | * Unsubscribe from all log events. 33 | * 34 | * @return stdClass|array|bool 35 | */ 36 | public function unsubscribe(string $name): stdClass|array|bool 37 | { 38 | return $this->connection->query('log.unsubscribe'); 39 | } 40 | 41 | /** 42 | * Get past log events. 43 | * 44 | * @return stdClass|array|bool 45 | */ 46 | public function getAll(array $sources = null): stdClass|array|bool 47 | { 48 | $response = $this->connection->query('log.list', ['sources' => $sources]); 49 | 50 | if (!is_bool($response) && property_exists($response, 'list')) 51 | return $response->list; 52 | 53 | return false; 54 | } 55 | 56 | /** 57 | * Send a log. 58 | * 59 | * @return stdClass|array|bool 60 | */ 61 | public function send(string $level, string $subsystem, string $event_id, string $message): stdClass|array|bool 62 | { 63 | return $this->connection->query('log.send', [ 64 | 'level' => $level, 'subsystem' => $subsystem, 'event_id' => $event_id, 'message' => $message 65 | ]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | UnrealIRCd RPC 2 | ============== 3 | 4 | This allows PHP scripts to control [UnrealIRCd](https://www.unrealircd.org/) 5 | via the [JSON-RPC interface](https://www.unrealircd.org/docs/JSON-RPC). 6 | 7 | This library is used by the 8 | [UnrealIRCd webpanel](https://github.com/unrealircd/unrealircd-webpanel/). 9 | 10 | If you are interested in helping out or would like to discuss API 11 | capabilities, join us at `#unreal-webpanel` at `irc.unrealircd.org` 12 | (IRC with TLS on port 6697). 13 | 14 | Installation 15 | ------------ 16 | ```bash 17 | composer require unrealircd/unrealircd-rpc:dev-main 18 | ``` 19 | 20 | UnrealIRCd setup 21 | ----------------- 22 | UnrealIRCd 6.0.6 or later is needed and you need to enable 23 | [JSON-RPC](https://www.unrealircd.org/docs/JSON-RPC) in it. 24 | After doing that, be sure to rehash the IRCd. 25 | 26 | Usage 27 | ----- 28 | For this example, create a file like `src/rpctest.php` with: 29 | ```php 30 | FALSE)); 40 | 41 | $bans = $rpc->serverban()->getAll(); 42 | foreach ($bans as $ban) 43 | echo "There's a $ban->type on $ban->name\n"; 44 | 45 | $users = $rpc->user()->getAll(); 46 | foreach ($users as $user) 47 | echo "User $user->name\n"; 48 | 49 | $channels = $rpc->channel()->getAll(); 50 | foreach ($channels as $channel) 51 | echo "Channel $channel->name ($channel->num_users user[s])\n"; 52 | ``` 53 | Then, run it on the command line with `php src/rpctest.php` 54 | 55 | If the example does not work, then make sure you have configured your 56 | UnrealIRCd correctly, with the same API username and password you use 57 | here, with an allowed IP, and changing the `wss://127.0.0.1:8600/` too 58 | if needed. 59 | -------------------------------------------------------------------------------- /lib/ServerBan.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Add a ban. 20 | * 21 | * @param string $user 22 | * @return stdClass|array|bool 23 | */ 24 | public function add(string $name, string $type, string $duration, string $reason): stdClass|array|bool 25 | { 26 | $response = $this->connection->query('server_ban.add', [ 27 | 'name' => $name, 28 | 'type' => $type, 29 | 'reason' => $reason, 30 | 'duration_string' => $duration ?? '1d', 31 | ]); 32 | 33 | if (is_bool($response)) 34 | return false; 35 | 36 | if (property_exists($response, 'tkl')) 37 | return $response->tkl; 38 | return FALSE; 39 | } 40 | 41 | /** 42 | * Delete a ban. 43 | * 44 | * @param string $name 45 | * @return stdClass|array|bool 46 | */ 47 | public function delete(string $name, string $type): stdClass|array|bool 48 | { 49 | $response = $this->connection->query('server_ban.del', [ 50 | 'name' => $name, 51 | 'type' => $type, 52 | ]); 53 | 54 | if (is_bool($response)) 55 | return false; 56 | 57 | if (property_exists($response, 'tkl')) 58 | return $response->tkl; 59 | return FALSE; 60 | } 61 | 62 | /** 63 | * Return a list of all bans. 64 | * 65 | * @return stdClass|array|bool 66 | * @throws Exception 67 | */ 68 | public function getAll(): stdClass|array|bool 69 | { 70 | $response = $this->connection->query('server_ban.list'); 71 | 72 | if (!is_bool($response)) { 73 | return $response->list; 74 | } 75 | 76 | throw new Exception('Invalid JSON Response from UnrealIRCd RPC.'); 77 | } 78 | 79 | /** 80 | * Get a specific ban. 81 | * 82 | * @return stdClass|array|bool 83 | * @throws Exception 84 | */ 85 | public function get(string $name, string $type): stdClass|array|bool 86 | { 87 | $response = $this->connection->query('server_ban.get', [ 88 | 'name' => $name, 89 | 'type' => $type 90 | ]); 91 | 92 | if (!is_bool($response)) { 93 | return $response->tkl; 94 | } 95 | 96 | return false; // didn't exist 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/Rpc.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Get information on all RPC modules loaded. 20 | * 21 | * @return stdClass|array|bool 22 | */ 23 | public function info(string $nick, string $reason): stdClass|array|bool 24 | { 25 | return $this->connection->query('rpc.info'); 26 | } 27 | 28 | /** 29 | * Set the name of the issuer that will make all the following RPC request 30 | * (eg. name of logged in user on a webpanel). Requires UnreaIRCd 6.0.8+. 31 | * @return stdClass|array|bool 32 | */ 33 | public function set_issuer(string $name): stdClass|array|bool 34 | { 35 | return $this->connection->query('rpc.set_issuer', [ 36 | 'name' => $name, 37 | ]); 38 | } 39 | 40 | /** 41 | * Add a timer. Requires UnrealIRCd 6.1.0+ 42 | * @param timer_id Name of the timer (so you can .del_timer later) 43 | * @param every_msec Every -this- milliseconds the command must be executed 44 | * @param method The JSON-RPC method to execute (lowlevel name, eg "stats.get") 45 | * @param params Parameters to the JSON-RPC call that will be executed, can be NULL 46 | * @param id Set JSON-RPC id to be used in the timer, leave NULL for auto id. 47 | * @return stdClass|array|bool 48 | */ 49 | public function add_timer(string $timer_id, int $every_msec, string $method, array|null $params = null, $id = null): stdClass|array|bool 50 | { 51 | if ($id === null) 52 | $id = random_int(100000, 999999); /* above the regular query() ids */ 53 | 54 | $request = [ 55 | "jsonrpc" => "2.0", 56 | "method" => $method, 57 | "params" => $params, 58 | "id" => $id 59 | ]; 60 | 61 | return $this->connection->query('rpc.add_timer', [ 62 | 'timer_id' => $timer_id, 63 | 'every_msec' => $every_msec, 64 | 'request' => $request, 65 | ]); 66 | } 67 | 68 | /** 69 | * Delete a timer. Requires UnrealIRCd 6.1.0+ 70 | * @param timer_id Name of the timer that was added through del_timer earlier. 71 | * @return stdClass|array|bool 72 | */ 73 | public function del_timer(string $timer_id) 74 | { 75 | return $this->connection->query('rpc.del_timer', [ 76 | 'timer_id' => $timer_id, 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/NameBan.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Add a name ban (QLine). 20 | * 21 | * @param string $name 22 | * @param string $reason 23 | * @param string $duration Optional 24 | * @param string $set_by Optional 25 | * @return stdClass|array|bool 26 | */ 27 | public function add(string $name, string $reason, string $duration = NULL, $set_by = NULL): stdClass|array|bool 28 | { 29 | $query = [ 30 | 'name' => $name, 31 | 'reason' => $reason, 32 | 'duration_string' => $duration ?? '0', 33 | ]; 34 | 35 | if ($set_by) 36 | $query['set_by'] = $set_by; 37 | 38 | $response = $this->connection->query('name_ban.add', $query); 39 | 40 | if (is_bool($response)) 41 | return false; 42 | 43 | if (property_exists($response, 'tkl')) 44 | return $response->tkl; 45 | return FALSE; 46 | } 47 | 48 | /** 49 | * Delete a ban. 50 | * 51 | * @param string $name 52 | * @return stdClass|array|bool 53 | */ 54 | public function delete(string $name): stdClass|array|bool 55 | { 56 | $response = $this->connection->query('name_ban.del', [ 57 | 'name' => $name, 58 | ]); 59 | 60 | if (is_bool($response)) 61 | return false; 62 | 63 | if (property_exists($response, 'tkl')) 64 | return $response->tkl; 65 | return FALSE; 66 | } 67 | 68 | /** 69 | * Return a list of all bans. 70 | * 71 | * @return stdClass|array|bool 72 | * @throws Exception 73 | */ 74 | public function getAll(): stdClass|array|bool 75 | { 76 | $response = $this->connection->query('name_ban.list'); 77 | 78 | if (!is_bool($response)) { 79 | return $response->list; 80 | } 81 | 82 | throw new Exception('Invalid JSON Response from UnrealIRCd RPC.'); 83 | } 84 | 85 | /** 86 | * Get a specific ban. 87 | * 88 | * @param string $name 89 | * @return stdClass|array|bool 90 | */ 91 | public function get(string $name): stdClass|array|bool 92 | { 93 | $response = $this->connection->query('name_ban.get', [ 94 | 'name' => $name, 95 | ]); 96 | 97 | if (!is_bool($response)) { 98 | return $response->tkl; 99 | } 100 | 101 | return false; // not found 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/Channel.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Return a list of channels users. 20 | * 21 | * @return stdClass|array|bool 22 | * @throws Exception 23 | */ 24 | public function getAll(int $object_detail_level=1): stdClass|array|bool 25 | { 26 | $response = $this->connection->query('channel.list', [ 27 | 'object_detail_level' => $object_detail_level, 28 | ]); 29 | 30 | if(!is_bool($response)) { 31 | return $response->list; 32 | } 33 | 34 | throw new Exception('Invalid JSON Response from UnrealIRCd RPC.'); 35 | } 36 | 37 | /** 38 | * Get a channel object 39 | * 40 | * @return stdClass|array|bool 41 | */ 42 | public function get(string $channel, int $object_detail_level=3): stdClass|array|bool 43 | { 44 | $response = $this->connection->query('channel.get', [ 45 | 'channel' => $channel, 46 | 'object_detail_level' => $object_detail_level, 47 | ]); 48 | 49 | if (!is_bool($response)) { 50 | return $response->channel; 51 | } 52 | return false; /* eg user not found */ 53 | } 54 | 55 | /** 56 | * Set and unset modes on a channel. 57 | * 58 | * @return stdClass|array|bool 59 | */ 60 | public function set_mode(string $channel, string $modes, string $parameters): stdClass|array|bool 61 | { 62 | return $this->connection->query('channel.set_mode', [ 63 | 'channel' => $channel, 64 | 'modes' => $modes, 65 | 'parameters' => $parameters, 66 | ]); 67 | } 68 | 69 | /** 70 | * Set the channel topic. 71 | * 72 | * @return stdClass|array|bool 73 | */ 74 | public function set_topic(string $channel, string $topic, 75 | string $set_by=null, string $set_at=null): stdClass|array|bool 76 | { 77 | return $this->connection->query('channel.set_topic', [ 78 | 'channel' => $channel, 79 | 'topic' => $topic, 80 | 'set_by' => $set_by, 81 | 'set_at' => $set_at, 82 | ]); 83 | } 84 | 85 | /** 86 | * Kick a user from the channel. 87 | * 88 | * @return stdClass|array|bool 89 | */ 90 | public function kick(string $channel, string $nick, string $reason): stdClass|array|bool 91 | { 92 | return $this->connection->query('channel.kick', [ 93 | 'nick' => $nick, 94 | 'channel' => $channel, 95 | 'reason' => $reason, 96 | ]); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/Server.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Return a list of all servers. 20 | * 21 | * @throws Exception 22 | */ 23 | public function getAll(): stdClass|array|bool 24 | { 25 | $response = $this->connection->query('server.list'); 26 | 27 | if(!is_bool($response)) { 28 | return $response->list; 29 | } 30 | 31 | throw new Exception('Invalid JSON Response from UnrealIRCd RPC.'); 32 | } 33 | 34 | /** 35 | * Return a server object 36 | * 37 | * @return stdClass|array|bool 38 | * @throws Exception 39 | */ 40 | public function get(string $server = null): stdClass|array|bool 41 | { 42 | $response = $this->connection->query('server.get', ['server' => $server]); 43 | 44 | if (!is_bool($response)) { 45 | return $response->server; 46 | } 47 | 48 | return false; // not found 49 | } 50 | 51 | /** 52 | * Rehash a server 53 | * 54 | * @return stdClass|array|bool 55 | * @throws Exception 56 | */ 57 | public function rehash(string $serv): stdClass|array|bool 58 | { 59 | return $this->connection->query('server.rehash', ["server" => $serv]); 60 | } 61 | 62 | /** 63 | * Connect to a server 64 | * 65 | * @param string $name The name of the server, e.g; irc.example.com 66 | * @return stdClass|array|bool 67 | * @throws Exception 68 | */ 69 | public function connect(string $name): stdClass|array|bool 70 | { 71 | return $this->connection->query('server.connect', [ 72 | 'link' => $name, 73 | ]); 74 | } 75 | 76 | /** 77 | * Disconnects a server 78 | * 79 | * @param string $name The name of the server, e.g; irc.example.com 80 | * @return stdClass|array|bool 81 | * @throws Exception 82 | */ 83 | public function disconnect(string $name, string $reason = "No reason"): stdClass|array|bool 84 | { 85 | return $this->connection->query('server.disconnect', [ 86 | 'link' => $name, 87 | 'reason' => $reason 88 | ]); 89 | } 90 | 91 | 92 | /** 93 | * List modules on the server 94 | * 95 | * @return stdClass|array|bool 96 | * @throws Exception 97 | */ 98 | public function module_list($name = NULL): stdClass|array|bool 99 | { 100 | $arr = []; 101 | if ($name) 102 | $arr['server'] = $name; 103 | 104 | return $this->connection->query('server.module_list', $arr); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /lib/ServerBanException.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Add a ban exceptions. 20 | * 21 | * @param string $user 22 | * @return stdClass|array|bool 23 | * @throws Exception 24 | */ 25 | public function add(string $name, string $types, string $reason, string $set_by = NULL, string $duration = NULL): stdClass|array|bool 26 | { 27 | $query = [ 28 | 'name' => $name, 29 | 'exception_types' => $types, 30 | 'reason' => $reason, 31 | ]; 32 | if ($set_by) 33 | $query['set_by'] = $set_by; 34 | 35 | if ($duration) 36 | $query['duration_string'] = $duration; 37 | 38 | $response = $this->connection->query('server_ban_exception.add', $query); 39 | 40 | if (is_bool($response)) 41 | return false; 42 | 43 | if (property_exists($response, 'tkl')) 44 | return $response->tkl; 45 | return FALSE; 46 | } 47 | 48 | /** 49 | * Delete a ban exceptions. 50 | * 51 | * @param string $name 52 | * @return stdClass|array|bool 53 | * @throws Exception 54 | */ 55 | public function delete(string $name): stdClass|array|bool 56 | { 57 | $response = $this->connection->query('server_ban_exception.del', [ 58 | 'name' => $name, 59 | ]); 60 | 61 | if (is_bool($response)) 62 | return false; 63 | 64 | if (property_exists($response, 'tkl')) 65 | return $response->tkl; 66 | return FALSE; 67 | } 68 | 69 | /** 70 | * Return a list of all exceptions. 71 | * 72 | * @return stdClass|array|bool 73 | * @throws Exception 74 | */ 75 | public function getAll(): stdClass|array|bool 76 | { 77 | $response = $this->connection->query('server_ban_exception.list', []); 78 | 79 | if (!is_bool($response)) { 80 | return $response->list; 81 | } 82 | 83 | throw new Exception('Invalid JSON Response from UnrealIRCd RPC.'); 84 | } 85 | 86 | /** 87 | * Get a specific ban exceptions. 88 | * 89 | * @return stdClass|array|bool 90 | * @throws Exception 91 | */ 92 | public function get(string $name): stdClass|array|bool 93 | { 94 | $response = $this->connection->query('server_ban_exception.get', [ 95 | 'name' => $name, 96 | ]); 97 | 98 | if (!is_bool($response)) { 99 | return $response->tkl; 100 | } 101 | 102 | return false; // didn't exist 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /lib/Message.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Send a PRIVMSG to a user. 20 | * 21 | * @param string $nick The nickname of the user to send the message to. 22 | * @param string $message The message to send. 23 | * @return stdClass|array|bool 24 | */ 25 | public function privmsg(string $nick, string $message): stdClass|array|bool 26 | { 27 | return $this->connection->query('message.privmsg', [ 28 | 'nick' => $nick, 29 | 'message' => $message, 30 | ]); 31 | } 32 | 33 | /** 34 | * Send a NOTICE to a user. 35 | * 36 | * @param string $nick The nickname of the user to send the notice to. 37 | * @param string $message The notice message to send. 38 | * @return stdClass|array|bool 39 | */ 40 | public function notice(string $nick, string $message): stdClass|array|bool 41 | { 42 | return $this->connection->query('message.notice', [ 43 | 'nick' => $nick, 44 | 'message' => $message, 45 | ]); 46 | } 47 | 48 | /** 49 | * Send a custom numeric message to a user. 50 | * 51 | * @param string $nick The nickname of the user to send the numeric to. 52 | * @param int $numeric The numeric code (1-999). 53 | * @param string $message The message text for the numeric. 54 | * @return stdClass|array|bool 55 | */ 56 | public function numeric(string $nick, int $numeric, string $message): stdClass|array|bool 57 | { 58 | return $this->connection->query('message.numeric', [ 59 | 'nick' => $nick, 60 | 'numeric' => $numeric, 61 | 'message' => $message, 62 | ]); 63 | } 64 | 65 | /** 66 | * Send a standard reply to a user (IRCv3 standard replies). 67 | * 68 | * @param string $nick The nickname of the user to send the reply to. 69 | * @param string $type The type of reply: 'FAIL', 'WARN', or 'NOTE'. 70 | * @param string $code The reply code. 71 | * @param string $description The description text. 72 | * @param string|null $context Optional context for the reply. 73 | * @return stdClass|array|bool 74 | */ 75 | public function standardreply(string $nick, string $type, string $code, string $description, ?string $context = null): stdClass|array|bool 76 | { 77 | $params = [ 78 | 'nick' => $nick, 79 | 'type' => $type, 80 | 'code' => $code, 81 | 'description' => $description, 82 | ]; 83 | 84 | if ($context !== null) { 85 | $params['context'] = $context; 86 | } 87 | 88 | return $this->connection->query('message.standardreply', $params); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/Spamfilter.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Add a spamfilter. 20 | * 21 | * @return stdClass|array|bool 22 | */ 23 | public function add(string $name, string $match_type, string $spamfilter_targets, string $ban_action, string $ban_duration, string $reason): stdClass|array|bool 24 | { 25 | $response = $this->connection->query('spamfilter.add', [ 26 | 'name' => $name, 27 | 'match_type' => $match_type, 28 | 'spamfilter_targets' => $spamfilter_targets, 29 | 'ban_action' => $ban_action, 30 | 'ban_duration' => $ban_duration, 31 | 'reason' => $reason, 32 | ]); 33 | 34 | if (is_bool($response)) 35 | return false; 36 | 37 | if (property_exists($response, 'tkl')) 38 | return $response->tkl; 39 | return FALSE; 40 | } 41 | 42 | /** 43 | * Delete a spamfilter. 44 | * 45 | * @return stdClass|array|bool 46 | */ 47 | public function delete(string $name, string $match_type, string $spamfilter_targets, string $ban_action): stdClass|array|bool 48 | { 49 | $response = $this->connection->query('spamfilter.del', [ 50 | 'name' => $name, 51 | 'match_type' => $match_type, 52 | 'spamfilter_targets' => $spamfilter_targets, 53 | 'ban_action' => $ban_action, 54 | ]); 55 | 56 | if (is_bool($response)) 57 | return false; 58 | 59 | if (property_exists($response, 'tkl')) 60 | return $response->tkl; 61 | return FALSE; 62 | } 63 | 64 | /** 65 | * Return a list of all spamfilters. 66 | * 67 | * @return stdClass|array|bool 68 | * @throws Exception 69 | */ 70 | public function getAll(): stdClass|array|bool 71 | { 72 | $response = $this->connection->query('spamfilter.list'); 73 | 74 | if (!is_bool($response)) { 75 | return $response->list; 76 | } 77 | 78 | throw new Exception('Invalid JSON Response from UnrealIRCd RPC.'); 79 | } 80 | 81 | /** 82 | * Get a specific spamfilter. 83 | * 84 | * @return stdClass|array|bool 85 | */ 86 | public function get(string $name, string $match_type, string $spamfilter_targets, string $ban_action): stdClass|array|bool 87 | { 88 | $response = $this->connection->query('spamfilter.get', [ 89 | 'name' => $name, 90 | 'match_type' => $match_type, 91 | 'spamfilter_targets' => $spamfilter_targets, 92 | 'ban_action' => $ban_action, 93 | ]); 94 | 95 | if (!is_bool($response)) { 96 | return $response->tkl; 97 | } 98 | 99 | return false; // not found 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/User.php: -------------------------------------------------------------------------------- 1 | connection = $conn; 16 | } 17 | 18 | /** 19 | * Return a list of all users. 20 | */ 21 | public function getAll(int $object_detail_level=2): stdClass|array|bool 22 | { 23 | $response = $this->connection->query('user.list', [ 24 | 'object_detail_level' => $object_detail_level, 25 | ]); 26 | 27 | if(!is_bool($response)) { 28 | return $response->list; 29 | } 30 | 31 | throw new Exception('Invalid JSON Response from UnrealIRCd RPC.'); 32 | } 33 | 34 | /** 35 | * Return a user object 36 | * 37 | * @return stdClass|array|bool 38 | */ 39 | public function get(string $nick, int $object_detail_level=4): stdClass|array|bool 40 | { 41 | $response = $this->connection->query('user.get', [ 42 | 'nick' => $nick, 43 | 'object_detail_level' => $object_detail_level, 44 | ]); 45 | 46 | if (!is_bool($response)) { 47 | return $response->client; 48 | } 49 | 50 | return false; // not found 51 | } 52 | 53 | /** 54 | * Set the nickname of a user (changes the nick) 55 | * 56 | * @return stdClass|array|bool 57 | */ 58 | public function set_nick(string $nick, string $newnick): stdClass|array|bool 59 | { 60 | return $this->connection->query('user.set_nick', [ 61 | 'nick' => $nick, 62 | 'newnick' => $newnick, 63 | ]); 64 | } 65 | 66 | /** 67 | * Set the username/ident of a user 68 | * 69 | * @return stdClass|array|bool 70 | */ 71 | public function set_username(string $nick, string $username): stdClass|array|bool 72 | { 73 | return $this->connection->query('user.set_username', [ 74 | 'nick' => $nick, 75 | 'username' => $username, 76 | ]); 77 | } 78 | 79 | /** 80 | * Set the realname/gecos of a user 81 | * 82 | * @return stdClass|array|bool 83 | */ 84 | public function set_realname(string $nick, string $realname): stdClass|array|bool 85 | { 86 | return $this->connection->query('user.set_realname', [ 87 | 'nick' => $nick, 88 | 'realname' => $realname, 89 | ]); 90 | } 91 | 92 | /** 93 | * Set a virtual host (vhost) on the user 94 | * 95 | * @return stdClass|array|bool 96 | */ 97 | public function set_vhost(string $nick, string $vhost): stdClass|array|bool 98 | { 99 | return $this->connection->query('user.set_vhost', [ 100 | 'nick' => $nick, 101 | 'vhost' => $vhost, 102 | ]); 103 | } 104 | 105 | /** 106 | * Change the user modes of a user. 107 | * 108 | * @return stdClass|array|bool 109 | */ 110 | public function set_mode(string $nick, string $mode, bool $hidden = false): stdClass|array|bool 111 | { 112 | return $this->connection->query('user.set_mode', [ 113 | 'nick' => $nick, 114 | 'modes' => $mode, 115 | 'hidden' => $hidden, 116 | ]); 117 | } 118 | 119 | /** 120 | * Change the snomask of a user (oper). 121 | * 122 | * @return stdClass|array|bool 123 | */ 124 | public function set_snomask(string $nick, string $snomask, bool $hidden = false): stdClass|array|bool 125 | { 126 | return $this->connection->query('user.set_snomask', [ 127 | 'nick' => $nick, 128 | 'snomask' => $snomask, 129 | 'hidden' => $hidden, 130 | ]); 131 | } 132 | 133 | /** 134 | * Make user an IRC Operator (oper). 135 | * 136 | * @return stdClass|array|bool 137 | */ 138 | public function set_oper(string $nick, string $oper_account, string $oper_class, 139 | string $class = null, string $modes = null, 140 | string $snomask = null, string $vhost = null): stdClass|array|bool 141 | { 142 | return $this->connection->query('user.set_oper', [ 143 | 'nick' => $nick, 144 | 'oper_account' => $oper_account, 145 | 'oper_class' => $oper_class, 146 | 'class' => $class, 147 | 'modes' => $modes, 148 | 'snomask' => $snomask, 149 | 'vhost' => $vhost, 150 | ]); 151 | } 152 | 153 | /** 154 | * Join a user to a channel. 155 | * 156 | * @return stdClass|array|bool 157 | */ 158 | public function join(string $nick, string $channel, 159 | string $key = null, bool $force = false): stdClass|array|bool 160 | { 161 | return $this->connection->query('user.join', [ 162 | 'nick' => $nick, 163 | 'channel' => $channel, 164 | 'key' => $key, 165 | 'force' => $force, 166 | ]); 167 | } 168 | 169 | /** 170 | * Part a user from a channel. 171 | * 172 | * @return stdClass|array|bool 173 | */ 174 | public function part(string $nick, string $channel, bool $force = false): stdClass|array|bool 175 | { 176 | return $this->connection->query('user.part', [ 177 | 'nick' => $nick, 178 | 'channel' => $channel, 179 | 'force' => $force, 180 | ]); 181 | } 182 | 183 | /** 184 | * Quit a user from IRC. Pretend it is a normal QUIT. 185 | * 186 | * @return stdClass|array|bool 187 | */ 188 | public function quit(string $nick, string $reason): stdClass|array|bool 189 | { 190 | return $this->connection->query('user.quit', [ 191 | 'nick' => $nick, 192 | 'reason' => $reason, 193 | ]); 194 | } 195 | 196 | /** 197 | * Kill a user from IRC. Show that the user is forcefully removed. 198 | * 199 | * @return stdClass|array|bool 200 | */ 201 | public function kill(string $nick, string $reason): stdClass|array|bool 202 | { 203 | return $this->connection->query('user.kill', [ 204 | 'nick' => $nick, 205 | 'reason' => $reason, 206 | ]); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/Connection.php: -------------------------------------------------------------------------------- 1 | connection = new WebSocket\Client($uri, [ 25 | 'context' => $context, 26 | 'headers' => [ 27 | 'Authorization' => sprintf('Basic %s', base64_encode($api_login)), 28 | ], 29 | 'timeout' => 10, 30 | ]); 31 | 32 | /* Start the connection now */ 33 | if (isset($options["issuer"])) 34 | { 35 | /* Set issuer and don't wait for the reply (async) */ 36 | $this->query('rpc.set_issuer', ['name' => $options["issuer"]], true); 37 | } else { 38 | /* Ping-pong */ 39 | $this->connection->ping(); 40 | } 41 | } 42 | 43 | /** 44 | * Encode and send a query to the RPC server. 45 | * 46 | * @note I'm not sure on the response type except that it may be either an object or array. 47 | * 48 | * @param string $method 49 | * @param array|null $params 50 | * @param bool $no_wait 51 | * 52 | * @return object|array|bool 53 | * @throws Exception 54 | */ 55 | public function query(string $method, array|null $params = null, $no_wait = false): object|array|bool 56 | { 57 | $id = random_int(1, 99999); 58 | 59 | $rpc = [ 60 | "jsonrpc" => "2.0", 61 | "method" => $method, 62 | "params" => $params, 63 | "id" => $id 64 | ]; 65 | 66 | $json_rpc = json_encode($rpc); 67 | $this->connection->text($json_rpc); 68 | 69 | if ($no_wait) 70 | return true; 71 | 72 | $starttime = time(); 73 | do { 74 | $reply = $this->connection->receive(); 75 | 76 | $reply = json_decode($reply); 77 | 78 | if (property_exists($reply, 'id') && ($id !== $reply->id)) 79 | { 80 | /* This is not our request. Perhaps we are streaming log events 81 | * or this is an asynchronous response to like set_issuer. 82 | * We don't care about that, continue. 83 | * NOTE: This does mean that this event info is "lost" 84 | */ 85 | continue; 86 | } 87 | 88 | if (property_exists($reply, 'result')) { 89 | $this->errno = 0; 90 | $this->error = NULL; 91 | return $reply->result; 92 | } 93 | 94 | if (property_exists($reply, 'error')) { 95 | $this->errno = $reply->error->code; 96 | $this->error = $reply->error->message; 97 | return false; 98 | } 99 | if (time() - $starttime > 10) 100 | throw new Exception('RPC request timed out'); 101 | } while(1); // wait for the reply to OUR request 102 | 103 | /* This should never happen */ 104 | throw new Exception('Invalid JSON-RPC response from UnrealIRCd: not an error and not a result.'); 105 | } 106 | 107 | /** 108 | * Grab and/or wait for next event. Used for log streaming. 109 | * @note This function will return NULL after a 10 second timeout, 110 | * this so the function is not entirely blocking. You can safely 111 | * retry the operation if the return value === NULL. 112 | * 113 | * @return object|array|bool|null 114 | * @throws Exception 115 | */ 116 | public function eventloop(): object|array|bool|null 117 | { 118 | $this->connection->setTimeout(2); /* lower timeout for socket loop */ 119 | $starttime = microtime(true); 120 | try { 121 | $reply = $this->connection->receive(); 122 | } catch (WebSocket\TimeoutException $e) { 123 | if (microtime(true) - $starttime < 1) 124 | { 125 | /* There's some bug in the library: if we 126 | * caught the timeout exception once (so 127 | * harmless) and then later the server gets 128 | * killed or closes the connection otherwise, 129 | * then it will again throw WebSocket\TimeoutException 130 | * even though it has nothing to do with timeouts. 131 | * We detect this by checking if the timeout 132 | * took less than 1 second, then we know for sure 133 | * that it wasn't really a timeout (since the 134 | * timeout is normally 10 seconds). 135 | */ 136 | throw $e; 137 | } 138 | return NULL; 139 | } 140 | 141 | $this->connection->setTimeout(10); /* set timeout back again */ 142 | 143 | $reply = json_decode($reply); 144 | 145 | if (property_exists($reply, 'result')) { 146 | $this->errno = 0; 147 | $this->error = NULL; 148 | return $reply->result; 149 | } 150 | 151 | /* This would be weird */ 152 | if (property_exists($reply, 'error')) { 153 | $this->errno = $reply->error->code; 154 | $this->error = $reply->error->message; 155 | return false; 156 | } 157 | 158 | /* This should never happen */ 159 | throw new Exception('Invalid JSON-RPC data from UnrealIRCd: not an error and not a result.'); 160 | } 161 | 162 | public function rpc(): Rpc 163 | { 164 | return new Rpc($this); 165 | } 166 | 167 | public function stats(): Stats 168 | { 169 | return new Stats($this); 170 | } 171 | 172 | public function user(): User 173 | { 174 | return new User($this); 175 | } 176 | 177 | public function channel(): Channel 178 | { 179 | return new Channel($this); 180 | } 181 | 182 | public function serverban(): ServerBan 183 | { 184 | return new ServerBan($this); 185 | } 186 | 187 | public function spamfilter(): Spamfilter 188 | { 189 | return new Spamfilter($this); 190 | } 191 | 192 | public function nameban(): NameBan 193 | { 194 | return new NameBan($this); 195 | } 196 | 197 | public function server(): Server 198 | { 199 | return new Server($this); 200 | } 201 | 202 | public function serverbanexception(): ServerBanException 203 | { 204 | return new ServerBanException($this); 205 | } 206 | 207 | public function log(): Log 208 | { 209 | return new Log($this); 210 | } 211 | 212 | public function message(): Message 213 | { 214 | return new Message($this); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "76c6847c49cf4f4f5f4aab9ec3674116", 8 | "packages": [ 9 | { 10 | "name": "phrity/net-uri", 11 | "version": "1.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/sirn-se/phrity-net-uri.git", 15 | "reference": "c6ecf127e7c99a41ce04d3cdcda7f51108dd96f7" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/c6ecf127e7c99a41ce04d3cdcda7f51108dd96f7", 20 | "reference": "c6ecf127e7c99a41ce04d3cdcda7f51108dd96f7", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "^7.4|^8.0", 25 | "psr/http-factory": "^1.0", 26 | "psr/http-message": "^1.0" 27 | }, 28 | "require-dev": { 29 | "php-coveralls/php-coveralls": "^2.0", 30 | "phpunit/phpunit": "^9.0", 31 | "squizlabs/php_codesniffer": "^3.0" 32 | }, 33 | "type": "library", 34 | "autoload": { 35 | "psr-4": { 36 | "": "src/" 37 | } 38 | }, 39 | "notification-url": "https://packagist.org/downloads/", 40 | "license": [ 41 | "MIT" 42 | ], 43 | "authors": [ 44 | { 45 | "name": "Sören Jensen", 46 | "email": "sirn@sirn.se", 47 | "homepage": "https://phrity.sirn.se" 48 | } 49 | ], 50 | "description": "PSR-7 Uri and PSR-17 UriFactory implementation", 51 | "homepage": "https://phrity.sirn.se/net-uri", 52 | "keywords": [ 53 | "psr-17", 54 | "psr-7", 55 | "uri", 56 | "uri factory" 57 | ], 58 | "support": { 59 | "issues": "https://github.com/sirn-se/phrity-net-uri/issues", 60 | "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.2.0" 61 | }, 62 | "time": "2022-11-30T07:20:06+00:00" 63 | }, 64 | { 65 | "name": "phrity/util-errorhandler", 66 | "version": "1.0.1", 67 | "source": { 68 | "type": "git", 69 | "url": "https://github.com/sirn-se/phrity-util-errorhandler.git", 70 | "reference": "dc9ac8fb70d733c48a9d9d1eb50f7022172da6bc" 71 | }, 72 | "dist": { 73 | "type": "zip", 74 | "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/dc9ac8fb70d733c48a9d9d1eb50f7022172da6bc", 75 | "reference": "dc9ac8fb70d733c48a9d9d1eb50f7022172da6bc", 76 | "shasum": "" 77 | }, 78 | "require": { 79 | "php": "^7.2|^8.0" 80 | }, 81 | "require-dev": { 82 | "php-coveralls/php-coveralls": "^2.0", 83 | "phpunit/phpunit": "^8.0|^9.0", 84 | "squizlabs/php_codesniffer": "^3.5" 85 | }, 86 | "type": "library", 87 | "autoload": { 88 | "psr-4": { 89 | "": "src/" 90 | } 91 | }, 92 | "notification-url": "https://packagist.org/downloads/", 93 | "license": [ 94 | "MIT" 95 | ], 96 | "authors": [ 97 | { 98 | "name": "Sören Jensen", 99 | "email": "sirn@sirn.se", 100 | "homepage": "https://phrity.sirn.se" 101 | } 102 | ], 103 | "description": "Inline error handler; catch and resolve errors for code block.", 104 | "homepage": "https://phrity.sirn.se/util-errorhandler", 105 | "keywords": [ 106 | "error", 107 | "warning" 108 | ], 109 | "support": { 110 | "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues", 111 | "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.0.1" 112 | }, 113 | "time": "2022-10-27T12:14:42+00:00" 114 | }, 115 | { 116 | "name": "psr/http-factory", 117 | "version": "1.0.2", 118 | "source": { 119 | "type": "git", 120 | "url": "https://github.com/php-fig/http-factory.git", 121 | "reference": "e616d01114759c4c489f93b099585439f795fe35" 122 | }, 123 | "dist": { 124 | "type": "zip", 125 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", 126 | "reference": "e616d01114759c4c489f93b099585439f795fe35", 127 | "shasum": "" 128 | }, 129 | "require": { 130 | "php": ">=7.0.0", 131 | "psr/http-message": "^1.0 || ^2.0" 132 | }, 133 | "type": "library", 134 | "extra": { 135 | "branch-alias": { 136 | "dev-master": "1.0.x-dev" 137 | } 138 | }, 139 | "autoload": { 140 | "psr-4": { 141 | "Psr\\Http\\Message\\": "src/" 142 | } 143 | }, 144 | "notification-url": "https://packagist.org/downloads/", 145 | "license": [ 146 | "MIT" 147 | ], 148 | "authors": [ 149 | { 150 | "name": "PHP-FIG", 151 | "homepage": "https://www.php-fig.org/" 152 | } 153 | ], 154 | "description": "Common interfaces for PSR-7 HTTP message factories", 155 | "keywords": [ 156 | "factory", 157 | "http", 158 | "message", 159 | "psr", 160 | "psr-17", 161 | "psr-7", 162 | "request", 163 | "response" 164 | ], 165 | "support": { 166 | "source": "https://github.com/php-fig/http-factory/tree/1.0.2" 167 | }, 168 | "time": "2023-04-10T20:10:41+00:00" 169 | }, 170 | { 171 | "name": "psr/http-message", 172 | "version": "1.1", 173 | "source": { 174 | "type": "git", 175 | "url": "https://github.com/php-fig/http-message.git", 176 | "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" 177 | }, 178 | "dist": { 179 | "type": "zip", 180 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", 181 | "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", 182 | "shasum": "" 183 | }, 184 | "require": { 185 | "php": "^7.2 || ^8.0" 186 | }, 187 | "type": "library", 188 | "extra": { 189 | "branch-alias": { 190 | "dev-master": "1.1.x-dev" 191 | } 192 | }, 193 | "autoload": { 194 | "psr-4": { 195 | "Psr\\Http\\Message\\": "src/" 196 | } 197 | }, 198 | "notification-url": "https://packagist.org/downloads/", 199 | "license": [ 200 | "MIT" 201 | ], 202 | "authors": [ 203 | { 204 | "name": "PHP-FIG", 205 | "homepage": "http://www.php-fig.org/" 206 | } 207 | ], 208 | "description": "Common interface for HTTP messages", 209 | "homepage": "https://github.com/php-fig/http-message", 210 | "keywords": [ 211 | "http", 212 | "http-message", 213 | "psr", 214 | "psr-7", 215 | "request", 216 | "response" 217 | ], 218 | "support": { 219 | "source": "https://github.com/php-fig/http-message/tree/1.1" 220 | }, 221 | "time": "2023-04-04T09:50:52+00:00" 222 | }, 223 | { 224 | "name": "psr/log", 225 | "version": "3.0.0", 226 | "source": { 227 | "type": "git", 228 | "url": "https://github.com/php-fig/log.git", 229 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" 230 | }, 231 | "dist": { 232 | "type": "zip", 233 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", 234 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", 235 | "shasum": "" 236 | }, 237 | "require": { 238 | "php": ">=8.0.0" 239 | }, 240 | "type": "library", 241 | "extra": { 242 | "branch-alias": { 243 | "dev-master": "3.x-dev" 244 | } 245 | }, 246 | "autoload": { 247 | "psr-4": { 248 | "Psr\\Log\\": "src" 249 | } 250 | }, 251 | "notification-url": "https://packagist.org/downloads/", 252 | "license": [ 253 | "MIT" 254 | ], 255 | "authors": [ 256 | { 257 | "name": "PHP-FIG", 258 | "homepage": "https://www.php-fig.org/" 259 | } 260 | ], 261 | "description": "Common interface for logging libraries", 262 | "homepage": "https://github.com/php-fig/log", 263 | "keywords": [ 264 | "log", 265 | "psr", 266 | "psr-3" 267 | ], 268 | "support": { 269 | "source": "https://github.com/php-fig/log/tree/3.0.0" 270 | }, 271 | "time": "2021-07-14T16:46:02+00:00" 272 | }, 273 | { 274 | "name": "textalk/websocket", 275 | "version": "1.6.3", 276 | "source": { 277 | "type": "git", 278 | "url": "https://github.com/Textalk/websocket-php.git", 279 | "reference": "67de79745b1a357caf812bfc44e0abf481cee012" 280 | }, 281 | "dist": { 282 | "type": "zip", 283 | "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/67de79745b1a357caf812bfc44e0abf481cee012", 284 | "reference": "67de79745b1a357caf812bfc44e0abf481cee012", 285 | "shasum": "" 286 | }, 287 | "require": { 288 | "php": "^7.4 | ^8.0", 289 | "phrity/net-uri": "^1.0", 290 | "phrity/util-errorhandler": "^1.0", 291 | "psr/http-message": "^1.0", 292 | "psr/log": "^1.0 | ^2.0 | ^3.0" 293 | }, 294 | "require-dev": { 295 | "php-coveralls/php-coveralls": "^2.0", 296 | "phpunit/phpunit": "^9.0", 297 | "squizlabs/php_codesniffer": "^3.5" 298 | }, 299 | "type": "library", 300 | "autoload": { 301 | "psr-4": { 302 | "WebSocket\\": "lib" 303 | } 304 | }, 305 | "notification-url": "https://packagist.org/downloads/", 306 | "license": [ 307 | "ISC" 308 | ], 309 | "authors": [ 310 | { 311 | "name": "Fredrik Liljegren" 312 | }, 313 | { 314 | "name": "Sören Jensen" 315 | } 316 | ], 317 | "description": "WebSocket client and server", 318 | "support": { 319 | "issues": "https://github.com/Textalk/websocket-php/issues", 320 | "source": "https://github.com/Textalk/websocket-php/tree/1.6.3" 321 | }, 322 | "time": "2022-11-07T18:59:33+00:00" 323 | } 324 | ], 325 | "packages-dev": [], 326 | "aliases": [], 327 | "minimum-stability": "stable", 328 | "stability-flags": [], 329 | "prefer-stable": false, 330 | "prefer-lowest": false, 331 | "platform": { 332 | "php": "^8.0|^8.1" 333 | }, 334 | "platform-dev": [], 335 | "plugin-api-version": "2.3.0" 336 | } 337 | --------------------------------------------------------------------------------