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

Add author links on articles #25

Merged
merged 5 commits into from
Sep 1, 2023
Merged

Add author links on articles #25

merged 5 commits into from
Sep 1, 2023

Conversation

MatheusRich
Copy link
Contributor

@MatheusRich MatheusRich commented Aug 24, 2023

Until now, the article writer had to manually add links for every author
mentioned in the article. This PR automates this process.

Now, we load an AuthorConfig with a preferred link for each author. If the
author is not in the config, we use the default #to-do link.

This class also has two additional advantages:

  1. It allows future improvements, like adding a preferred name instead of
    automatically using the one from Slack.
  2. It allows users to update their links instead of asking someone else to
    do so. This is in line with our value of self-management.

Other changes:

  • I updated the prompt on OpenAiWriter to prevent it from removing the author
    links.
  • I updated some specs to use a Factory instead of ad-hoc objects.
  • Updated ruby-openai gem and use ChatGPT 3.5 model

Also add a small test factory module.
Up to now, the article writer had to manually add links for every author
mentioned in the article. This PR automates of this process.

Now we load an `AuthorConfig` that has a preferred link for each author. If the
author is not in the config, we use the default `#to-do` link.

This class also has two additional advantages:

1. It allows future improvements, like adding a preferred name, instead of
   automatically using the one from Slack.
2. It allows users to update their own links, instead of asking someone else to
   do it. This is in line with [our value of self-management].

Other changes:

- I updated the prompt on `OpenAiWriter`, to prevent it from removing the author
links.
- I update some specs to use a Factory instead of ad-hoc objects.

[our value of self-management]: https://thoughtbot.com/playbook/our-company/purpose-mission-values#self-management
Copy link
Contributor Author

@MatheusRich MatheusRich Aug 24, 2023

Choose a reason for hiding this comment

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

This list is based on previous editions of This Week in #dev, so I know which links these authors want to use.

@@ -9,8 +9,8 @@ class Slack::Client

extend Dry::Monads[:result]

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

Choose a reason for hiding this comment

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

It feels a bit strange to do this inside the Slack::Client class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does it feel like Slack::Client knows too much about your domain?
Should it also know about the "rupee-gold" emoji, your keywords "TIL", "tip"?

Just rubber-ducking :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should it also know about the "rupee-gold" emoji, your keywords "TIL", "tip"?

Those bother me less than the AuthorConfig, because at least they're related to a slack message. We do have a MessagesQuery class, which is a builder for a Slack message search query.

If we could extract the "Interesting Messages" concept somehow, that could be useful.

Maybe a new class?

# I'm not a fan of this name, but this is just to spark an idea
class InterestingMessagesQueries
  def initialize(slack_channel)
    @slack_channel = slack_channel
  end

  def til_messages_query = interesting_messages.with_topic("TIL")

  def tip_messages = interesting_messages.with_topic("tip")

  def golden_messages = interesting_messages.with_reaction("rupee-gold")

  private

  def interesting_messages
    MessagesQuery
      .new
      .on_channel(@slack_channel)
      .sent_after_last_friday
  end
end

Copy link
Contributor

Choose a reason for hiding this comment

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

Good points. I've also been thinking about the "layers" of the app. Something like this might make sense:

  • Client objects: SDK or wrappers around SDK code: Slack::Client, OpenAi::Client. We could think of them as part of the "infrastructure layer"

  • Query and Service objects: part of the "domain layer". They use Client objects to do their thing, and return raw data, or rich objects, that are part of our domain.

  • View objects: part of the "presentation layer". OpenAiWriter, SimpleWriter. They shouldn't know about Client objects. They would receive domain data and present it.

  • GoldMiner object, and any other object we need to coordinate execution: part of the "application layer", the way through which we interact with the application.

That's my Rails brain thinking :D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's useful. I'd love to take some time to investigate this together and do some refactoring.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, any time!

permalink: match.permalink
)
end
end

def fetch_author(message)
def build_author(message)
Slack::User.new(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd love to call this class Author, instead of Slack::User. Maybe there are two objects here?

  1. Slack::User: contains:
    • id (Slack id)
    • username (Slack username)
  2. BlogPost::Author: contains:
    • name (which I'm planning to pull from a config instead of only using what's on Slack)
    • link: preferred link to use

And the BlogPost::Author might receive a Slack::User on its constructor.

Thoughts?

@mike-burns
Copy link

Is it possible to share that YAML with the one that Giant Robots uses?

@MatheusRich
Copy link
Contributor Author

@mike-burns That would be cool, and I thought about it too. The challenge is that we need to map someone's Slack username to the name on that YAML file. We have no common "id" or "foreign key" on both systems.

Alternatively, both could use Hub as the source of truth and have it generate a YAML with all the info we might need.

Comment on lines 8 to 21
def self.default
YAML
.load_file(DEFAULT_CONFIG_PATH)
.transform_values { |author| author["link"] }
.then { |links| new(links) }
end

def initialize(author_config = {})
@author_config = author_config
end

def link_for(slack_username)
@author_config.fetch(slack_username, DEFAULT_AUTHOR_LINK)
end
Copy link
Contributor

@elias19r elias19r Sep 1, 2023

Choose a reason for hiding this comment

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

minor: Just noticed some "name disparity": AuthorConfig cannot be initialized with the contents of author_config.yml because it actually expects "author links" in the initialization.

If we plan to support more config fields in the future, we could load the hash from the YAML file as is, not transforming it, and update the link_for method. Something along the lines:

# frozen_string_literal: true

module GoldMiner
  class AuthorConfig
    DEFAULT_AUTHOR_LINK = "#to-do"
    DEFAULT_CONFIG_PATH = "lib/config/author_config.yml"

    def self.default
      YAML
        .load_file(DEFAULT_CONFIG_PATH)
        .then { |author_config| new(author_config) }
    end

    def initialize(author_config = {})
      @author_config = author_config
    end

    def link_for(slack_username)
      @author_config.dig(slack_username, "link") || DEFAULT_AUTHOR_LINK
    end
  end
end

Alternatively, we could rename the file and class like author_config.yml --> author_links.yml (and update the content) and AuthorConfig --> AuthorLinks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively, we could rename the file and class like author_config.yml --> author_links.yml (and update the content) and AuthorConfig --> AuthorLinks

That's how it started, but I figured we could add more fields (like name) later, so I changed the class to AuthorConfig.

AuthorConfig cannot be initialized with the contents of author_config.yml because it actually expects "author links" in the initialization.

You're absolutely right! This is reminiscent of when it was AuthorLinks instead. TY for catching this.

lib/config/author_config.yml Outdated Show resolved Hide resolved
Copy link
Contributor

@elias19r elias19r left a comment

Choose a reason for hiding this comment

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

Thank you for working on this!

I don't have many suggestions, I just left a minor comment about AuthorConfig

MatheusRich and others added 2 commits September 1, 2023 13:41
Co-authored-by:  Elias Rodrigues <eliasrodrigues.info@gmail.com>
@MatheusRich MatheusRich merged commit 37c33e7 into main Sep 1, 2023
3 checks passed
@MatheusRich MatheusRich deleted the mr/author-links branch September 1, 2023 18:28
MatheusRich added a commit that referenced this pull request Sep 8, 2023
While coding #25, I noticed (as correctly pointed out by @elias19r) some weird dependencies in the app: The Slack Client knew too much about the business logic. This PR is a first move in a new direction. More changes will come in the future.

Here are the major changes:

- **Rewrite `Slack::Client`**:
  - This class used to know too much about the app-specific needs.
    It's now a thin wrapper around the Slack API client gem.
  - A major change though, is that it automatically fetches the message
    author real name, so it issues an additional API call for each message.
    To remedy the performance hits, all the author names are requested in
    parallel, with a cache to avoid unnecessary requests.

- **Introduce `GoldMiner::SlackExplorer`**
    - This class is responsible for "exploring" Slack and "mining" interesting
    messages. Those messages, later become a blog post.
    - Now that we have a simple Slack client, we can put the business logic
    in the explorer. This also opens the door for more explorers in the future,
    like a Twitter explorer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants