Skip to content

Commit

Permalink
Introducing Quest (#71)
Browse files Browse the repository at this point in the history
* Restructure matchmaking.yml

* Introduce class to wrap matchmaking config

* Add #has_group? to Matchmaking::Config

* Refactor CollectGroups

* Support size_strategy and config in MatchmakingGroup

* Specifies option for PairByFewestEncounters strategy

* Add #[] method to matchmaking config class

* Introduce strict sizing to genetic strategy

* Update slack-ruby-client

* Add #complete_as! method to ProtractedMatch

* Refactor participant collection to take protractions into account

* Add reason to PendingNotification

* Save completion_check notifications

* Add slack-ruby-block-kit gem

* Refactor login message service

* Refactor new_match message service

* Add body key for slack message locales

* Add quest_protraction message service

* Refactor tests for slack message building

* Handle quest_protraction reason

* Refactor logic around sending login link

* Refactor SendSlackMessage to support block kit

* Add faraday gem explicitly

* Use match id for action_id

* Create initial protracted match for quests

* Add protract! method and delegates

* Add SendResponseMessage service for slack hooks

* Add HandleInteraction service

* Add interaction endpoint

* Add way to run a single group

* Update a message text

* Enable quest group
  • Loading branch information
tuxagon authored Apr 25, 2024
1 parent eca1041 commit fc72c3a
Show file tree
Hide file tree
Showing 66 changed files with 1,460 additions and 463 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ gem "tailwindcss-rails"
gem "importmap-rails"
gem "hotwire-rails"
gem "heroicon"
gem "slack-ruby-block-kit"
gem "faraday"

gem "bugsnag"
gem "sendgrid-actionmailer"
Expand All @@ -38,4 +40,5 @@ group :test do
gem "rspec"
gem "rspec-rails"
gem "timecop"
gem "webmock"
end
36 changes: 26 additions & 10 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,17 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
bootsnap (1.13.0)
msgpack (~> 1.2)
bugsnag (6.24.2)
concurrent-ruby (~> 1.0)
builder (3.2.4)
concurrent-ruby (1.1.10)
crack (0.4.5)
rexml
crass (1.0.6)
debug (1.7.2)
irb (>= 1.5.0)
Expand All @@ -83,20 +87,21 @@ GEM
dotenv (= 2.8.1)
railties (>= 3.2)
erubi (1.11.0)
faraday (2.6.0)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-mashify (0.1.1)
faraday (~> 2.0)
hashie
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.0.2)
faraday-net_http (3.1.0)
net-http
ffi (1.15.5)
foreman (0.87.2)
gli (2.21.0)
gli (2.21.1)
globalid (1.0.0)
activesupport (>= 5.0)
hashdiff (1.0.1)
hashie (5.0.0)
heroicon (1.0.0)
rails (>= 5.2)
Expand Down Expand Up @@ -131,7 +136,9 @@ GEM
sorbet-eraser (~> 0.3.1)
sorbet-runtime (~> 0.5.9204)
msgpack (1.6.0)
multipart-post (2.2.3)
multipart-post (2.4.0)
net-http (0.4.1)
uri
net-imap (0.3.1)
net-protocol
net-pop (0.1.2)
Expand All @@ -154,6 +161,7 @@ GEM
ast (~> 2.4.1)
racc
pg (1.4.4)
public_suffix (5.0.1)
puma (6.0.0)
nio4r (~> 2.0)
racc (1.6.0)
Expand Down Expand Up @@ -235,20 +243,20 @@ GEM
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
ruby_http_client (3.5.5)
sendgrid-actionmailer (3.2.0)
mail (~> 2.7)
sendgrid-ruby (~> 6.4)
sendgrid-ruby (6.6.2)
ruby_http_client (~> 3.4)
slack-ruby-client (2.0.0)
slack-ruby-block-kit (0.23.0)
zeitwerk (~> 2.6)
slack-ruby-client (2.3.0)
faraday (>= 2.0)
faraday-mashify
faraday-multipart
gli
hashie
websocket-driver
sorbet-eraser (0.3.1)
sorbet-runtime (0.5.11178)
sprockets (4.2.0)
Expand Down Expand Up @@ -291,7 +299,12 @@ GEM
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
websocket-driver (0.7.5)
uri (0.13.0)
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.6.6)
Expand All @@ -310,6 +323,7 @@ DEPENDENCIES
bugsnag
debug
dotenv-rails
faraday
foreman
heroicon
hotwire-rails
Expand All @@ -324,12 +338,14 @@ DEPENDENCIES
rspec
rspec-rails
sendgrid-actionmailer
slack-ruby-block-kit
slack-ruby-client
sprockets-rails
standard
tailwindcss-rails
timecop
todo_or_die
webmock

RUBY VERSION
ruby 3.1.2p20
Expand Down
21 changes: 21 additions & 0 deletions app/controllers/slack/interaction_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Slack
class InteractionController < SlashCommandController
def handle
if block_actions?
Slack::HandleInteraction.new(payload).call
end

head :ok
end

private

def payload
@payload ||= JSON.parse(params[:payload])
end

def block_actions?
payload["type"] == "block_actions"
end
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Slack
class LoginWithSlackCommandController < SlashCommandController
def handle
Auth::SendsLoginLink.new.call(slack_user_id: params["user_id"])
Auth::SendLoginLink.new.call(slack_user_id: params["user_id"])

render json: {
response_type: "ephemeral",
Expand Down
22 changes: 22 additions & 0 deletions app/models/historical_match.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class HistoricalMatch < ApplicationRecord
enum status: {scoreable: "scoreable", archived: "archived"}

has_many :pending_notifications, dependent: :nullify
has_one :protracted_match, dependent: :destroy

validates :matched_on, :grouping, presence: true
validate :at_least_two_members
Expand All @@ -11,6 +12,27 @@ class HistoricalMatch < ApplicationRecord
scope :older_than, ->(date) { where("created_at::date < '#{date.to_date}'") }
scope :for_user, ->(user) { with_member(user.slack_user_id) }

delegate :protract!, to: :protracted_match, allow_nil: true
delegate :complete!, to: :protracted_match, allow_nil: true

def self.protracted_in(grouping)
find_by_sql([<<~SQL.squish, grouping])
with protracted_matches as (
select
hm.*,
row_number() over (partition by unnest(hm.members) order by hm.matched_on desc) as rn
from historical_matches hm
inner join protracted_matches pm ON hm.id = pm.historical_match_id and pm.completed_at is null
where hm.grouping = ?
), most_recent_protracted as (
select distinct on (id) *
from protracted_matches
where rn = 1
)
select * from most_recent_protracted
SQL
end

private

def at_least_two_members
Expand Down
41 changes: 22 additions & 19 deletions app/models/matchmaking_group.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
class MatchmakingGroup < ApplicationRecord
SIZE_STRATEGIES = {
exact_size: "exact_size",
flexible_size: "flexible_size"
}.freeze
DEFAULT_SIZE_STRATEGY = SIZE_STRATEGIES[:flexible_size]

validate :name_not_in_config
validates :name, uniqueness: true

def self.name_exists?(name)
Rails.application.config.x.matchmaking.to_h.transform_keys(&:to_s).key?(name) || exists?(name: name)
end
attr_writer :protractable

def active?
is_active
end
alias_attribute :active?, :is_active
alias_attribute :slack_channel, :slack_channel_name

def active
is_active
def self.name_exists?(name)
Rails.application.config.x.matchmaking.has_group?(name) || exists?(name: name)
end

def active=(value)
self.is_active = value
def size_strategy
@strategy || DEFAULT_SIZE_STRATEGY
end

def channel
slack_channel_name
def size_strategy=(strategy)
@strategy = strategy if SIZE_STRATEGIES.value?(strategy)
end

def channel=(value)
self.slack_channel_name = value
def flexible_size?
size_strategy == SIZE_STRATEGIES[:flexible_size]
end

def size
target_size
def exact_size?
size_strategy == SIZE_STRATEGIES[:exact_size]
end

def size=(value)
self.target_size = value
def protractable?
!!@protractable
end

private

def name_not_in_config
if Rails.application.config.x.matchmaking.to_h.key?(name.intern)
if Rails.application.config.x.matchmaking.has_group?(name)
errors.add(:name, "cannot be the same as a key in the matchmaking config")
end
end
Expand Down
7 changes: 7 additions & 0 deletions app/models/pending_notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ class PendingNotification < ApplicationRecord

scope :for_grouping, ->(grouping) { includes(:historical_match).where(historical_match: {grouping: grouping}) }

scope :new_match_reason, -> { where(reason: "new_match").or(where(reason: nil)) }
scope :quest_protraction_reason, -> { where(reason: "quest_protraction") }

def use_slack?
strategy == "slack"
end

def use_email?
strategy == "email"
end

def reason
super || "new_match"
end
end
14 changes: 14 additions & 0 deletions app/models/protracted_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class ProtractedMatch < ApplicationRecord
belongs_to :historical_match

validates :historical_match, presence: true
validates :completed_by, :completed_at, presence: true, if: -> { completed_at.present? || completed_by.present? }

def complete!(completed_by)
update!(completed_by: completed_by, completed_at: Time.zone.now)
end

def protract!(protracted_by)
update!(last_protracted_by: protracted_by)
end
end
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
module Auth
class SendsLoginLink
class SendLoginLink
def initialize
@finds_or_creates_user = FindsOrCreatesUser.new
@generates_token = GeneratesToken.new
@builds_login_slack_message = Slack::BuildsLoginSlackMessage.new
@build_login_message = Slack::BuildLoginMessage.new
@opens_slack_conversation = Slack::OpensSlackConversation.new
@sends_slack_message = Slack::SendsSlackMessage.new
@send_slack_message = Slack::SendSlackMessage.new
end

def call(slack_user_id:)
Expand All @@ -16,9 +16,9 @@ def call(slack_user_id:)

conversation = @opens_slack_conversation.call(users: [slack_user_id])

@sends_slack_message.call(
@send_slack_message.call(
channel: conversation,
blocks: @builds_login_slack_message.render(user: user)
blocks: @build_login_message.call(user: user)
)
end
end
Expand Down
19 changes: 1 addition & 18 deletions app/services/collect_groups.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,6 @@ def initialize

def call
extra_groups = MatchmakingGroup.all
readonly_groups + extra_groups
end

private

def readonly_groups
@config.to_h.map do |name, group_config|
normalized = group_config.to_h.transform_keys do |key|
next :target_size if key.intern == :size
next :is_active if key.intern == :active
next :slack_channel_name if key.intern == :channel
key
end

group = MatchmakingGroup.new(normalized.merge(name: name))
group.define_singleton_method(:readonly?) { true }
group
end
@config.groups + extra_groups
end
end
11 changes: 9 additions & 2 deletions app/services/matchmaking/choose_strategy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ class ChooseStrategy
def call(group)
return nil unless group&.active?

return Strategies::PairByFewestEncounters.new if group.target_size == 2
if group.target_size == 2
return Strategies::PairByFewestEncounters.new(
allow_third_participant: group.flexible_size?
)
end

Strategies::ArrangeGroupsGenetically.new(target_group_size: group.target_size)
Strategies::ArrangeGroupsGenetically.new(
target_group_size: group.target_size,
strict_group_size: group.exact_size?
)
end
end
end
Loading

0 comments on commit fc72c3a

Please sign in to comment.