From bac80790502c5a16aec486fb462f73f44d2b1394 Mon Sep 17 00:00:00 2001 From: Artem Musalitin <38328305+MrRTi@users.noreply.github.com> Date: Tue, 22 Aug 2023 15:25:57 +0300 Subject: [PATCH] Add files upload to rooms (#46) * Add form data to request helper * Add upload_file method to rooms * Add file upload to docs --- README.md | 2 + docs/channels.md | 14 ++ docs/groups.md | 16 +- lib/rocket_chat/messages/room.rb | 28 +++- lib/rocket_chat/request_helper.rb | 14 +- spec/fixtures/files/image.png | Bin 0 -> 2335 bytes spec/fixtures/files/iterm-theme.itermcolors | 6 + spec/fixtures/files/not_accepted.svg | 4 + .../room/upload/file_upload_success.json | 62 ++++++++ .../file_upload_without_message_success.json | 40 +++++ .../room/upload/no_accepted_error.json | 5 + .../messages/room/upload/no_file_error.json | 5 + .../room/upload/png_upload_success.json | 73 +++++++++ spec/shared/room_behaviors.rb | 148 ++++++++++++++++++ 14 files changed, 414 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/files/image.png create mode 100644 spec/fixtures/files/iterm-theme.itermcolors create mode 100644 spec/fixtures/files/not_accepted.svg create mode 100644 spec/fixtures/messages/room/upload/file_upload_success.json create mode 100644 spec/fixtures/messages/room/upload/file_upload_without_message_success.json create mode 100644 spec/fixtures/messages/room/upload/no_accepted_error.json create mode 100644 spec/fixtures/messages/room/upload/no_file_error.json create mode 100644 spec/fixtures/messages/room/upload/png_upload_success.json diff --git a/README.md b/README.md index 6f2b06d..f0ffa6e 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ This gem supports the following Rocket.Chat APIs (Tested against Rocket.Chat v0. * [/api/v1/channels.online](docs/channels.md#channelsonline) * [/api/v1/channels.members](docs/channels.md#channelsmembers) * /api/v1/channels.unarchive +* [/api/v1/rooms.upload](docs/channels.md#channelsupload_file) #### Groups * /api/v1/groups.archive @@ -92,6 +93,7 @@ This gem supports the following Rocket.Chat APIs (Tested against Rocket.Chat v0. * /api/v1/groups.setType * [/api/v1/groups.members](docs/groups.md#groupsmembers) * /api/v1/groups.unarchive +* [/api/v1/rooms.upload](docs/groups.md#groupsupload_file) #### Users * [/api/v1/users.create](docs/users.md#userscreate) diff --git a/docs/channels.md b/docs/channels.md index 6b5ae5c..bafd8e6 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -182,3 +182,17 @@ session = rocket_server.login('username', 'password') session.channels.members(name: 'some_channel_name') ``` + + +### channels.upload_file + +Upload file to the room + +```ruby +require 'rocketchat' + +rocket_server = RocketChat::Server.new('http://your.server.address/') +session = rocket_server.login('username', 'password') +session.channels.upload_file(room_id: 'GENERAL', file: File, msg: "Optional Message", description: "Optional Description", tmid: "Optional thread message id") + +``` diff --git a/docs/groups.md b/docs/groups.md index 1a4b36c..85684c0 100644 --- a/docs/groups.md +++ b/docs/groups.md @@ -69,4 +69,18 @@ rocket_server = RocketChat::Server.new('http://your.server.address/') session = rocket_server.login('username', 'password') session.groups.members(name: 'some_channel_name') -``` \ No newline at end of file +``` + + +### channels.upload_file + +Upload file to the room + +```ruby +require 'rocketchat' + +rocket_server = RocketChat::Server.new('http://your.server.address/') +session = rocket_server.login('username', 'password') +session.groups.upload_file(room_id: 'GENERAL', file: File, msg: "Optional Message", description: "Optional Description", tmid: "Optional thread message id") + +``` diff --git a/lib/rocket_chat/messages/room.rb b/lib/rocket_chat/messages/room.rb index ce97f9a..7904d3c 100644 --- a/lib/rocket_chat/messages/room.rb +++ b/lib/rocket_chat/messages/room.rb @@ -9,6 +9,8 @@ class Room # rubocop:disable Metrics/ClassLength include RoomSupport include UserSupport + API_PREFIX = '/api/v1' + def self.inherited(subclass) field = subclass.name.split('::')[-1].downcase collection = "#{field}s" @@ -27,7 +29,7 @@ def initialize(session) # Full API path to call def self.api_path(method) - "/api/v1/#{collection}.#{method}" + "#{API_PREFIX}/#{collection}.#{method}" end # @@ -304,6 +306,25 @@ def members(room_id: nil, name: nil, offset: nil, count: nil, sort: nil) response['members'].map { |hash| RocketChat::User.new hash } if response['success'] end + # + # *.upload* REST API + # @param [String] room_id Rocket.Chat room id + # @param [File] file that should be uploaded to Rocket.Chat room + # @param [Hash] rest_params Optional params (msg, description, tmid) + # @return [RocketChat::Message] + # @raise [HTTPError, StatusError] + # + # https://developer.rocket.chat/reference/api/rest-api/endpoints/rooms-endpoints/upload-file-to-a-room + def upload_file(room_id:, file:, **rest_params) + response = session.request_json( + "#{API_PREFIX}/rooms.upload/#{room_id}", + method: :post, + form_data: file_upload_hash(file: file, **rest_params) + ) + + RocketChat::Message.new response['message'] if response['success'] + end + private attr_reader :session @@ -323,6 +344,11 @@ def validate_attribute(attribute) raise ArgumentError, "Unsettable attribute: #{attribute || 'nil'}" unless \ self.class.settable_attributes.include?(attribute) end + + def file_upload_hash(**params) + permited_keys_for_file_upload = %i[file msg description tmid] + Util.slice_hash(params, *permited_keys_for_file_upload) + end end end end diff --git a/lib/rocket_chat/request_helper.rb b/lib/rocket_chat/request_helper.rb index 5aa43ed..5d75838 100644 --- a/lib/rocket_chat/request_helper.rb +++ b/lib/rocket_chat/request_helper.rb @@ -104,11 +104,14 @@ def create_http(options) def create_request(path, options) headers = get_headers(options) - body = options[:body]&.reject { |_key, value| value.nil? } + body = reject_nils(options[:body]) if options[:method] == :post req = Net::HTTP::Post.new(path, headers) add_body(req, body) if body + + form_data = reject_nils(options[:form_data]) + add_form_data(req, form_data) if form_data else uri = path uri += "?#{URI.encode_www_form(body)}" if body @@ -126,5 +129,14 @@ def add_body(request, body) request.body = body.to_s end end + + def add_form_data(request, form_data) + form_data = form_data.transform_keys(&:to_s) if form_data.is_a? Hash + request.set_form(form_data, 'multipart/form-data') + end + + def reject_nils(data) + data&.reject { |_key, value| value.nil? } + end end end diff --git a/spec/fixtures/files/image.png b/spec/fixtures/files/image.png new file mode 100644 index 0000000000000000000000000000000000000000..aa613599201f8ac36246f65c18e760f6f3c82903 GIT binary patch literal 2335 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 + + + + + diff --git a/spec/fixtures/files/not_accepted.svg b/spec/fixtures/files/not_accepted.svg new file mode 100644 index 0000000..73d56fa --- /dev/null +++ b/spec/fixtures/files/not_accepted.svg @@ -0,0 +1,4 @@ + + + Sorry, your browser does not support inline SVG. + \ No newline at end of file diff --git a/spec/fixtures/messages/room/upload/file_upload_success.json b/spec/fixtures/messages/room/upload/file_upload_success.json new file mode 100644 index 0000000..731f054 --- /dev/null +++ b/spec/fixtures/messages/room/upload/file_upload_success.json @@ -0,0 +1,62 @@ +{ + "message": { + "_id": "%{message_id}", + "rid": "%{room_id}", + "ts": "2023-07-06T12:05:07.127Z", + "file": { + "_id": "%{file_id}", + "name": "%{file_name}", + "type": "application/octet-stream" + }, + "files": [ + { + "_id": "%{file_id}", + "name": "%{file_name}", + "type": "application/octet-stream" + } + ], + "attachments": [ + { + "ts": "1970-01-01T00:00:00.000Z", + "title": "%{file_name}", + "title_link": "/file-upload/%{file_id}/%{file_name}", + "title_link_download": true, + "type": "file", + "description": "%{description}", + "format": "%{format}", + "size": 8347, + "descriptionMd": [ + { + "type": "PARAGRAPH", + "value": [ + { + "type": "PLAIN_TEXT", + "value": "%{description}" + } + ] + } + ] + } + ], + "msg": "%{msg}", + "u": { + "_id": "%{user_id}", + "username": "admin", + "name": "Admin" + }, + "_updatedAt": "2023-07-06T12:05:07.264Z", + "urls": [], + "md": [ + { + "type": "PARAGRAPH", + "value": [ + { + "type": "PLAIN_TEXT", + "value": "%{msg}" + } + ] + } + ] + }, + "success": true +} \ No newline at end of file diff --git a/spec/fixtures/messages/room/upload/file_upload_without_message_success.json b/spec/fixtures/messages/room/upload/file_upload_without_message_success.json new file mode 100644 index 0000000..745ef4b --- /dev/null +++ b/spec/fixtures/messages/room/upload/file_upload_without_message_success.json @@ -0,0 +1,40 @@ + +{ + "message": { + "_id": "%{message_id}", + "rid": "%{room_id}", + "ts": "2023-07-07T10:03:44.048Z", + "file": { + "_id": "%{file_id}", + "name": "%{file_name}", + "type": "application/octet-stream" + }, + "files": [ + { + "_id": "%{file_id}", + "name": "%{file_name}", + "type": "application/octet-stream" + } + ], + "attachments": [ + { + "ts": "1970-01-01T00:00:00.000Z", + "title": "%{file_name}", + "title_link": "/file-upload/%{file_id}/%{file_name}", + "title_link_download": true, + "type": "file", + "format": "%{format}", + "size": 8347 + } + ], + "msg": "", + "u": { + "_id": "%{user_id}", + "username": "rocket_admin", + "name": "Admin" + }, + "_updatedAt": "2023-07-07T10:03:44.097Z", + "urls": [] + }, + "success": true +} \ No newline at end of file diff --git a/spec/fixtures/messages/room/upload/no_accepted_error.json b/spec/fixtures/messages/room/upload/no_accepted_error.json new file mode 100644 index 0000000..4f7c8c7 --- /dev/null +++ b/spec/fixtures/messages/room/upload/no_accepted_error.json @@ -0,0 +1,5 @@ +{ + "success": false, + "error": "File type is not accepted. [error-invalid-file-type]", + "errorType": "error-invalid-file-type" +} \ No newline at end of file diff --git a/spec/fixtures/messages/room/upload/no_file_error.json b/spec/fixtures/messages/room/upload/no_file_error.json new file mode 100644 index 0000000..881b2a8 --- /dev/null +++ b/spec/fixtures/messages/room/upload/no_file_error.json @@ -0,0 +1,5 @@ +{ + "success": false, + "error": "[No file uploaded]", + "errorType": "No file uploaded" +} \ No newline at end of file diff --git a/spec/fixtures/messages/room/upload/png_upload_success.json b/spec/fixtures/messages/room/upload/png_upload_success.json new file mode 100644 index 0000000..2d6578c --- /dev/null +++ b/spec/fixtures/messages/room/upload/png_upload_success.json @@ -0,0 +1,73 @@ +{ + "message": { + "_id": "%{message_id}", + "rid": "%{room_id}", + "ts": "2023-07-07T08:05:32.625Z", + "file": { + "_id": "%{file_id}", + "name": "%{file_name}", + "type": "image/png" + }, + "files": [ + { + "_id": "%{file_id}", + "name": "%{file_name}", + "type": "image/png" + }, + { + "_id": "%{preview_file_id}", + "name": "%{file_name}", + "type": "image/png" + } + ], + "attachments": [ + { + "ts": "1970-01-01T00:00:00.000Z", + "title": "%{file_name}", + "title_link": "/file-upload/%{file_id}/%{file_name}", + "title_link_download": true, + "image_dimensions": { + "width": 480, + "height": 300 + }, + "image_preview": "/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUACADASIAAhEBAxEB/8QAGQAAAwADAAAAAAAAAAAAAAAAAAIDAQQH/8QAHxABAAICAgIDAAAAAAAAAAAAAQACAxESIRMxBEFS/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAUC/8QAFREBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhEDEQA/AOFx8PEuche4FtG+PUal26V0SgiFza8jr1JsrZCzyO4rY/MALutfUxRS4kIQK4jy5kv2Tcr8XE1XXo3CEzSP/9k=", + "image_url": "/file-upload/%{preview_file_id}/%{file_name}", + "image_type": "image/png", + "image_size": 103724, + "type": "file", + "description": "%{description}", + "descriptionMd": [ + { + "type": "PARAGRAPH", + "value": [ + { + "type": "PLAIN_TEXT", + "value": "%{description}" + } + ] + } + ] + } + ], + "msg": "%{msg}", + "u": { + "_id": "%{user_id}", + "username": "admin", + "name": "Admin" + }, + "_updatedAt": "2023-07-07T08:05:32.669Z", + "urls": [], + "md": [ + { + "type": "PARAGRAPH", + "value": [ + { + "type": "PLAIN_TEXT", + "value": "%{msg}" + } + ] + } + ] + }, + "success": true +} \ No newline at end of file diff --git a/spec/shared/room_behaviors.rb b/spec/shared/room_behaviors.rb index c8a4895..6bf6ef4 100644 --- a/spec/shared/room_behaviors.rb +++ b/spec/shared/room_behaviors.rb @@ -682,6 +682,109 @@ end end + describe '#upload_file' do + subject(:upload) { scope.upload_file(room_id: room_id, file: file, **rest_params) } + + let(:file) { '' } + let(:room_id) { 'rmid12345' } + let(:path) { "#{described_class::API_PREFIX}/rooms.upload/#{room_id}" } + let(:rest_params) { { msg: 'Text message', description: 'Description' } } + let(:response) { file_upload_response(room_id: room_id) } + + context 'with file upload' do + let(:file) { File.open('spec/fixtures/files/iterm-theme.itermcolors') } + + before do + stub_authed_request(:post, path).to_return(body: response, status: 200) + end + + context 'with text message' do + it { expect { upload }.not_to raise_error } + it { expect(upload).to be_a(RocketChat::Message) } + end + + context 'without text message' do + let(:rest_params) { {} } + let(:response) { file_upload_without_message_response(room_id: room_id) } + + it { expect { upload }.not_to raise_error } + it { expect(upload).to be_a(RocketChat::Message) } + end + end + + context 'with image upload' do + let(:file) { File.open('spec/fixtures/files/image.png') } + let(:response) { png_upload_response(room_id: room_id) } + + before do + stub_authed_request(:post, path).to_return(body: response, status: 200) + end + + it { expect { upload }.not_to raise_error } + it { expect(upload).to be_a(RocketChat::Message) } + end + + context 'when not accepted error is raised' do + before do + stub_authed_request(:post, path).to_return(body: response, status: 400) + end + + let(:file) { File.open('spec/fixtures/files/not_accepted.svg') } + let(:response) { upload_response('no_accepted_error.json') } + let(:error_message) { 'File type is not accepted. [error-invalid-file-type]' } + + it 'raises not accepted error' do + expect { upload }.to( + raise_error(RocketChat::StatusError, error_message) + ) + end + end + + context 'when no-file-error is raised' do + before do + stub_authed_request(:post, path).to_return(body: response, status: 400) + end + + let(:error_message) { '[No file uploaded]' } + + context 'when no file is sent' do + let(:file) { nil } + let(:response) { upload_response('no_file_error.json') } + + it 'raises a no file error' do + expect { upload }.to( + raise_error(RocketChat::StatusError, error_message) + ) + end + end + + context 'when string is used instead of file object' do + let(:file) { 'abc' } + let(:response) { upload_response('no_file_error.json') } + + it 'raises a no file error' do + expect { upload }.to( + raise_error(RocketChat::StatusError, error_message) + ) + end + end + end + + context 'with an invalid session token' do + before do + stub_unauthed_request(:post, path) + end + + let(:token) { RocketChat::Token.new(authToken: nil, groupId: nil) } + + it 'raises an authentication status error' do + expect { upload }.to( + raise_error(RocketChat::StatusError, 'You must be logged in to do this.') + ) + end + end + end + ### Room request/response helpers def room_response(name) @@ -697,4 +800,49 @@ def room_response(name) status: 200 } end + + ### File response helpers + + def reusable_upload_response_params + { + msg: 'Text message', + description: 'Description', + room_id: 'GENERAL', + file_id: 'f1234', + user_id: 'u1234', + message_id: 'm1234' + } + end + + def file_params + { + **reusable_upload_response_params, + file_name: 'iterm-theme.itermcolors', + format: 'ITERMCOLORS' + } + end + + def file_upload_response(params_override = {}) + upload_response('file_upload_success.json', file_params.merge(params_override)) + end + + def file_upload_without_message_response(params_override = {}) + upload_response('file_upload_without_message_success.json', file_params.merge(params_override)) + end + + def image_params + { + **reusable_upload_response_params, + file_name: 'image.png', + preview_file_id: 'fp1234' + } + end + + def png_upload_response(params_override = {}) + upload_response('png_upload_success.json', image_params.merge(params_override)) + end + + def upload_response(file_name, params = {}) + File.read("spec/fixtures/messages/room/upload/#{file_name}") % params + end end