diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e02bc..06b5b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -350,4 +350,4 @@ before continuing with v3.0.0 of this library. - Added `client.search` - Added `client.update_users_partial` - Added `client.update_user_partial` -- Added `client.reactivate_user` +- Added `client.reactivate_user` \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d0cd50..d24186a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,3 @@ - # :recycle: Contributing We welcome code changes that improve this library or fix a problem, please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github. We are very happy to merge your code in the official repository. Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first. See our license file for more details. @@ -63,6 +62,34 @@ Recommended settings: } ``` +For Docker-based development, you can use: + +```shell +$ make lint_with_docker # Run linters in Docker +$ make lint-fix_with_docker # Fix linting issues in Docker +$ make test_with_docker # Run tests in Docker +$ make check_with_docker # Run both linters and tests in Docker +$ make sorbet_with_docker # Run Sorbet type checker in Docker +``` + +You can customize the Ruby version used in Docker by setting the RUBY_VERSION variable: + +```shell +$ RUBY_VERSION=3.1 make test_with_docker +``` + +By default, the API client connects to the production Stream Chat API. You can override this by setting the STREAM_CHAT_URL environment variable: + +```shell +$ STREAM_CHAT_URL=http://localhost:3030 make test +``` + +When running tests in Docker, the `test_with_docker` command automatically sets up networking to allow the Docker container to access services running on your host machine via `host.docker.internal`. This is particularly useful for connecting to a local Stream Chat server: + +```shell +$ STREAM_CHAT_URL=http://host.docker.internal:3030 make test_with_docker +``` + ### Commit message convention This repository follows a commit message convention in order to automatically generate the [CHANGELOG](./CHANGELOG.md). Make sure you follow the rules of [conventional commits](https://www.conventionalcommits.org/) when opening a pull request. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6ab2e83 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +STREAM_KEY ?= NOT_EXIST +STREAM_SECRET ?= NOT_EXIST +RUBY_VERSION ?= 3.0 +STREAM_CHAT_URL ?= https://chat.stream-io-api.com + +# These targets are not files +.PHONY: help check test lint lint-fix test_with_docker lint_with_docker lint-fix_with_docker + +help: ## Display this help message + @echo "Please use \`make \` where is one of" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; \ + {printf "\033[36m%-40s\033[0m %s\n", $$1, $$2}' + +lint: ## Run linters + bundle exec rubocop + +lint-fix: ## Fix linting issues + bundle exec rubocop -a + +test: ## Run tests + STREAM_KEY=$(STREAM_KEY) STREAM_SECRET=$(STREAM_SECRET) bundle exec rspec + +check: lint test ## Run linters + tests + +console: ## Start a console with the gem loaded + bundle exec rake console + +lint_with_docker: ## Run linters in Docker (set RUBY_VERSION to change Ruby version) + docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec rubocop" + +lint-fix_with_docker: ## Fix linting issues in Docker (set RUBY_VERSION to change Ruby version) + docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec rubocop -a" + +test_with_docker: ## Run tests in Docker (set RUBY_VERSION to change Ruby version) + docker run -t -i -w /code -v $(PWD):/code --add-host=host.docker.internal:host-gateway -e STREAM_KEY=$(STREAM_KEY) -e STREAM_SECRET=$(STREAM_SECRET) -e "STREAM_CHAT_URL=http://host.docker.internal:3030" ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec rspec" + +check_with_docker: lint_with_docker test_with_docker ## Run linters + tests in Docker (set RUBY_VERSION to change Ruby version) + +sorbet: ## Run Sorbet type checker + bundle exec srb tc + +sorbet_with_docker: ## Run Sorbet type checker in Docker (set RUBY_VERSION to change Ruby version) + docker run -t -i -w /code -v $(PWD):/code ruby:$(RUBY_VERSION) sh -c "gem install bundler && bundle install && bundle exec srb tc" + +coverage: ## Generate test coverage report + COVERAGE=true bundle exec rspec + @echo "Coverage report available at ./coverage/index.html" + +reviewdog: ## Run reviewdog for CI + bundle exec rubocop --format json | reviewdog -f=rubocop -name=rubocop -reporter=github-pr-review \ No newline at end of file diff --git a/README.md b/README.md index a5184e7..f7d2926 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,29 @@ deleted_message = client.delete_message(m1['message']['id']) ``` +### Reminders + +```ruby +# Create a reminder for a message +reminder = client.create_reminder(m1['message']['id'], 'bob-1', DateTime.now + 1) + +# Create a reminder without a notification time (just mark for later) +reminder = client.create_reminder(m1['message']['id'], 'bob-1') + +# Update a reminder +updated_reminder = client.update_reminder(m1['message']['id'], 'bob-1', DateTime.now + 2) + +# Delete a reminder +client.delete_reminder(m1['message']['id'], 'bob-1') + +# Query reminders for a user +reminders = client.query_reminders('bob-1') + +# Query reminders with filters +filter = { 'channel_cid' => 'messaging:bob-and-jane' } +reminders = client.query_reminders('bob-1', filter) +``` + ### Devices ```ruby diff --git a/lib/stream-chat/client.rb b/lib/stream-chat/client.rb index 8d4944c..fa587e1 100644 --- a/lib/stream-chat/client.rb +++ b/lib/stream-chat/client.rb @@ -7,6 +7,7 @@ require 'faraday/net_http_persistent' require 'jwt' require 'time' +require 'date' require 'sorbet-runtime' require 'stream-chat/channel' require 'stream-chat/errors' @@ -688,7 +689,7 @@ def delete_channels(cids, hard_delete: false) # Revoke tokens for an application issued since the given date. sig { params(before: T.any(DateTime, String)).returns(StreamChat::StreamResponse) } def revoke_tokens(before) - before = T.cast(before, DateTime).rfc3339 if before.instance_of?(DateTime) + before = before.rfc3339 if before.instance_of?(DateTime) update_app_settings({ 'revoke_tokens_issued_before' => before }) end @@ -701,7 +702,7 @@ def revoke_user_token(user_id, before) # Revoke tokens for users issued since. sig { params(user_ids: T::Array[String], before: T.any(DateTime, String)).returns(StreamChat::StreamResponse) } def revoke_users_token(user_ids, before) - before = T.cast(before, DateTime).rfc3339 if before.instance_of?(DateTime) + before = before.rfc3339 if before.instance_of?(DateTime) updates = [] user_ids.map do |user_id| @@ -939,6 +940,55 @@ def query_threads(filter, sort: nil, **options) post('threads', data: params) end + # Creates a reminder for a message. + # @param message_id [String] The ID of the message to create a reminder for + # @param user_id [String] The ID of the user creating the reminder + # @param remind_at [DateTime, nil] When to remind the user (optional) + # @return [StreamChat::StreamResponse] API response + sig { params(message_id: String, user_id: String, remind_at: T.nilable(DateTime)).returns(StreamChat::StreamResponse) } + def create_reminder(message_id, user_id, remind_at = nil) + data = { user_id: user_id } + data[:remind_at] = remind_at.rfc3339 if remind_at.instance_of?(DateTime) + post("messages/#{message_id}/reminders", data: data) + end + + # Updates a reminder for a message. + # @param message_id [String] The ID of the message with the reminder + # @param user_id [String] The ID of the user who owns the reminder + # @param remind_at [DateTime, nil] When to remind the user (optional) + # @return [StreamChat::StreamResponse] API response + sig { params(message_id: String, user_id: String, remind_at: T.nilable(DateTime)).returns(StreamChat::StreamResponse) } + def update_reminder(message_id, user_id, remind_at = nil) + data = { user_id: user_id } + data[:remind_at] = remind_at.rfc3339 if remind_at + patch("messages/#{message_id}/reminders", data: data) + end + + # Deletes a reminder for a message. + # @param message_id [String] The ID of the message with the reminder + # @param user_id [String] The ID of the user who owns the reminder + # @return [StreamChat::StreamResponse] API response + sig { params(message_id: String, user_id: String).returns(StreamChat::StreamResponse) } + def delete_reminder(message_id, user_id) + delete("messages/#{message_id}/reminders", params: { user_id: user_id }) + end + + # Queries reminders based on filter conditions. + # @param user_id [String] The ID of the user whose reminders to query + # @param filter_conditions [Hash] Conditions to filter reminders + # @param sort [Array, nil] Sort parameters (default: [{ field: 'remind_at', direction: 1 }]) + # @param options [Hash] Additional query options like limit, offset + # @return [StreamChat::StreamResponse] API response with reminders + sig { params(user_id: String, filter_conditions: T::Hash[T.untyped, T.untyped], sort: T.nilable(T::Array[T::Hash[T.untyped, T.untyped]]), options: T.untyped).returns(StreamChat::StreamResponse) } + def query_reminders(user_id, filter_conditions = {}, sort: nil, **options) + params = options.merge({ + filter_conditions: filter_conditions, + sort: sort || [{ field: 'remind_at', direction: 1 }], + user_id: user_id + }) + post('reminders/query', data: params) + end + private sig { returns(T::Hash[String, String]) } diff --git a/spec/channel_spec.rb b/spec/channel_spec.rb index 0962463..9ff2755 100644 --- a/spec/channel_spec.rb +++ b/spec/channel_spec.rb @@ -185,6 +185,7 @@ def loop_times(times) end it 'can mark messages as read' do + @channel.add_members([@random_user[:id]]) response = @channel.mark_read(@random_user[:id]) expect(response).to include 'event' expect(response['event']['type']).to eq 'message.read' diff --git a/spec/client_spec.rb b/spec/client_spec.rb index b0e5dc9..05379ef 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -1020,4 +1020,97 @@ def loop_times(times) expect(response['threads'].length).to be >= 1 end end + + describe 'reminders' do + before do + @client = StreamChat::Client.from_env + @channel_id = SecureRandom.uuid + @channel = @client.channel('messaging', channel_id: @channel_id) + @channel.create('john') + @channel.update_partial({ config_overrides: { user_message_reminders: true } }) + @message = @channel.send_message({ 'text' => 'Hello world' }, 'john') + @message_id = @message['message']['id'] + @user_id = 'john' + end + + describe 'create_reminder' do + it 'create reminder' do + remind_at = DateTime.now + 1 + response = @client.create_reminder(@message_id, @user_id, remind_at) + + expect(response).to include('reminder') + expect(response['reminder']).to include('message_id', 'user_id', 'remind_at') + expect(response['reminder']['message_id']).to eq(@message_id) + expect(response['reminder']['user_id']).to eq(@user_id) + end + + it 'create reminder without remind_at' do + response = @client.create_reminder(@message_id, @user_id) + + expect(response).to include('reminder') + expect(response['reminder']).to include('message_id', 'user_id') + expect(response['reminder']['message_id']).to eq(@message_id) + expect(response['reminder']['user_id']).to eq(@user_id) + expect(response['reminder']['remind_at']).to be_nil + end + end + + describe 'update_reminder' do + before do + @client.create_reminder(@message_id, @user_id) + end + + it 'update reminder' do + new_remind_at = DateTime.now + 2 + response = @client.update_reminder(@message_id, @user_id, new_remind_at) + + expect(response).to include('reminder') + expect(response['reminder']).to include('message_id', 'user_id', 'remind_at') + expect(response['reminder']['message_id']).to eq(@message_id) + expect(response['reminder']['user_id']).to eq(@user_id) + expect(DateTime.parse(response['reminder']['remind_at'])).to be_within(1).of(new_remind_at) + end + end + + describe 'delete_reminder' do + before do + @client.create_reminder(@message_id, @user_id) + end + + it 'delete reminder' do + response = @client.delete_reminder(@message_id, @user_id) + expect(response).to be_a(Hash) + end + end + + describe 'query_reminders' do + before do + @reminder = @client.create_reminder(@message_id, @user_id) + end + + it 'query reminders' do + # Query reminders for the user + response = @client.query_reminders(@user_id) + + expect(response).to include('reminders') + expect(response['reminders']).to be_an(Array) + expect(response['reminders'].length).to be >= 1 + end + + it 'query reminders with channel filter' do + # Query reminders for the user in a specific channel + filter = { 'channel_cid' => @channel.cid } + response = @client.query_reminders(@user_id, filter) + + expect(response).to include('reminders') + expect(response['reminders']).to be_an(Array) + expect(response['reminders'].length).to be >= 1 + + # All reminders should have a channel_cid + response['reminders'].each do |reminder| + expect(reminder).to include('channel_cid') + end + end + end + end end