├── .gitignore ├── tests ├── TestCase.php ├── UsersTest.php ├── WebHooksTest.php ├── UnitSupport.php └── RoomsTest.php ├── src ├── Resources │ ├── ResourceInterface.php │ ├── Groups.php │ ├── AbstractResource.php │ ├── Users.php │ ├── Messages.php │ └── Rooms.php ├── Adapters │ ├── AdapterInterface.php │ ├── SyncAdapterInterface.php │ ├── StreamAdapterInterface.php │ ├── AbstractClient.php │ ├── HttpAdapter.php │ └── StreamAdapter.php ├── Support │ ├── Observer.php │ └── JsonStream.php ├── WebHook.php ├── Route.php └── Client.php ├── .travis.yml ├── phpunit.xml ├── composer.json ├── .scrutinizer.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertInternalType('array', $this->client()->users->current()); 19 | $this->assertEquals($this->userId(), $this->client()->users->current()['id'] ?? null); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Adapters/SyncAdapterInterface.php: -------------------------------------------------------------------------------- 1 | client() 22 | ->notify($this->debugHookId()) 23 | ->error('Travis CI Unit error notification test'); 24 | } 25 | 26 | /** 27 | * @return void 28 | */ 29 | public function testNormalNotify() 30 | { 31 | $this->client() 32 | ->notify($this->debugHookId()) 33 | ->info('Travis CI Unit info notification test'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | fast_finish: true 5 | include: 6 | - php: 7.0 7 | env: setup=lowest 8 | - php: 7.0 9 | env: setup=highest 10 | - php: 7.1 11 | env: setup=lowest 12 | - php: 7.1 13 | env: setup=highest 14 | - php: 7.2 15 | env: setup=lowest 16 | - php: 7.2 17 | env: setup=highest 18 | - php: nightly 19 | env: setup=lowest 20 | - php: nightly 21 | env: setup=highest 22 | allow_failures: 23 | - php: nightly 24 | 25 | sudo: false 26 | 27 | cache: 28 | directories: 29 | - $HOME/.composer/cache 30 | 31 | before_script: 32 | - composer self-update -q 33 | - if [ -z "$setup" ]; then composer install; fi; 34 | - if [ "$setup" = "lowest" ]; then composer update --prefer-lowest --no-interaction --prefer-dist --no-suggest; fi; 35 | - if [ "$setup" = "highest" ]; then composer update --no-interaction --prefer-dist --no-suggest; fi; 36 | 37 | script: vendor/bin/phpunit 38 | -------------------------------------------------------------------------------- /src/Support/Observer.php: -------------------------------------------------------------------------------- 1 | subscribers[] = $closure; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * @param $data 36 | * @return void 37 | */ 38 | public function fire($data) 39 | { 40 | foreach ($this->subscribers as $subscriber) { 41 | $subscriber($data); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | src 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serafim/gitter-api", 3 | "description": "Gitter async api", 4 | "keywords": [ 5 | "gitter", 6 | "api", 7 | "async", 8 | "react", 9 | "client" 10 | ], 11 | "license": "MIT", 12 | "type": "library", 13 | "authors": [ 14 | { 15 | "name": "Nesmeyanov Kirill", 16 | "email": "nesk@xakep.ru" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.0", 21 | "ext-sockets": "*", 22 | "ext-json": "*", 23 | "ext-mbstring": "*", 24 | "guzzlehttp/guzzle": "~6.0", 25 | "react/event-loop": "~0.4", 26 | "react/dns": "~0.4", 27 | "clue/buzz-react": "~1.0", 28 | "psr/log": "~1.0", 29 | "serafim/evacuator": "~3.0" 30 | }, 31 | "require-dev": { 32 | "monolog/monolog": "~1.0", 33 | "phpunit/phpunit": "~5.5", 34 | "phpdocumentor/phpdocumentor": "^2.0" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Gitter\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Gitter\\Tests\\": "tests" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Adapters/AbstractClient.php: -------------------------------------------------------------------------------- 1 | 'application/json', 33 | 'Content-Type' => 'application/json', 34 | 'Authorization' => sprintf('Bearer %s', $client->token()) 35 | ]; 36 | } 37 | 38 | /** 39 | * @param array $options 40 | * @return AdapterInterface 41 | */ 42 | public function setOptions(array $options = []): AdapterInterface 43 | { 44 | $this->options = array_merge_recursive($this->options, $options); 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * @param Client $client 51 | * @param string $message 52 | */ 53 | protected function debugLog(Client $client, string $message) 54 | { 55 | if ($client->logger !== null) { 56 | $client->logger->debug($message); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: 3 | - "src/*" 4 | build: 5 | environment: 6 | php: 7 | version: 7.0.8 8 | checks: 9 | php: 10 | fix_php_opening_tag: true 11 | remove_php_closing_tag: true 12 | one_class_per_file: true 13 | side_effects_or_types: true 14 | no_mixed_inline_html: true 15 | require_braces_around_control_structures: true 16 | php5_style_constructor: true 17 | no_global_keyword: true 18 | avoid_usage_of_logical_operators: true 19 | psr2_class_declaration: true 20 | no_underscore_prefix_in_properties: true 21 | no_underscore_prefix_in_methods: true 22 | blank_line_after_namespace_declaration: true 23 | single_namespace_per_use: true 24 | psr2_switch_declaration: true 25 | psr2_control_structure_declaration: true 26 | avoid_superglobals: true 27 | security_vulnerabilities: true 28 | no_exit: true 29 | filter: { } 30 | coding_style: 31 | php: 32 | braces: 33 | classes_functions: 34 | class: new-line 35 | function: new-line 36 | closure: end-of-line 37 | if: 38 | opening: end-of-line 39 | for: 40 | opening: end-of-line 41 | while: 42 | opening: end-of-line 43 | do_while: 44 | opening: end-of-line 45 | switch: 46 | opening: end-of-line 47 | try: 48 | opening: end-of-line 49 | upper_lower_casing: 50 | keywords: 51 | general: lower 52 | constants: 53 | true_false_null: lower -------------------------------------------------------------------------------- /tests/UnitSupport.php: -------------------------------------------------------------------------------- 1 | client()->authId(); 46 | } 47 | 48 | /** 49 | * @return Client 50 | * @throws \InvalidArgumentException 51 | */ 52 | public function client() 53 | { 54 | $logger = new Logger('phpunit'); 55 | $logger->pushHandler(new StreamHandler(STDOUT, Logger::DEBUG)); 56 | 57 | $client = new Client($this->token(), $logger); 58 | 59 | 60 | if (0 === stripos(PHP_OS, 'WIN')) { // Windows SSL bugfix 61 | $client->viaHttp()->setOptions(['verify' => false]); 62 | } 63 | 64 | return $client; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function token(): string 71 | { 72 | return $_ENV['token'] ?? $_SERVER['token'] ?? ''; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Resources/Groups.php: -------------------------------------------------------------------------------- 1 | fetch(Route::get('groups')); 45 | } 46 | 47 | /** 48 | * List of rooms nested under the specified group. 49 | * 50 | * @param string $groupId 51 | * @return array 52 | * @throws \Throwable 53 | * @throws \Exception 54 | * @throws \InvalidArgumentException 55 | */ 56 | public function rooms(string $groupId): array 57 | { 58 | return $this->fetch(Route::get('groups/{groupId}/rooms')->with('groupId', $groupId)); 59 | } 60 | 61 | /** 62 | * @return \Generator 63 | * @throws \Throwable 64 | * @throws \Exception 65 | * @throws \InvalidArgumentException 66 | */ 67 | public function getIterator(): \Generator 68 | { 69 | $groups = $this->all(); 70 | 71 | foreach ($groups as $i => $group) { 72 | yield $i => $group; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Resources/AbstractResource.php: -------------------------------------------------------------------------------- 1 | client = $client; 39 | } 40 | 41 | /** 42 | * @return Client 43 | */ 44 | protected function client(): Client 45 | { 46 | return $this->client; 47 | } 48 | 49 | /** 50 | * @param Route $route 51 | * @return array|mixed 52 | * @throws \Exception 53 | * @throws \Throwable 54 | * @throws \InvalidArgumentException 55 | */ 56 | protected function fetch(Route $route): array 57 | { 58 | $rescue = (new Evacuator(function () use ($route) { 59 | return (array)$this->viaHttp()->request($route); 60 | })) 61 | // If response has status code 4xx 62 | ->onError(function (ClientException $e) { 63 | $this->client->logger->error(\get_class($e) . ' ' . $e->getMessage()); 64 | 65 | switch ($e->getResponse()->getStatusCode()) { 66 | case 429: // 429 Too Many Requests 67 | sleep(2); 68 | return null; 69 | } 70 | 71 | throw $e; 72 | }) 73 | // Other 74 | ->onError(function (\Exception $e) { 75 | $this->client->logger->error($e->getMessage()); 76 | throw $e; 77 | }) 78 | ->retries($this->client->getRetriesCount()) 79 | ->invoke(); 80 | 81 | return $rescue; 82 | } 83 | 84 | /** 85 | * @param Route $route 86 | * @return Observer 87 | * @throws \Throwable 88 | * @throws \InvalidArgumentException 89 | */ 90 | protected function stream(Route $route): Observer 91 | { 92 | return (new Evacuator(function() use ($route) { 93 | return $this->viaStream()->request($route); 94 | })) 95 | ->onError(function (\Exception $e) { 96 | $this->client->logger->error($e->getMessage()); 97 | }) 98 | ->retries($this->client->getRetriesCount()) 99 | ->invoke(); 100 | } 101 | 102 | /** 103 | * @return AdapterInterface|HttpAdapter 104 | * @throws \InvalidArgumentException 105 | */ 106 | protected function viaHttp(): HttpAdapter 107 | { 108 | return $this->client->viaHttp(); 109 | } 110 | 111 | /** 112 | * @return AdapterInterface|StreamAdapter 113 | */ 114 | protected function viaStream(): StreamAdapter 115 | { 116 | return $this->client->viaStream(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Adapters/HttpAdapter.php: -------------------------------------------------------------------------------- 1 | client = $client; 42 | $this->options = $this->injectToken($client, []); 43 | $this->guzzle = new Guzzle($this->options); 44 | } 45 | 46 | /** 47 | * @param Client $client 48 | * @param array $options 49 | * @return array 50 | */ 51 | private function injectToken(Client $client, array $options): array 52 | { 53 | $options['headers'] = array_merge( 54 | $options['headers'] ?? [], 55 | $this->buildHeaders($client) 56 | ); 57 | 58 | return $options; 59 | } 60 | 61 | /** 62 | * @param Route $route 63 | * @return array 64 | * @throws \RuntimeException 65 | * @throws \InvalidArgumentException 66 | * @throws \GuzzleHttp\Exception\GuzzleException 67 | */ 68 | public function request(Route $route): array 69 | { 70 | list($method, $uri) = [$route->method(), $route->build()]; 71 | $options = $this->prepareRequestOptions($route); 72 | 73 | // Log request 74 | $this->debugLog($this->client, ' -> ' . $method . ' ' . $uri); 75 | if ($options['body'] ?? false) { 76 | $this->debugLog($this->client, ' -> body ' . $options['body']); 77 | } 78 | 79 | // End log request 80 | $response = $this->guzzle->request($method, $uri, $options); 81 | 82 | // Log response 83 | $this->debugLog($this->client, ' <- ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase()); 84 | $this->debugLog($this->client, ' <- ' . (string)$response->getBody()); 85 | // End log response 86 | 87 | return $this->parseResponse($response); 88 | } 89 | 90 | /** 91 | * @param Route $route 92 | * @return array 93 | */ 94 | private function prepareRequestOptions(Route $route): array 95 | { 96 | $options = []; 97 | 98 | if ($route->method() !== 'GET' && $route->getBody() !== null) { 99 | $options['body'] = $route->getBody(); 100 | } 101 | 102 | return array_merge($this->options, $options); 103 | } 104 | 105 | /** 106 | * @param ResponseInterface $response 107 | * @return array 108 | * @throws \RuntimeException 109 | */ 110 | private function parseResponse(ResponseInterface $response): array 111 | { 112 | $data = json_decode((string)$response->getBody(), true); 113 | 114 | if (json_last_error() !== JSON_ERROR_NONE) { 115 | throw new \RuntimeException(json_last_error_msg()); 116 | } 117 | 118 | return $data; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Adapters/StreamAdapter.php: -------------------------------------------------------------------------------- 1 | client = $client; 55 | $this->loop = $loop; 56 | $this->browser = new Browser($loop); 57 | } 58 | 59 | /** 60 | * @return LoopInterface 61 | */ 62 | public function getEventLoop(): LoopInterface 63 | { 64 | return $this->loop; 65 | } 66 | 67 | /** 68 | * @param Route $route 69 | * @return Observer 70 | * @throws \InvalidArgumentException 71 | */ 72 | public function request(Route $route): Observer 73 | { 74 | $observer = new Observer(); 75 | 76 | $this->promise($route)->then(function (ResponseInterface $response) use ($observer) { 77 | $this->onConnect($response, $observer); 78 | }); 79 | 80 | return $observer; 81 | } 82 | 83 | /** 84 | * @param Route $route 85 | * @return Promise 86 | * @throws \InvalidArgumentException 87 | */ 88 | private function promise(Route $route): Promise 89 | { 90 | list($method, $uri) = [$route->method(), $route->build()]; 91 | 92 | // Log request 93 | $this->debugLog($this->client, ' -> ' . $method . ' ' . $uri); 94 | 95 | return $this->browser 96 | ->withOptions(['streaming' => true]) 97 | ->{strtolower($method)}($route->build(), $this->buildHeaders($this->client)); 98 | } 99 | 100 | /** 101 | * @param ResponseInterface $response 102 | * @param Observer $observer 103 | */ 104 | private function onConnect(ResponseInterface $response, Observer $observer) 105 | { 106 | $json = new JsonStream(); 107 | 108 | // Log response 109 | $this->debugLog($this->client, ' <- ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase()); 110 | 111 | /* @var $body ReadableStreamInterface */ 112 | $body = $response->getBody(); 113 | 114 | $body->on('data', function ($chunk) use ($json, $observer) { 115 | // Log response chunk 116 | $this->debugLog($this->client, ' <- ' . $chunk); 117 | 118 | $json->push($chunk, function ($object) use ($observer) { 119 | $observer->fire($object); 120 | }); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/WebHook.php: -------------------------------------------------------------------------------- 1 | hookId = $hookId; 49 | 50 | if (!$this->hookId) { 51 | throw new \InvalidArgumentException('Invalid Hook Id'); 52 | } 53 | } 54 | 55 | /** 56 | * @param string $level 57 | * @return WebHook 58 | */ 59 | public function withLevel(string $level): WebHook 60 | { 61 | $this->level = $level; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @param string $message 68 | * @return array 69 | * @throws \Throwable 70 | * @throws \GuzzleHttp\Exception\ClientException 71 | * @throws \Exception 72 | * @throws \RuntimeException 73 | * @throws \InvalidArgumentException 74 | */ 75 | public function error(string $message): array 76 | { 77 | return $this->withLevel(static::HOOK_LEVEL_ERROR)->send($message); 78 | } 79 | 80 | /** 81 | * @param string $message 82 | * @return array 83 | * @throws \Throwable 84 | * @throws \GuzzleHttp\Exception\ClientException 85 | * @throws \Exception 86 | * @throws \RuntimeException 87 | * @throws \InvalidArgumentException 88 | */ 89 | public function info(string $message): array 90 | { 91 | return $this->withLevel(static::HOOK_LEVEL_INFO)->send($message); 92 | } 93 | 94 | /** 95 | * @param string $type 96 | * @return $this|WebHook 97 | */ 98 | public function withIcon(string $type): WebHook 99 | { 100 | $this->icon = $type; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * @param string $message 107 | * @return array 108 | * @throws \Throwable 109 | * @throws \GuzzleHttp\Exception\ClientException 110 | * @throws \Exception 111 | * @throws \RuntimeException 112 | * @throws \InvalidArgumentException 113 | */ 114 | public function send(string $message): array 115 | { 116 | return $this->fetch($this->buildRoute($message)); 117 | } 118 | 119 | /** 120 | * @param string $message 121 | * @return Route 122 | */ 123 | private function buildRoute(string $message): Route 124 | { 125 | $icon = $this->level === static::HOOK_LEVEL_ERROR ? 'error' : $this->level; 126 | 127 | $route = Route::post($this->hookId) 128 | ->toWebhook() 129 | ->withBody('message', $message) 130 | ->withBody('errorLevel', $this->level); 131 | 132 | if ($this->icon !== null) { 133 | $route->withBody('icon', $icon); 134 | } 135 | 136 | return $route; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Support/JsonStream.php: -------------------------------------------------------------------------------- 1 | bufferSize = $bufferSize; 63 | } 64 | 65 | /** 66 | * @param LoggerInterface $logger 67 | */ 68 | public function setLogger(LoggerInterface $logger) 69 | { 70 | $this->logger = $logger; 71 | } 72 | 73 | /** 74 | * @throws \OutOfBoundsException 75 | */ 76 | private function checkSize() 77 | { 78 | if ($this->bufferSize > 0 && \strlen($this->buffer) > $this->bufferSize) { 79 | throw new \OutOfBoundsException( 80 | sprintf('Memory leak detected. Buffer size out of %s bytes', $this->bufferSize) 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * @param string $data 87 | * @return bool 88 | */ 89 | private function shouldStarts(string $data): bool 90 | { 91 | return $this->buffer === '' && \in_array($data[0], self::JSON_STARTS_WITH, true);; 92 | } 93 | 94 | /** 95 | * @param string $data 96 | * @param \Closure|null $callback 97 | * @return mixed|null 98 | */ 99 | public function push(string $data, \Closure $callback = null) 100 | { 101 | // Buffer are empty and input starts with "[" or "{" 102 | $canBeBuffered = $this->shouldStarts($data); 103 | 104 | // Data can be starts buffering 105 | if ($canBeBuffered) { 106 | $this->endsWith = $data[0] === '[' ? ']' : '}'; 107 | } 108 | 109 | // Add chunks for non empty buffer 110 | if ($canBeBuffered || $this->buffer !== '') { 111 | $this->buffer .= $data; 112 | 113 | if ($this->isEnds($data)) { 114 | $object = \json_decode($this->buffer, true); 115 | 116 | if (\json_last_error() === JSON_ERROR_NONE) { 117 | $this->buffer = ''; 118 | 119 | if ($callback !== null) { 120 | $callback($object); 121 | } 122 | 123 | return $object; 124 | } 125 | } 126 | } 127 | 128 | return null; 129 | } 130 | 131 | /** 132 | * @param string $text 133 | * @return bool 134 | */ 135 | private function isEnds(string $text): bool 136 | { 137 | $text = \trim($text); 138 | 139 | return $text && $text[\strlen($text) - 1] === $this->endsWith; 140 | } 141 | 142 | /** 143 | * @param StreamInterface $stream 144 | * @return \Generator 145 | * @throws \OutOfBoundsException 146 | * @throws \RuntimeException 147 | */ 148 | public function stream(StreamInterface $stream): \Generator 149 | { 150 | while (!$stream->eof()) { 151 | $data = $stream->read($this->chunkSize); 152 | 153 | $output = $this->push($data); 154 | if ($output !== null) { 155 | yield $output; 156 | } 157 | 158 | $this->checkSize(); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Route.php: -------------------------------------------------------------------------------- 1 | route($route); 61 | $this->method($method); 62 | $this->toApi(); 63 | } 64 | 65 | /** 66 | * @param string $url 67 | * @return $this|Route 68 | */ 69 | public function to(string $url): Route 70 | { 71 | $this->url = $url; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @return Route 78 | */ 79 | public function toApi(): Route 80 | { 81 | return $this->to('https://api.gitter.im/v1/'); 82 | } 83 | 84 | /** 85 | * @return Route 86 | */ 87 | public function toStream(): Route 88 | { 89 | return $this->to('https://stream.gitter.im/v1/'); 90 | } 91 | 92 | /** 93 | * @return Route 94 | */ 95 | public function toSocket(): Route 96 | { 97 | return $this->to('wss://ws.gitter.im/'); 98 | } 99 | 100 | /** 101 | * @return Route 102 | */ 103 | public function toFaye(): Route 104 | { 105 | return $this->to('https://gitter.im/api/v1/'); 106 | } 107 | 108 | /** 109 | * @return Route 110 | */ 111 | public function toWebhook(): Route 112 | { 113 | return $this->to('https://webhooks.gitter.im/e/'); 114 | } 115 | 116 | /** 117 | * @param string|null $route 118 | * @return string 119 | */ 120 | public function route(string $route = null): string 121 | { 122 | if ($route !== null) { 123 | $this->route = $route; 124 | } 125 | 126 | return $this->route; 127 | } 128 | 129 | /** 130 | * @param string|null $method 131 | * @return string 132 | */ 133 | public function method(string $method = null): string 134 | { 135 | if ($method !== null) { 136 | $this->method = \strtoupper($method); 137 | } 138 | 139 | return $this->method; 140 | } 141 | 142 | /** 143 | * @param string $parameter 144 | * @param string|int $value 145 | * @return $this|Route 146 | */ 147 | public function with(string $parameter, $value): Route 148 | { 149 | $this->parameters[$parameter] = (string)$value; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @param array $parameters 156 | * @return $this|Route 157 | */ 158 | public function withMany(array $parameters): Route 159 | { 160 | foreach ($parameters as $parameter => $value) { 161 | $this->with($parameter, $value); 162 | } 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * @param string $field 169 | * @param $value 170 | * @return Route|$this 171 | */ 172 | public function withBody(string $field, $value): Route 173 | { 174 | $this->body[$field] = $value; 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * @return string|null 181 | */ 182 | public function getBody() 183 | { 184 | if (\count($this->body)) { 185 | return \json_encode($this->body); 186 | } 187 | 188 | return null; 189 | } 190 | 191 | /** 192 | * @param array $parameters 193 | * @return string 194 | * @throws \InvalidArgumentException 195 | */ 196 | public function build(array $parameters = []): string 197 | { 198 | if ($this->url === null) { 199 | throw new \InvalidArgumentException('Can not build route string. URL does not set'); 200 | } 201 | 202 | $route = $this->route; 203 | $query = $parameters = array_merge($this->parameters, $parameters); 204 | 205 | foreach ($parameters as $parameter => $value) { 206 | $updatedRoute = str_replace(sprintf('{%s}', $parameter), $value, $route); 207 | 208 | if ($updatedRoute !== $route) { 209 | unset($query[$parameter]); 210 | } 211 | 212 | $route = $updatedRoute; 213 | } 214 | 215 | return $this->url . $route . '?' . http_build_query($query); 216 | } 217 | 218 | /** 219 | * @param string $name 220 | * @param array $arguments 221 | * @return static 222 | */ 223 | public static function __callStatic(string $name, array $arguments = []) 224 | { 225 | return new static($arguments[0] ?? '', $name); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/Resources/Users.php: -------------------------------------------------------------------------------- 1 | currentUser === null) { 44 | $users = $this->fetch(Route::get('user')); 45 | 46 | if (isset($users[0])) { 47 | $this->currentUser = $users[0]; 48 | } else { 49 | throw new \RuntimeException('Can not fetch current user'); 50 | } 51 | } 52 | 53 | return $this->currentUser; 54 | } 55 | 56 | /** 57 | * @return string 58 | * @throws \Throwable 59 | * @throws \RuntimeException 60 | * @throws \Exception 61 | * @throws \InvalidArgumentException 62 | */ 63 | public function currentUserId(): string 64 | { 65 | return (string)($this->current()['id'] ?? null); 66 | } 67 | 68 | /** 69 | * List of Rooms the user is part of. 70 | * 71 | * @param string|null $userId User id 72 | * @return array 73 | * @throws \RuntimeException 74 | * @throws \InvalidArgumentException 75 | * @throws \Throwable 76 | * @throws \Exception 77 | */ 78 | public function rooms(string $userId = null): array 79 | { 80 | return $this->fetch( 81 | Route::get('user/{userId}/rooms') 82 | ->with('userId', $userId ?? $this->currentUserId()) 83 | ); 84 | } 85 | 86 | /** 87 | * You can retrieve unread items and mentions using the following endpoint. 88 | * 89 | * @param string $roomId 90 | * @param string|null $userId 91 | * @return array 92 | * @throws \RuntimeException 93 | * @throws \InvalidArgumentException 94 | * @throws \Throwable 95 | * @throws \Exception 96 | */ 97 | public function unreadItems(string $roomId, string $userId = null): array 98 | { 99 | return $this->fetch( 100 | Route::get('user/{userId}/rooms/{roomId}/unreadItems') 101 | ->withMany(['userId' => $userId ?? $this->currentUserId(), 'roomId' => $roomId]) 102 | ); 103 | } 104 | 105 | /** 106 | * There is an additional endpoint nested under rooms that you can use to mark chat messages as read 107 | * 108 | * @param string $roomId 109 | * @param array $messageIds 110 | * @param string|null $userId 111 | * @return array 112 | * @throws \RuntimeException 113 | * @throws \InvalidArgumentException 114 | * @throws \Throwable 115 | * @throws \Exception 116 | */ 117 | public function markAsRead(string $roomId, array $messageIds, string $userId = null): array 118 | { 119 | return $this->fetch( 120 | Route::post('user/{userId}/rooms/{roomId}/unreadItems') 121 | ->withMany(['userId' => $userId ?? $this->currentUserId(), 'roomId' => $roomId]) 122 | ->withBody('chat', $messageIds) 123 | ); 124 | } 125 | 126 | /** 127 | * List of the user's GitHub Organisations and their respective Room if available. 128 | * 129 | * @param string|null $userId 130 | * @return array 131 | * @throws \RuntimeException 132 | * @throws \InvalidArgumentException 133 | * @throws \Throwable 134 | * @throws \Exception 135 | */ 136 | public function orgs(string $userId = null): array 137 | { 138 | return $this->fetch( 139 | Route::get('user/{userId}/orgs') 140 | ->with('userId', $userId ?? $this->currentUserId()) 141 | ); 142 | } 143 | 144 | /** 145 | * List of the user's GitHub Repositories and their respective Room if available. 146 | * 147 | * Note: It'll return private repositories if the current user has granted Gitter privileges to access them. 148 | * 149 | * @param string|null $userId 150 | * @return array 151 | * @throws \RuntimeException 152 | * @throws \InvalidArgumentException 153 | * @throws \Throwable 154 | * @throws \Exception 155 | */ 156 | public function repos(string $userId = null): array 157 | { 158 | return $this->fetch( 159 | Route::get('user/{userId}/repos') 160 | ->with('userId', $userId ?? $this->currentUserId()) 161 | ); 162 | } 163 | 164 | /** 165 | * List of Gitter channels nested under the current user. 166 | * 167 | * @param string|null $userId 168 | * @return array 169 | * @throws \RuntimeException 170 | * @throws \InvalidArgumentException 171 | * @throws \Throwable 172 | * @throws \Exception 173 | */ 174 | public function channels(string $userId = null): array 175 | { 176 | return $this->fetch( 177 | Route::get('user/{userId}/channels') 178 | ->with('userId', $userId ?? $this->currentUserId()) 179 | ); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tests/RoomsTest.php: -------------------------------------------------------------------------------- 1 | rooms = $this->client()->rooms; 34 | 35 | $this->assertInternalType('array', $this->rooms->join($this->debugRoomId())); 36 | } 37 | 38 | /** 39 | * @return void 40 | * @throws \InvalidArgumentException 41 | * @throws \RuntimeException 42 | * @throws \Throwable 43 | */ 44 | public function testAllAction() 45 | { 46 | $this->assertInternalType('array', $this->rooms->all()); 47 | } 48 | 49 | /** 50 | * @return void 51 | * @throws \InvalidArgumentException 52 | * @throws \RuntimeException 53 | * @throws \Throwable 54 | */ 55 | public function testLeaveAndJoinAction() 56 | { 57 | $this->assertInternalType('array', $this->rooms->leave($this->debugRoomId())); 58 | 59 | $this->assertInternalType('array', $this->rooms->join($this->debugRoomId())); 60 | } 61 | 62 | /** 63 | * @return void 64 | * @throws \InvalidArgumentException 65 | * @throws \RuntimeException 66 | * @throws \Throwable 67 | */ 68 | public function testFindByNameAction() 69 | { 70 | $this->assertInternalType('array', $this->rooms->findByName('KarmaBot/KarmaTest')); 71 | } 72 | 73 | /** 74 | * @return void 75 | * @throws \InvalidArgumentException 76 | * @throws \RuntimeException 77 | * @throws \Throwable 78 | */ 79 | public function testKickAction() 80 | { 81 | $this->assertInternalType('array', $this->rooms->kick($this->debugRoomId(), $this->userId())); 82 | 83 | $this->assertInternalType('array', $this->rooms->join($this->debugRoomId())); 84 | } 85 | 86 | /** 87 | * @return void 88 | * @throws \InvalidArgumentException 89 | * @throws \RuntimeException 90 | * @throws \Throwable 91 | */ 92 | public function testUpdateTopicAction() 93 | { 94 | $topic = md5(microtime(true) . random_int(0, PHP_INT_MAX)); 95 | 96 | try { 97 | $response = $this->rooms->topic($this->debugRoomId(), $topic); 98 | 99 | $this->assertInternalType('array', $response); 100 | $this->assertArrayHasKey('topic', $response); 101 | $this->assertEquals($topic, $response['topic']); 102 | 103 | } catch (ClientException $exception) { 104 | $this->assertEquals(403, $exception->getCode()); 105 | } 106 | } 107 | 108 | /** 109 | * @return void 110 | * @throws \InvalidArgumentException 111 | * @throws \RuntimeException 112 | * @throws \Throwable 113 | */ 114 | public function testUpdateIndexAction() 115 | { 116 | try { 117 | $response = $this->rooms->searchIndex($this->debugRoomId(), true); 118 | $this->assertInternalType('array', $response); 119 | 120 | $response = $this->rooms->searchIndex($this->debugRoomId(), false); 121 | $this->assertInternalType('array', $response); 122 | 123 | } catch (ClientException $exception) { 124 | $this->assertEquals(403, $exception->getCode()); 125 | } 126 | } 127 | 128 | /** 129 | * @return void 130 | * @throws \InvalidArgumentException 131 | * @throws \RuntimeException 132 | * @throws \Throwable 133 | */ 134 | public function testUpdateTagsAction() 135 | { 136 | try { 137 | $response = $this->rooms->tags($this->debugRoomId(), ['a', 'b', 'asdasd']); 138 | $this->assertInternalType('array', $response); 139 | 140 | } catch (ClientException $exception) { 141 | $this->assertEquals(403, $exception->getCode()); 142 | } 143 | } 144 | 145 | /** 146 | * @return void 147 | */ 148 | public function testDeleteRoomAction() 149 | { 150 | // $this->rooms->delete(...) 151 | } 152 | 153 | /** 154 | * @return void 155 | * @throws \InvalidArgumentException 156 | * @throws \RuntimeException 157 | * @throws \Throwable 158 | */ 159 | public function testUsersListAction() 160 | { 161 | $response = $this->rooms->users($this->debugRoomId()); 162 | $this->assertInstanceOf(\Traversable::class, $response); 163 | } 164 | 165 | /** 166 | * @return void 167 | * @throws \InvalidArgumentException 168 | * @throws \Throwable 169 | */ 170 | public function testMessagesEvent() 171 | { 172 | $client = $this->client(); 173 | 174 | $message = []; 175 | 176 | $client->rooms->messages($this->debugRoomId())->subscribe(function ($data) use (&$message, $client) { 177 | $message = $data; 178 | $client->loop()->stop(); 179 | }); 180 | 181 | $client->loop->addTimer(1, function () use ($client) { 182 | $client->messages->create($this->debugRoomId(), 'DEBUG'); 183 | }); 184 | 185 | $client->loop->addTimer(10, function () { 186 | throw new TimeoutException('Test execution timeout'); 187 | }); 188 | 189 | $client->connect(); 190 | 191 | $this->assertEquals('DEBUG', $message['text'] ?? ''); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Resources/Messages.php: -------------------------------------------------------------------------------- 1 | allBeforeId($roomId, null, $query); 47 | } 48 | 49 | /** 50 | * Returns all messages before target message id. 51 | * 52 | * @param string $roomId 53 | * @param string|null $beforeId 54 | * @param string|null $query 55 | * @return \Generator 56 | * @throws \Exception 57 | * @throws \InvalidArgumentException 58 | * @throws \Throwable 59 | */ 60 | public function allBeforeId(string $roomId, string $beforeId = null, string $query = null) 61 | { 62 | $limit = 100; 63 | $route = $this->routeForMessagesIterator($roomId, $limit, $query); 64 | 65 | do { 66 | if ($beforeId !== null) { 67 | $route->with('beforeId', $beforeId); 68 | } 69 | 70 | $response = \array_reverse($this->fetch($route)); 71 | 72 | foreach ($response as $message) { 73 | yield $message; 74 | } 75 | 76 | $beforeId = \count($response) > 0 ? \end($response)['id'] : null; 77 | } while (\count($response) >= $limit); 78 | } 79 | 80 | /** 81 | * @param string $roomId 82 | * @param int $limit 83 | * @param string|null $query 84 | * @return Route 85 | */ 86 | private function routeForMessagesIterator(string $roomId, int $limit, string $query = null): Route 87 | { 88 | $route = Route::get('rooms/{roomId}/chatMessages') 89 | ->with('roomId', $roomId) 90 | ->with('limit', (string)$limit); 91 | 92 | if ($query !== null) { 93 | $route->with('q', $query); 94 | } 95 | 96 | return $route; 97 | } 98 | 99 | /** 100 | * Returns all messages after target message id. 101 | * 102 | * @param string $roomId 103 | * @param string|null $afterId 104 | * @param string|null $query 105 | * @return \Generator 106 | * @throws \Exception 107 | * @throws \InvalidArgumentException 108 | * @throws \Throwable 109 | */ 110 | public function allAfterId(string $roomId, string $afterId = null, string $query = null) 111 | { 112 | $limit = 100; 113 | $route = $this->routeForMessagesIterator($roomId, $limit, $query); 114 | 115 | do { 116 | if ($afterId !== null) { 117 | $route->with('afterId', $afterId); 118 | } 119 | 120 | $response = (array)$this->fetch($route); 121 | 122 | foreach ($response as $message) { 123 | yield $message; 124 | } 125 | 126 | $afterId = \count($response) > 0 ? end($response)['id'] : null; 127 | } while (\count($response) >= $limit); 128 | } 129 | 130 | /** 131 | * There is also a way to retrieve a single message using its id. 132 | * 133 | * @param string $roomId Room id 134 | * @param string $messageId Message id 135 | * @return array 136 | * @throws \InvalidArgumentException 137 | * @throws \Throwable 138 | * @throws \Exception 139 | */ 140 | public function find(string $roomId, string $messageId): array 141 | { 142 | return $this->fetch( 143 | Route::get('rooms/{roomId}/chatMessages/{messageId}') 144 | ->withMany(['roomId' => $roomId, 'messageId' => $messageId]) 145 | ); 146 | } 147 | 148 | /** 149 | * Send a message to a room. 150 | * 151 | * @param string $roomId Room id 152 | * @param string $content Message body 153 | * @return array 154 | * @throws \InvalidArgumentException 155 | * @throws \Throwable 156 | * @throws \Exception 157 | */ 158 | public function create(string $roomId, string $content): array 159 | { 160 | return $this->fetch( 161 | Route::post('rooms/{roomId}/chatMessages') 162 | ->with('roomId', $roomId) 163 | ->withBody('text', $content) 164 | ); 165 | } 166 | 167 | /** 168 | * Delete a message. 169 | * 170 | * @param string $roomId 171 | * @param string $messageId 172 | * @return array 173 | * @throws \RuntimeException 174 | * @throws \InvalidArgumentException 175 | * @throws \Throwable 176 | * @throws \Exception 177 | * @throws \GuzzleHttp\Exception\ClientException 178 | */ 179 | public function delete(string $roomId, string $messageId): array 180 | { 181 | return $this->update($roomId, $messageId, ''); 182 | } 183 | 184 | /** 185 | * Update a message. 186 | * 187 | * @param string $roomId Room id 188 | * @param string $messageId Message id 189 | * @param string $content New message body 190 | * @return array 191 | * @throws \InvalidArgumentException 192 | * @throws \Throwable 193 | * @throws \Exception 194 | */ 195 | public function update(string $roomId, string $messageId, string $content): array 196 | { 197 | return $this->fetch( 198 | Route::put('rooms/{roomId}/chatMessages/{messageId}') 199 | ->withMany(['roomId' => $roomId, 'messageId' => $messageId]) 200 | ->withBody('text', $content) 201 | ); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitter API Client 4.0 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/serafim/gitter-api/v/stable)](https://packagist.org/packages/serafim/gitter-api) 4 | [![https://travis-ci.org/SerafimArts/gitter-api](https://travis-ci.org/SerafimArts/gitter-api.svg)](https://travis-ci.org/SerafimArts/gitter-api/builds) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/SerafimArts/gitter-api/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/SerafimArts/gitter-api/?branch=master) 6 | [![License](https://poser.pugx.org/serafim/gitter-api/license)](https://packagist.org/packages/serafim/gitter-api) 7 | [![Total Downloads](https://poser.pugx.org/serafim/gitter-api/downloads)](https://packagist.org/packages/serafim/gitter-api) 8 | 9 | 10 | - [Version 3.0.x](https://github.com/SerafimArts/gitter-api/tree/5bf22f2b5bbc517600937fbbaa44037a89688a82) 11 | - [Version 2.1.x](https://github.com/SerafimArts/gitter-api/tree/967ef646afa3181fbb10ec6669538c4911866731) 12 | - [Version 2.0.x](https://github.com/SerafimArts/gitter-api/tree/8ad7f4d06c5f8196ada5798799cd8c1d5f55a974) 13 | - [Version 1.1.x](https://github.com/SerafimArts/gitter-api/tree/26c3640a1d933db8ad27bd3c10f8bc42ff936c47) 14 | - [Version 1.0.x](https://github.com/SerafimArts/gitter-api/tree/f955ade02128e868d494baf0acc021bc257c1807) 15 | 16 | ## Installation 17 | 18 | `composer require serafim/gitter-api` 19 | 20 | ## Creating a Gitter Client 21 | 22 | 23 | ```php 24 | use Gitter\Client; 25 | 26 | $client = new Client($token); 27 | // OR 28 | $client = new Client($token, $logger); // $logger are instance of \Psr\Log\LoggerInterface 29 | 30 | // ... SOME ACTIONS ... 31 | 32 | $client->connect(); // Locks current runtime and starts an EventLoop (for streaming requests) 33 | ``` 34 | 35 | ## Resources 36 | 37 | ```php 38 | // $client = new \Gitter\Client('token'); 39 | 40 | $client->groups; // Groups resource (Traversable) 41 | $client->messages; // Messages resource 42 | $client->rooms; // Rooms resource (Traversable) 43 | $client->users; // Users resource 44 | ``` 45 | 46 | ### Example 47 | 48 | ```php 49 | foreach ($client->rooms as $room) { 50 | var_dump($room); 51 | } 52 | ``` 53 | 54 | ### Streaming 55 | 56 | ```php 57 | $observer = $client->rooms->messages('roomId'); // Observer 58 | 59 | $observer->subscribe(function ($message) { 60 | var_dump($message); 61 | }); 62 | 63 | // Connect to stream! 64 | $client->connect(); 65 | ``` 66 | 67 | ## Available resources 68 | 69 | ### Groups 70 | 71 | List groups the current user is in. 72 | 73 | - `$client->groups->all(): array` 74 | 75 | List of rooms nested under the specified group. 76 | 77 | - `$client->groups->rooms(string $roomId): array` 78 | 79 | ### Messages 80 | 81 | List of messages in a room in historical reversed order. 82 | 83 | - `$client->messages->all(string $roomId[, string $query]): \Generator` 84 | 85 | There is also a way to retrieve a single message using its id. 86 | 87 | - `$client->messages->find(string $roomId, string $messageId): array` 88 | 89 | Send a message to a room. 90 | 91 | - `$client->messages->create(string $roomId, string $content): array` 92 | 93 | Update a message. 94 | 95 | - `$client->messages->update(string $roomId, string $messageId, string $content): array` 96 | 97 | Delete a message. 98 | 99 | - `$client->messages->delete(string $roomId, string $messageId): array` 100 | 101 | ### Rooms 102 | 103 | List rooms the current user is in. 104 | 105 | - `$client->rooms->all([string $query]): array` 106 | 107 | Join user into a room. 108 | 109 | - `$client->rooms->joinUser(string $roomId, string $userId): array` 110 | 111 | Join current user into a room. 112 | 113 | - `$client->rooms->join(string $roomId): array` 114 | 115 | Join current user into a room by room name (URI). 116 | 117 | - `$client->rooms->joinByName(string $name): array` 118 | 119 | Find room by room name (URI). 120 | 121 | - `$client->rooms->findByName(string $name): array` 122 | 123 | Kick user from target room. 124 | 125 | - `$client->rooms->kick(string $roomId, string $userId): array` 126 | 127 | This can be self-inflicted to leave the the room and remove room from your left menu. 128 | 129 | - `$client->rooms->leave(string $roomId): array` 130 | 131 | Sets up a new topic of target room. 132 | 133 | - `$client->rooms->topic(string $roomId, string $topic): array` 134 | 135 | Sets the room is indexed by search engines. 136 | 137 | - `$client->rooms->searchIndex(string $roomId, bool $enabled): array` 138 | 139 | Sets the tags that define the room 140 | 141 | - `$client->rooms->tags(string $roomId, array $tags): array` 142 | 143 | If you hate one of the rooms - you can destroy it! 144 | 145 | - `$client->rooms->delete(string $roomId): array` 146 | 147 | List of users currently in the room. 148 | 149 | - `$client->rooms->users(string $roomId[, string $query]: \Generator` 150 | 151 | Use the streaming API to listen events. 152 | 153 | - `$client->rooms->events(string $roomId): Observer` 154 | 155 | Use the streaming API to listen messages. 156 | 157 | - `$client->rooms->messages(string $roomId): Observer` 158 | 159 | ### Users 160 | 161 | Returns the current user logged in. 162 | 163 | - `$client->users->current(): array` 164 | - `$client->users->currentUserId(): string` 165 | 166 | List of Rooms the user is part of. 167 | 168 | - `$client->users->rooms([string $userId]): array` 169 | 170 | You can retrieve unread items and mentions using the following endpoint. 171 | 172 | - `$client->users->unreadItems(string $roomId[, string $userId]): array` 173 | 174 | There is an additional endpoint nested under rooms that you can use to mark chat messages as read 175 | 176 | - `$client->users->markAsRead(string $roomId, array $messageIds[, string $userId]): array` 177 | 178 | List of the user's GitHub Organisations and their respective Room if available. 179 | 180 | - `$client->users->orgs([string $userId]): array` 181 | 182 | List of the user's GitHub Repositories and their respective Room if available. 183 | 184 | - `$client->users->repos([string $userId]): array` 185 | 186 | List of Gitter channels nested under the user. 187 | 188 | - `$client->users->channels([string $userId]): array` 189 | 190 | ## Custom WebHook Notifications 191 | 192 | Create a "Custom Webhook": 193 | - Open your chat 194 | - Click on "Room Settings" button 195 | - Click on "Integrations" 196 | - Select "Custom" 197 | - Remember yor Hook Id, like `2b66cf4653faa342bbe8` inside `https://webhooks.gitter.im/e/` url. 198 | 199 | ```php 200 | $client->notify($hookId) 201 | // ->error($message) - Send "Error" message 202 | // ->info($message) - Send "Info" message 203 | // ->withLevel(...) - Sets up level 204 | ->send('Your message with markdown'); // Send message with markdown content 205 | ``` 206 | 207 | ## Custom routing 208 | 209 | ```php 210 | $route = Route::get('rooms/{roomId}/chatMessages') 211 | ->with('roomId', $roomId) 212 | ->toStream(); 213 | 214 | // Contains "GET https://stream.gitter.im/v1/rooms/.../chatMessages" url 215 | 216 | $client->viaStream()->request($route)->subscribe(function($message) { 217 | var_dump($message); 218 | // Subscribe on every message in target room (Realtime subscribtion) 219 | }); 220 | 221 | $client->connect(); 222 | ``` 223 | 224 | Available route methods: 225 | 226 | - `Route::get(string $route)` - GET http method 227 | - `Route::post(string $route)` - POST http method 228 | - `Route::put(string $route)` - PUT http method 229 | - `Route::patch(string $route)` - PATCH http method 230 | - `Route::delete(string $route)` - DELETE http method 231 | - `Route::options(string $route)` - OPTIONS http method 232 | - `Route::head(string $route)` - HEAD http method 233 | - `Route::connect(string $route)` - CONNECT http method 234 | - `Route::trace(string $route)` - TRACE http method 235 | 236 | Route arguments: 237 | 238 | - `$route->with(string $key, string $value)` - Add route or GET query parameter 239 | - `$route->withMany(array $parameters)` - Add route or GET query parameters 240 | - `$route->withBody(string $key, string $value)` - Add POST, PUT, DELETE, etc body parameter 241 | 242 | 243 | See more info about API into [Documentation](https://developer.gitter.im/docs/welcome) 244 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | token = $token; 90 | $this->loop = Factory::create(); 91 | 92 | if (null === ($this->logger = $logger)) { 93 | $this->logger = new NullLogger(); 94 | } 95 | } 96 | 97 | /** 98 | * @param string $token 99 | * @return Client 100 | */ 101 | public function updateToken(string $token): Client 102 | { 103 | $this->token = $token; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * @param int $count 110 | * @return Client 111 | */ 112 | public function retries(int $count): Client 113 | { 114 | $this->retries = $count; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * @return int 121 | */ 122 | public function getRetriesCount(): int 123 | { 124 | return $this->retries; 125 | } 126 | 127 | /** 128 | * @return void 129 | */ 130 | public function clear() 131 | { 132 | $this->loop->stop(); 133 | $this->storage = []; 134 | } 135 | 136 | /** 137 | * @return SyncAdapterInterface|AdapterInterface 138 | * @throws \InvalidArgumentException 139 | */ 140 | public function viaHttp(): SyncAdapterInterface 141 | { 142 | if ($this->http === null) { 143 | $this->http = new HttpAdapter($this); 144 | } 145 | 146 | return $this->http; 147 | } 148 | 149 | /** 150 | * @return StreamAdapterInterface|AdapterInterface 151 | */ 152 | public function viaStream(): StreamAdapterInterface 153 | { 154 | if ($this->streaming === null) { 155 | $this->streaming = new StreamAdapter($this, $this->loop); 156 | } 157 | 158 | return $this->streaming; 159 | } 160 | 161 | /** 162 | * @return void 163 | */ 164 | public function connect() 165 | { 166 | $this->loop->run(); 167 | } 168 | 169 | /** 170 | * @param string $hookId 171 | * @return WebHook 172 | * @throws \InvalidArgumentException 173 | */ 174 | public function notify(string $hookId): WebHook 175 | { 176 | return new WebHook($this, $hookId); 177 | } 178 | 179 | /** 180 | * @param string $resource 181 | * @return Groups|Messages|Rooms|Users|null|LoggerInterface|LoopInterface|string 182 | */ 183 | public function __get(string $resource) 184 | { 185 | $resolve = function (string $resource) { 186 | switch ($resource) { 187 | // == RESOURCES == 188 | case 'users': 189 | return new Users($this); 190 | case 'groups': 191 | return new Groups($this); 192 | case 'messages': 193 | return new Messages($this); 194 | case 'rooms': 195 | return new Rooms($this); 196 | 197 | // == COMMON === 198 | case 'loop': 199 | return $this->loop(); 200 | case 'token': 201 | return $this->token(); 202 | case 'logger': 203 | return $this->logger(); 204 | } 205 | 206 | return null; 207 | }; 208 | 209 | if (! isset($this->storage[$resource])) { 210 | $this->storage[$resource] = $resolve($resource); 211 | } 212 | 213 | return $this->storage[$resource]; 214 | } 215 | 216 | /** 217 | * @param string $name 218 | * @param $value 219 | * @return void 220 | */ 221 | public function __set(string $name, $value) 222 | { 223 | switch ($name) { 224 | // == COMMON === 225 | case 'loop': 226 | $this->loop($value); 227 | break; 228 | 229 | case 'token': 230 | $this->token($value); 231 | break; 232 | 233 | case 'logger': 234 | $this->logger($value); 235 | break; 236 | 237 | default: 238 | $this->{$name} = $value; 239 | } 240 | } 241 | 242 | /** 243 | * @param LoopInterface|null $loop 244 | * @return LoopInterface 245 | */ 246 | public function loop(LoopInterface $loop = null): LoopInterface 247 | { 248 | if ($loop !== null) { 249 | $this->loop = $loop; 250 | } 251 | 252 | return $this->loop; 253 | } 254 | 255 | /** 256 | * @param string|null $token 257 | * @return string 258 | */ 259 | public function token(string $token = null): string 260 | { 261 | if ($token !== null) { 262 | $this->token = $token; 263 | } 264 | 265 | return $this->token; 266 | } 267 | 268 | /** 269 | * @param LoggerInterface|null $logger 270 | * @return LoggerInterface 271 | */ 272 | public function logger(LoggerInterface $logger = null): LoggerInterface 273 | { 274 | if ($logger !== null) { 275 | $this->logger = $logger; 276 | } 277 | 278 | return $this->logger; 279 | } 280 | 281 | /** 282 | * @return array 283 | * @throws \Throwable 284 | * @throws \GuzzleHttp\Exception\ClientException 285 | * @throws \Exception 286 | * @throws \RuntimeException 287 | * @throws \InvalidArgumentException 288 | */ 289 | public function authUser(): array 290 | { 291 | return $this->users->current(); 292 | } 293 | 294 | /** 295 | * @return string 296 | * @throws \Throwable 297 | * @throws \GuzzleHttp\Exception\ClientException 298 | * @throws \Exception 299 | * @throws \RuntimeException 300 | * @throws \InvalidArgumentException 301 | */ 302 | public function authId(): string 303 | { 304 | return $this->users->currentUserId(); 305 | } 306 | 307 | /** 308 | * @param string $name 309 | * @throws \LogicException 310 | */ 311 | public function __unset(string $name) 312 | { 313 | switch ($name) { 314 | case 'logger': 315 | $this->logger = null; 316 | break; 317 | case 'loop': 318 | throw new \LogicException('Can not remove EventLoop'); 319 | case 'token': 320 | throw new \LogicException('Can not remove token value.'); 321 | case 'users': 322 | case 'groups': 323 | case 'messages': 324 | case 'rooms': 325 | throw new \LogicException('Resource ' . $name . ' can not be removed'); 326 | } 327 | } 328 | 329 | /** 330 | * @param string $name 331 | * @return bool 332 | */ 333 | public function __isset(string $name): bool 334 | { 335 | if (\in_array($name, ['users', 'groups', 'messages', 'rooms', 'loop', 'token'], true)) { 336 | return true; 337 | } 338 | 339 | if ($name === 'logger') { 340 | return $this->logger !== null; 341 | } 342 | 343 | return property_exists($this, $name) && $this->{$name} !== null; 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/Resources/Rooms.php: -------------------------------------------------------------------------------- 1 | fetch(Route::get('rooms')->with('q', $query)); 73 | } 74 | 75 | return $this->fetch(Route::get('rooms')); 76 | } 77 | 78 | /** 79 | * To join a room you'll need to provide a URI for it. 80 | * Said URI can represent a GitHub Org, a GitHub Repo or a Gitter Channel. 81 | * - If the room exists and the user has enough permission to access it, it'll be added to the room. 82 | * - If the room doesn't exist but the supplied URI represents a GitHub Org or GitHub Repo the user 83 | * is an admin of, the room will be created automatically and the user added. 84 | * 85 | * @param string $roomId Required ID of the room you would like to join 86 | * @param string $userId Required ID of the user 87 | * @return array 88 | * @throws \InvalidArgumentException 89 | * @throws \Throwable 90 | * @throws \Exception 91 | */ 92 | public function joinUser(string $roomId, string $userId): array 93 | { 94 | return $this->fetch( 95 | Route::post('user/{userId}/rooms')->with('userId', $userId) 96 | ->withBody('id', $roomId) 97 | ); 98 | } 99 | 100 | /** 101 | * Join to target room 102 | * 103 | * @param string $roomId Required ID of the room you would like to join 104 | * @return array 105 | * @throws \RuntimeException 106 | * @throws \InvalidArgumentException 107 | * @throws \Throwable 108 | * @throws \Exception 109 | * @throws \GuzzleHttp\Exception\ClientException 110 | */ 111 | public function join(string $roomId): array 112 | { 113 | return $this->joinUser($roomId, $this->client()->authId()); 114 | } 115 | 116 | /** 117 | * To join a room you'll need to provide a URI for it. 118 | * 119 | * Said URI can represent a GitHub Org, a GitHub Repo or a Gitter Channel. 120 | * - If the room exists and the user has enough permission to access it, it'll be added to the room. 121 | * - If the room doesn't exist but the supplied URI represents a GitHub Org or GitHub Repo the user 122 | * 123 | * is an admin of, the room will be created automatically and the user added. 124 | * 125 | * @param string $name Required URI of the room you would like to join 126 | * @return array 127 | * @throws \InvalidArgumentException 128 | * @throws \Throwable 129 | * @throws \Exception 130 | */ 131 | public function joinByName(string $name): array 132 | { 133 | return $this->fetch(Route::post('rooms')->withBody('uri', $name)); 134 | } 135 | 136 | /** 137 | * @param string $name 138 | * @return array 139 | * @throws \InvalidArgumentException 140 | * @throws \Throwable 141 | * @throws \Exception 142 | */ 143 | public function findByName(string $name): array 144 | { 145 | return $this->joinByName($name); 146 | } 147 | 148 | /** 149 | * Kick target user from target room 150 | * 151 | * @param string $roomId Required ID of the room 152 | * @param string $userId Required ID of the user 153 | * @return array 154 | * @throws \InvalidArgumentException 155 | * @throws \Throwable 156 | * @throws \Exception 157 | */ 158 | public function kick(string $roomId, string $userId): array 159 | { 160 | return $this->fetch( 161 | Route::delete('rooms/{roomId}/users/{userId}') 162 | ->with('roomId', $roomId) 163 | ->with('userId', $userId) 164 | ); 165 | } 166 | 167 | /** 168 | * This can be self-inflicted to leave the the room and remove room from your left menu. 169 | * 170 | * @param string $roomId Required ID of the room 171 | * @return array 172 | * @throws \RuntimeException 173 | * @throws \InvalidArgumentException 174 | * @throws \Throwable 175 | * @throws \Exception 176 | * @throws \GuzzleHttp\Exception\ClientException 177 | */ 178 | public function leave(string $roomId): array 179 | { 180 | return $this->kick($roomId, $this->client()->authId()); 181 | } 182 | 183 | /** 184 | * Sets up a new topic of target room 185 | * 186 | * @param string $roomId Room id 187 | * @param string $topic Room topic 188 | * @return mixed 189 | * @throws \InvalidArgumentException 190 | * @throws \Throwable 191 | * @throws \Exception 192 | */ 193 | public function topic(string $roomId, string $topic): array 194 | { 195 | return $this->fetch( 196 | Route::put('rooms/{roomId}') 197 | ->with('roomId', $roomId) 198 | ->withBody('topic', $topic) 199 | ); 200 | } 201 | 202 | /** 203 | * Sets the room is indexed by search engines 204 | * 205 | * @param string $roomId Room id 206 | * @param bool $enabled Enable or disable room indexing 207 | * @return array 208 | * @throws \InvalidArgumentException 209 | * @throws \Throwable 210 | * @throws \Exception 211 | */ 212 | public function searchIndex(string $roomId, bool $enabled = true): array 213 | { 214 | return $this->fetch( 215 | Route::put('rooms/{roomId}') 216 | ->with('roomId', $roomId) 217 | ->withBody('noindex', !$enabled) 218 | ); 219 | } 220 | 221 | /** 222 | * Sets the tags that define the room 223 | * 224 | * @param string $roomId Room id 225 | * @param array $tags Target tags 226 | * @return array 227 | * @throws \InvalidArgumentException 228 | * @throws \Throwable 229 | * @throws \Exception 230 | */ 231 | public function tags(string $roomId, array $tags = []): array 232 | { 233 | return $this->fetch( 234 | Route::put('rooms/{roomId}') 235 | ->with('roomId', $roomId) 236 | ->withBody('tags', implode(', ', $tags)) 237 | ); 238 | } 239 | 240 | /** 241 | * If you hate one of the rooms - you can destroy it! 242 | * Fatality. 243 | * 244 | * @internal BE CAREFUL: THIS IS VERY DANGEROUS OPERATION. 245 | * 246 | * @param string $roomId Target room id 247 | * @return array 248 | * @throws \InvalidArgumentException 249 | * @throws \Throwable 250 | * @throws \Exception 251 | */ 252 | public function delete(string $roomId): array 253 | { 254 | return $this->fetch( 255 | Route::delete('rooms/{roomId}') 256 | ->with('roomId', $roomId) 257 | ); 258 | } 259 | 260 | /** 261 | * List of Users currently in the room. 262 | * 263 | * @param string $roomId Target room id 264 | * @param string $query Optional query for users search 265 | * @return \Generator 266 | * @throws \InvalidArgumentException 267 | * @throws \Throwable 268 | * @throws \Exception 269 | */ 270 | public function users(string $roomId, string $query = null): \Generator 271 | { 272 | $skip = 0; 273 | $limit = 30; 274 | 275 | do { 276 | $route = Route::get('rooms/{roomId}/users') 277 | ->withMany([ 278 | 'roomId' => $roomId, 279 | 'skip' => $skip, 280 | 'limit' => $limit, 281 | ]); 282 | 283 | if ($query !== null) { 284 | $route->with('q', $query); 285 | } 286 | 287 | yield from $response = $this->fetch($route); 288 | 289 | } while(\count($response) >= $limit && ($skip += $limit)); 290 | } 291 | 292 | /** 293 | * Use the streaming API to listen events. 294 | * The streaming API allows real-time access to messages fetching. 295 | * 296 | * @param string $roomId 297 | * @return Observer 298 | * @throws \InvalidArgumentException 299 | * @throws \Throwable 300 | */ 301 | public function events(string $roomId): Observer 302 | { 303 | return $this->stream( 304 | Route::get('rooms/{roomId}/events') 305 | ->with('roomId', $roomId) 306 | ->toStream() 307 | ); 308 | } 309 | 310 | /** 311 | * Use the streaming API to listen messages. 312 | * The streaming API allows real-time access to messages fetching. 313 | * 314 | * @param string $roomId 315 | * @return Observer 316 | * @throws \InvalidArgumentException 317 | * @throws \Throwable 318 | */ 319 | public function messages(string $roomId): Observer 320 | { 321 | return $this->stream( 322 | Route::get('rooms/{roomId}/chatMessages') 323 | ->with('roomId', $roomId) 324 | ->toStream() 325 | ); 326 | } 327 | 328 | /** 329 | * @return \Generator 330 | * @throws \InvalidArgumentException 331 | * @throws \Throwable 332 | * @throws \Exception 333 | */ 334 | public function getIterator(): \Generator 335 | { 336 | $rooms = $this->all(); 337 | 338 | foreach ($rooms as $i => $room) { 339 | yield $i => $room; 340 | } 341 | } 342 | } 343 | --------------------------------------------------------------------------------