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

Use reactions for a better categorization of news (closes #41, closes #42) #54

Merged
merged 1 commit into from
Aug 29, 2022
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
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added

- Add a new command, `category`, that displays the list of categories and their associated labels (#39).
- Support a main label for categories (#39).
- Support blockquotes (#38).
- Brackets (`<` and `>`) are now unescaped in the messages in order to be properly interpreted in the final
Markdown document.
- New lines are now retained if they appear in the first message of a thread. New lines in replies are still deleted:
this is required because replies must be displayed in a Markdown list.
- Add a new category, `ignore` (#42). This category allows you to ignore a link when generating show notes.

### Changed

- All commands associated keywords are now displayed in the help message (#48).
- Update categories order and labels (#39).
- Messages (thread or replies) are ignored if they contain a user mention (#40).
- Use reactions (with emojis) for a better categorization of news (#41, #42).

### Fixed

Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ A slack bot that automates show notes creation for _Les Cast Codeurs podcast_.
This bot is using the [Socket Mode](https://api.slack.com/apis/connections/socket) and can be set up
without much hassle.

### Register bot in your workspace

First you need to create a [Slack app](https://api.slack.com/start) and install it in your workspace :

1. go to [https://api.slack.com/apps](https://api.slack.com/apps),
Expand All @@ -21,6 +23,38 @@ First you need to create a [Slack app](https://api.slack.com/start) and install
6. save the _Bot User OAuth Token_ for later in _Features > OAuth & Permissions_ (it will be referred
as `SLACK_BOT_TOKEN`).

### Create customised emojis

Then you need to [add customised emojis to your
workspace](https://slack.com/intl/fr-fr/help/articles/206870177-Ajouter-un-%C3%A9moji-personnalis%C3%A9-et-des-alias-dans-votre-espace-de-travail).
Those will allow you to categorize news using reactions.

The following emojis must be added (suggested emojis can also be found in [this directory](/emojis)) :

| Category | Name | Suggested image |
|------------------------------|--------------|----------------------------------------------------------|
| Langages | lcc_lang | https://openmoji.org/library/emoji-E1C1/ |
| Librairies | lcc_lib | https://openmoji.org/library/emoji-1F4DA/ |
| Infrastructure | lcc_infra | https://openmoji.org/library/emoji-1F3E3/ |
| Cloud | lcc_cloud | https://openmoji.org/library/emoji-1F32C/ |
| Web | lcc_web | https://openmoji.org/library/emoji-E055/ |
| Data | lcc_data | https://openmoji.org/library/emoji-1F4BE/ |
| Outillage | lcc_outil | https://openmoji.org/library/emoji-1F6E0/ |
| Architecture | lcc_archi | https://openmoji.org/library/emoji-1F9D1-200D-1F3EB/ |
| Méthodologies | lcc_methodo | https://openmoji.org/library/emoji-1F9D1-200D-1F373/ |
| Sécurité | lcc_secu | https://openmoji.org/library/emoji-1F46E/ |
| Loi, société et organisation | lcc_loi | https://openmoji.org/library/emoji-1F9D1-200D-2696-FE0F/ |
| Outils de l’épisode | lcc_outil_ep | https://openmoji.org/library/emoji-1F984/ |
| Rubrique débutant | lcc_debutant | https://openmoji.org/library/emoji-1F9D1-200D-1F393/ |
| Conférences | lcc_conf | https://openmoji.org/library/emoji-1F9D1-200D-1F3A4/ |
| Messages exclus | lcc_exclude | https://openmoji.org/library/emoji-274C/ |
| Messages inclus | lcc_include | https://openmoji.org/library/emoji-2714/ |

Note also that aliases does not work because the name returned by the Slack API is not the name of the alias, but the
name of the aliased emoji.

### Create the GitHub token

Then you need to create a GitHub [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
and a _publication_ repository (a repository where the show notes will be published) :

Expand Down
2 changes: 2 additions & 0 deletions emojis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
All emojis in this directory are from [OpenMoji](https://openmoji.org/) and are free to use under the CC BY-SA 4.0
license.
Binary file added emojis/lcc_archi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_cloud.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_conf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_data.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_debutant.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_exclude.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_include.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_infra.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_lang.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_lib.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_loi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_methodo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_outil.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_outil_ep.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_secu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added emojis/lcc_web.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static java.util.Objects.requireNonNull;
import static org.slf4j.LoggerFactory.getLogger;

import com.lescastcodeurs.bot.slack.SlackClient;
import com.lescastcodeurs.bot.slack.SlackThread;
import com.slack.api.model.event.AppMentionEvent;
import io.quarkus.qute.Location;
Expand Down
80 changes: 41 additions & 39 deletions src/main/java/com/lescastcodeurs/bot/ShowNote.java
Original file line number Diff line number Diff line change
@@ -1,74 +1,76 @@
package com.lescastcodeurs.bot;

import static com.lescastcodeurs.bot.ShowNoteCategory.EXCLUDE;
import static com.lescastcodeurs.bot.ShowNoteCategory.INCLUDE;
import static java.util.Objects.requireNonNull;
import static java.util.function.Predicate.not;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.DOTALL;

import com.lescastcodeurs.bot.slack.SlackReply;
import com.lescastcodeurs.bot.slack.SlackThread;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** A wrapper around {@link SlackThread} to allow customization for show notes generation. */
public class ShowNote {

// First wildcard is non-greedy : only the first link is considered.
private static final Pattern SHOW_NOTE_PATTERN =
Pattern.compile(
"^.*?(?<note><https?://[^>]+>)\\s*(\\((?<category>[^)]+)\\))?.*$",
CASE_INSENSITIVE | DOTALL);

// This patten works against the generated markdown. Must be kept in sync with SHOW_NOTE_PATTERN.
private static final Pattern CATEGORY_ERASER_PATTERN =
Pattern.compile(
"^(?<before>.*?\\(https?://[^)]+\\))(?<category>\\s*\\([^)]+\\))?(?<after>.*)$",
CASE_INSENSITIVE | DOTALL);

private final SlackThread thread;
private final String markdown;
private final Matcher urlMatcher;

public ShowNote(SlackThread thread) {
this.thread = requireNonNull(thread);
this.markdown = thread.asMarkdown();
this.urlMatcher = SHOW_NOTE_PATTERN.matcher(thread.text());
}

public boolean isShowNote() {
boolean userMessage = !thread.isAppMessage();
boolean containsLink = urlMatcher.matches();
boolean hasNoMention = !thread.hasMention();
return containsLink && userMessage && hasNoMention;
}
public ShowNoteCategory category() {
ShowNoteCategory category = null;

public String timestamp() {
return thread.timestamp();
for (String reaction : thread.reactions()) {
Optional<ShowNoteCategory> guessed = ShowNoteCategory.find(reaction);
if (guessed.isPresent()) {
category = guessed.get();
}
}

return category;
}

public String text() {
Optional<ShowNoteCategory> category = ShowNoteCategory.find(urlMatcher.group("category"));
public boolean isShowNote() {
if (thread.isAppMessage()) {
return false; // application or bot message
}

if (category.isPresent()) {
Matcher markdownMatcher = CATEGORY_ERASER_PATTERN.matcher(markdown);
if (markdownMatcher.matches()) {
return markdownMatcher.replaceFirst("${before}${after}");
ShowNoteCategory category = category();
if (category == null) {
if (thread.hasMention()) {
return false;
} else {
return thread.hasLink();
}
}

return markdown;
return category != EXCLUDE;
}

public ShowNoteCategory category() {
String category = urlMatcher.group("category");
return ShowNoteCategory.find(category).orElse(ShowNoteCategory.NEWS);
public String timestamp() {
return thread.timestamp();
}

public String text() {
return thread.asMarkdown();
}

public List<String> comments() {
return thread.replies().stream()
.filter(not(SlackReply::hasMention))
.filter(
reply -> {
if (reply.isAppMessage()) {
return false;
} else if (reply.reactions().contains(EXCLUDE.reaction())) {
return false;
} else if (reply.reactions().contains(INCLUDE.reaction())) {
return true;
} else {
return !reply.hasMention();
}
})
.map(SlackReply::asMarkdown)
.flatMap(String::lines)
.filter(not(String::isBlank))
Expand Down
94 changes: 29 additions & 65 deletions src/main/java/com/lescastcodeurs/bot/ShowNoteCategory.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,39 @@

import static java.util.Objects.requireNonNull;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/** Les cast codeurs podcast categories (by order of appearance in the show notes). */
public enum ShowNoteCategory {
LANGUAGES("Langages", "lang", "langage", "langages", "langs", "language", "languages"),
LIBRARIES("Librairies", "lib", "libs", "libraries", "librairies", "librairie", "library"),
INFRASTRUCTURE("Infrastructure", "infra", "infrastructure"),
CLOUD("Cloud", "cloud", "iaas"),
WEB("Web", "web", "www"),
DATA("Data", "data", "db"),
TOOLING("Outillage", "outil", "outils", "tool", "tools", "outillage", "tooling"),
ARCHITECTURE("Architecture", "archi", "arch", "architecture", "architectures"),
METHODOLOGIES(
"Méthodologies",
"methodo",
"metodo",
"methode",
"methodologie",
"methodologies",
"methodology"),
SECURITY("Sécurité", "secu", "sec", "securite", "security", "secure"),
SOCIETY(
"Loi, société et organisation",
"loi",
"law",
"societe",
"society",
"org",
"orga",
"organisation",
"organization"),
TOOL_OF_THE_EPISODE("Outils de l’épisode", "outil-ep", "outil-episode"),
BEGINNERS("Rubrique débutant", "debutant", "debutants", "beginner", "beginners"),
CONFERENCES("Conférences", "conf", "conferences", "conference"),
// This category must not be used in template.
IGNORED("Ignoré", "ignore", "ignorer", "ignored", "exclu", "exclude", "exclure"),
// Fall-back for unrecognized categories.
NEWS("Non catégorisé", "news", "nouvelles", "nouvelle");
LANGUAGES("Langages", "lcc_lang"),
LIBRARIES("Librairies", "lcc_lib"),
INFRASTRUCTURE("Infrastructure", "lcc_infra"),
CLOUD("Cloud", "lcc_cloud"),
WEB("Web", "lcc_web"),
DATA("Data", "lcc_data"),
TOOLING("Outillage", "lcc_outil"),
ARCHITECTURE("Architecture", "lcc_archi"),
METHODOLOGIES("Méthodologies", "lcc_methodo"),
SECURITY("Sécurité", "lcc_secu"),
SOCIETY("Loi, société et organisation", "lcc_loi"),
TOOL_OF_THE_EPISODE("Outils de l’épisode", "lcc_outil_ep"),
BEGINNERS("Rubrique débutant", "lcc_debutant"),
CONFERENCES("Conférences", "lcc_conf"),

// special categories
INCLUDE(
"Messages inclus",
"lcc_include"), // Fall-back for unknown categories / force message inclusion.
EXCLUDE("Messages exclus", "lcc_exclude"); // Force message exclusion.

private final String description;
private final List<String> labels;

/**
* Returns the category matching the given non-null label, or {@link #NEWS} if the label does not
* match any category.
*
* <p>Note that label are case-sensitive and accents-insensitive.
*/
public static Optional<ShowNoteCategory> find(String label) {
if (label != null) {
String normalized = StringUtils.normalize(label);
private final String reaction;

/** Returns the category matching the given non-null label. */
public static Optional<ShowNoteCategory> find(String reaction) {
if (reaction != null) {
for (ShowNoteCategory category : values()) {
if (category.labels().contains(normalized)) {
if (category.reaction.equals(reaction)) {
return Optional.of(category);
}
}
Expand All @@ -66,29 +43,16 @@ public static Optional<ShowNoteCategory> find(String label) {
return Optional.empty();
}

ShowNoteCategory(String description, String... labels) {
ShowNoteCategory(String description, String reaction) {
this.description = requireNonNull(description);
this.labels = List.copyOf(Arrays.stream(labels).map(StringUtils::normalize).toList());

if (this.labels.size() < 2) {
throw new IllegalArgumentException(
"at least two labels are required (formatting help is easier with this constraint)");
}
this.reaction = requireNonNull(reaction);
}

public String description() {
return description;
}

public String mainLabel() {
return labels.get(0);
}

public List<String> alternateLabels() {
return labels.subList(1, labels.size());
}

public List<String> labels() {
return labels;
public String reaction() {
return reaction;
}
}
14 changes: 8 additions & 6 deletions src/main/java/com/lescastcodeurs/bot/ShowNotes.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ public List<ShowNote> notes() {
return notes;
}

public List<ShowNote> notes(String label) {
ShowNoteCategory category =
ShowNoteCategory.find(label)
.orElseThrow(
() -> new IllegalArgumentException("no category found for label " + label));
return notes().stream().filter(n -> n.category() == category).toList();
public List<ShowNote> notes(String name) {
ShowNoteCategory category = ShowNoteCategory.valueOf(name);
return notes().stream()
.filter(
n ->
n.category() == category
|| (n.category() == null && category == ShowNoteCategory.INCLUDE))
.toList();
}
}
Loading