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

Introduce SlackExplorer #33

Merged
merged 7 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 14 additions & 3 deletions lib/gold_miner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ module GoldMiner
def mine_in(slack_channel, slack_client: GoldMiner::Slack::Client, env_file: ".env")
Dotenv.load!(env_file)

slack_client
.build(api_token: ENV["SLACK_API_TOKEN"])
.fmap { |client| client.search_interesting_messages_in(slack_channel) }
prepare(slack_client)
.fmap { |client| explore(slack_channel, client) }
end

def convert_messages_to_blogpost(channel, messages, blog_post_builder: GoldMiner::BlogPost)
Expand All @@ -25,4 +24,16 @@ def convert_messages_to_blogpost(channel, messages, blog_post_builder: GoldMiner
writer: BlogPost::Writer.from_env
)
end

private

def prepare(slack_client)
slack_client.build(api_token: ENV["SLACK_API_TOKEN"])
end

def explore(slack_channel, slack_client)
SlackExplorer
.new(slack_client, AuthorConfig.default)
.explore(slack_channel, start_on: Helpers::Time.last_friday)
end
end
55 changes: 55 additions & 0 deletions lib/gold_miner/blog_post/simple_writer/topic_extractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module GoldMiner
class BlogPost
class SimpleWriter
module TopicExtractor
LANGUAGE_MATCHERS = {
"Ruby" => ["ruby", "ruby on rails"],
"Elixir" => ["elixir"],
"JavaScript" => ["javascript", "js", "node", "nodejs", "yarn", "npm"],
"TypeScript" => ["typescript", "ts"],
"SQL" => ["sql"],
"CSS" => ["css"]
}.freeze
TOOL_MATCHERS = {
"Ruby on Rails" => ["ruby on rails"],
"React" => ["react", "reactjs"],
"React Native" => ["react native"],
"Tailwind" => ["tailwind css", "tailwindcss", "tailwind"]
}
TECHNIQUE_MATCHERS = {
"Refactoring" => ["refactor", "refactoring"],
"Testing" => ["test", "tests", "testing"]
}.freeze
PARDIGM_MATCHERS = {
"Functional Programming" => ["functional programming"],
"OOP" => ["object oriented programming", "oop"]
}.freeze
OTHER_MATCHERS = {
"TIL" => ["til", "today i learned", "today i learnt"],
"Tip" => ["tip", "tips"]
}
TOPIC_MATCHERS = {
**LANGUAGE_MATCHERS,
**TECHNIQUE_MATCHERS,
**TOOL_MATCHERS,
**PARDIGM_MATCHERS,
**OTHER_MATCHERS
}.freeze

def self.call(message_text)
words = message_text.downcase.split(/\W/)
sanitized_text = words.join(" ")
topics = Set[]

TOPIC_MATCHERS.each do |topic_label, topic_matchers|
topic_matchers.each do |topic_matcher|
topics << topic_label if sanitized_text.match?(/\b#{topic_matcher}\b/)
end
end

topics.to_a
end
end
end
end
end
47 changes: 0 additions & 47 deletions lib/gold_miner/messages_query.rb

This file was deleted.

95 changes: 36 additions & 59 deletions lib/gold_miner/slack/client.rb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOLD_EMOJI = "rupee-gold"

can be deleted from Slack::Client because it has been moved to SlackExplorer

Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
# frozen_string_literal: true

require "dry/monads"
require "async"
require "slack-ruby-client"

module GoldMiner
class Slack::Client
GOLD_EMOJI = "rupee-gold"

extend Dry::Monads[:result]

def self.build(api_token:, slack_client: ::Slack::Web::Client, author_config: AuthorConfig.default)
client = new(api_token, slack_client, author_config)
def self.build(api_token:, slack_client: ::Slack::Web::Client)
client = new(api_token, slack_client)

begin
client.auth_test
Expand All @@ -24,81 +21,61 @@ def self.build(api_token:, slack_client: ::Slack::Web::Client, author_config: Au
end
end

def initialize(api_token, slack_client, author_config)
def initialize(api_token, slack_client)
@slack = slack_client.new(token: api_token)
@author_config = author_config
@user_name_cache = {}
end

def auth_test
@slack.auth_test
end

def search_interesting_messages_in(channel)
interesting_messages = interesting_messages_query(channel)
Sync do
til_messages_task = Async do
extract_messages_from_result(
@slack.search_messages(query: interesting_messages.with_topic("TIL"))
)
end
tip_messages_task = Async do
extract_messages_from_result(
@slack.search_messages(query: interesting_messages.with_topic("tip"))
)
end
golden_messages_task = Async do
extract_messages_from_result(
@slack.search_messages(query: interesting_messages.with_reaction(GOLD_EMOJI))
)
end

[til_messages_task, tip_messages_task, golden_messages_task]
.flat_map(&:wait)
.uniq { |message| message[:permalink] }
end
def search_messages(query)
@slack
.search_messages(query:)
.tap { |response|
warn_on_multiple_pages(response)
fetch_author_names(response)
}
end

private_class_method :new

private

def interesting_messages_query(channel)
MessagesQuery
.new
.on_channel(channel)
.sent_after_last_friday
# For simplicity, I'm not handling API pagination yet
def warn_on_multiple_pages(result)
if result.messages.paging.pages > 1
warn "[WARNING] Found more than one page of results, only the first page will be processed"
end
end

def extract_messages_from_result(result)
warn_on_multiple_pages(result)
# Unfortunately, the Slack API doesn't return the real name of the
# author of a message, so we need to make an additional API call for
# each message.
def fetch_author_names(response)
Sync do
author_names = response.messages.matches.map { |message|
Async { [message.id, real_name_for(message.user)] }
}.map(&:wait).to_h

result.messages.matches.map do |match|
Slack::Message.new(
text: match.text,
author: build_author(match),
permalink: match.permalink
)
response.messages.matches.each { |message|
message.author_real_name = author_names[message.id]
}
end
end

def build_author(message)
Slack::User.new(
id: message.user,
name: fetch_author(message.user),
username: message.username,
link: @author_config.link_for(message.username)
)
end

def fetch_author(user_id)
@slack.users_info(user: user_id).user.profile.real_name
def real_name_for(user_id)
@user_name_cache[user_id] ||=
user_info(user_id)
.user
.profile
.real_name
# .tap { |name| @user_name_cache[user_id] = name }
end

# For simplicity, I'm not handling API pagination yet
def warn_on_multiple_pages(result)
if result.messages.paging.pages > 1
warn "[WARNING] Found more than one page of results, only the first page will be processed"
end
def user_info(user_id)
@slack.users_info(user: user_id)
end
end
end
45 changes: 45 additions & 0 deletions lib/gold_miner/slack/messages_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module GoldMiner
module Slack
class MessagesQuery
attr_reader :channel, :start_date, :topic, :reaction

def initialize(channel: nil, start_date: nil, topic: nil, reaction: nil)
@channel = channel
@start_date = start_date
@topic = topic
@reaction = reaction
end

def on_channel(new_channel)
with(channel: new_channel)
end

def sent_after(new_start_date)
with(start_date: new_start_date)
end

def with_topic(new_topic)
with(topic: new_topic)
end

def with_reaction(new_reaction)
with(reaction: new_reaction)
end

def with(**new_attributes)
old_attributes = {channel: channel, start_date: start_date, topic: topic, reaction: reaction}

self.class.new(**old_attributes.merge(new_attributes))
end

def to_s
[
topic,
channel && "in:#{channel}",
start_date && "after:#{start_date}",
reaction && "has::#{reaction}:"
].compact.join(" ")
end
end
end
end
61 changes: 61 additions & 0 deletions lib/gold_miner/slack_explorer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
require "async"

module GoldMiner
class SlackExplorer
def initialize(slack_client, author_config)
@slack = slack_client
@author_config = author_config
end

def explore(channel, start_on:)
interesting_messages = interesting_messages_query(channel, start_on)

Sync do
til_messages_task = Async { search_messages(interesting_messages.with_topic("TIL")) }
tip_messages_task = Async { search_messages(interesting_messages.with_topic("tip")) }
hand_picked_messages_task = Async { search_messages(interesting_messages.with_reaction("rupee-gold")) }

merge_messages(til_messages_task, tip_messages_task, hand_picked_messages_task)
end
end

private

def interesting_messages_query(channel, start_on)
Slack::MessagesQuery
.new
.on_channel(channel)
.sent_after(Date.parse(start_on.to_s))
end

def merge_messages(*search_tasks)
search_tasks
.flat_map { |task| task.wait.messages.matches }
.uniq { |message| message[:permalink] }
.map { |message|
Slack::Message.new(
text: message.text,
author: author_of(message),
permalink: message.permalink
)
}
end

def author_of(message)
Slack::User.new(
name: message.author_real_name,
link: link_for(message.username),
id: message.user,
username: message.username
)
end

def search_messages(query)
@slack.search_messages(query.to_s)
end

def link_for(username)
@author_config.link_for(username)
end
end
end
Loading