├── SECURITY.md ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release.yml │ ├── ci.yml │ └── initiate_release.yml ├── lib └── GetStream │ └── Stream │ ├── Constant.php │ ├── Feed.php │ ├── StreamWrongInputException.php │ ├── FeedInterface.php │ ├── StreamFeedException.php │ ├── Activities.php │ ├── ActivitiesOperation.php │ ├── Analytics.php │ ├── Util.php │ ├── Signer.php │ ├── ClientInterface.php │ ├── Personalization.php │ ├── Batcher.php │ ├── BaseFeedInterface.php │ ├── Users.php │ ├── Collections.php │ ├── Reactions.php │ ├── BaseFeed.php │ └── Client.php ├── .php-cs-fixer.dist.php ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .versionrc.js ├── scripts └── get_changelog_diff.js ├── composer.json ├── assets └── logo.svg ├── .phan └── config.php └── CHANGELOG.md /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @JimmyPettersson85 @xernobyl @vagruchi @itsmeadi 2 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Constant.php: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->in(__DIR__) 6 | ; 7 | 8 | $config = new PhpCsFixer\Config(); 9 | return $config->setRules([ 10 | '@PSR2' => true, 11 | 'array_syntax' => ['syntax' => 'short'], 12 | ]) 13 | ->setFinder($finder) 14 | ; 15 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT=8-bullseye 2 | FROM mcr.microsoft.com/vscode/devcontainers/php:0-${VARIANT} 3 | 4 | RUN pecl install ast && \ 5 | echo "extension=ast.so" >> "$PHP_INI_DIR/php.ini-development" && \ 6 | mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" 7 | 8 | ENV PHAN_ALLOW_XDEBUG 0 9 | ENV PHAN_DISABLE_XDEBUG_WARN 1 -------------------------------------------------------------------------------- /.versionrc.js: -------------------------------------------------------------------------------- 1 | const versionFileUpdater = { 2 | VERSION_REGEX: /VERSION = '(.+)'/, 3 | 4 | readVersion: function (contents) { 5 | const version = this.VERSION_REGEX.exec(contents)[1]; 6 | return version; 7 | }, 8 | 9 | writeVersion: function (contents, version) { 10 | return contents.replace(this.VERSION_REGEX.exec(contents)[0], `VERSION = '${version}'`); 11 | } 12 | } 13 | 14 | module.exports = { 15 | bumpFiles: [{ filename: './lib/GetStream/Stream/Constant.php', updater: versionFileUpdater }], 16 | } 17 | -------------------------------------------------------------------------------- /scripts/get_changelog_diff.js: -------------------------------------------------------------------------------- 1 | /* 2 | Here we're trying to parse the latest changes from CHANGELOG.md file. 3 | The changelog looks like this: 4 | 5 | ## 0.0.3 6 | - Something #3 7 | ## 0.0.2 8 | - Something #2 9 | ## 0.0.1 10 | - Something #1 11 | 12 | In this case we're trying to extract "- Something #3" since that's the latest change. 13 | */ 14 | module.exports = () => { 15 | const fs = require('fs') 16 | 17 | changelog = fs.readFileSync('CHANGELOG.md', 'utf8') 18 | releases = changelog.match(/## [?[0-9](.+)/g) 19 | 20 | current_release = changelog.indexOf(releases[0]) 21 | previous_release = changelog.indexOf(releases[1]) 22 | 23 | latest_changes = changelog.substr(current_release, previous_release - current_release) 24 | 25 | return latest_changes 26 | } 27 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/FeedInterface.php: -------------------------------------------------------------------------------- 1 | getPrevious(); 18 | if ($e && $e instanceof ClientException) { 19 | $headerValues = $e->getResponse()->getHeader("x-ratelimit-" . $headerName); 20 | 21 | if ($headerValues) { 22 | return $headerValues[0]; 23 | } 24 | } 25 | 26 | return null; 27 | } 28 | 29 | public function getRateLimitLimit() 30 | { 31 | return $this->getRateLimitValue("limit"); 32 | } 33 | 34 | public function getRateLimitRemaining() 35 | { 36 | return $this->getRateLimitValue("remaining"); 37 | } 38 | 39 | public function getRateLimitReset() 40 | { 41 | return $this->getRateLimitValue("reset"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: 7 | - main 8 | 9 | jobs: 10 | Release: 11 | name: 🚀 Release 12 | if: github.event.pull_request.merged && startsWith(github.head_ref, 'release-') 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - uses: actions/github-script@v6 20 | with: 21 | script: | 22 | const get_change_log_diff = require('./scripts/get_changelog_diff.js') 23 | core.exportVariable('CHANGELOG', get_change_log_diff()) 24 | 25 | // Getting the release version from the PR source branch 26 | // Source branch looks like this: release-1.0.0 27 | const version = context.payload.pull_request.head.ref.split('-')[1] 28 | core.exportVariable('VERSION', version) 29 | 30 | - name: Create release on GitHub 31 | uses: ncipollo/release-action@v1 32 | with: 33 | body: ${{ env.CHANGELOG }} 34 | tag: ${{ env.VERSION }} 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-stream/stream", 3 | "description": "A PHP client for Stream (https://getstream.io)", 4 | "keywords": [ 5 | "stream", 6 | "newsfeed", 7 | "feedly" 8 | ], 9 | "homepage": "https://getstream.io", 10 | "license": "BSD-3-Clause", 11 | "authors": [ 12 | { 13 | "name": "Tommaso Barbugli", 14 | "email": "support@getstream.io" 15 | } 16 | ], 17 | "support": { 18 | "issues": "https://github.com/GetStream/stream-php/issues", 19 | "docs": "https://getstream.io/activity-feeds/docs/?language=php" 20 | }, 21 | "require": { 22 | "php": ">=8.0", 23 | "guzzlehttp/guzzle": "^7.5.0", 24 | "firebase/php-jwt": "^v6.4.0" 25 | }, 26 | "require-dev": { 27 | "friendsofphp/php-cs-fixer": "^3.14.0", 28 | "phan/phan": "^5.4.0", 29 | "phpunit/phpunit": "^9.6.3" 30 | }, 31 | "autoload": { 32 | "psr-0": { 33 | "GetStream\\Stream": "lib/" 34 | }, 35 | "psr-4": { 36 | "GetStream\\Stubs\\": "tests/stubs/", 37 | "GetStream\\Unit\\": "tests/unit/", 38 | "GetStream\\Integration\\": "tests/integration/" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Activities.php: -------------------------------------------------------------------------------- 1 | client = $client; 20 | $this->api_key = $api_key; 21 | $this->token = $token; 22 | } 23 | 24 | /** 25 | * @param string $resource 26 | * @param string $action 27 | * 28 | * @return array 29 | */ 30 | protected function getHttpRequestHeaders($resource, $action) 31 | { 32 | $headers = parent::getHttpRequestHeaders($resource, $action); 33 | $headers['Authorization'] = $this->token; 34 | 35 | return $headers; 36 | } 37 | 38 | public function _getActivities($query_params, $enrich = false) 39 | { 40 | if (empty($query_params)) { 41 | return; 42 | } 43 | 44 | $url = 'activities/'; 45 | 46 | if ($enrich) { 47 | $url = "enrich/$url"; 48 | } 49 | 50 | return $this->makeHttpRequest($url, 'GET', [], $query_params); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.head_ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | build: 11 | name: 🧪 Test & lint 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 1 15 | matrix: 16 | php-versions: ['8.0', '8.1', '8.2'] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 # gives the commit message linter access to all previous commits 22 | 23 | # - name: Commit lint 24 | # if: ${{ matrix.php-versions == '8.0' }} 25 | # uses: wagoid/commitlint-github-action@v4 26 | 27 | - name: Setup PHP 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php-versions }} 31 | extensions: ast, mbstring, intl 32 | ini-values: post_max_size=256M, max_execution_time=180 33 | tools: composer:v2 34 | 35 | - name: Deps 36 | run: composer install --no-interaction 37 | 38 | - name: Quality 39 | if: matrix.php-versions == '8.0' 40 | run: | 41 | vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --dry-run --stop-on-violation 42 | vendor/bin/phan --no-progress-bar 43 | 44 | - name: Test 45 | env: 46 | STREAM_API_KEY: ${{ secrets.STREAM_API_KEY }} 47 | STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} 48 | run: vendor/bin/phpunit 49 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/ActivitiesOperation.php: -------------------------------------------------------------------------------- 1 | client = $client; 20 | $this->api_key = $api_key; 21 | $this->token = $token; 22 | } 23 | 24 | /** 25 | * @param string $resource 26 | * @param string $action 27 | * 28 | * @return array 29 | */ 30 | protected function getHttpRequestHeaders($resource, $action) 31 | { 32 | $headers = parent::getHttpRequestHeaders($resource, $action); 33 | $headers['Authorization'] = $this->token; 34 | 35 | return $headers; 36 | } 37 | 38 | public function partiallyUpdateActivity($data) 39 | { 40 | return $this->makeHttpRequest('activity/', 'POST', $data); 41 | } 42 | 43 | public function updateActivities($activities) 44 | { 45 | if (empty($activities)) { 46 | return; 47 | } 48 | 49 | return $this->makeHttpRequest('activities/', 'POST', compact('activities')); 50 | } 51 | 52 | public function getAppActivities($data = []) 53 | { 54 | $params = []; 55 | foreach ($data as $key => $value) { 56 | $params[$key] = implode(',', $value); 57 | } 58 | 59 | return $this->makeHttpRequest('activities/', 'GET', [], $params); 60 | } 61 | 62 | public function activityPartialUpdate($data = []) 63 | { 64 | return $this->makeHttpRequest('activity/', 'POST', $data); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/initiate_release.yml: -------------------------------------------------------------------------------- 1 | name: Create release PR 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "The new version number. Example: 1.40.1" 8 | required: true 9 | 10 | jobs: 11 | init_release: 12 | name: 🚀 Create release PR 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 # gives the changelog generator access to all previous commits 18 | 19 | - name: Update CHANGELOG.md, Client.php and push release branch 20 | env: 21 | VERSION: ${{ github.event.inputs.version }} 22 | run: | 23 | npx --yes standard-version@9.3.2 --release-as "$VERSION" --skip.tag --skip.commit --tag-prefix= 24 | git config --global user.name 'github-actions' 25 | git config --global user.email 'release@getstream.io' 26 | git checkout -q -b "release-$VERSION" 27 | git commit -am "chore(release): $VERSION" 28 | git push -q -u origin "release-$VERSION" 29 | 30 | - name: Get changelog diff 31 | uses: actions/github-script@v6 32 | with: 33 | script: | 34 | const get_change_log_diff = require('./scripts/get_changelog_diff.js') 35 | core.exportVariable('CHANGELOG', get_change_log_diff()) 36 | 37 | - name: Open pull request 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | run: | 41 | gh pr create \ 42 | -t "chore(release): release ${{ github.event.inputs.version }}" \ 43 | -b "# :rocket: ${{ github.event.inputs.version }} 44 | Make sure to use squash & merge when merging! 45 | Once this is merged, another job will kick off automatically and publish the package. 46 | # :memo: Changelog 47 | ${{ env.CHANGELOG }}" 48 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.195.0/containers/php 3 | { 4 | "name": "PHP", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update VARIANT to pick a PHP version: 8, 8.0, 7, 7.4 9 | // Append -bullseye or -buster to pin to an OS version. 10 | // Use -bullseye variants on local on arm64/Apple Silicon. 11 | "VARIANT": "8-bullseye" 12 | } 13 | }, 14 | "settings": { 15 | "php.validate.executablePath": "/usr/local/bin/php", 16 | "php-cs-fixer.onsave": true, 17 | "php-cs-fixer.config": ".php-cs-fixer.dist.php", 18 | }, 19 | "extensions": [ 20 | "pkief.material-icon-theme", 21 | "eamodio.gitlens", 22 | "visualstudioexptteam.vscodeintellicode", 23 | "github.copilot", 24 | "felixfbecker.php-debug", 25 | "bmewburn.vscode-intelephense-client", 26 | "junstyle.php-cs-fixer" 27 | ], 28 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 29 | // "forwardPorts": [8080], 30 | // Use 'portsAttributes' to set default properties for specific forwarded ports. More info: https://code.visualstudio.com/docs/remote/devcontainerjson-reference. 31 | "portsAttributes": { 32 | "8000": { 33 | "label": "Stream PHP SDK", 34 | "onAutoForward": "notify" 35 | } 36 | }, 37 | // Use 'otherPortsAttributes' to configure any ports that aren't configured using 'portsAttributes'. 38 | // "otherPortsAttributes": { 39 | // "onAutoForward": "silent" 40 | // }, 41 | // Use 'postCreateCommand' to run commands after the container is created. 42 | // "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html" 43 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 44 | "remoteUser": "vscode" 45 | } 46 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Analytics.php: -------------------------------------------------------------------------------- 1 | client = $client; 22 | $this->api_key = $api_key; 23 | $this->token = $token; 24 | } 25 | 26 | /** 27 | * @param string $resource 28 | * @param string $action 29 | * 30 | * @return array 31 | */ 32 | protected function getHttpRequestHeaders($resource, $action) 33 | { 34 | $headers = parent::getHttpRequestHeaders($resource, $action); 35 | $headers['Authorization'] = $this->token; 36 | 37 | return $headers; 38 | } 39 | 40 | /** 41 | * @param string $targetUrl 42 | * @param array $events 43 | * 44 | * @return string 45 | */ 46 | public function createRedirectUrl($targetUrl, $events) 47 | { 48 | $query_params = $this->getHttpRequestHeaders('analytics', '*'); 49 | $query_params['api_key'] = $this->api_key; 50 | $query_params['url'] = $targetUrl; 51 | $query_params['auth_type'] = 'jwt'; 52 | $query_params['authorization'] = $query_params['Authorization']; 53 | $query_params['events'] = json_encode($events); 54 | 55 | unset( 56 | $query_params['Authorization'], 57 | $query_params['stream-auth-type'], 58 | $query_params['Content-Type'], 59 | $query_params['X-Stream-Client'] 60 | ); 61 | 62 | return static::API_ENDPOINT . 'redirect/?' . http_build_query($query_params); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Util.php: -------------------------------------------------------------------------------- 1 | '*', 22 | 'user_id' => '*', 23 | 'feed_id' => '*', 24 | 'resource' => $resource, 25 | ], $apiSecret, 'HS256'); 26 | 27 | 28 | $stack = $streamClient->getCustomHttpHandlerStack(); 29 | if (!$stack) { 30 | $stack = HandlerStack::create(); 31 | } 32 | $stack->push(function (callable $handler) use ($token, $apiKey) { 33 | return function (RequestInterface $request, array $options) use ($handler, $token, $apiKey) { 34 | // Add authentication headers. 35 | $request = $request 36 | ->withAddedHeader('Authorization', $token) 37 | ->withAddedHeader('Stream-Auth-Type', 'jwt') 38 | ->withAddedHeader('Content-Type', 'application/json') 39 | ->withAddedHeader('X-Stream-Client', 'stream-php-client-' . Constant::VERSION); 40 | // Add a api_key query param. 41 | $queryParams = \GuzzleHttp\Psr7\Query::parse($request->getUri()->getQuery()); 42 | $query = http_build_query($queryParams + ['api_key' => $apiKey]); 43 | $request = $request->withUri($request->getUri()->withQuery($query)); 44 | return $handler($request, $options); 45 | }; 46 | }); 47 | return $stack; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | STREAM MARK 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.phan/config.php: -------------------------------------------------------------------------------- 1 | null, 20 | 21 | // A list of directories that should be parsed for class and 22 | // method information. After excluding the directories 23 | // defined in exclude_analysis_directory_list, the remaining 24 | // files will be statically analyzed for errors. 25 | // 26 | // Thus, both first-party and third-party code being used by 27 | // your application should be included in this list. 28 | 'directory_list' => [ 29 | 'lib', 30 | 'vendor', 31 | ], 32 | 33 | // A regex used to match every file name that you want to 34 | // exclude from parsing. Actual value will exclude every 35 | // "test", "tests", "Test" and "Tests" folders found in 36 | // "vendor/" directory. 37 | 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', 38 | 39 | // A directory list that defines files that will be excluded 40 | // from static analysis, but whose class and method 41 | // information should be included. 42 | // 43 | // Generally, you'll want to include the directories for 44 | // third-party code (such as "vendor/") in this list. 45 | // 46 | // n.b.: If you'd like to parse but not analyze 3rd 47 | // party code, directories containing that code 48 | // should be added to both the `directory_list` 49 | // and `exclude_analysis_directory_list` arrays. 50 | 'exclude_analysis_directory_list' => [ 51 | 'vendor/' 52 | ], 53 | ]; 54 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Signer.php: -------------------------------------------------------------------------------- 1 | api_key = $api_key ?? ''; 27 | $this->api_secret = $api_secret ?? ''; 28 | } 29 | 30 | /** 31 | * @param string $value 32 | * @return string 33 | */ 34 | public function signature($value) 35 | { 36 | $digest = hash_hmac('sha1', $value, sha1($this->api_secret, true), true); 37 | 38 | return trim(strtr(base64_encode($digest), '+/', '-_'), '='); 39 | } 40 | 41 | /** 42 | * @param RequestInterface $request 43 | * 44 | * @return RequestInterface 45 | */ 46 | public function signRequest(RequestInterface $request) 47 | { 48 | $signatureString = sprintf( 49 | "(request-target): %s %s\ndate: %s", 50 | mb_strtolower($request->getMethod()), 51 | $request->getRequestTarget(), 52 | $request->getHeaderLine('date') 53 | ); 54 | 55 | $signature = base64_encode(hash_hmac('sha256', $signatureString, $this->api_secret, true)); 56 | 57 | $header = sprintf( 58 | 'Signature keyId="%s",algorithm="hmac-sha256",headers="(request-target) date",signature="%s"', 59 | $this->api_key, 60 | $signature 61 | ); 62 | 63 | return $request->withHeader('Authorization', $header); 64 | } 65 | 66 | /** 67 | * @param string $feedId 68 | * @param string $resource 69 | * @param string $action 70 | * @return string 71 | */ 72 | public function jwtScopeToken($feedId, $resource, $action) 73 | { 74 | $payload = [ 75 | 'action' => $action, 76 | 'feed_id' => $feedId, 77 | 'resource' => $resource, 78 | ]; 79 | 80 | return JWT::encode($payload, $this->api_secret, 'HS256'); 81 | } 82 | 83 | /** 84 | * @param string $user_id 85 | * @param array $extra_data 86 | * @return string 87 | */ 88 | public function jwtUserSessionToken($user_id, $extra_data) 89 | { 90 | $payload = [ 91 | 'user_id' => $user_id, 92 | ]; 93 | foreach ($extra_data as $name => $value) { 94 | $payload[$name] = $value; 95 | } 96 | return JWT::encode($payload, $this->api_secret, 'HS256'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/ClientInterface.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 35 | $this->apiSecret = $apiSecret; 36 | $this->client = new GuzzleClient([ 37 | 'base_uri' => self::API_ENDPOINT, 38 | 'timeout' => $streamClient->timeout, 39 | 'handler' => Util::handlerStack($streamClient, $apiKey, $apiSecret, 'personalization'), 40 | ]); 41 | } 42 | 43 | /** 44 | * @param string $resource 45 | * @param array $params 46 | * 47 | * @return array 48 | */ 49 | public function get($resource, array $params) 50 | { 51 | return $this->request('GET', $resource, $params); 52 | } 53 | 54 | /** 55 | * @param string $resource 56 | * @param array $params 57 | * 58 | * @return array 59 | */ 60 | public function post($resource, array $params) 61 | { 62 | return $this->request('POST', $resource, $params); 63 | } 64 | 65 | /** 66 | * @param string $resource 67 | * @param array $params 68 | * 69 | * @return array 70 | */ 71 | public function delete($resource, array $params) 72 | { 73 | return $this->request('DELETE', $resource, $params); 74 | } 75 | 76 | /** 77 | * @param string $method 78 | * @param string $resource 79 | * @param array $params 80 | * 81 | * @return array 82 | */ 83 | private function request($method, $resource, array $params) 84 | { 85 | $queryParams = ['api_key' => $this->apiKey]; 86 | $queryParams += $params; 87 | 88 | $uri = $resource .'/?'. http_build_query($queryParams); 89 | 90 | try { 91 | $response = $this->client->request($method, $uri); 92 | } catch (ClientException $e) { 93 | $response = $e->getResponse(); 94 | $msg = $response->getBody(); 95 | $code = $response->getStatusCode(); 96 | $previous = $e; 97 | throw new StreamFeedException($msg, $code, $previous); 98 | } 99 | 100 | $body = $response->getBody()->getContents(); 101 | 102 | return json_decode($body, true); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Batcher.php: -------------------------------------------------------------------------------- 1 | client = $client; 22 | $this->signer = $signer; 23 | $this->api_key = $api_key; 24 | } 25 | 26 | /** 27 | * @return \GuzzleHttp\HandlerStack 28 | */ 29 | public function getHandlerStack() 30 | { 31 | $stack = HandlerStack::create(); 32 | $stack->push(function (callable $handler) { 33 | return function (RequestInterface $request, array $options) use ($handler) { 34 | return $handler($this->signer->signRequest($request), $options); 35 | }; 36 | }); 37 | 38 | return $stack; 39 | } 40 | 41 | /** 42 | * @param array $activityData 43 | * @param array $feeds 44 | * 45 | * @throws StreamFeedException 46 | * 47 | * @return array 48 | */ 49 | public function addToMany($activityData, $feeds) 50 | { 51 | $data = [ 52 | 'feeds' => $feeds, 53 | 'activity' => $activityData, 54 | ]; 55 | 56 | return $this->makeHttpRequest('feed/add_to_many/', 'POST', $data); 57 | } 58 | 59 | /** 60 | * @param array $follows 61 | * @param int $activity_copy_limit 62 | * 63 | * @throws StreamFeedException 64 | * 65 | * @return array 66 | * $follows = [ 67 | * ['source' => 'flat:1', 'target' => 'user:1'], 68 | * ['source' => 'flat:1', 'target' => 'user:3'] 69 | * ] 70 | */ 71 | public function followMany($follows, $activity_copy_limit = null) 72 | { 73 | $query_params = []; 74 | if ($activity_copy_limit !== null) { 75 | $query_params["activity_copy_limit"] = $activity_copy_limit; 76 | } 77 | return $this->makeHttpRequest('follow_many/', 'POST', $follows, $query_params); 78 | } 79 | 80 | /** 81 | * @param array $unfollows 82 | * 83 | * @throws StreamFeedException 84 | * 85 | * @return array 86 | * $unfollows = [ 87 | * ['source' => 'user:1', 'target' => 'timeline:1'], 88 | * ['source' => 'user:2', 'target' => 'timeline:2', 'keep_history' => true] 89 | * ] 90 | */ 91 | public function unfollowMany($unfollows) 92 | { 93 | $query_params = []; 94 | return $this->makeHttpRequest('unfollow_many/', 'POST', $unfollows, $query_params); 95 | } 96 | 97 | /** 98 | * @param string $method 99 | * 100 | * @throws StreamFeedException 101 | * 102 | * @return mixed 103 | */ 104 | public function test($method) 105 | { 106 | return $this->makeHttpRequest('test/auth/digest/', $method); 107 | } 108 | 109 | /** 110 | * @param string $resource 111 | * @param string $action 112 | * 113 | * @return array 114 | */ 115 | protected function getHttpRequestHeaders($resource, $action) 116 | { 117 | return [ 118 | 'Content-Type' => 'application/json', 119 | 'Date' => gmdate('D, d M Y H:i:s T'), 120 | 'X-Api-Key' => $this->api_key, 121 | ]; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/BaseFeedInterface.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 33 | $this->apiSecret = $apiSecret; 34 | $this->client = new GuzzleClient([ 35 | 'base_uri' => $streamClient->getBaseUrl().'/'.$streamClient->api_version.'/', 36 | 'timeout' => $streamClient->timeout, 37 | 'handler' => Util::handlerStack($streamClient, $apiKey, $apiSecret, 'users'), 38 | ]); 39 | } 40 | 41 | private function doRequest($method, $endpoint, $params=null) 42 | { 43 | if ($params === null) { 44 | $params = []; 45 | } 46 | if ($method === 'POST' || $method === 'PUT') { 47 | $params = ['json' => $params]; 48 | } 49 | try { 50 | $response = $this->client->request($method, $endpoint, $params); 51 | } catch (ClientException $e) { 52 | $response = $e->getResponse(); 53 | $msg = $response->getBody(); 54 | $code = $response->getStatusCode(); 55 | $previous = $e; 56 | throw new StreamFeedException($msg, $code, $previous); 57 | } 58 | return $response; 59 | } 60 | 61 | /** 62 | * @param string $userId 63 | * @param ?array $data 64 | * @param bool $getOrCreate 65 | * 66 | * @return array 67 | */ 68 | public function add($userId, array $data=null, $getOrCreate=null) 69 | { 70 | $endpoint = 'user/'; 71 | $payload = [ 72 | 'id' => $userId, 73 | ]; 74 | if ($data !== null) { 75 | $payload['data'] = $data; 76 | } 77 | if ($getOrCreate) { 78 | $endpoint .= '?get_or_create=true'; 79 | } 80 | $response = $this->doRequest('POST', $endpoint, $payload); 81 | $body = $response->getBody()->getContents(); 82 | return json_decode($body, true); 83 | } 84 | 85 | /** 86 | * @param string $userId 87 | * 88 | * @return string 89 | */ 90 | public function createReference($userId) 91 | { 92 | $myUserId = $userId; 93 | if (is_array($userId) && array_key_exists('id', $userId)) { 94 | $myUserId = $userId['id']; 95 | } 96 | return 'SU:' . $myUserId; 97 | } 98 | 99 | /** 100 | * @param string $userId 101 | * 102 | * @return array 103 | */ 104 | public function delete($userId) 105 | { 106 | $response = $this->doRequest('DELETE', 'user/' . $userId . '/'); 107 | $body = $response->getBody()->getContents(); 108 | return json_decode($body, true); 109 | } 110 | 111 | /** 112 | * @param string $userId 113 | * 114 | * @return array 115 | */ 116 | public function get($userId) 117 | { 118 | $response = $this->doRequest('GET', 'user/' . $userId . '/'); 119 | $body = $response->getBody()->getContents(); 120 | return json_decode($body, true); 121 | } 122 | 123 | /** 124 | * @param string $userId 125 | * @param ?array $data 126 | 127 | * @return array 128 | */ 129 | public function update($userId, array $data=null) 130 | { 131 | $payload = []; 132 | if ($data !== null) { 133 | $payload['data'] = $data; 134 | } 135 | $response = $this->doRequest('PUT', 'user/' . $userId . '/', $payload); 136 | $body = $response->getBody()->getContents(); 137 | return json_decode($body, true); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [7.1.0](https://github.com/GetStream/stream-php/compare/7.0.1...7.1.0) (2023-10-31) 6 | 7 | ### [7.0.1](https://github.com/GetStream/stream-php/compare/v7.0.0...v7.0.1) (2023-02-21) 8 | 9 | ## [7.0.0](https://github.com/GetStream/stream-php/compare/6.0.0...7.0.0) (2023-02-21) 10 | 11 | ## [6.0.0](https://github.com/GetStream/stream-php/compare/5.2.0...6.0.0) (2022-08-29) 12 | 13 | 14 | ### Features 15 | 16 | * drop php 7.3 support ([#117](https://github.com/GetStream/stream-php/issues/117)) ([f97432b](https://github.com/GetStream/stream-php/commit/f97432bfafb9adfa963ef3254a6e2575fb4d7b01)) 17 | 18 | ## [5.2.0](https://github.com/GetStream/stream-php/compare/5.1.1...5.2.0) (2022-08-29) 19 | 20 | 21 | ### Features 22 | 23 | * **activities:** add get activities api ([#111](https://github.com/GetStream/stream-php/issues/111)) ([971f6a6](https://github.com/GetStream/stream-php/commit/971f6a6135fd591603278289d01a0e3962785095)) 24 | * **guzzle:** add custom http middleware possibility ([#112](https://github.com/GetStream/stream-php/issues/112)) ([6960b3f](https://github.com/GetStream/stream-php/commit/6960b3f9b67170be845404d87c0ffc2e48237a46)) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * bump guzzle for security and add 8.2 ([#114](https://github.com/GetStream/stream-php/issues/114)) ([31bc4f8](https://github.com/GetStream/stream-php/commit/31bc4f80740bb0192d8d4a14b23837bcf11a4d4d)) 30 | * fix phan errors ([1b15faa](https://github.com/GetStream/stream-php/commit/1b15faa24c9f29d289f8565fff13cced6e31edcf)) 31 | * pr comment fixes ([3bd65f3](https://github.com/GetStream/stream-php/commit/3bd65f32c714c39114a9e7cbc52adf14b9f06acc)) 32 | 33 | ## 5.1.1 - 2021-09-28 34 | * Replace deprecated query parse of Guzzle 35 | 36 | ## 5.1.0 - 2021-06-21 37 | * Add target feeds extra data support for reactions 38 | 39 | ## 5.0.2 - 2021-06-08 40 | * Accept user_id for own reactions 41 | * Handle deprecated cs fixer config 42 | 43 | ## 5.0.1 - 2021-04-08 44 | * Fix namespacing issue for constant initialization of version 45 | 46 | ## 5.0.0 - 2021-03-26 47 | * Drop support for PHP 7.2 and add 8.0 support 48 | * Fix undefined constant deprecation from 7.2 49 | * Move to github actions and add static analysis 50 | 51 | ## 4.1.1 - 2021-01-26 52 | * Fix type of activity_id in remove_activity in docblock 53 | 54 | ## 4.1.0 - 2020-08-21 55 | * Add kinds filter into getActivities for a feed or a Client 56 | * Fix version header 57 | * Support guzzle 7 58 | 59 | ## 4.0.1 - 2019-11-25 60 | * PHP 7.4 61 | * Fix for targetFeeds in reactions()->addChild 62 | 63 | ## 4.0.0 - 2019-11-25 64 | * Upgrade dependencies, drop php5.x support, update tests 65 | 66 | ## 3.0.2 - 2019-11-12 67 | * Add support for enrichment in getActivities 68 | 69 | ## 3.0.1 - 2019-07-13 70 | * More flexible collections upsert 71 | 72 | ## 3.0.0 - 2019-02-11 73 | * Add support for users, collections, reactions, enrichment 74 | * Add `Client::doPartiallyUpdateActivity` 75 | * Add `Client::batchPartialActivityUpdate` methods 76 | * Add `Client::getActivities` methods 77 | * Remove deprecated methods on Feed Class 78 | 79 | ## 2.9.1 - 2018-12-03 80 | * Added RateLimit methods to StreamFeedException 81 | 82 | ## 2.9.0 - 2018-10-08 83 | * Added `Client::createUserSessionToken` method 84 | 85 | ## 2.8.0 - 2018-09-06 86 | * Added unfollow many endpoint. 87 | * Added collection references helpers. 88 | 89 | ## 2.4.2 - 2017-10-03 90 | * Silently return nothing when meaningless input is given on `Client::updateActivities` method. 91 | 92 | ## 2.4.1 - 2017-09-26 93 | * Cleaned up test suite and separated integration tests from unit tests in test builds 94 | * Fixed guzzle request options 95 | * Fixed json encoding errors by letting guzzle handle it 96 | 97 | ## 2.4.0 - 2017-08-31 98 | * Add support for update to target 99 | 100 | ## 2.3.0 - 2017-03-20 101 | * Add support for activity_copy_limit parameter (follow_many) 102 | 103 | ## 2.2.9 - 2016-10-15 104 | * Updates to testing, support for more versions 105 | 106 | ## 2.2.8 - 2016-06-29 107 | * Update to php-jwt 3.0 108 | 109 | ## 2.0.1 - 2014-11-11 110 | * Simplified syntax to create feeds, follow and unfollow feeds. 111 | * Default HTTP timeout of 3s 112 | 113 | ## 1.3.4 - 2014-09-08 114 | * Add support for mark read (notifications feeds) 115 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Collections.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 33 | $this->apiSecret = $apiSecret; 34 | $this->client = new GuzzleClient([ 35 | 'base_uri' => $streamClient->getBaseUrl().'/'.$streamClient->api_version.'/', 36 | 'timeout' => $streamClient->timeout, 37 | 'handler' => Util::handlerStack($streamClient, $apiKey, $apiSecret, 'collections'), 38 | ]); 39 | } 40 | 41 | private function doRequest($method, $endpoint, $params=null) 42 | { 43 | if ($params === null) { 44 | $params = []; 45 | } 46 | if ($method === 'POST' || $method === 'PUT') { 47 | $params = ['json' => $params]; 48 | } 49 | if ($method === 'GET' && $params !== null) { 50 | $endpoint .= '?' . http_build_query($params); 51 | } 52 | try { 53 | $response = $this->client->request($method, $endpoint, $params); 54 | } catch (ClientException $e) { 55 | $response = $e->getResponse(); 56 | $msg = $response->getBody(); 57 | $code = $response->getStatusCode(); 58 | $previous = $e; 59 | throw new StreamFeedException($msg, $code, $previous); 60 | } 61 | return $response; 62 | } 63 | 64 | /** 65 | * @param string $collectionName 66 | * @param array $data 67 | * @param string $id (optional) 68 | * @param string $user_id (optional) 69 | * 70 | * @return array 71 | */ 72 | public function add($collectionName, array $data, $id=null, $user_id=null) 73 | { 74 | $payload = ["id" => $id, "data" => $data, "user_id" => $user_id]; 75 | $response = $this->doRequest('POST', 'collections/' . $collectionName . '/', $payload); 76 | $body = $response->getBody()->getContents(); 77 | return json_decode($body, true); 78 | } 79 | 80 | /** 81 | * @param string $collectionName 82 | * @param string $id 83 | * 84 | * @return string 85 | */ 86 | public function createReference($collectionName, $id) 87 | { 88 | return "SO:".$collectionName.":".$id; 89 | } 90 | 91 | /** 92 | * @param string $collectionName 93 | * @param string $id 94 | * 95 | * @return array 96 | */ 97 | public function delete($collectionName, $id) 98 | { 99 | $response = $this->doRequest('DELETE', 'collections/' . $collectionName . '/' . $id . '/'); 100 | $body = $response->getBody()->getContents(); 101 | return json_decode($body, true); 102 | } 103 | 104 | /** 105 | * @param string $collectionName 106 | * @param array $ids 107 | * 108 | * @return array 109 | */ 110 | public function deleteMany($collectionName, array $ids) 111 | { 112 | $ids = join(',', $ids); 113 | $queryParams = ['collection_name' => $collectionName, 'ids' => $ids]; 114 | $response = $this->client->request('DELETE', 'collections/?'.http_build_query($queryParams)); 115 | $body = $response->getBody()->getContents(); 116 | return json_decode($body, true); 117 | } 118 | 119 | /** 120 | * @param string $collectionName 121 | * @param string $id 122 | * 123 | * @return array 124 | */ 125 | public function get($collectionName, $id) 126 | { 127 | $response = $this->doRequest('GET', 'collections/' . $collectionName . '/' . $id . '/'); 128 | $body = $response->getBody()->getContents(); 129 | return json_decode($body, true); 130 | } 131 | 132 | /** 133 | * @param string $collectionName 134 | * @param array $ids 135 | * 136 | * @return array 137 | */ 138 | public function select($collectionName, array $ids) 139 | { 140 | $mappedIds = array_map(function ($id) use ($collectionName) { 141 | return sprintf('%s:%s', $collectionName, $id); 142 | }, $ids); 143 | $params = ['foreign_ids' => join(',', $mappedIds)]; 144 | $response = $this->doRequest('GET', 'meta/', $params); 145 | $body = $response->getBody()->getContents(); 146 | return json_decode($body, true); 147 | } 148 | 149 | /** 150 | * @param string $collectionName 151 | * @param string $id 152 | * @param array $data 153 | * 154 | * @return array 155 | */ 156 | public function update($collectionName, $id, array $data) 157 | { 158 | $payload = ["data" => $data]; 159 | $response = $this->doRequest('PUT', 'collections/' . $collectionName . '/' . $id . '/', $payload); 160 | $body = $response->getBody()->getContents(); 161 | return json_decode($body, true); 162 | } 163 | 164 | /** 165 | * @param string $collectionName 166 | * @param array $data 167 | * 168 | * @return array 169 | */ 170 | public function upsert($collectionName, array $data) 171 | { 172 | if (!is_array($data)) { 173 | $data = [$data]; 174 | } 175 | $response = $this->doRequest('POST', 'meta/', ['data' => [$collectionName => $data]]); 176 | $body = $response->getBody()->getContents(); 177 | return json_decode($body, true); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Reactions.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 33 | $this->apiSecret = $apiSecret; 34 | $this->client = new GuzzleClient([ 35 | 'base_uri' => $streamClient->getBaseUrl().'/'.$streamClient->api_version.'/', 36 | 'timeout' => $streamClient->timeout, 37 | 'handler' => Util::handlerStack($streamClient, $apiKey, $apiSecret, 'reactions'), 38 | ]); 39 | } 40 | 41 | private function doRequest($method, $endpoint, $params=null) 42 | { 43 | if ($params === null) { 44 | $params = []; 45 | } 46 | if ($method === 'POST' || $method === 'PUT') { 47 | $params = ['json' => $params]; 48 | } 49 | if ($method === 'GET' && $params !== null) { 50 | $endpoint .= '?' . http_build_query($params); 51 | } 52 | try { 53 | $response = $this->client->request($method, $endpoint, $params); 54 | } catch (ClientException $e) { 55 | $response = $e->getResponse(); 56 | $msg = $response->getBody(); 57 | $code = $response->getStatusCode(); 58 | $previous = $e; 59 | throw new StreamFeedException($msg, $code, $previous); 60 | } 61 | return $response; 62 | } 63 | 64 | /** 65 | * @param string $kind 66 | * @param string $activityId 67 | * @param string $userId 68 | * @param ?array $data 69 | * @param ?array $targetFeeds 70 | * @param ?array $targetFeedsExtraData 71 | * 72 | * @return array 73 | */ 74 | public function add($kind, $activityId, $userId, array $data=null, array $targetFeeds=null, $targetFeedsExtraData=null) 75 | { 76 | $payload = [ 77 | 'kind' => $kind, 78 | 'activity_id' => $activityId, 79 | 'user_id' => $userId, 80 | ]; 81 | if ($data !== null) { 82 | $payload['data'] = $data; 83 | } 84 | if ($targetFeeds !== null) { 85 | $payload['target_feeds'] = $targetFeeds; 86 | } 87 | if ($targetFeedsExtraData !== null) { 88 | $payload['target_feeds_extra_data'] = $targetFeedsExtraData; 89 | } 90 | $response = $this->doRequest('POST', 'reaction/', $payload); 91 | $body = $response->getBody()->getContents(); 92 | return json_decode($body, true); 93 | } 94 | 95 | /** 96 | * @param string $kind 97 | * @param string $parentId 98 | * @param string $userId 99 | * @param ?array $data 100 | * @param ?array $targetFeeds 101 | * @param ?array $targetFeedsExtraData 102 | * 103 | * @return array 104 | */ 105 | public function addChild($kind, $parentId, $userId, array $data=null, array $targetFeeds=null, $targetFeedsExtraData=null) 106 | { 107 | $payload = [ 108 | 'kind' => $kind, 109 | 'parent' => $parentId, 110 | 'user_id' => $userId, 111 | ]; 112 | if ($data !== null) { 113 | $payload['data'] = $data; 114 | } 115 | if ($targetFeeds !== null) { 116 | $payload['target_feeds'] = $targetFeeds; 117 | } 118 | if ($targetFeedsExtraData !== null) { 119 | $payload['target_feeds_extra_data'] = $targetFeedsExtraData; 120 | } 121 | $response = $this->doRequest('POST', 'reaction/', $payload); 122 | $body = $response->getBody()->getContents(); 123 | return json_decode($body, true); 124 | } 125 | 126 | /** 127 | * @param string $reactionId 128 | * 129 | * @return string 130 | */ 131 | public function createReference($reactionId) 132 | { 133 | $myReactionId = $reactionId; 134 | if (is_array($reactionId) && array_key_exists('id', $reactionId)) { 135 | $myReactionId = $reactionId['id']; 136 | } 137 | return 'SR:' . $myReactionId; 138 | } 139 | 140 | /** 141 | * @param string $reactionId 142 | * @param bool $soft // soft delete the reaction so it can be restored afterwards 143 | * 144 | * @return array 145 | */ 146 | public function delete($reactionId, bool $soft=false) 147 | { 148 | $response = $this->doRequest('DELETE', 'reaction/' . $reactionId . '/?soft=' . ($soft ? 'true' : 'false')); 149 | $body = $response->getBody()->getContents(); 150 | return json_decode($body, true); 151 | } 152 | 153 | /** 154 | * @param string $reactionId 155 | * 156 | * @return array 157 | */ 158 | public function restore($reactionId) 159 | { 160 | $response = $this->doRequest('PUT', 'reaction/' . $reactionId . '/restore/'); 161 | $body = $response->getBody()->getContents(); 162 | return json_decode($body, true); 163 | } 164 | 165 | /** 166 | * @param string $lookupField 167 | * @param string $lookupValue 168 | * @param string $kind 169 | * @param ?array $params // for pagination parameters e.g. ["limit" => "10"] 170 | * 171 | * @return array 172 | */ 173 | public function filter($lookupField, $lookupValue, $kind=null, array $params=null) 174 | { 175 | if (!in_array($lookupField, ["reaction_id", "activity_id", "user_id"])) { 176 | throw new StreamFeedException("Invalid request parameters"); 177 | } 178 | $endpoint = "reaction/" . $lookupField . "/" . $lookupValue . "/"; 179 | if ($kind !== null) { 180 | $endpoint .= $kind . "/"; 181 | } 182 | $response = $this->doRequest('GET', $endpoint, $params); 183 | $body = $response->getBody()->getContents(); 184 | return json_decode($body, true); 185 | } 186 | 187 | 188 | /** 189 | * @param string $reactionId 190 | * 191 | * @return array 192 | */ 193 | public function get($reactionId) 194 | { 195 | $response = $this->doRequest('GET', 'reaction/' . $reactionId . '/'); 196 | $body = $response->getBody()->getContents(); 197 | return json_decode($body, true); 198 | } 199 | 200 | /** 201 | * @param string $reactionId 202 | * @param ?array $data 203 | * @param ?array $targetFeeds 204 | * @param array $targetFeedsExtraData 205 | * 206 | * @return array 207 | */ 208 | public function update($reactionId, array $data=null, array $targetFeeds=null, $targetFeedsExtraData=null) 209 | { 210 | $payload = []; 211 | if ($data !== null) { 212 | $payload['data'] = $data; 213 | } 214 | if ($targetFeeds !== null) { 215 | $payload['target_feeds'] = $targetFeeds; 216 | } 217 | if ($targetFeedsExtraData !== null) { 218 | $payload['target_feeds_extra_data'] = $targetFeedsExtraData; 219 | } 220 | $response = $this->doRequest('PUT', 'reaction/' . $reactionId . '/', $payload); 221 | $body = $response->getBody()->getContents(); 222 | return json_decode($body, true); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/BaseFeed.php: -------------------------------------------------------------------------------- 1 | validFeedSlug($feed_slug)) { 69 | throw new StreamFeedException('feed_slug can only contain alphanumeric characters or underscores'); 70 | } 71 | 72 | if (!$this->validUserId($user_id)) { 73 | throw new StreamFeedException('user_id can only contain alphanumeric characters, underscores or dashes'); 74 | } 75 | 76 | $this->slug = $feed_slug; 77 | $this->user_id = $user_id; 78 | $this->id = "$feed_slug:$user_id"; 79 | $this->base_feed_url = "feed/{$feed_slug}/{$user_id}"; 80 | 81 | $this->token = $token; 82 | $this->api_key = $api_key; 83 | 84 | $this->client = $client; 85 | } 86 | 87 | /** 88 | * @param string $feed_slug 89 | * 90 | * @return bool 91 | */ 92 | public function validFeedSlug($feed_slug) 93 | { 94 | return (preg_match('/^\w+$/', $feed_slug) === 1); 95 | } 96 | 97 | /** 98 | * @param string $user_id 99 | * 100 | * @return bool 101 | */ 102 | public function validUserId($user_id) 103 | { 104 | return (preg_match('/^[-\w]+$/', $user_id) === 1); 105 | } 106 | 107 | /** 108 | * @return string 109 | */ 110 | public function getReadonlyToken() 111 | { 112 | return $this->client->createFeedJWTToken($this, '*', 'read'); 113 | } 114 | 115 | /** 116 | * @return string 117 | */ 118 | public function getToken() 119 | { 120 | return $this->token; 121 | } 122 | 123 | /** 124 | * @return string 125 | */ 126 | public function getId() 127 | { 128 | return $this->id; 129 | } 130 | 131 | /** 132 | * @return string 133 | */ 134 | public function getSlug() 135 | { 136 | return $this->slug; 137 | } 138 | 139 | /** 140 | * @return string 141 | */ 142 | public function getUserId() 143 | { 144 | return $this->user_id; 145 | } 146 | 147 | /** 148 | * @param array $to 149 | * 150 | * @return array 151 | */ 152 | public function signToField($to) 153 | { 154 | return array_map(function ($recipient) { 155 | $bits = explode(':', $recipient); 156 | $recipient_feed = $this->client->feed($bits[0], $bits[1]); 157 | $recipient_token = $recipient_feed->getToken(); 158 | 159 | return "$recipient $recipient_token"; 160 | }, $to); 161 | } 162 | 163 | /** 164 | * @param array $activity 165 | * @return mixed 166 | * 167 | * @throws StreamFeedException 168 | */ 169 | public function addActivity($activity) 170 | { 171 | if (array_key_exists('to', $activity)) { 172 | $activity['to'] = $this->signToField($activity['to']); 173 | } 174 | 175 | return $this->makeHttpRequest("{$this->base_feed_url}/", 'POST', $activity, [], 'feed', 'write'); 176 | } 177 | 178 | /** 179 | * @param array $activities 180 | * @return mixed 181 | * 182 | * @throws StreamFeedException 183 | */ 184 | public function addActivities($activities) 185 | { 186 | foreach ($activities as &$activity) { 187 | if (array_key_exists('to', $activity)) { 188 | $activity['to'] = $this->signToField($activity['to']); 189 | } 190 | } 191 | 192 | return $this->makeHttpRequest("{$this->base_feed_url}/", 'POST', compact('activities'), [], 'feed', 'write'); 193 | } 194 | 195 | /** 196 | * @param string $activity_id 197 | * @param bool $foreign_id 198 | * @return mixed 199 | * 200 | * @throws StreamFeedException 201 | */ 202 | public function removeActivity($activity_id, $foreign_id = false) 203 | { 204 | $query_params = []; 205 | 206 | if ($foreign_id === true) { 207 | $query_params['foreign_id'] = 1; 208 | } 209 | 210 | return $this->makeHttpRequest("{$this->base_feed_url}/{$activity_id}/", 'DELETE', [], $query_params, 'feed', 'delete'); 211 | } 212 | 213 | /** 214 | * @param int $offset 215 | * @param int $limit 216 | * @param array $options 217 | * @return mixed 218 | * 219 | * @throws StreamFeedException 220 | */ 221 | public function getActivities($offset = 0, $limit = 20, $options = [], $enrich=false, $reactions = null) 222 | { 223 | if ($options === null) { 224 | $options = []; 225 | } 226 | $query_params = ['offset' => $offset, 'limit' => $limit]; 227 | if (array_key_exists('mark_read', $options) && is_array($options['mark_read'])) { 228 | $options['mark_read'] = implode(',', $options['mark_read']); 229 | } 230 | if (array_key_exists('mark_seen', $options) && is_array($options['mark_seen'])) { 231 | $options['mark_seen'] = implode(',', $options['mark_seen']); 232 | } 233 | $query_params = array_merge($query_params, $options); 234 | 235 | if ($reactions !== null) { 236 | if (!is_array($reactions)) { 237 | throw new StreamFeedException("reactions argument should be an associative array"); 238 | } 239 | if (isset($reactions["own"]) && $reactions["own"]) { 240 | $query_params["withOwnReactions"] = true; 241 | $enrich = true; 242 | } 243 | if (isset($reactions["recent"]) && $reactions["recent"]) { 244 | $query_params["withRecentReactions"] = true; 245 | $enrich = true; 246 | } 247 | if (isset($reactions["counts"]) && $reactions["counts"]) { 248 | $query_params["withReactionCounts"] = true; 249 | $enrich = true; 250 | } 251 | if (isset($reactions["kinds"]) && $reactions["kinds"]) { 252 | $query_params["reactionKindsFilter"] = implode(",", $reactions["kinds"]); 253 | $enrich = true; 254 | } 255 | } 256 | 257 | $prefix_enrich = $enrich ? 'enrich/' : ''; 258 | 259 | return $this->makeHttpRequest("{$prefix_enrich}{$this->base_feed_url}/", 'GET', [], $query_params, 'feed', 'read'); 260 | } 261 | 262 | /** 263 | * @param string $targetFeedSlug 264 | * @param string $targetUserId 265 | * @param int $activityCopyLimit 266 | * 267 | * @return mixed 268 | * 269 | * @throws StreamFeedException 270 | */ 271 | public function follow($targetFeedSlug, $targetUserId, $activityCopyLimit = 300) 272 | { 273 | $data = [ 274 | 'target' => "$targetFeedSlug:$targetUserId", 275 | 'activity_copy_limit' => $activityCopyLimit 276 | ]; 277 | if (null !== $this->client) { 278 | $target_feed = $this->client->feed($targetFeedSlug, $targetUserId); 279 | $data['target_token'] = $target_feed->getToken(); 280 | } 281 | 282 | return $this->makeHttpRequest("{$this->base_feed_url}/follows/", 'POST', $data, [], 'follower', 'write'); 283 | } 284 | 285 | /** 286 | * @param int $offset 287 | * @param int $limit 288 | * @return mixed 289 | * 290 | * @throws StreamFeedException 291 | */ 292 | public function followers($offset = 0, $limit = 25) 293 | { 294 | $query_params = [ 295 | 'limit' => $limit, 296 | 'offset' => $offset, 297 | ]; 298 | 299 | return $this->makeHttpRequest("{$this->base_feed_url}/followers/", 'GET', [], $query_params, 'follower', 'read'); 300 | } 301 | 302 | /** 303 | * @param int $offset 304 | * @param int $limit 305 | * @param array $filter 306 | * @return mixed 307 | * 308 | * @throws StreamFeedException 309 | */ 310 | public function following($offset = 0, $limit = 25, $filter = []) 311 | { 312 | $query_params = [ 313 | 'limit' => $limit, 314 | 'offset' => $offset, 315 | 'filter' => implode(',', $filter), 316 | ]; 317 | return $this->makeHttpRequest("{$this->base_feed_url}/follows/", 'GET', [], $query_params, 'follower', 'read'); 318 | } 319 | 320 | /** 321 | * @param string $targetFeedSlug 322 | * @param string $targetUserId 323 | * @param bool $keepHistory 324 | * 325 | * @return mixed 326 | * 327 | * @throws StreamFeedException 328 | */ 329 | public function unfollow($targetFeedSlug, $targetUserId, $keepHistory = false) 330 | { 331 | $queryParams = []; 332 | if ($keepHistory) { 333 | $queryParams['keep_history'] = 'true'; 334 | } 335 | $targetFeedId = "$targetFeedSlug:$targetUserId"; 336 | return $this->makeHttpRequest("{$this->base_feed_url}/follows/{$targetFeedId}/", 'DELETE', [], $queryParams, 'follower', 'delete'); 337 | } 338 | 339 | /** 340 | * @param string $foreign_id 341 | * @param string $time 342 | * @param array $new_targets 343 | * @param array $added_targets 344 | * @param array $removed_targets 345 | * @return mixed 346 | * 347 | * @throws StreamFeedException 348 | */ 349 | public function updateActivityToTargets($foreign_id, $time, $new_targets = [], $added_targets = [], $removed_targets = []) 350 | { 351 | $data = [ 352 | 'foreign_id' => $foreign_id, 353 | 'time' => $time, 354 | ]; 355 | 356 | if ($new_targets) { 357 | $data['new_targets'] = $new_targets; 358 | } 359 | 360 | if ($added_targets) { 361 | $data['added_targets'] = $added_targets; 362 | } 363 | 364 | if ($removed_targets) { 365 | $data['removed_targets'] = $removed_targets; 366 | } 367 | return $this->makeHttpRequest("feed_targets/{$this->slug}/{$this->user_id}/activity_to_targets/", 'POST', $data, [], 'feed_targets', 'write'); 368 | } 369 | 370 | /** 371 | * @return \GuzzleHttp\HandlerStack 372 | */ 373 | public function getHandlerStack() 374 | { 375 | return HandlerStack::create(); 376 | } 377 | 378 | /** 379 | * @return \GuzzleHttp\Client 380 | */ 381 | public function getHttpClient() 382 | { 383 | $handler = $this->client->getCustomHttpHandlerStack(); 384 | if (!$handler) { 385 | $handler = $this->getHandlerStack(); 386 | } 387 | 388 | return new GuzzleClient([ 389 | 'base_uri' => $this->client->getBaseUrl(), 390 | 'timeout' => $this->client->timeout, 391 | 'handler' => $handler, 392 | 'headers' => ['Accept-Encoding' => 'gzip'], 393 | ]); 394 | } 395 | 396 | public function setGuzzleDefaultOption($option, $value) 397 | { 398 | $this->guzzleOptions[$option] = $value; 399 | } 400 | 401 | /** 402 | * @param string $resource 403 | * @param string $action 404 | * @return array 405 | */ 406 | protected function getHttpRequestHeaders($resource, $action) 407 | { 408 | $token = $this->client->createFeedJWTToken($this, $resource, $action); 409 | 410 | return [ 411 | 'Authorization' => $token, 412 | 'Content-Type' => 'application/json', 413 | 'stream-auth-type' => 'jwt', 414 | 'X-Stream-Client' => 'stream-php-client-' . Constant::VERSION, 415 | ]; 416 | } 417 | 418 | /** 419 | * @param string $uri 420 | * @param string $method 421 | * @param array $data 422 | * @param array $query_params 423 | * @param string $resource 424 | * @param string $action 425 | * @return mixed 426 | * @throws StreamFeedException 427 | */ 428 | public function makeHttpRequest($uri, $method, $data = [], $query_params = [], $resource = '', $action = '') 429 | { 430 | $query_params['api_key'] = $this->api_key; 431 | $client = $this->getHttpClient(); 432 | $headers = $this->getHttpRequestHeaders($resource, $action); 433 | 434 | $uri = (new Uri($this->client->buildRequestUrl($uri))) 435 | ->withQuery(http_build_query($query_params)); 436 | 437 | $options = $this->guzzleOptions; 438 | $options['headers'] = $headers; 439 | 440 | if ($method === 'POST') { 441 | $options['json'] = $data; 442 | } 443 | 444 | try { 445 | $response = $client->request($method, $uri, $options); 446 | } catch (ClientException $e) { 447 | $response = $e->getResponse(); 448 | $msg = $response->getBody(); 449 | $code = $response->getStatusCode(); 450 | $previous = $e; 451 | throw new StreamFeedException($msg, $code, $previous); 452 | } 453 | 454 | $body = $response->getBody()->getContents(); 455 | 456 | return json_decode($body, true); 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /lib/GetStream/Stream/Client.php: -------------------------------------------------------------------------------- 1 | api_key = $api_key; 62 | $this->api_secret = $api_secret; 63 | $this->signer = new Signer($api_key, $api_secret); 64 | $this->api_version = $api_version; 65 | $this->timeout = $timeout; 66 | $this->location = $location; 67 | $this->protocol = 'https'; 68 | } 69 | 70 | /** 71 | * @param HandlerStack $handler 72 | * @return void 73 | */ 74 | public function setCustomHttpHandler($handler) 75 | { 76 | $this->customHttpHandler = $handler; 77 | } 78 | 79 | /** 80 | * @return HandlerStack|null 81 | */ 82 | public function getCustomHttpHandlerStack() 83 | { 84 | return $this->customHttpHandler; 85 | } 86 | 87 | /** 88 | * @param string|null $url 89 | * @return Client 90 | * @throws Exception 91 | */ 92 | public static function herokuConnect($url = null) 93 | { 94 | if ($url === null) { 95 | $url = getenv('STREAM_URL'); 96 | } 97 | 98 | $parsed_url = parse_url($url); 99 | '@phan-var array $parsed_url'; 100 | 101 | $api_key = $parsed_url['user']; 102 | $api_secret = $parsed_url['pass']; 103 | 104 | if ($api_key == '' || $api_secret == '') { 105 | throw new Exception('url malformed'); 106 | } 107 | $client = new static($api_key, $api_secret); 108 | $location = explode('stream-io-api.com', $parsed_url['host'])[0]; 109 | $location = str_replace('.', '', $location); 110 | $client->setLocation($location); 111 | return $client; 112 | } 113 | 114 | /** 115 | * @param string $protocol 116 | */ 117 | public function setProtocol($protocol) 118 | { 119 | $this->protocol = $protocol; 120 | } 121 | 122 | /** 123 | * @param string $location 124 | */ 125 | public function setLocation($location) 126 | { 127 | $this->location = $location; 128 | } 129 | 130 | /** 131 | * @param string $user_id 132 | * @param ?array $extra_data 133 | * @return string 134 | */ 135 | public function createUserSessionToken($user_id, array $extra_data=null) 136 | { 137 | if (is_null($extra_data)) { 138 | $extra_data = []; 139 | } 140 | return $this->createUserToken($user_id, $extra_data); 141 | } 142 | 143 | /** 144 | * @param string $user_id 145 | * @param ?array $extra_data 146 | * @return string 147 | */ 148 | public function createUserToken($user_id, array $extra_data=null) 149 | { 150 | if (is_null($extra_data)) { 151 | $extra_data = []; 152 | } 153 | return $this->signer->jwtUserSessionToken($user_id, $extra_data); 154 | } 155 | 156 | /** 157 | * @param BaseFeedInterface $feed 158 | * @param string $resource 159 | * @param string $action 160 | * @return string 161 | */ 162 | public function createFeedJWTToken($feed, $resource, $action) 163 | { 164 | $feedId = "{$feed->getSlug()}{$feed->getUserId()}"; 165 | return $this->signer->jwtScopeToken($feedId, $resource, $action); 166 | } 167 | 168 | /** 169 | * @param string $feed_slug 170 | * @param string $user_id 171 | * @param string|null $token 172 | * @return FeedInterface 173 | */ 174 | public function feed($feed_slug, $user_id, $token = null) 175 | { 176 | if (null === $token) { 177 | $token = $this->signer->signature($feed_slug . $user_id); 178 | } 179 | return new Feed($this, $feed_slug, $user_id, $this->api_key, $token); 180 | } 181 | 182 | /** 183 | * @return Batcher 184 | */ 185 | public function batcher() 186 | { 187 | return new Batcher($this, $this->signer, $this->api_key); 188 | } 189 | 190 | /** 191 | * @return Personalization 192 | */ 193 | public function personalization() 194 | { 195 | return new Personalization($this, $this->api_key, $this->api_secret); 196 | } 197 | 198 | /** 199 | * @return Collections 200 | */ 201 | public function collections() 202 | { 203 | return new Collections($this, $this->api_key, $this->api_secret); 204 | } 205 | 206 | /** 207 | * @return Reactions 208 | */ 209 | public function reactions() 210 | { 211 | return new Reactions($this, $this->api_key, $this->api_secret); 212 | } 213 | 214 | /** 215 | * @return Users 216 | */ 217 | public function users() 218 | { 219 | return new Users($this, $this->api_key, $this->api_secret); 220 | } 221 | 222 | /** 223 | * @return string 224 | */ 225 | public function getBaseUrl() 226 | { 227 | $baseUrl = getenv('STREAM_BASE_URL'); 228 | if (!$baseUrl) { 229 | $api_endpoint = static::API_ENDPOINT; 230 | $localPort = getenv('STREAM_LOCAL_API_PORT'); 231 | if ($localPort) { 232 | $baseUrl = "http://localhost:$localPort/api"; 233 | } else { 234 | if ($this->location) { 235 | $subdomain = "{$this->location}-api"; 236 | } else { 237 | $subdomain = 'api'; 238 | } 239 | $baseUrl = "{$this->protocol}://{$subdomain}." . $api_endpoint; 240 | } 241 | } 242 | return $baseUrl; 243 | } 244 | 245 | /** 246 | * @param string $uri 247 | * @return string 248 | */ 249 | public function buildRequestUrl($uri) 250 | { 251 | $baseUrl = $this->getBaseUrl(); 252 | return "{$baseUrl}/{$this->api_version}/{$uri}"; 253 | } 254 | 255 | public function getActivities($ids=null, $foreign_id_times=null, $enrich=false, $reactions = null) 256 | { 257 | if ($ids!==null) { 258 | $query_params = ["ids" => join(',', $ids)]; 259 | } else { 260 | $fids = []; 261 | $times = []; 262 | foreach ($foreign_id_times as $fit) { 263 | $fids[] = $fit[0]; 264 | try { 265 | $times[] = $fit[1]->format(DateTime::ISO8601); 266 | } catch (Exception $e) { 267 | // assume it's in the right format already 268 | $times[] = $fit[1]; 269 | } 270 | } 271 | $query_params = [ 272 | "foreign_ids" => join(',', $fids), 273 | "timestamps" => join(',', $times) 274 | ]; 275 | } 276 | 277 | if ($reactions !== null) { 278 | if (!is_array($reactions)) { 279 | throw new StreamFeedException("reactions argument should be an associative array"); 280 | } 281 | if (isset($reactions["own"]) && $reactions["own"]) { 282 | $query_params["withOwnReactions"] = true; 283 | if (isset($reactions["user_id"]) && $reactions["user_id"]) { 284 | $query_params["user_id"] = $reactions["user_id"]; 285 | } 286 | $enrich = true; 287 | } 288 | if (isset($reactions["recent"]) && $reactions["recent"]) { 289 | $query_params["withRecentReactions"] = true; 290 | $enrich = true; 291 | } 292 | if (isset($reactions["counts"]) && $reactions["counts"]) { 293 | $query_params["withReactionCounts"] = true; 294 | $enrich = true; 295 | } 296 | if (isset($reactions["kinds"]) && $reactions["kinds"]) { 297 | $query_params["reactionKindsFilter"] = implode(",", $reactions["kinds"]); 298 | $enrich = true; 299 | } 300 | } 301 | 302 | $token = $this->signer->jwtScopeToken('*', 'activities', '*'); 303 | $activities = new Activities($this, $this->api_key, $token); 304 | return $activities->_getActivities($query_params, $enrich); 305 | } 306 | 307 | public function batchPartialActivityUpdate($data) 308 | { 309 | if (count($data) > 100) { 310 | throw new Exception("Max 100 activities allowed in batch update"); 311 | } 312 | $token = $this->signer->jwtScopeToken('*', 'activities', '*'); 313 | $activityUpdateOp = new ActivitiesOperation($this, $this->api_key, $token); 314 | return $activityUpdateOp->partiallyUpdateActivity(["changes" => $data]); 315 | } 316 | 317 | public function doPartialActivityUpdate($id=null, $foreign_id=null, $time=null, $set=null, $unset=null) 318 | { 319 | $token = $this->signer->jwtScopeToken('*', 'activities', '*'); 320 | if ($id === null && ($foreign_id === null || $time === null)) { 321 | throw new Exception( 322 | "The id or foreign_id+time parameters must be provided and not be None" 323 | ); 324 | } 325 | if ($id !== null && ($foreign_id !== null || $time !== null)) { 326 | throw new Exception( 327 | "Only one of the id or the foreign_id+time parameters can be provided" 328 | ); 329 | } 330 | 331 | $data = ["set" => $set, "unset" => $unset]; 332 | 333 | if ($id !== null) { 334 | $data["id"] = $id; 335 | } else { 336 | $data["foreign_id"] = $foreign_id; 337 | $data["time"] = $time; 338 | } 339 | $activityUpdateOp = new ActivitiesOperation($this, $this->api_key, $token); 340 | return $activityUpdateOp->partiallyUpdateActivity($data); 341 | } 342 | 343 | public function updateActivities($activities) 344 | { 345 | if (empty($activities)) { 346 | return; 347 | } 348 | $token = $this->signer->jwtScopeToken('*', 'activities', '*'); 349 | $activityUpdateOp = new ActivitiesOperation($this, $this->api_key, $token); 350 | return $activityUpdateOp->updateActivities($activities); 351 | } 352 | 353 | public function updateActivity($activity) 354 | { 355 | return $this->updateActivities([$activity]); 356 | } 357 | 358 | private function getAppActivities($data) 359 | { 360 | $token = $this->signer->jwtScopeToken('*', 'activities', '*'); 361 | $op = new ActivitiesOperation($this, $this->api_key, $token); 362 | return $op->getAppActivities($data); 363 | } 364 | 365 | /** 366 | * Retrieves activities for the current app having the given IDs. 367 | * @param array $ids 368 | * @return mixed 369 | */ 370 | public function getActivitiesById($ids = []) 371 | { 372 | return $this->getAppActivities(['ids' => $ids]); 373 | } 374 | 375 | /** 376 | * Retrieves activities for the current app having the given list of [foreign ID, time] elements. 377 | * @param array $foreignIdTimes 378 | * @return mixed 379 | */ 380 | public function getActivitiesByForeignId($foreignIdTimes = []) 381 | { 382 | $foreignIds = []; 383 | $timestamps = []; 384 | foreach ($foreignIdTimes as $fidTime) { 385 | if (!is_array($fidTime) || count($fidTime) != 2) { 386 | throw new StreamFeedException('malformed foreign ID and time combination'); 387 | } 388 | array_push($foreignIds, $fidTime[0]); 389 | array_push($timestamps, $fidTime[1]); 390 | } 391 | return $this->getAppActivities(['foreign_ids' => $foreignIds, 'timestamps' => $timestamps]); 392 | } 393 | 394 | private function activityPartialUpdate($data = []) 395 | { 396 | $token = $this->signer->jwtScopeToken('*', 'activities', '*'); 397 | $op = new ActivitiesOperation($this, $this->api_key, $token); 398 | return $op->activityPartialUpdate($data); 399 | } 400 | 401 | /** 402 | * Performs an activity partial update by the given activity ID. 403 | * @param string $id 404 | * @param mixed $set 405 | * @param array $unset 406 | * @return mixed 407 | */ 408 | public function activityPartialUpdateById($id, $set = [], $unset = []) 409 | { 410 | return $this->activityPartialUpdate(['id' => $id, 'set' => $set, 'unset' => $unset]); 411 | } 412 | 413 | /** 414 | * Performs an activity partial update by the given foreign ID and time. 415 | * @param string $foreign_id 416 | * @param DateTime|int $time 417 | * @param mixed $set 418 | * @param array $unset 419 | * @return mixed 420 | */ 421 | public function activityPartialUpdateByForeignId($foreign_id, $time, $set = [], $unset = []) 422 | { 423 | return $this->activityPartialUpdate( 424 | [ 425 | 'foreign_id' => $foreign_id, 426 | 'time' => $time, 427 | 'set' => $set, 428 | 'unset' => $unset 429 | ] 430 | ); 431 | } 432 | 433 | /** 434 | * Creates a redirect url for tracking the given events in the context of 435 | * getstream.io/personalization 436 | * @param string $targetUrl 437 | * @param array $events 438 | * @return string 439 | */ 440 | public function createRedirectUrl($targetUrl, $events) 441 | { 442 | $token = $this->signer->jwtScopeToken('*', 'redirect_and_track', '*'); 443 | $analytics = new Analytics($this, $this->api_key, $token); 444 | return $analytics->createRedirectUrl($targetUrl, $events); 445 | } 446 | } 447 | --------------------------------------------------------------------------------