Skip to content

Commit

Permalink
Proactively clear old Zoom reminders (#9)
Browse files Browse the repository at this point in the history
* R4DS to DSLC

* Refactor reminder clearing.

And add separate reminder clearer
  • Loading branch information
jonthegeek authored May 24, 2024
1 parent 43c0476 commit 328ac91
Show file tree
Hide file tree
Showing 24 changed files with 387 additions and 154 deletions.
2 changes: 1 addition & 1 deletion .github/SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ For additional reprex pointers, check out the [Get help!](https://www.tidyverse.

Armed with your reprex, the next step is to figure out where to ask.

* If it's a question: It's best to ask on the [R4DS Online Learning Community Slack](https://r4ds.io/join). Other options include [Posit Community](https://community.rstudio.com/), and StackOverflow. There are more people there to answer questions.
* If it's a question: It's best to ask on the [Data Science Learning Community Slack](https://DSLC.io/join). Other options include [Posit Community](https://community.rstudio.com/), and StackOverflow. There are more people there to answer questions.

* If it's a bug: you're in the right place, [file an issue](https://github.com/r4ds/bookclubcron/issues/new).

Expand Down
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Package: bookclubcron
Title: Automated Tasks for R4DS Book Clubs
Title: Automated Tasks for DSLC Book Clubs
Version: 0.0.1
Authors@R:
person("Jon", "Harmon", , "[email protected]", role = c("aut", "cre"),
Expand All @@ -13,10 +13,12 @@ BugReports: https://github.com/r4ds/bookclubcron/issues
Imports:
cli,
dplyr,
fastmatch,
fs,
glue,
googlesheets4,
httr2,
keyring,
lubridate,
memoise,
purrr,
Expand Down
7 changes: 5 additions & 2 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Generated by roxygen2: do not edit by hand

export(dslc_slack_channels)
export(dslc_youtube_playlists)
export(process_youtube)
export(process_zoom)
export(r4ds_slack_channels)
export(r4ds_youtube_playlists)
export(reset_the)
export(slack_default_token)
export(slack_set_token)
importFrom(fastmatch,"%fin%")
importFrom(rlang,"%||%")
importFrom(rlang,.data)
importFrom(rlang,.env)
1 change: 1 addition & 0 deletions R/bookclubcron-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"_PACKAGE"

## usethis namespace: start
#' @importFrom fastmatch %fin%
#' @importFrom rlang %||%
#' @importFrom rlang .data
#' @importFrom rlang .env
Expand Down
2 changes: 1 addition & 1 deletion R/clubs.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# update the spreadsheet if it's wrong, so it can be right when the meeting is
# posted!

r4ds_active_clubs <- function() {
dslc_active_clubs <- function() {
# TODO: Load info about running clubs. Right now we only use the length, and
# it isn't super important, so just return a long-enough object.
return(1:50)
Expand Down
227 changes: 212 additions & 15 deletions R/slack.R
Original file line number Diff line number Diff line change
@@ -1,53 +1,250 @@
#' Cache or Fetch R4DS Slack channels
#' Cache or Fetch DSLC Slack channels
#'
#' Fetch public and private R4DS Slack channel information.
#' Fetch public and private DSLC Slack channel information.
#'
#' @inheritParams .fetch_dslc_slack_channels
#' @param refresh Get fresh data?
#'
#' @inherit .fetch_r4ds_slack_channels return
#' @inherit .fetch_dslc_slack_channels return
#' @export
r4ds_slack_channels <- function(refresh = FALSE) {
dslc_slack_channels <- function(refresh = FALSE,
token = slack_default_token()) {
if (refresh) {
.cache_r4ds_slack_channels()
.cache_dslc_slack_channels(token)
return(the$slack_channels)
}

return(
rlang::env_cache(
the,
"slack_channels",
.fetch_r4ds_slack_channels()
.fetch_dslc_slack_channels(token)
)
)
}

#' Cache R4DS Slack channels
#' Cache DSLC Slack channels
#'
#' Set R4DS slack channels in the package `the` environment.
#' Set DSLC slack channels in the package `the` environment.
#'
#' @return A dataframe with information about R4DS Slack channels, invisibly.
#' @inheritParams .fetch_dslc_slack_channels
#'
#' @return A dataframe with information about DSLC Slack channels, invisibly.
#' @keywords internal
.cache_r4ds_slack_channels <- function() {
.cache_dslc_slack_channels <- function(token = slack_default_token()) {
return(
rlang::env_bind(
the,
slack_channels = .fetch_r4ds_slack_channels()
slack_channels = .fetch_dslc_slack_channels(token)
)
)
}

#' Fetch R4DS Slack channels
#' Fetch DSLC Slack channels
#'
#' @inheritParams slackteams::get_conversations_list
#'
#' @return A dataframe with information about R4DS Slack channels.
#' @return A dataframe with information about DSLC Slack channels.
#' @keywords internal
.fetch_r4ds_slack_channels <- function() {
.fetch_dslc_slack_channels <- function(token = slack_default_token()) {
return(
dplyr::select(
slackteams::get_conversations_list(
type = c("public_channel", "private_channel"),
exclude_archived = TRUE
exclude_archived = TRUE,
token = token
),
"id", "name", "is_private", "created", "is_general"
)
)
}

#' Fetch a Slack token
#'
#' Fetch the Slack token using the keyring package (if available), or an
#' environment variable with the same name.
#'
#' @param key_name The name of the keyring key or the environment variable.
#'
#' @return The token value as a string, or NULL (invisibly).
#' @export
#'
#' @examples
#' token <- slack_default_token()
#' nchar(token)
slack_default_token <- function(key_name = "SLACK_API_TOKEN") {
key_value <- .keyring_try(key_name) %||% Sys.getenv(key_name)
return(invisible(key_value))
}

.keyring_try <- function(key_name, keyring = NULL) {
tryCatch(
keyring::key_get(key_name, keyring = keyring),
error = function(e) NULL
)
}

#' Set a Slack API token
#'
#' Use the keyring package to store an API key.
#'
#' @param key_name The name of the key.
#' @param keyring The name of a specific keyring, passed on to
#' `keyring::set_key` if available.
#'
#' @return The key, invisibly.
#' @export
slack_set_token <- function(key_name = "SLACK_API_TOKEN", keyring = NULL) {
rlang::check_installed("keyring", "to save the API token securely.") # nocov
keyring::key_set(key_name, prompt = "Slack API token", keyring = keyring) # nocov
return(invisible(.keyring_try(key_name, keyring = keyring))) # nocov
}

remove_slack_reminders <- function(channel_name,
min_age_minutes = 55,
max_msgs_to_check = Inf,
token = slack_default_token(),
slack_channels = dslc_slack_channels(
token = token
)) {
channel_id <- .slack_channel_name_to_id(channel_name, token, slack_channels)
old_reminder_messages <- .slack_reminder_messages(
channel_id,
min_age_minutes = 55,
max_msgs_to_check = max_msgs_to_check,
token = token,
slack_channels = slack_channels
)
.delete_slack_messages(old_reminder_messages, channel_id)
}

.slack_reminder_messages <- function(channel_id,
min_age_minutes = NULL,
max_msgs_to_check = Inf,
token = slack_default_token(),
slack_channels = dslc_slack_channels(
token = token
)) {
channel_messages <- dslc_slack_channel_messages(
channel_id,
max_results = max_msgs_to_check,
token = token,
slack_channels = slack_channels
)
.filter_slack_messages(
channel_messages,
user = "USLACKBOT",
text = "Join Zoom Meeting",
min_age_minutes = min_age_minutes
)
}

.slack_channel_name_to_id <- function(channel_name,
token = slack_default_token(),
slack_channels = dslc_slack_channels(
token = token
)) {
channel_name <- .validate_channel_name(channel_name, token, slack_channels)
slack_channels$id[slack_channels$name == channel_name]
}

.validate_channel_name <- function(channel_name,
token = slack_default_token(),
slack_channels = dslc_slack_channels(
token = token
)) {
if (channel_name %in% slack_channels$name) {
return(channel_name)
}
cli::cli_abort(
"Cannot find channel {channel_name}.",
class = "bookclubcron-error-channel_name"
)
}

dslc_slack_channel_messages <- function(channel_id,
max_results = Inf,
token = slack_default_token(),
slack_channels = dslc_slack_channels(
token = token
)) {
slackthreads::conversations(
channel_id,
token = token,
max_results = max_results,
limit = min(1000, max_results)
)
}

.filter_slack_messages <- function(channel_messages,
user = NULL,
text = NULL,
min_age_minutes = NULL) {
purrr::keep(
channel_messages,
\(x) {
(is.null(user) || x[["user"]] %fin% user) &&
(is.null(text) || stringr::str_detect(x[["text"]], text)) &&
(is.null(min_age_minutes) || .ts_is_older(x[["ts"]], min_age_minutes))
}
)
}

.ts_is_older <- function(ts, min_age_minutes) {
.ts_age(ts) > lubridate::minutes(min_age_minutes)
}

.ts_age <- function(ts) {
lubridate::now() - .ts_as_datetime(ts)
}

.ts_as_datetime <- function(ts) {
lubridate::as_datetime(as.numeric(ts))
}

.delete_slack_message <- function(channel_id,
timestamp,
token = slack_default_token()) {
slackcalls::post_slack(
slack_method = "chat.delete",
channel = channel_id,
ts = timestamp,
token = token
)
}

.delete_slack_messages <- function(messages,
channel_id,
token = slack_default_token()) {
for (msg in messages) {
.delete_slack_message(channel_id, msg$ts, token = token)
}
}

dslc_book_club_channels <- function(token = slack_default_token(),
slack_channels = dslc_slack_channels(
token = token
)) {
slack_channels |>
dplyr::filter(stringr::str_starts(name, "book_club-")) |>
dplyr::pull(.data$name)
}

remove_all_club_reminders <- function(min_age_minutes = 55,
token = slack_default_token(),
slack_channels = dslc_slack_channels(
token = token
)) {
club_channels <- dslc_book_club_channels(
token = token,
slack_channels = slack_channels
)
for (channel_name in club_channels) {
remove_slack_reminders(
channel_name,
min_age_minutes = min_age_minutes,
token = token,
slack_channels = slack_channels
)
}
}
Loading

0 comments on commit 328ac91

Please sign in to comment.