From 3275123ca1a02243f0c31196004393b115c268dd Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Sun, 10 Sep 2023 09:18:53 -0300 Subject: [PATCH 1/9] Remove comment --- lib/gold_miner/slack/client.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/gold_miner/slack/client.rb b/lib/gold_miner/slack/client.rb index b3d55dc..fa0bed8 100644 --- a/lib/gold_miner/slack/client.rb +++ b/lib/gold_miner/slack/client.rb @@ -71,7 +71,6 @@ def real_name_for(user_id) .user .profile .real_name - # .tap { |name| @user_name_cache[user_id] = name } end def user_info(user_id) From 2272dd5b9b19393c0573030871896fb51fcb4d66 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Sun, 10 Sep 2023 10:14:47 -0300 Subject: [PATCH 2/9] Make `Slack::Client` return Slack objects --- lib/gold_miner/slack/client.rb | 16 +++++++++++++++- lib/gold_miner/slack/message.rb | 10 +--------- lib/gold_miner/slack/user.rb | 12 +----------- spec/gold_miner/slack/client_spec.rb | 8 ++++---- spec/support/test_factories.rb | 6 +++--- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/lib/gold_miner/slack/client.rb b/lib/gold_miner/slack/client.rb index fa0bed8..55e3de1 100644 --- a/lib/gold_miner/slack/client.rb +++ b/lib/gold_miner/slack/client.rb @@ -33,9 +33,23 @@ def auth_test def search_messages(query) @slack .search_messages(query:) - .tap { |response| + .then { |response| warn_on_multiple_pages(response) fetch_author_names(response) + + response.messages.matches + } + .map { |message| + Slack::Message.new( + id: message.id, + text: message.text, + user: Slack::User.new( + id: message.user, + name: message.author_real_name, + username: message.username + ), + permalink: message.permalink + ) } end diff --git a/lib/gold_miner/slack/message.rb b/lib/gold_miner/slack/message.rb index 09b55e7..bd963cd 100644 --- a/lib/gold_miner/slack/message.rb +++ b/lib/gold_miner/slack/message.rb @@ -2,16 +2,8 @@ module GoldMiner module Slack - Message = Data.define(:text, :author, :permalink) do + Message = Data.define(:id, :text, :user, :permalink) do alias_method :[], :public_send - - def as_conversation - <<~MARKDOWN - #{author.name_with_link_reference} says: #{text} - - #{author.reference_link} - MARKDOWN - end end end end diff --git a/lib/gold_miner/slack/user.rb b/lib/gold_miner/slack/user.rb index 4a4b006..60d5fbd 100644 --- a/lib/gold_miner/slack/user.rb +++ b/lib/gold_miner/slack/user.rb @@ -2,16 +2,6 @@ module GoldMiner module Slack - User = Data.define(:id, :name, :username, :link) do - alias_method :to_s, :name - - def name_with_link_reference - "[#{name}][#{username}]" - end - - def reference_link - "[#{username}]: #{link}" - end - end + User = Data.define(:id, :name, :username) end end diff --git a/spec/gold_miner/slack/client_spec.rb b/spec/gold_miner/slack/client_spec.rb index 6bf2fc1..f1a66cf 100644 --- a/spec/gold_miner/slack/client_spec.rb +++ b/spec/gold_miner/slack/client_spec.rb @@ -76,11 +76,11 @@ ) slack = described_class.build(api_token: token).value! - messages = slack.search_messages(search_query).messages.matches + messages = slack.search_messages(search_query) expect(messages).to match_array [ - msg1.merge("author_real_name" => user1.name), - msg2.merge("author_real_name" => user2.name) + TestFactories.create_slack_message(id: msg1["id"], text: msg1["text"], user: user1, permalink: msg1["permalink"]), + TestFactories.create_slack_message(id: msg2["id"], text: msg2["text"], user: user2, permalink: msg2["permalink"]) ] end @@ -108,7 +108,7 @@ ) slack = described_class.build(api_token: token).value! - slack.search_messages(search_query).messages.matches + slack.search_messages(search_query) expect(WebMock).to have_requested(:post, "https://slack.com/api/users.info").once end diff --git a/spec/support/test_factories.rb b/spec/support/test_factories.rb index 8014dc6..497b11e 100644 --- a/spec/support/test_factories.rb +++ b/spec/support/test_factories.rb @@ -5,16 +5,16 @@ def create_slack_user(overriden_attributes = {}) default_attributes = { id: "U123", name: "John Doe", - username: "john.doe", - link: "https://example.com/slack/authors/john.doe" + username: "john.doe" } GoldMiner::Slack::User.new(**default_attributes.merge(overriden_attributes)) end def create_slack_message(overriden_attributes = {}) default_attributes = { + id: "msg-id", text: "Hello world", - author: create_slack_user, + user: create_slack_user, permalink: "https://example.com/slack/messages/123" } GoldMiner::Slack::Message.new(**default_attributes.merge(overriden_attributes)) From 87a3f2edbf2f5ac3d607e617d00cd69521170126 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Sun, 10 Sep 2023 21:42:58 -0300 Subject: [PATCH 3/9] Update `SlackExplorer` to return `GoldNuggets` `GoldNuggets` is a broader concept than Slack messages. It can come from any source (Slack messages, Tweets, Toots, etc.), so this makes the app less Slack-specific. I also introduced the concept of an `Author`, to separate that from a `Slack::User`. --- lib/gold_miner/author.rb | 15 ++++ lib/gold_miner/gold_nugget.rb | 5 ++ lib/gold_miner/slack_explorer.rb | 17 +++-- spec/gold_miner/slack_explorer_spec.rb | 97 ++++++++++---------------- spec/support/test_factories.rb | 18 +++++ 5 files changed, 81 insertions(+), 71 deletions(-) create mode 100644 lib/gold_miner/author.rb create mode 100644 lib/gold_miner/gold_nugget.rb diff --git a/lib/gold_miner/author.rb b/lib/gold_miner/author.rb new file mode 100644 index 0000000..03a06ee --- /dev/null +++ b/lib/gold_miner/author.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module GoldMiner + Author = Data.define(:id, :name, :link) do + alias_method :to_s, :name + + def name_with_link_reference + "[#{name}][#{id}]" + end + + def reference_link + "[#{id}]: #{link}" + end + end +end diff --git a/lib/gold_miner/gold_nugget.rb b/lib/gold_miner/gold_nugget.rb new file mode 100644 index 0000000..c59ca5a --- /dev/null +++ b/lib/gold_miner/gold_nugget.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module GoldMiner + GoldNugget = Data.define(:content, :author, :source) +end diff --git a/lib/gold_miner/slack_explorer.rb b/lib/gold_miner/slack_explorer.rb index 2ce4263..debcbb3 100644 --- a/lib/gold_miner/slack_explorer.rb +++ b/lib/gold_miner/slack_explorer.rb @@ -30,23 +30,22 @@ def interesting_messages_query(channel, start_on) def merge_messages(*search_tasks) search_tasks - .flat_map { |task| task.wait.messages.matches } + .flat_map(&:wait) .uniq { |message| message[:permalink] } .map { |message| - Slack::Message.new( - text: message.text, + GoldNugget.new( + content: message.text, author: author_of(message), - permalink: message.permalink + source: 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 + Author.new( + id: message.user.username, + name: message.user.name, + link: link_for(message.user.username) ) end diff --git a/spec/gold_miner/slack_explorer_spec.rb b/spec/gold_miner/slack_explorer_spec.rb index a873a6f..9f3360e 100644 --- a/spec/gold_miner/slack_explorer_spec.rb +++ b/spec/gold_miner/slack_explorer_spec.rb @@ -7,80 +7,53 @@ it "returns uniq interesting Slack messages sent on dev channel since last friday" do travel_to "2022-10-07" do user1 = TestFactories.create_slack_user(id: "user-id-1", name: "User 1", username: "username-1") + author1 = TestFactories.create_author(name: user1.name, id: user1.username) user2 = TestFactories.create_slack_user(id: "user-id-2", name: "User 2", username: "username-2") + author2 = TestFactories.create_author(name: user2.name, id: user2.username) user3 = TestFactories.create_slack_user(id: "user-id-3", name: "User 3", username: "username-3") + author3 = TestFactories.create_author(name: user3.name, id: user3.username) author_config = GoldMiner::AuthorConfig.new({ - user1.username => {"link" => user1.link}, - user2.username => {"link" => user2.link}, - user3.username => {"link" => user3.link} + user1.username => {"link" => author1.link}, + user2.username => {"link" => author2.link}, + user3.username => {"link" => author3.link} }) slack_client = instance_double(GoldMiner::Slack::Client) date = "2022-09-30" + msg1 = TestFactories.create_slack_message( + "text" => "TIL", + "user" => user1, + "permalink" => "https:///message-1-permalink.com" + ) + msg2 = TestFactories.create_slack_message( + "text" => "Ruby tip/TIL: Array#sample...", + "user" => user2, + "permalink" => "https:///message-2-permalink.com" + ) + msg3 = TestFactories.create_slack_message( + "text" => "Ruby tip: have fun!", + "user" => user2, + "permalink" => "https:///message-3-permalink.com" + ) + msg4 = TestFactories.create_slack_message( + "text" => "CSS clamp() is so cool!", + "user" => user3, + "permalink" => "https:///message-4-permalink.com" + ) stub_slack_message_search_requests(slack_client, { - "TIL in:dev after:#{date}" => { - "ok" => true, - "messages" => { - "matches" => [ - {"text" => "TIL", - "user" => user1.id, - "username" => user1.username, - "author_real_name" => user1.name, - "permalink" => "https:///message-1-permalink.com"}, - {"text" => "Ruby tip/TIL: Array#sample...", - "user" => user2.id, - "username" => user2.username, - "author_real_name" => user2.name, - "permalink" => "https:///message-2-permalink.com"} - ], - "paging" => {"pages" => 1} - } - }, - "tip in:dev after:#{date}" => { - "ok" => true, - "messages" => { - "matches" => [ - {"text" => "Ruby tip/TIL: Array#sample...", - "user" => user2.id, - "username" => user2.username, - "author_real_name" => user2.name, - "permalink" => "https:///message-2-permalink.com"}, - {"text" => "Ruby tip: have fun!", - "user" => user2.id, - "username" => user2.username, - "author_real_name" => user2.name, - "permalink" => "https:///message-3-permalink.com"} - ], - "paging" => {"pages" => 1} - } - }, - "in:dev after:#{date} has::rupee-gold:" => { - "messages" => { - "matches" => [ - {"text" => "Ruby tip/TIL: Array#sample...", - "user" => user2.id, - "username" => user2.username, - "author_real_name" => user2.name, - "permalink" => "https:///message-2-permalink.com"}, - {"text" => "CSS clamp() is so cool!", - "user" => user3.id, - "username" => user3.username, - "author_real_name" => user3.name, - "permalink" => "https:///message-4-permalink.com"} - ], - "paging" => {"pages" => 1} - } - } + "TIL in:dev after:#{date}" => [msg1, msg2], + "tip in:dev after:#{date}" => [msg2, msg3], + "in:dev after:#{date} has::rupee-gold:" => [msg2, msg4] }) explorer = described_class.new(slack_client, author_config) messages = explorer.explore("dev", start_on: date) expect(messages).to match_array [ - GoldMiner::Slack::Message.new(text: "TIL", author: user1, permalink: "https:///message-1-permalink.com"), - GoldMiner::Slack::Message.new(text: "Ruby tip/TIL: Array#sample...", author: user2, permalink: "https:///message-2-permalink.com"), - GoldMiner::Slack::Message.new(text: "Ruby tip: have fun!", author: user2, permalink: "https:///message-3-permalink.com"), - GoldMiner::Slack::Message.new(text: "CSS clamp() is so cool!", author: user3, permalink: "https:///message-4-permalink.com") + TestFactories.create_gold_nugget(content: msg1.text, author: author1, source: msg1.permalink), + TestFactories.create_gold_nugget(content: msg2.text, author: author2, source: msg2.permalink), + TestFactories.create_gold_nugget(content: msg3.text, author: author2, source: msg3.permalink), + TestFactories.create_gold_nugget(content: msg4.text, author: author3, source: msg4.permalink) ] end end @@ -92,7 +65,7 @@ allow(mock_client).to receive(:search_messages) { sleep seconds_of_sleep - double("Search result", messages: double("Slack messages", matches: [], paging: double("Slack paging", pages: 1))) + [] } slack_explorer = described_class.new(mock_client, GoldMiner::AuthorConfig.new({})) @@ -109,7 +82,7 @@ def stub_slack_message_search_requests(client, requests) requests.map do |query, response| - allow(client).to receive(:search_messages).with(query).and_return(deep_open_struct(response)) + allow(client).to receive(:search_messages).with(query).and_return(response) end end end diff --git a/spec/support/test_factories.rb b/spec/support/test_factories.rb index 497b11e..5aadcd1 100644 --- a/spec/support/test_factories.rb +++ b/spec/support/test_factories.rb @@ -1,6 +1,24 @@ module TestFactories extend self + def create_author(overriden_attributes = {}) + default_attributes = { + id: "author-id", + name: "John Doe", + link: "https://example.com/users/john.doe" + } + GoldMiner::Author.new(**default_attributes.merge(overriden_attributes)) + end + + def create_gold_nugget(overriden_attributes = {}) + default_attributes = { + content: "TIL about the difference betweeen .size and .count in Rails", + source: "https://example.com/messages/1", + author: create_author + } + GoldMiner::GoldNugget.new(**default_attributes.merge(overriden_attributes)) + end + def create_slack_user(overriden_attributes = {}) default_attributes = { id: "U123", From 7681c91c6d9d73bd26f1a57220e9b08af18c838f Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Sun, 10 Sep 2023 21:50:00 -0300 Subject: [PATCH 4/9] Fix blog post specs --- lib/gold_miner.rb | 4 ++-- lib/gold_miner/blog_post/simple_writer.rb | 12 +++++----- spec/gold_miner/blog_post_spec.rb | 28 +++++++++++------------ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/gold_miner.rb b/lib/gold_miner.rb index b68051d..b1ba923 100644 --- a/lib/gold_miner.rb +++ b/lib/gold_miner.rb @@ -16,10 +16,10 @@ def mine_in(slack_channel, slack_client: GoldMiner::Slack::Client, env_file: ".e .fmap { |client| explore(slack_channel, client) } end - def convert_messages_to_blogpost(channel, messages, blog_post_builder: GoldMiner::BlogPost) + def convert_messages_to_blogpost(channel, gold_nuggets, blog_post_builder: GoldMiner::BlogPost) blog_post_builder.new( slack_channel: channel, - messages: messages, + messages: gold_nuggets, since: Helpers::Time.last_friday, writer: BlogPost::Writer.from_env ) diff --git a/lib/gold_miner/blog_post/simple_writer.rb b/lib/gold_miner/blog_post/simple_writer.rb index 596c94f..44c018e 100644 --- a/lib/gold_miner/blog_post/simple_writer.rb +++ b/lib/gold_miner/blog_post/simple_writer.rb @@ -7,16 +7,16 @@ def initialize(topic_extractor: TopicExtractor) @topic_extractor = topic_extractor end - def extract_topics_from(message) - @topic_extractor.call(message[:text]) + def extract_topics_from(gold_nugget) + @topic_extractor.call(gold_nugget.content) end - def give_title_to(message) - message[:permalink] + def give_title_to(gold_nugget) + gold_nugget.source end - def summarize(message) - message[:text] + def summarize(gold_nugget) + gold_nugget.content end end end diff --git a/spec/gold_miner/blog_post_spec.rb b/spec/gold_miner/blog_post_spec.rb index 27fc90d..0357f15 100644 --- a/spec/gold_miner/blog_post_spec.rb +++ b/spec/gold_miner/blog_post_spec.rb @@ -4,16 +4,16 @@ RSpec.describe GoldMiner::BlogPost do describe "#to_s" do - it "creates a blogpost from a list of messages" do + it "creates a blogpost from a list of gold nuggets" do travel_to "2022-10-07" do - author1 = TestFactories.create_slack_user(id: "U123", name: "John Doe", username: "john.doe", link: "https://example.com/john.doe") - author2 = TestFactories.create_slack_user(id: "U456", name: "Jane Smith", username: "jane.smith", link: "https://example.com/jane.smith") - messages = [ - GoldMiner::Slack::Message.new(text: "TIL 1", author: author1, permalink: "http://permalink-1.com"), - GoldMiner::Slack::Message.new(text: "TIL 2", author: author2, permalink: "http://permalink-2.com"), - GoldMiner::Slack::Message.new(text: "Tip 1", author: author1, permalink: "http://permalink-3.com") + author1 = TestFactories.create_author(name: "John Doe", id: "john.doe", link: "https://example.com/john.doe") + author2 = TestFactories.create_author(name: "Jane Smith", id: "jane.smith", link: "https://example.com/jane.smith") + gold_nuggets = [ + TestFactories.create_gold_nugget(content: "TIL 1", author: author1, source: "http://permalink-1.com"), + TestFactories.create_gold_nugget(content: "TIL 2", author: author2, source: "http://permalink-2.com"), + TestFactories.create_gold_nugget(content: "Tip 1", author: author1, source: "http://permalink-3.com") ] - blogpost = GoldMiner::BlogPost.new(slack_channel: "design", messages: messages, since: "2022-09-30") + blogpost = GoldMiner::BlogPost.new(slack_channel: "design", messages: gold_nuggets, since: "2022-09-30") result = blogpost.to_s @@ -74,16 +74,16 @@ def summarize(message) "test" end end - author1 = TestFactories.create_slack_user(id: "U123", name: "John Doe", username: "john.doe", link: "https://example.com/john.doe") - author2 = TestFactories.create_slack_user(id: "U456", name: "Jane Smith", username: "jane.smith", link: "https://example.com/jane.smith") - messages = [ - GoldMiner::Slack::Message.new(text: "TIL 1", author: author1, permalink: "http://permalink-1.com"), - GoldMiner::Slack::Message.new(text: "TIL 2", author: author2, permalink: "http://permalink-2.com") + author1 = TestFactories.create_author(name: "John Doe", id: "john.doe", link: "https://example.com/john.doe") + author2 = TestFactories.create_author(name: "Jane Smith", id: "jane.smith", link: "https://example.com/jane.smith") + gold_nuggets = [ + TestFactories.create_gold_nugget(content: "TIL 1", author: author1, source: "http://permalink-1.com"), + TestFactories.create_gold_nugget(content: "TIL 2", author: author2, source: "http://permalink-2.com") ] seconds_of_sleep = 0.5 blogpost = GoldMiner::BlogPost.new( slack_channel: "design", - messages: messages, + messages: gold_nuggets, since: "2022-09-30", writer: sleep_writer.new(seconds_of_sleep: seconds_of_sleep) ) From 1c06e24082bf163c53156944ffde8eba2b48888a Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Sun, 10 Sep 2023 21:58:17 -0300 Subject: [PATCH 5/9] Rename messages to gold_nuggets --- lib/gold_miner.rb | 2 +- lib/gold_miner/blog_post.rb | 26 ++++++++--------- spec/gold_miner/blog_post_spec.rb | 4 +-- spec/gold_miner_spec.rb | 48 +++++++++++++++---------------- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/lib/gold_miner.rb b/lib/gold_miner.rb index b1ba923..8cb83f8 100644 --- a/lib/gold_miner.rb +++ b/lib/gold_miner.rb @@ -19,7 +19,7 @@ def mine_in(slack_channel, slack_client: GoldMiner::Slack::Client, env_file: ".e def convert_messages_to_blogpost(channel, gold_nuggets, blog_post_builder: GoldMiner::BlogPost) blog_post_builder.new( slack_channel: channel, - messages: gold_nuggets, + gold_nuggets: gold_nuggets, since: Helpers::Time.last_friday, writer: BlogPost::Writer.from_env ) diff --git a/lib/gold_miner/blog_post.rb b/lib/gold_miner/blog_post.rb index f51555b..87bace3 100644 --- a/lib/gold_miner/blog_post.rb +++ b/lib/gold_miner/blog_post.rb @@ -4,9 +4,9 @@ module GoldMiner class BlogPost - def initialize(slack_channel:, messages:, since:, writer: SimpleWriter.new) + def initialize(slack_channel:, gold_nuggets:, since:, writer: SimpleWriter.new) @slack_channel = slack_channel - @messages = messages + @gold_nuggets = gold_nuggets @since = since @writer = writer end @@ -51,16 +51,16 @@ def tags end def highlights - @messages - .map { |message| Async { highlight_from(message) } } + @gold_nuggets + .map { |gold_nugget| Async { highlight_from(gold_nugget) } } .map(&:wait) .join("\n") .chomp("") end - def highlight_from(message) - title_task = Async { @writer.give_title_to(message) } - summary_task = Async { @writer.summarize(message) } + def highlight_from(gold_nugget) + title_task = Async { @writer.give_title_to(gold_nugget) } + summary_task = Async { @writer.summarize(gold_nugget) } <<~MARKDOWN ## #{title_task.wait} @@ -80,19 +80,19 @@ def topic_tags end def topics - @topics ||= @messages - .map { |message| Async { topics_from(message) } } + @topics ||= @gold_nuggets + .map { |gold_nugget| Async { topics_from(gold_nugget) } } .flat_map(&:wait) .uniq end - def topics_from(message) - @writer.extract_topics_from(message) + def topics_from(gold_nugget) + @writer.extract_topics_from(gold_nugget) end def authors - @messages - .map { |message| message.author.name_with_link_reference } + @gold_nuggets + .map { |gold_nugget| gold_nugget.author.name_with_link_reference } .uniq .sort .then { |authors| Helpers::Sentence.from(authors) } diff --git a/spec/gold_miner/blog_post_spec.rb b/spec/gold_miner/blog_post_spec.rb index 0357f15..fe20407 100644 --- a/spec/gold_miner/blog_post_spec.rb +++ b/spec/gold_miner/blog_post_spec.rb @@ -13,7 +13,7 @@ TestFactories.create_gold_nugget(content: "TIL 2", author: author2, source: "http://permalink-2.com"), TestFactories.create_gold_nugget(content: "Tip 1", author: author1, source: "http://permalink-3.com") ] - blogpost = GoldMiner::BlogPost.new(slack_channel: "design", messages: gold_nuggets, since: "2022-09-30") + blogpost = GoldMiner::BlogPost.new(slack_channel: "design", gold_nuggets: gold_nuggets, since: "2022-09-30") result = blogpost.to_s @@ -83,7 +83,7 @@ def summarize(message) seconds_of_sleep = 0.5 blogpost = GoldMiner::BlogPost.new( slack_channel: "design", - messages: gold_nuggets, + gold_nuggets: gold_nuggets, since: "2022-09-30", writer: sleep_writer.new(seconds_of_sleep: seconds_of_sleep) ) diff --git a/spec/gold_miner_spec.rb b/spec/gold_miner_spec.rb index 0b82c03..85af6fd 100644 --- a/spec/gold_miner_spec.rb +++ b/spec/gold_miner_spec.rb @@ -15,47 +15,45 @@ end it "returns interesting messages from the given channel" do - message_author = TestFactories.create_slack_user(link: "#to-do") - slack_message = TestFactories.create_slack_message(author: message_author) - search_result = deep_open_struct({ - messages: { - matches: [ - { - text: slack_message.text, - user: message_author.id, - username: message_author.username, - author_real_name: message_author.name, - permalink: slack_message.permalink - } - ] - } - }) + message_author = TestFactories.create_slack_user + slack_message = TestFactories.create_slack_message(user: message_author) + search_result = [slack_message] slack_client = instance_double(GoldMiner::Slack::Client, search_messages: search_result) slack_client_builder = double(GoldMiner::Slack::Client, build: Success(slack_client)) result = GoldMiner.mine_in("dev", slack_client: slack_client_builder, env_file: "./spec/fixtures/.env.test") - expect(result.value!).to eq [slack_message] + expect(result.value!).to eq [ + TestFactories.create_gold_nugget( + content: slack_message.text, + author: TestFactories.create_author( + id: message_author.username, + name: message_author.name, + link: "#to-do" + ), + source: slack_message.permalink + ) + ] end end describe ".convert_messages_to_blogpost" do - it "converts slack messages to a blogpost and writes it to a file" do + it "converts slack messages to a blogpost" do travel_to "2022-10-07" do with_env("OPEN_AI_API_TOKEN" => nil) do channel = "dev" - messages = [ - {text: "text", author_username: "user1", permalink: "http://permalink-1.com"}, - {text: "text2", author_username: "user2", permalink: "http://permalink-2.com"} + gold_nuggets = [ + TestFactories.create_gold_nugget, + TestFactories.create_gold_nugget ] blog_post_builder = spy("BlogPost builder") - GoldMiner.convert_messages_to_blogpost(channel, messages, blog_post_builder: blog_post_builder) + GoldMiner.convert_messages_to_blogpost(channel, gold_nuggets, blog_post_builder: blog_post_builder) expect(blog_post_builder).to have_received(:new).with( slack_channel: channel, - messages: messages, + gold_nuggets: gold_nuggets, since: "2022-09-30", writer: instance_of(GoldMiner::BlogPost::SimpleWriter) ) @@ -68,14 +66,14 @@ travel_to "2022-10-07" do with_env("OPEN_AI_API_TOKEN" => "test-token") do channel = "dev" - messages = [] + gold_nuggets = [] blog_post_builder = spy("BlogPost builder") - GoldMiner.convert_messages_to_blogpost(channel, messages, blog_post_builder: blog_post_builder) + GoldMiner.convert_messages_to_blogpost(channel, gold_nuggets, blog_post_builder: blog_post_builder) expect(blog_post_builder).to have_received(:new).with( slack_channel: channel, - messages: messages, + gold_nuggets: gold_nuggets, since: "2022-09-30", writer: instance_of(GoldMiner::BlogPost::OpenAiWriter) ) From 326d3e2ace6bc36c4d926d036d0f87b30b3b469d Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Sun, 10 Sep 2023 22:08:13 -0300 Subject: [PATCH 6/9] Fix OpenAiWritter --- lib/gold_miner/blog_post/open_ai_writer.rb | 32 ++++---- lib/gold_miner/gold_nugget.rb | 10 ++- .../blog_post/open_ai_writer_spec.rb | 82 +++++++++---------- 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/lib/gold_miner/blog_post/open_ai_writer.rb b/lib/gold_miner/blog_post/open_ai_writer.rb index 7568087..c6680bc 100644 --- a/lib/gold_miner/blog_post/open_ai_writer.rb +++ b/lib/gold_miner/blog_post/open_ai_writer.rb @@ -10,35 +10,35 @@ def initialize(open_ai_api_token:, fallback_writer:, open_ai_client: OpenAI::Cli @fallback_writer = fallback_writer end - def extract_topics_from(message) - topics_json = ask_openai("Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{message[:text]}") + def extract_topics_from(gold_nugget) + topics_json = ask_openai("Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{gold_nugget.content}") if (topics = try_parse_json(topics_json)) topics else - fallback_topics_for(message) + fallback_topics_for(gold_nugget) end end - def give_title_to(message) - title = ask_openai("Give a small title to this text: #{message[:text]}") + def give_title_to(gold_nugget) + title = ask_openai("Give a small title to this text: #{gold_nugget.content}") title = title&.delete_prefix('"')&.delete_suffix('"') - title || fallback_title_for(message) + title || fallback_title_for(gold_nugget) end - def summarize(message) + def summarize(gold_nugget) summary = ask_openai <<~PROMPT Summarize the following markdown message without removing the author's blog link. Return the summary as markdown. Message: - #{message.as_conversation} + #{gold_nugget.as_conversation} PROMPT if summary - "#{summary}\n\nSource: #{message[:permalink]}" + "#{summary}\n\nSource: #{gold_nugget.source}" else - fallback_summary_for(message) + fallback_summary_for(gold_nugget) end end @@ -63,16 +63,16 @@ def ask_openai(prompt) nil end - def fallback_title_for(message) - @fallback_writer.give_title_to(message) + def fallback_title_for(gold_nugget) + @fallback_writer.give_title_to(gold_nugget) end - def fallback_summary_for(message) - @fallback_writer.summarize(message) + def fallback_summary_for(gold_nugget) + @fallback_writer.summarize(gold_nugget) end - def fallback_topics_for(message) - @fallback_writer.extract_topics_from(message) + def fallback_topics_for(gold_nugget) + @fallback_writer.extract_topics_from(gold_nugget) end def try_parse_json(json) diff --git a/lib/gold_miner/gold_nugget.rb b/lib/gold_miner/gold_nugget.rb index c59ca5a..7434500 100644 --- a/lib/gold_miner/gold_nugget.rb +++ b/lib/gold_miner/gold_nugget.rb @@ -1,5 +1,13 @@ # frozen_string_literal: true module GoldMiner - GoldNugget = Data.define(:content, :author, :source) + GoldNugget = Data.define(:content, :author, :source) do + def as_conversation + <<~MARKDOWN + #{author.name_with_link_reference} says: #{content} + + #{author.reference_link} + MARKDOWN + end + end end diff --git a/spec/gold_miner/blog_post/open_ai_writer_spec.rb b/spec/gold_miner/blog_post/open_ai_writer_spec.rb index 6a71234..c6ce98e 100644 --- a/spec/gold_miner/blog_post/open_ai_writer_spec.rb +++ b/spec/gold_miner/blog_post/open_ai_writer_spec.rb @@ -16,18 +16,18 @@ it "returns a list of topics from the message text" do token = "valid-token" writer = described_class.new(open_ai_api_token: token, fallback_writer: double("fallback_writer")) - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget open_ai_topics = ["Ruby", "Enumerable"] request = stub_open_ai_request( token: token, - prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{message[:text]}", + prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{gold_nugget.content}", response_status: 200, response_body: { "choices" => [{"message" => {"role" => "assistant", "content" => open_ai_topics.to_json}}] } ) - topics = writer.extract_topics_from(message) + topics = writer.extract_topics_from(gold_nugget) expect(topics).to eq(open_ai_topics) expect(request).to have_been_requested.once @@ -36,11 +36,11 @@ context "when OpenAI returns a JSON wrapped in backticks" do it "removes the backticks and parses the JSON" do token = "valid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget json = '`["Ruby"]`' request = stub_open_ai_request( token: token, - prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{message[:text]}", + prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{gold_nugget.content}", response_status: 200, response_body: { "choices" => [{"message" => {"role" => "assistant", "content" => json}}] @@ -48,7 +48,7 @@ ) writer = described_class.new(open_ai_api_token: token, fallback_writer: double("fallback_writer")) - topics = writer.extract_topics_from(message) + topics = writer.extract_topics_from(gold_nugget) expect(topics).to eq(["Ruby"]) expect(request).to have_been_requested.once @@ -58,11 +58,11 @@ context "when OpenAI returns an invalid JSON" do it "uses the fallback writer" do token = "valid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget invalid_json = '{"Ruby"}' request = stub_open_ai_request( token: token, - prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{message[:text]}", + prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{gold_nugget.content}", response_status: 200, response_body: { "choices" => [{"message" => {"role" => "assistant", "content" => invalid_json}}] @@ -72,10 +72,10 @@ fallback_writer = stub_fallback_writer(extract_topics_from: fallback_topics) writer = described_class.new(open_ai_api_token: token, fallback_writer: fallback_writer) - topics = writer.extract_topics_from(message) + topics = writer.extract_topics_from(gold_nugget) expect(topics).to eq(fallback_topics) - expect(fallback_writer).to have_received(:extract_topics_from).with(message) + expect(fallback_writer).to have_received(:extract_topics_from).with(gold_nugget) expect(request).to have_been_requested.once end end @@ -83,37 +83,37 @@ context "with an invalid token" do it "warns about the error" do token = "invalid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget open_ai_error = "Incorrect API key provided: #{token}. You can find your API key at https://beta.openai.com." request = stub_open_ai_error( token: token, - prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{message[:text]}", + prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{gold_nugget.content}", response_error: open_ai_error ) writer = described_class.new(open_ai_api_token: token, fallback_writer: stub_fallback_writer) expect { - writer.extract_topics_from(message) + writer.extract_topics_from(gold_nugget) }.to output("[WARNING] OpenAI error: #{open_ai_error}\n").to_stderr expect(request).to have_been_requested.once end it "uses the fallback writer to extract topics" do token = "invalid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget request = stub_open_ai_error( token: token, - prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{message[:text]}", + prompt: "Extract the 3 most relevant topics, if possible in one word, from this text as a single parseable JSON array: #{gold_nugget.content}", response_error: "Some error" ) fallback_topics = ["Ruby"] fallback_writer = stub_fallback_writer(extract_topics_from: fallback_topics) writer = described_class.new(open_ai_api_token: token, fallback_writer: fallback_writer) - topics = writer.extract_topics_from(message) + topics = writer.extract_topics_from(gold_nugget) expect(topics).to eq(fallback_topics) - expect(fallback_writer).to have_received(:extract_topics_from).with(message) + expect(fallback_writer).to have_received(:extract_topics_from).with(gold_nugget) expect(request).to have_been_requested.once end end @@ -124,15 +124,15 @@ allow(mock_client_instance).to receive(:chat).and_raise(SocketError) mock_client_class = class_double(OpenAI::Client, new: mock_client_instance) token = "valid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget fallback_topics = ["Ruby"] fallback_writer = stub_fallback_writer(extract_topics_from: fallback_topics) writer = described_class.new(open_ai_api_token: token, fallback_writer: fallback_writer, open_ai_client: mock_client_class) - topics = writer.extract_topics_from(message) + topics = writer.extract_topics_from(gold_nugget) expect(topics).to eq(fallback_topics) - expect(fallback_writer).to have_received(:extract_topics_from).with(message) + expect(fallback_writer).to have_received(:extract_topics_from).with(gold_nugget) end end end @@ -141,18 +141,18 @@ it "returns the message permalink" do token = "valid-token" writer = described_class.new(open_ai_api_token: token, fallback_writer: double("fallback_writer")) - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget open_ai_title = "\n\n\"The Power of Enumerable#each_with_object\"" request = stub_open_ai_request( token: token, - prompt: "Give a small title to this text: #{message[:text]}", + prompt: "Give a small title to this text: #{gold_nugget.content}", response_status: 200, response_body: { "choices" => [{"message" => {"role" => "assistant", "content" => open_ai_title}}] } ) - title = writer.give_title_to(message) + title = writer.give_title_to(gold_nugget) expected_title = open_ai_title.strip.delete('"') expect(title).to eq(expected_title) @@ -162,37 +162,37 @@ context "with an invalid token" do it "warns about the error" do token = "invalid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget open_ai_error = "Incorrect API key provided: #{token}. You can find your API key at https://beta.openai.com." request = stub_open_ai_error( token: token, - prompt: "Give a small title to this text: #{message[:text]}", + prompt: "Give a small title to this text: #{gold_nugget.content}", response_error: open_ai_error ) writer = described_class.new(open_ai_api_token: token, fallback_writer: stub_fallback_writer) expect { - writer.give_title_to(message) + writer.give_title_to(gold_nugget) }.to output("[WARNING] OpenAI error: #{open_ai_error}\n").to_stderr expect(request).to have_been_requested.once end it "uses the fallback writer to return a title" do token = "invalid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget request = stub_open_ai_error( token: token, - prompt: "Give a small title to this text: #{message[:text]}", + prompt: "Give a small title to this text: #{gold_nugget.content}", response_error: "Some error" ) fallback_title = "[TODO]" fallback_writer = stub_fallback_writer(give_title_to: fallback_title) writer = described_class.new(open_ai_api_token: token, fallback_writer: fallback_writer) - title = writer.give_title_to(message) + title = writer.give_title_to(gold_nugget) expect(title).to eq(fallback_title) - expect(fallback_writer).to have_received(:give_title_to).with(message) + expect(fallback_writer).to have_received(:give_title_to).with(gold_nugget) expect(request).to have_been_requested.once end end @@ -201,24 +201,24 @@ describe "#summarize" do it "returns a summary of the given text" do token = "valid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget open_ai_summary = "\n\nEnumerable#each_with_object is like #reduce, but easier to understand." request = stub_open_ai_request( token: token, prompt: - "Summarize the following markdown message without removing the author's blog link. Return the summary as markdown.\n\nMessage:\n#{message.as_conversation}", + "Summarize the following markdown message without removing the author's blog link. Return the summary as markdown.\n\nMessage:\n#{gold_nugget.as_conversation}", response_status: 200, response_body: { "choices" => [{"message" => {"role" => "assistant", "content" => open_ai_summary}}] } ) writer = described_class.new(open_ai_api_token: token, fallback_writer: stub_fallback_writer) - summary = writer.summarize(message) + summary = writer.summarize(gold_nugget) expect(summary).to eq <<~SUMMARY.strip #{open_ai_summary.strip} - Source: #{message[:permalink]} + Source: #{gold_nugget.source} SUMMARY expect(request).to have_been_requested.once end @@ -226,39 +226,39 @@ context "with an invalid token" do it "warns about the error" do token = "invalid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget open_ai_error = "Incorrect API key provided: #{token}. You can find your API key at https://beta.openai.com." request = stub_open_ai_error( token: token, prompt: - "Summarize the following markdown message without removing the author's blog link. Return the summary as markdown.\n\nMessage:\n#{message.as_conversation}", + "Summarize the following markdown message without removing the author's blog link. Return the summary as markdown.\n\nMessage:\n#{gold_nugget.as_conversation}", response_error: open_ai_error ) writer = described_class.new(open_ai_api_token: token, fallback_writer: stub_fallback_writer) expect { - writer.summarize(message) + writer.summarize(gold_nugget) }.to output("[WARNING] OpenAI error: #{open_ai_error}\n").to_stderr expect(request).to have_been_requested.once end it "uses the fallback writer to return a summary" do token = "invalid-token" - message = TestFactories.create_slack_message + gold_nugget = TestFactories.create_gold_nugget request = stub_open_ai_error( token: token, prompt: - "Summarize the following markdown message without removing the author's blog link. Return the summary as markdown.\n\nMessage:\n#{message.as_conversation}", + "Summarize the following markdown message without removing the author's blog link. Return the summary as markdown.\n\nMessage:\n#{gold_nugget.as_conversation}", response_error: "Some error" ) fallback_summary = "[TODO]" fallback_writer = stub_fallback_writer(summarize: fallback_summary) writer = described_class.new(open_ai_api_token: token, fallback_writer: fallback_writer) - title = writer.summarize(message) + title = writer.summarize(gold_nugget) expect(title).to eq(fallback_summary) - expect(fallback_writer).to have_received(:summarize).with(message) + expect(fallback_writer).to have_received(:summarize).with(gold_nugget) expect(request).to have_been_requested.once end end From ec04073c691642c2c9ac5694425b4205124f65e9 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Sun, 10 Sep 2023 22:09:06 -0300 Subject: [PATCH 7/9] Fix message specs --- spec/gold_miner/gold_nugget_spec.rb | 24 ++++++++++++++++++++++++ spec/gold_miner/slack/message_spec.rb | 26 ++++---------------------- 2 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 spec/gold_miner/gold_nugget_spec.rb diff --git a/spec/gold_miner/gold_nugget_spec.rb b/spec/gold_miner/gold_nugget_spec.rb new file mode 100644 index 0000000..8d1c2a1 --- /dev/null +++ b/spec/gold_miner/gold_nugget_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe GoldMiner::GoldNugget do + describe "#as_conversation" do + it "returns the gold nugget content with the author name and link" do + author = TestFactories.create_author(name: "Matz", id: "the-ruby-matz", link: "https://example.com/matz") + gold_nugget = described_class.new( + content: "TIL", + author: author, + source: "https:///message-1-permalink.com" + ) + + conversation = gold_nugget.as_conversation + + expect(conversation).to eq <<~MARKDOWN + [Matz][the-ruby-matz] says: TIL + + [the-ruby-matz]: https://example.com/matz + MARKDOWN + end + end +end diff --git a/spec/gold_miner/slack/message_spec.rb b/spec/gold_miner/slack/message_spec.rb index 34d1c7c..edd9961 100644 --- a/spec/gold_miner/slack/message_spec.rb +++ b/spec/gold_miner/slack/message_spec.rb @@ -5,35 +5,17 @@ RSpec.describe GoldMiner::Slack::Message do describe "#[]" do it "returns the message attribute" do - author = TestFactories.create_slack_user + user = TestFactories.create_slack_user message = described_class.new( + id: "message-1", text: "TIL", - author: author, + user: user, permalink: "https:///message-1-permalink.com" ) expect(message[:text]).to eq "TIL" - expect(message[:author]).to eq author + expect(message[:user]).to eq user expect(message[:permalink]).to eq "https:///message-1-permalink.com" end end - - describe "#as_conversation" do - it "returns the message content with the author name and link" do - author = TestFactories.create_slack_user(name: "Matz", username: "the-ruby-matz", link: "https://example.com/matz") - message = described_class.new( - text: "TIL", - author: author, - permalink: "https:///message-1-permalink.com" - ) - - conversation = message.as_conversation - - expect(conversation).to eq <<~MARKDOWN - [Matz][the-ruby-matz] says: TIL - - [the-ruby-matz]: https://example.com/matz - MARKDOWN - end - end end From 9f36896d51a634fec472f29e7ff142f1dbb09e2a Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Sun, 10 Sep 2023 22:12:40 -0300 Subject: [PATCH 8/9] Fix simple writer specs --- .../blog_post/simple_writer_spec.rb | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/gold_miner/blog_post/simple_writer_spec.rb b/spec/gold_miner/blog_post/simple_writer_spec.rb index 1460b82..78c9605 100644 --- a/spec/gold_miner/blog_post/simple_writer_spec.rb +++ b/spec/gold_miner/blog_post/simple_writer_spec.rb @@ -11,20 +11,20 @@ it "delegates to the topic extractor" do topics = ["topic-#{rand}"] topic_extractor = double(:topic_extractor, call: topics) - message = {text: "message text"} + gold_nugget = TestFactories.create_gold_nugget(content: "message text") writer = described_class.new(topic_extractor: topic_extractor) - extracted_topics = writer.extract_topics_from(message) + extracted_topics = writer.extract_topics_from(gold_nugget) expect(extracted_topics).to eq(topics) - expect(topic_extractor).to have_received(:call).with(message[:text]) + expect(topic_extractor).to have_received(:call).with(gold_nugget.content) end it "has a default topic extractor" do - message = {text: "message text"} + gold_nugget = TestFactories.create_gold_nugget(content: "message text") writer = described_class.new - extracted_topics = writer.extract_topics_from(message) + extracted_topics = writer.extract_topics_from(gold_nugget) expect(extracted_topics).to be_a Array end @@ -32,10 +32,10 @@ describe "#give_title_to" do it "returns the message permalink" do - message = {permalink: "https://permalink.com", text: "message text"} + gold_nugget = TestFactories.create_gold_nugget(source: "https://permalink.com", content: "message text") writer = described_class.new - title = writer.give_title_to(message) + title = writer.give_title_to(gold_nugget) expect(title).to eq("https://permalink.com") end @@ -43,12 +43,12 @@ describe "#summarize" do it "returns the given text" do - message = {text: "message text"} + gold_nugget = TestFactories.create_gold_nugget(content: "message text") writer = described_class.new - summary = writer.summarize(message) + summary = writer.summarize(gold_nugget) - expect(summary).to eq(message[:text]) + expect(summary).to eq(gold_nugget.content) end end end From ec7aa37c6b3090cc007432e9f48e463b54f6b3f3 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Mon, 11 Sep 2023 09:37:10 -0600 Subject: [PATCH 9/9] Small refactor --- lib/gold_miner/slack/client.rb | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/gold_miner/slack/client.rb b/lib/gold_miner/slack/client.rb index 55e3de1..04b5ee2 100644 --- a/lib/gold_miner/slack/client.rb +++ b/lib/gold_miner/slack/client.rb @@ -37,19 +37,18 @@ def search_messages(query) warn_on_multiple_pages(response) fetch_author_names(response) - response.messages.matches - } - .map { |message| - Slack::Message.new( - id: message.id, - text: message.text, - user: Slack::User.new( - id: message.user, - name: message.author_real_name, - username: message.username - ), - permalink: message.permalink - ) + response.messages.matches.map { |message| + Slack::Message.new( + id: message.id, + text: message.text, + user: Slack::User.new( + id: message.user, + name: message.author_real_name, + username: message.username + ), + permalink: message.permalink + ) + } } end