Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Enable contributing over api #1716

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a9fa3fd
Add basic api with specs and onboard method
mattwr18 Oct 13, 2023
7d4babb
Provide basic auth to api via jwt
mattwr18 Oct 13, 2023
41c1309
Do not update contributor in onboard step if found
mattwr18 Oct 13, 2023
c366a56
Adapt api to expected responses, simplify auth
mattwr18 Oct 13, 2023
1f69750
Add missing endpoints and test
mattwr18 Oct 13, 2023
7bf4828
Add 100eyes api token to ansible playbook
mattwr18 Oct 18, 2023
0d20844
Fix current request logic
mattwr18 Oct 18, 2023
df44bc8
Update test after implementation changes
mattwr18 Oct 18, 2023
21ae031
Hardcode invalid token in spec
mattwr18 Oct 18, 2023
a83c837
Add expectation to see the response.body from failure
mattwr18 Oct 18, 2023
290e2fc
Revert "Add expectation to see the response.body from failure"
mattwr18 Oct 18, 2023
c6de9e8
Add debugging to ci workflow
mattwr18 Oct 18, 2023
46f63ca
Remove wait time mintues
mattwr18 Oct 18, 2023
6ba2e56
Add debug to after tests fail
mattwr18 Oct 18, 2023
a32fbfd
Start debugging before fails occur
mattwr18 Oct 18, 2023
9fc5bf7
Fix specs
mattwr18 Oct 18, 2023
524f1b1
Remove debugging step from ci workflow after fixing specs
mattwr18 Oct 18, 2023
bffef2c
Set token and invalid token to different values
mattwr18 Oct 18, 2023
e834335
Ensure first name of contributor to avoid errors
mattwr18 Oct 23, 2023
5014727
Return error message for invalid contributor creation
mattwr18 Oct 23, 2023
908b9cf
Capitalize first names before save
mattwr18 Oct 23, 2023
a9af91a
Add exernal channel to contributors
mattwr18 Oct 23, 2023
439e364
Return active state of contributor
mattwr18 Oct 23, 2023
8507318
Favor titleize to account for multiple first names
mattwr18 Oct 24, 2023
95fb801
Render error status if update fails
mattwr18 Oct 26, 2023
728160c
Add UI to display phone number on contributors show
mattwr18 Oct 26, 2023
c4466f2
Hide features not available with contributing over api
mattwr18 Oct 26, 2023
7e05c97
Make all channels optional, api only option
mattwr18 Oct 26, 2023
530b585
Update initial setup
mattwr18 Oct 30, 2023
838b6c4
Merge branch 'main' into 1715_enable_contributing_over_api
mattwr18 Nov 1, 2023
eb9da69
Revert making Postmark config optional
mattwr18 Nov 1, 2023
ae5db51
Fix unmerged file
mattwr18 Nov 1, 2023
4980cef
Merge branch 'main' into 1715_enable_contributing_over_api
mattwr18 Jan 9, 2024
dececc8
Remove empty comment
mattwr18 Jan 9, 2024
0c2d020
Merge branch 'main' into 1715_enable_contributing_over_api
mattwr18 Jan 23, 2024
b10ceec
Improve setting channels logic
mattwr18 Jan 23, 2024
b9c0d9d
Improve logic to determine when an instance is api only
mattwr18 Jan 23, 2024
0fce666
Add scope for direct messages
mattwr18 Jan 25, 2024
1f9d4c6
Add enpoint to post direct messages
mattwr18 Jan 29, 2024
fa657d8
Move ApiJsonResponses to concern
mattwr18 Jan 29, 2024
2d36a5e
Add attrs to direct message
mattwr18 Jan 29, 2024
7203feb
Add MessageGenerator service object
mattwr18 Jan 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ansible/generate_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ traefik:
rails:
environment: production
hundred_eyes_project_name: 100eyes
hundred_eyes_api_token:
threema:
api_identity:
api_secret:
api_identity:
api_secret:
private_key: # your threema private key *without* `private:` prefix
telegram_bot:
api_key:
Expand Down
231 changes: 116 additions & 115 deletions ansible/inventories/staging/host_vars/staging.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ansible/roles/installation/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
DOCKER_IMAGE_TAG: "{{ docker_image_tag }}"
RAILS_ENV: "{{ rails.environment }}"
HUNDRED_EYES_PROJECT_NAME: "{{ rails.hundred_eyes_project_name }}"
HUNDRED_EYES_API_TOKEN: "{{ rails.hundred_eyes_api_token | default('') }}"
TELEGRAM_BOT_API_KEY: "{{ rails.telegram_bot.api_key | default('') }}"
TELEGRAM_BOT_USERNAME: "{{ rails.telegram_bot.username | default('') }}"
POSTGRES_HOST: "{{ postgres_host }}"
Expand Down
10 changes: 6 additions & 4 deletions app/components/chat_messages_group/chat_messages_group.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
<% end %>

<div class="ChatMessagesGroup-actions">
<a href="<%= conversation_link %>">
<%= c 'icon', icon: 'reply-arrow', styles: [:inline] %>
<%= I18n.t('components.chat_messages_group.reply') %>
</a>
<% unless Setting.api_token.present? %>
<a href="<%= conversation_link %>">
<%= c 'icon', icon: 'reply-arrow', styles: [:inline] %>
<%= I18n.t('components.chat_messages_group.reply') %>
</a>
<% end %>
<a href="<%= add_message_link %>">
<%= c 'icon', icon: 'add', styles: [:inline] %>
<%= I18n.t('components.chat_messages_group.add_message') %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<%= c 'textarea', field.input_defaults %>
<% end %>

<%= c 'field', object: contributor, attr: :phone do |field| %>
<%= c 'input', field.input_defaults %>
<% end %>

<%= c 'submit_button', label: I18n.t('save') %>
<% end %>
<% end %>
Expand Down
16 changes: 9 additions & 7 deletions app/components/contributors_index/contributors_index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
<p><%= t('.subheading') %></p>
</div>

<%= c 'copy_button',
label: t('.actions.copy_invite'),
styles: [:colorNavBar, :customIcon],
url: invites_url,
key: 'url',
custom_icon: 'onboarding-ticket'
%>
<% unless api_only_instance? %>
<%= c 'copy_button',
label: t('.actions.copy_invite'),
styles: [:colorNavBar, :customIcon],
url: invites_url,
key: 'url',
custom_icon: 'onboarding-ticket'
%>
<% end %>

<% header.tab_bar do %>
<%= c('tab_bar', items: [
Expand Down
5 changes: 5 additions & 0 deletions app/components/contributors_index/contributors_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,10 @@ def inactive_contributors_count
def unsubscribed_contributors_count
tag_list.present? && state == :unsubscribed ? filter_count : unsubscribed_count
end

def api_only_instance?
configured_messengers_hash = Setting.channels.except('email')
Setting.api_token.present? && configured_messengers_hash.values.all?(false)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<%= t '.help_text' %>
</p>
<ul class="OnboardingChannelsCheckboxes-list">
<% Setting.channels.each do |key, value| %>
<% configured_channels.each do |key, value| %>
<li class="OnboardingChannelsCheckboxes-listItem">
<%= c 'checkbox', id: "setting[channels][#{key}]", checked: value %>
<label for=<%= "setting[channels][#{key}]" %>><%= key.to_s.camelize %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# frozen_string_literal: true

module OnboardingChannelsCheckboxes
class OnboardingChannelsCheckboxes < ApplicationComponent; end
class OnboardingChannelsCheckboxes < ApplicationComponent
private

def configured_channels
Setting.channels.select { |_key, value| value }
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
%>
<%= c 'flex' do %>
<%= c 'contributors_status_bar', organization: organization %>
<%= c 'copy_button',
class: 'Button--secondary ProfileContributorsSection-copyInviteButton',
label: I18n.t('profile.contributors_section.copy_invite'),
url: invites_url,
key: 'url',
custom_icon: 'plus-sign'
%>
<% unless api_only_instance? %>
<%= c 'copy_button',
class: 'Button--secondary ProfileContributorsSection-copyInviteButton',
label: I18n.t('profile.contributors_section.copy_invite'),
url: invites_url,
key: 'url',
custom_icon: 'plus-sign'
%>
<% end %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@ def initialize(organization:)
end

attr_reader :organization

private

def api_only_instance?
configured_messengers_hash = Setting.channels.except('email')
Setting.api_token.present? && configured_messengers_hash.values.all?(false)
end
end
end
26 changes: 14 additions & 12 deletions app/components/request_form/request_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,21 @@
label: t('.insert_placeholder_button'),
data: { action: 'click->request-form#insertPlaceholderAtCursor' }
%>
<%= c 'button',
style: :secondary,
class: 'RequestForm-insertPlaceholderButton',
type: 'button',
data: { action: 'request-form#insertImage' } do %>
<%= field.file_field(:request,
:files,
data: { request_form_target: 'imageInput' },
multiple: true,
accept: 'image/*'
<% unless Setting.api_token.present? %>
<%= c 'button',
style: :secondary,
class: 'RequestForm-insertPlaceholderButton',
type: 'button',
data: { action: 'request-form#insertImage' } do %>
<%= field.file_field(:request,
:files,
data: { request_form_target: 'imageInput' },
multiple: true,
accept: 'image/*'

) %>
<%= t('.attach_image_to_message') %>
) %>
<%= t('.attach_image_to_message') %>
<% end %>
<% end %>
<% end %>
<%= c 'textarea', field.input_defaults.merge(
Expand Down
150 changes: 150 additions & 0 deletions app/controllers/api_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# frozen_string_literal: true

class ApiController < ApplicationController
include ApiJsonResponses

skip_before_action :require_login, :verify_authenticity_token
before_action :contributor
before_action :authorize_api_access, except: :direct_message
before_action :authenciate_user, only: :direct_message
rescue_from JWT::DecodeError, with: :render_unauthorized

def show
if contributor
render_json_show_contributor
else
render_not_found
end
end

def create
if contributor
render_json_created_contributor
return
end
contributor = Contributor.new(onboard_params.merge(data_processing_consented_at: Time.current, external_id: external_id))

if contributor.save
render_json_created_contributor
else
render json: { status: 'error', message: contributor.errors.full_messages.join(' ') }, status: :unprocessable_entity
end
end

def update
if contributor
if contributor.update(phone: update_contributor_params[:phone_number])
render_json_updated_contributor
else
render json: { status: 'error', message: contributor.errors.full_messages.join(' ') }, status: :unprocessable_entity
end
else
render_not_found
end
end

def current_request
if contributor
current_request = contributor.active_request
render json: {
status: 'ok',
data: {
id: current_request.id,
personalized_text: current_request.personalized_text(contributor),
contributor_replies_count: contributor.replies.where(request_id: current_request.id).count
}
}, status: :ok
else
render_not_found
end
end

def messages
if contributor
params = {
request: contributor.active_request,
text: messages_params[:text],
sender: contributor
}
raw_data = {
io: StringIO.new(JSON.generate(messages_params)),
filename: 'api.json',
content_type: 'application/json'
}
message = MessageGenerator.generate_message(params: params, raw_data: raw_data)

if message.save!
render_created_message(message)
else
render_creation_failed
end
else
render_not_found
end
end

def direct_message
if contributor
params = {
request: contributor.active_request,
text: direct_message_params[:text],
sender: current_user,
recipient: contributor
}
raw_data = {
io: StringIO.new(JSON.generate(direct_message_params)),
filename: 'api.json',
content_type: 'application/json'
}
message = MessageGenerator.generate_message(params: params, raw_data: raw_data)

if message.save!
render_created_message(message)
else
render_creation_failed
end
else
render_not_found
end
end

private

attr_reader :current_user

def contributor
@contributor ||= Contributor.find_by(external_id: external_id)
end

def authorize_api_access
authenticate_or_request_with_http_token do |token, _options|
ActiveSupport::SecurityUtils.secure_compare(token, Setting.api_token)
end
end

def authenciate_user
decoded_token = JWT.decode(direct_message_params[:jwt], Setting.api_token, true, { algorithm: 'HS256' }).first.with_indifferent_access
@current_user = User.find_by(email: decoded_token[:email], encrypted_password: decoded_token[:encrypted_password])
render_not_found unless @current_user
end

def external_id
request.headers['X-100eyes-External-Id']
end

def onboard_params
params.permit(:first_name, :external_channel)
end

def update_contributor_params
params.permit(:phone_number)
end

def messages_params
params.permit(:text)
end

def direct_message_params
params.permit(:text, :jwt)
end
end
2 changes: 1 addition & 1 deletion app/controllers/charts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def joined_inbound(group_keys)

def joined_outbound(group_keys)
grouped_requests = group_messages(Request.unscoped, group_keys).count
grouped_messages = group_messages(Message.unscoped.where(sender_type: [User.name, nil], broadcasted: false),
grouped_messages = group_messages(Message.unscoped.direct_messages,
group_keys).count
grouped_requests.merge(grouped_messages) { |_key, oldval, newval| oldval + newval }
end
Expand Down
Loading