roomId = $roomId; $this->client = $client; } /** * Set user profile within a room. * * This sets displayname and avatar_url for the logged in user only in a * specific room. It does not change the user's global user profile. * * @param string|null $displayname * @param string|null $avatarUrl * @param string $reason * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function setUserProfile(?string $displayname = null, ?string $avatarUrl = null, string $reason = "Changing room profile information") { $member = $this->api()->getMembership($this->roomId, $this->client->userId()); if ($member['membership'] != 'join') { throw new \Exception("Can't set profile if you have not joined the room."); } if (!$displayname) { $displayname = $member["displayname"]; } if (!$avatarUrl) { $avatarUrl = $member["avatar_url"]; } $this->api()->setMembership( $this->roomId, $this->client->userId(), 'join', $reason, [ "displayname" => $displayname, "avatar_url" => $avatarUrl ] ); } /** * Calculates the display name for a room. * * @return string */ public function displayName() { if ($this->name) { return $this->name; } elseif ($this->canonicalAlias) { return $this->canonicalAlias; } // Member display names without me $members = array_reduce($this->getJoinedMembers(), function (array $all, User $u) { if ($this->client->userId() != $u->userId()) { $all[] = $u->getDisplayName($this); } return $all; }, []); sort($members); switch (count($members)) { case 0: return 'Empty room'; case 1: return $members[0]; case 2: return sprintf("%s and %s", $members[0], $members[1]); default: return sprintf("%s and %d others.", $members[0], count($members)); } } /** * Send a plain text message to the room. * * @param string $text * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendText(string $text) { return $this->api()->sendMessage($this->roomId, $text); } public function getHtmlContent(string $html, ?string $body = null, string $msgType = 'm.text') { return [ 'body' => $body ?: strip_tags($html), 'msgtype' => $msgType, 'format' => "org.matrix.custom.html", 'formatted_body' => $html, ]; } /** * Send an html formatted message. * * @param string $html The html formatted message to be sent. * @param string|null $body The unformatted body of the message to be sent. * @param string $msgType * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendHtml(string $html, ?string $body = null, string $msgType = 'm.text') { $content = $this->getHtmlContent($html, $body, $msgType); return $this->api()->sendMessageEvent($this->roomId, 'm.room.message', $content); } /** * @param string $type * @param array $data * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function setAccountData(string $type, array $data) { return $this->api()->setRoomAccountData($this->client->userId(), $this->roomId, $type, $data); } /** * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function getTags() { return $this->api()->getUserTags($this->client->userId(), $this->roomId); } /** * @param string $tag * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function removeTag(string $tag) { return $this->api()->removeUserTag($this->client->userId(), $this->roomId, $tag); } /** * @param string $tag * @param float|null $order * @param array $content * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function addTag(string $tag, ?float $order = null, array $content = []) { return $this->api()->addUserTag($this->client->userId(), $this->roomId, $tag, $order, $content); } /** * Send an emote (/me style) message to the room. * * @param string $text * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendEmote(string $text) { return $this->api()->sendEmote($this->roomId, $text); } /** * Send a pre-uploaded file to the room. * * See http://matrix.org/docs/spec/r0.4.0/client_server.html#m-file for fileinfo. * * @param string $url The mxc url of the file. * @param string $name The filename of the image. * @param array $fileinfo Extra information about the file * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendFile(string $url, string $name, array $fileinfo) { return $this->api()->sendContent($this->roomId, $url, $name, 'm.file', $fileinfo); } /** * Send a notice (from bot) message to the room. * * @param string $text * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendNotice(string $text) { return $this->api()->sendNotice($this->roomId, $text); } /** * Send a pre-uploaded image to the room. * * See http://matrix.org/docs/spec/r0.0.1/client_server.html#m-image for imageinfo * * @param string $url The mxc url of the image. * @param string $name The filename of the image. * @param array $fileinfo Extra information about the image. * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendImage(string $url, string $name, ?array $fileinfo) { return $this->api()->sendContent($this->roomId, $url, $name, 'm.image', $fileinfo); } /** * Send a location to the room. * See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-location for thumb_info * * @param string $geoUri The geo uri representing the location. * @param string $name Description for the location. * @param array $thumbInfo Metadata about the thumbnail, type ImageInfo. * @param string|null $thumbUrl URL to the thumbnail of the location. * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendLocation(string $geoUri, string $name, ?array $thumbInfo, ?string $thumbUrl = null) { return $this->api()->sendLocation($this->roomId, $geoUri, $name, $thumbUrl, $thumbInfo); } /** * Send a pre-uploaded video to the room. * See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-video for videoinfo * * @param string $url The mxc url of the video. * @param string $name The filename of the video. * @param array $videoinfo Extra information about the video. * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendVideo(string $url, string $name, ?array $videoinfo) { return $this->api()->sendContent($this->roomId, $url, $name, 'm.video', $videoinfo); } /** * Send a pre-uploaded audio to the room. * See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-audio for audioinfo * * @param string $url The mxc url of the video. * @param string $name The filename of the video. * @param array $audioinfo Extra information about the video. * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function sendAudio(string $url, string $name, ?array $audioinfo) { return $this->api()->sendContent($this->roomId, $url, $name, 'm.audio', $audioinfo); } /** * Redacts the message with specified event_id for the given reason. * * See https://matrix.org/docs/spec/r0.0.1/client_server.html#id112 * * @param string $eventId * @param string|null $reason * @return array|string * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws Exceptions\MatrixRequestException */ public function redactMessage(string $eventId, ?string $reason = null) { return $this->api()->redactEvent($this->roomId, $eventId, $reason); } /** * Add a callback handler for events going to this room. * * @param callable $cb (func(room, event)): Callback called when an event arrives. * @param string|null $eventType The event_type to filter for. * @return string Unique id of the listener, can be used to identify the listener. */ public function addListener(callable $cb, ?string $eventType = null) { $listenerId = uniqid(); $this->listeners[] = [ 'uid' => $listenerId, 'callback' => $cb, 'event_type' => $eventType, ]; return $listenerId; } /** * Remove listener with given uid. * * @param string $uid */ public function removeListener(string $uid) { $this->listeners = array_filter($this->listeners, function ($l) use ($uid) { return $l['uid'] != $uid; }); } /** * Add a callback handler for ephemeral events going to this room. * * @param callable $cb (func(room, event)): Callback called when an ephemeral event arrives. * @param string|null $eventType The event_type to filter for. * @return string Unique id of the listener, can be used to identify the listener. */ public function addEphemeralListener(callable $cb, ?string $eventType = null) { $listenerId = uniqid(); $this->ephemeralListeners[] = [ 'uid' => $listenerId, 'callback' => $cb, 'event_type' => $eventType, ]; return $listenerId; } /** * Remove ephemeral listener with given uid. * * @param string $uid */ public function removeEphemeralListener(string $uid) { $this->ephemeralListeners = array_filter($this->ephemeralListeners, function ($l) use ($uid) { return $l['uid'] != $uid; }); } /** * Add a callback handler for state events going to this room. * * @param callable $cb Callback called when an event arrives. * @param string|null $eventType The event_type to filter for. */ public function addStateListener(callable $cb, ?string $eventType = null) { $this->stateListeners[] = [ 'callback' => $cb, 'event_type' => $eventType, ]; } public function putEvent(array $event) { $this->events[] = $event; if (count($this->events) > $this->eventHistoryLimit) { array_pop($this->events); } if (array_key_exists('state_event', $event)) { $this->processStateEvent($event); } // Dispatch for room-specific listeners foreach ($this->listeners as $l) { if (!$l['event_type'] || $l['event_type'] == $event['event_type']) { $l['cb']($this, $event); } } } public function putEphemeralEvent(array $event) { // Dispatch for room-specific listeners foreach ($this->ephemeralListeners as $l) { if (!$l['event_type'] || $l['event_type'] == $event['event_type']) { $l['cb']($this, $event); } } } /** * Get the most recent events for this room. * * @return array */ public function getEvents(): array { return $this->events; } /** * Invite a user to this room. * * @param string $userId * @return bool Whether invitation was sent. * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException */ public function inviteUser(string $userId): bool { try { $this->api()->inviteUser($this->roomId, $userId); } catch (MatrixRequestException $e) { return false; } return true; } /** * Kick a user from this room. * * @param string $userId The matrix user id of a user. * @param string $reason A reason for kicking the user. * @return bool Whether user was kicked. * @throws Exceptions\MatrixException */ public function kickUser(string $userId, string $reason = ''): bool { try { $this->api()->kickUser($this->roomId, $userId, $reason); } catch (MatrixRequestException $e) { return false; } return true; } /** * Ban a user from this room. * * @param string $userId The matrix user id of a user. * @param string $reason A reason for banning the user. * @return bool Whether user was banned. * @throws Exceptions\MatrixException */ public function banUser(string $userId, string $reason = ''): bool { try { $this->api()->banUser($this->roomId, $userId, $reason); } catch (MatrixRequestException $e) { return false; } return true; } /** * Leave the room. * * @return bool Leaving the room was successful. * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException */ public function leave() { try { $this->api()->leaveRoom($this->roomId); // @todo This is not implemented from the original library? // $this->client->forgetRoom($this->roomId); } catch (MatrixRequestException $e) { return false; } return true; } /** * Updates $this->name and returns true if room name has changed. * @return bool * @throws Exceptions\MatrixException */ public function updateRoomName() { try { $response = $this->api()->getRoomName($this->roomId); $newName = array_get($response, 'name', $this->name); $this->name = $newName; if ($this->name != $newName) { $this->name = $newName; return true; } } catch (MatrixRequestException $e) { } return false; } /** * Return True if room name successfully changed. * * @param string $name * @return bool * @throws Exceptions\MatrixException */ public function setRoomName(string $name) { try { $this->api()->setRoomName($this->roomId, $name); $this->name = $name; } catch (MatrixRequestException $e) { return false; } return true; } /** * Send a state event to the room. * * @param string $eventType The type of event that you are sending. * @param array $content An object with the content of the message. * @param string $stateKey Optional. A unique key to identify the state. * @throws Exceptions\MatrixException */ public function sendStateEvent(string $eventType, array $content, string $stateKey = '') { $this->api()->sendStateEvent($this->roomId, $eventType, $content, $stateKey); } /** * Updates $this->topic and returns true if room topic has changed. * * @return bool * @throws Exceptions\MatrixException */ public function updateRoomTopic() { try { $response = $this->api()->getRoomTopic($this->roomId); $oldTopic = $this->topic; $this->topic = array_get($response, 'topic', $this->topic); } catch (MatrixRequestException $e) { return false; } return $oldTopic == $this->topic; } /** * Return True if room topic successfully changed. * * @param string $topic * @return bool * @throws Exceptions\MatrixException */ public function setRoomTopic(string $topic) { try { $this->api()->setRoomTopic($this->roomId, $topic); $this->topic = $topic; } catch (MatrixRequestException $e) { return false; } return true; } /** * Get aliases information from room state. * * @return bool True if the aliases changed, False if not * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException */ public function updateAliases() { try { $response = $this->api()->getRoomState($this->roomId); $oldAliases = $this->aliases; foreach ($response as $chunk) { if ($aliases = array_get($chunk, 'content.aliases')) { $this->aliases = $aliases; } } } catch (MatrixRequestException $e) { return false; } // @todo Validate this logic. return $this->aliases !== $oldAliases; } /** * Add an alias to the room and return True if successful. * * @param string $alias * @return bool * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException */ public function addRoomAlias(string $alias) { try { $this->api()->setRoomAlias($this->roomId, $alias); } catch (MatrixRequestException $e) { return false; } return true; } public function getJoinedMembers() { if ($this->_members) { return array_values($this->_members); } $response = $this->api()->getRoomMembers($this->roomId); foreach ($response['chunk'] as $event) { if (array_get($event, 'event.membership') == 'join') { $userId = $event['state_key']; $this->addMember($userId, array_get($event, 'content.displayname')); } } return array_values($this->_members); } protected function addMember(string $userId, ?string $displayname) { if ($displayname) { $this->membersDisplaynames[$userId] = $displayname; } if (array_key_exists($userId, $this->_members)) { return; } if (array_key_exists($userId, $this->client->users)) { $this->_members[$userId] = $this->client->users[$userId]; return; } $this->_members[$userId] = new User($this->api(), $userId, $displayname); $this->client->users[$userId] = $this->_members[$userId]; } /** * Backfill handling of previous messages. * * @param bool $reverse When false messages will be backfilled in their original * order (old to new), otherwise the order will be reversed (new to old). * @param int $limit Number of messages to go back. * @throws Exceptions\MatrixException * @throws Exceptions\MatrixHttpLibException * @throws MatrixRequestException */ public function backfillPreviousMessages(bool $reverse = false, int $limit = 10) { $res = $this->api()->getRoomMessages($this->roomId, $this->prevBatch, 'b', $limit); $events = $res['chunk']; if (!$reverse) { $events = array_reverse($events); } foreach ($events as $event) { $this->putEvent($event); } } /** * Modify the power level for a subset of users * * @param array $users Power levels to assign to specific users, in the form * {"@name0:host0": 10, "@name1:host1": 100, "@name3:host3", None} * A level of None causes the user to revert to the default level * as specified by users_default. * @param int $userDefault Default power level for users in the room * @return bool * @throws Exceptions\MatrixException */ public function modifyUserPowerLevels(array $users = null, int $userDefault = null) { try { $content = $this->api()->getPowerLevels($this->roomId); if ($userDefault) { $content['user_default'] = $userDefault; } if ($users) { if (array_key_exists('users', $content)) { $content['users'] = array_merge($content['users'], $content); } else { $content['users'] = $users; } // Remove any keys with value null foreach ($content['users'] as $user => $pl) { if (!$pl) { unset($content['users'][$user]); } } } $this->api()->setPowerLevels($this->roomId, $content); } catch (MatrixRequestException $e) { return false; } return true; } /** * Modifies room power level requirements. * * @param array $events Power levels required for sending specific event types, * in the form {"m.room.whatever0": 60, "m.room.whatever2": None}. * Overrides events_default and state_default for the specified * events. A level of None causes the target event to revert to the * default level as specified by events_default or state_default. * @param array $extra Key/value pairs specifying the power levels required for * various actions: * * - events_default(int): Default level for sending message events * - state_default(int): Default level for sending state events * - invite(int): Inviting a user * - redact(int): Redacting an event * - ban(int): Banning a user * - kick(int): Kicking a user * @return bool * @throws Exceptions\MatrixException */ public function modifyRequiredPowerLevels(array $events = [], array $extra = []) { try { $content = $this->api()->getPowerLevels($this->roomId); $content = array_merge($content, $extra); foreach ($content as $k => $v) { if (!$v) { unset($content[$k]); } } if ($events) { if (array_key_exists('events', $content)) { $content["events"] = array_merge($content["events"], $events); } else { $content["events"] = $events; } // Remove any keys with value null foreach ($content['event'] as $event => $pl) { if (!$pl) { unset($content['event'][$event]); } } } $this->api()->setPowerLevels($this->roomId, $content); } catch (MatrixRequestException $e) { return false; } return true; } /** * Set how the room can be joined. * * @param bool $inviteOnly If True, users will have to be invited to join * the room. If False, anyone who knows the room link can join. * @return bool True if successful, False if not * @throws Exceptions\MatrixException */ public function setInviteOnly(bool $inviteOnly) { $joinRule = $inviteOnly ? 'invite' : 'public'; try { $this->api()->setJoinRule($this->roomId, $joinRule); $this->inviteOnly = $inviteOnly; } catch (MatrixRequestException $e) { return false; } return true; } /** * Set whether guests can join the room and return True if successful. * * @param bool $allowGuest * @return bool * @throws Exceptions\MatrixException */ public function setGuestAccess(bool $allowGuest) { $guestAccess = $allowGuest ? 'can_join' : 'forbidden'; try { $this->api()->setGuestAccess($this->roomId, $guestAccess); $this->guestAccess = $allowGuest; } catch (MatrixRequestException $e) { return false; } return true; } /** * Enables encryption in the room. * * NOTE: Once enabled, encryption cannot be disabled. * * @return bool True if successful, False if not * @throws Exceptions\MatrixException */ public function enableEncryption() { try { $this->sendStateEvent('m.room.encryption', ['algorithm' => 'm.megolm.v1.aes-sha2']); $this->encrypted = true; } catch (MatrixRequestException $e) { return false; } return true; } public function processStateEvent(array $stateEvent) { if (!array_key_exists('type', $stateEvent)) { return; } $etype = $stateEvent['type']; $econtent = $stateEvent['content']; $clevel = $this->client->cacheLevel(); // Don't keep track of room state if caching turned off if ($clevel >= Cache::SOME) { switch ($etype) { case 'm.room.name': $this->name = array_get($econtent, 'name'); break; case 'm.room.canonical_alias': $this->canonicalAlias = array_get($econtent, 'alias'); break; case 'm.room.topic': $this->topic = array_get($econtent, 'topic'); break; case 'm.room.aliases': $this->aliases = array_get($econtent, 'aliases'); break; case 'm.room.join_rules': $this->inviteOnly = $econtent["join_rule"] == "invite"; break; case 'm.room.guest_access': $this->guestAccess = $econtent["guest_access"] == "can_join"; break; case 'm.room.encryption': $this->encrypted = array_get($econtent, 'algorithm') ? true : $this->encrypted; break; case 'm.room.member': // tracking room members can be large e.g. #matrix:matrix.org if ($clevel == Cache::ALL) { if ($econtent['membership'] == 'join') { $userId = $stateEvent['state_key']; $this->addMember($userId, array_get($econtent, 'displayname')); } elseif (in_array($econtent["membership"], ["leave", "kick", "invite"])) { unset($this->_members[array_get($stateEvent, 'state_key')]); } } break; } } foreach ($this->stateListeners as $listener) { if (!$listener['event_type'] || $listener['event_type'] == $stateEvent['type']) { $listener['cb']($stateEvent); } } } public function getMembersDisplayNames(): array { return $this->membersDisplaynames; } protected function api(): MatrixHttpApi { return $this->client->api(); } }