diff --git a/R/client.R b/R/client.R index b05da67..81b78d8 100644 --- a/R/client.R +++ b/R/client.R @@ -119,7 +119,7 @@ list_sharepoint_sites <- function(tenant=Sys.getenv("CLIMICROSOFT365_TENANT", "c app <- choose_app(app) login <- do_login(tenant, app, scopes, ...) - login$get_user$list_sharepoint_sites() + login$get_user()$list_sharepoint_sites() } #' @rdname client diff --git a/R/ms_channel.R b/R/ms_channel.R index b8407af..b71e603 100644 --- a/R/ms_channel.R +++ b/R/ms_channel.R @@ -18,8 +18,8 @@ #' - `send_message(body, content_type, attachments)`: Sends a new message to the channel. See below. #' - `list_messages(n=50)`: Retrieves the messages in the channel. By default, this is limited to the 50 most recent messages; set the `n` argument to change this. #' - `get_message(message_id)`: Retrieves a specific message in the channel. -#' - `delete_message(message_id, confirm=TRUE)`: Deletes a message. By default, ask for confirmation first. -#' - `list_files()`: List the files for the channel. +#' - `delete_message(message_id, confirm=TRUE)`: Deletes a message. By default, ask for confirmation first. You can only delete your own messages. +#' - `list_files()`: List the files for the channel. See [ms_drive] for the arguments available. #' - `upload_file()`: Uploads a file to the channel. #' - `download_file()`: Downloads a file from the channel. #' @@ -35,7 +35,7 @@ #' Note that message attachments are actually uploaded to the channel's file listing (a directory in the team's primary shared document folder). Support for attachments is somewhat experimental, so if you want to be sure that it works, upload the file separately using the `upload_file()` method. #' #' @seealso -#' [ms_team], [ms_chat_message] +#' [ms_team], [ms_drive], [ms_chat_message] #' #' [Microsoft Graph overview](https://docs.microsoft.com/en-us/graph/overview), #' [Microsoft Teams API reference](https://docs.microsoft.com/en-us/graph/api/resources/teams-api-overview?view=graph-rest-1.0) @@ -114,7 +114,7 @@ public=list( private$get_drive()$download_file(src, dest, ...) }, - upload_file=function(src, dest, ...) + upload_file=function(src, dest=basename(src), ...) { dest <- file.path(self$properties$displayName, dest) private$get_drive()$upload_file(src, dest, ...) diff --git a/R/ms_chat_message.R b/R/ms_chat_message.R index cf5e92a..38c4fde 100644 --- a/R/ms_chat_message.R +++ b/R/ms_chat_message.R @@ -1,6 +1,6 @@ #' Teams chat message #' -#' Class representing a message in a Teams channel or chat. +#' Class representing a message in a Teams channel. Currently Microsoft365R only supports channels, not chats between individuals. #' #' @docType class #' @section Fields: @@ -10,14 +10,14 @@ #' - `properties`: The item properties (metadata). #' @section Methods: #' - `new(...)`: Initialize a new object. Do not call this directly; see 'Initialization' below. -#' - `delete(confirm=TRUE)`: Delete this item. By default, ask for confirmation first. -#' - `update(...)`: Update the item's properties (metadata) in Microsoft Graph. To update the list _data_, update the `fields` property. See the examples below. -#' - `do_operation(...)`: Carry out an arbitrary operation on the item. -#' - `sync_fields()`: Synchronise the R object with the item metadata in Microsoft Graph. +#' - `delete(confirm=TRUE)`: Delete this message. Currently the Graph API does not support deleting Teams messages, so this method is disabled. +#' - `update(...)`: Update the message's properties (metadata) in Microsoft Graph. +#' - `do_operation(...)`: Carry out an arbitrary operation on the message. +#' - `sync_fields()`: Synchronise the R object with the message metadata in Microsoft Graph. #' - `send_reply(body, content_type, attachments)`: Sends a reply to the message. See below. #' - `list_replies(n=50)`: List the replies to this message. By default, this is limited to the 50 most recent replies; set the `n` argument to change this. #' - `get_reply(message_id)`: Retrieves a specific reply to the message. -#' - `delete_reply(message_id, confirm=TRUE)`: Deletes a reply to the message. By default, ask for confirmation first. +#' - `delete_reply(message_id, confirm=TRUE)`: Deletes a reply to the message. Currently the Graph API does not support deleting Teams messages, so this method is disabled. #' #' @section Initialization: #' Creating new objects of this class should be done via the `get_message` and `list_messages` method of the [ms_team] class. Calling the `new()` method for this class only constructs the R object; it does not call the Microsoft Graph API to retrieve or create the actual message. @@ -28,9 +28,9 @@ #' - `content_type`: Either "text" (the default) or "html". #' - `attachments`: Optional vector of filenames. #' -#' Teams channels don't support nested replies, so replying to a reply will fail. +#' Teams channels don't support nested replies, so any methods dealing with replies will fail if the message object is itself a reply. #' -#' Note that message attachments are actually uploaded to the channel's file listing (a directory in the team's primary shared document folder). Support for attachments is somewhat experimental, so if you want to be sure that it works, upload the file separately using the `upload_file()` method. +#' Note that message attachments are actually uploaded to the channel's file listing (a directory in the team's primary shared document folder). Support for attachments is somewhat experimental, so if you want to be sure that it works, upload the file separately using the channel's `upload_file()` method. #' #' @seealso #' [ms_team], [ms_channel] @@ -60,6 +60,8 @@ public=list( self$type <- "Teams message" parent <- properties$channelIdentity private$api_type <- file.path("teams", parent[[1]], "channels", parent[[2]], "messages") + if(!is.null(properties$replyToId)) + private$api_type <- file.path(private$api_type, properties$replyToId, "replies") super$initialize(token, tenant, properties) }, @@ -88,9 +90,15 @@ public=list( delete_reply=function(message_id, confirm=TRUE) { + private$assert_not_nested_reply() self$get_reply(message_id)$delete(confirm=confirm) }, + delete=function(confirm=TRUE) + { + stop("Deleting Teams messages is not currently supported", call.=FALSE) + }, + print=function(...) { parent <- self$properties$channelIdentity diff --git a/R/ms_team.R b/R/ms_team.R index 75e8607..1b8195f 100644 --- a/R/ms_team.R +++ b/R/ms_team.R @@ -14,7 +14,7 @@ #' - `update(...)`: Update the team metadata in Microsoft Graph. #' - `do_operation(...)`: Carry out an arbitrary operation on the team. #' - `sync_fields()`: Synchronise the R object with the team metadata in Microsoft Graph. -#' - `list_channels()`: List the channels for this team. +#' - `list_channels(filter=NULL)`: List the channels for this team. Optionally, supply an OData expression to filter the list. #' - `get_channel(channel_name, channel_id)`: Retrieve a channel. If the name and ID are not specified, returns the primary channel. #' - `list_drives()`: List the drives (shared document libraries) associated with this team. #' - `get_drive(drive_id)`: Retrieve a shared document library for this team. If the ID is not specified, this returns the default document library. @@ -52,9 +52,10 @@ public=list( super$initialize(token, tenant, properties) }, - list_channels=function() + list_channels=function(filter=NULL) { - res <- private$get_paged_list(self$do_operation("channels")) + opts <- if(!is.null(filter)) list(`$filter`=filter) + res <- private$get_paged_list(self$do_operation("channels", options=opts)) private$init_list_objects(res, "channel", team_id=self$properties$id) }, @@ -62,11 +63,11 @@ public=list( { if(!is.null(channel_name) && is.null(channel_id)) { - channels <- self$list_channels() - n <- which(sapply(channels, function(ch) ch$properties$displayName == channel_name)) - if(length(n) != 1) + filter <- sprintf("displayName eq '%s'", channel_name) + channels <- self$list_channels(filter=filter) + if(length(channels) != 1) stop("Invalid channel name", call.=FALSE) - return(channels[[n]]) + return(channels[[1]]) } op <- if(is.null(channel_name) && is.null(channel_id)) "primaryChannel" diff --git a/man/ms_channel.Rd b/man/ms_channel.Rd index 5f74592..9ba0053 100644 --- a/man/ms_channel.Rd +++ b/man/ms_channel.Rd @@ -32,8 +32,8 @@ Class representing a Microsoft Teams channel. \item \code{send_message(body, content_type, attachments)}: Sends a new message to the channel. See below. \item \code{list_messages(n=50)}: Retrieves the messages in the channel. By default, this is limited to the 50 most recent messages; set the \code{n} argument to change this. \item \code{get_message(message_id)}: Retrieves a specific message in the channel. -\item \code{delete_message(message_id, confirm=TRUE)}: Deletes a message. By default, ask for confirmation first. -\item \code{list_files()}: List the files for the channel. +\item \code{delete_message(message_id, confirm=TRUE)}: Deletes a message. By default, ask for confirmation first. You can only delete your own messages. +\item \code{list_files()}: List the files for the channel. See \link{ms_drive} for the arguments available. \item \code{upload_file()}: Uploads a file to the channel. \item \code{download_file()}: Downloads a file from the channel. } @@ -79,7 +79,7 @@ chan$upload_file("mydocument.docx") } } \seealso{ -\link{ms_team}, \link{ms_chat_message} +\link{ms_team}, \link{ms_drive}, \link{ms_chat_message} \href{https://docs.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, \href{https://docs.microsoft.com/en-us/graph/api/resources/teams-api-overview?view=graph-rest-1.0}{Microsoft Teams API reference} diff --git a/man/ms_chat_message.Rd b/man/ms_chat_message.Rd index fbda72d..4db78f0 100644 --- a/man/ms_chat_message.Rd +++ b/man/ms_chat_message.Rd @@ -8,7 +8,7 @@ An R6 object of class \code{ms_chat_message}, inheriting from \code{ms_object}. } \description{ -Class representing a message in a Teams channel or chat. +Class representing a message in a Teams channel. Currently Microsoft365R only supports channels, not chats between individuals. } \section{Fields}{ @@ -24,14 +24,14 @@ Class representing a message in a Teams channel or chat. \itemize{ \item \code{new(...)}: Initialize a new object. Do not call this directly; see 'Initialization' below. -\item \code{delete(confirm=TRUE)}: Delete this item. By default, ask for confirmation first. -\item \code{update(...)}: Update the item's properties (metadata) in Microsoft Graph. To update the list \emph{data}, update the \code{fields} property. See the examples below. -\item \code{do_operation(...)}: Carry out an arbitrary operation on the item. -\item \code{sync_fields()}: Synchronise the R object with the item metadata in Microsoft Graph. +\item \code{delete(confirm=TRUE)}: Delete this message. Currently the Graph API does not support deleting Teams messages, so this method is disabled. +\item \code{update(...)}: Update the message's properties (metadata) in Microsoft Graph. +\item \code{do_operation(...)}: Carry out an arbitrary operation on the message. +\item \code{sync_fields()}: Synchronise the R object with the message metadata in Microsoft Graph. \item \code{send_reply(body, content_type, attachments)}: Sends a reply to the message. See below. \item \code{list_replies(n=50)}: List the replies to this message. By default, this is limited to the 50 most recent replies; set the \code{n} argument to change this. \item \code{get_reply(message_id)}: Retrieves a specific reply to the message. -\item \code{delete_reply(message_id, confirm=TRUE)}: Deletes a reply to the message. By default, ask for confirmation first. +\item \code{delete_reply(message_id, confirm=TRUE)}: Deletes a reply to the message. Currently the Graph API does not support deleting Teams messages, so this method is disabled. } } @@ -49,9 +49,9 @@ To reply to a message, use the \code{send_reply()} method. This has arguments: \item \code{attachments}: Optional vector of filenames. } -Teams channels don't support nested replies, so replying to a reply will fail. +Teams channels don't support nested replies, so any methods dealing with replies will fail if the message object is itself a reply. -Note that message attachments are actually uploaded to the channel's file listing (a directory in the team's primary shared document folder). Support for attachments is somewhat experimental, so if you want to be sure that it works, upload the file separately using the \code{upload_file()} method. +Note that message attachments are actually uploaded to the channel's file listing (a directory in the team's primary shared document folder). Support for attachments is somewhat experimental, so if you want to be sure that it works, upload the file separately using the channel's \code{upload_file()} method. } \examples{ diff --git a/man/ms_team.Rd b/man/ms_team.Rd index 623a503..f9cc9fd 100644 --- a/man/ms_team.Rd +++ b/man/ms_team.Rd @@ -28,7 +28,7 @@ Class representing a team in Microsoft Teams. \item \code{update(...)}: Update the team metadata in Microsoft Graph. \item \code{do_operation(...)}: Carry out an arbitrary operation on the team. \item \code{sync_fields()}: Synchronise the R object with the team metadata in Microsoft Graph. -\item \code{list_channels()}: List the channels for this team. +\item \code{list_channels(filter=NULL)}: List the channels for this team. Optionally, supply an OData expression to filter the list. \item \code{get_channel(channel_name, channel_id)}: Retrieve a channel. If the name and ID are not specified, returns the primary channel. \item \code{list_drives()}: List the drives (shared document libraries) associated with this team. \item \code{get_drive(drive_id)}: Retrieve a shared document library for this team. If the ID is not specified, this returns the default document library. diff --git a/tests/testthat/test03_sharepoint.R b/tests/testthat/test03_sharepoint.R index ab171c3..5924d84 100644 --- a/tests/testthat/test03_sharepoint.R +++ b/tests/testthat/test03_sharepoint.R @@ -26,29 +26,30 @@ test_that("SharePoint client works", expect_error(get_sharepoint_site(site_name=site_name, site_url=site_url, site_id=site_id, tenant=tenant, app=app)) - site1 <- get_sharepoint_site(site_name=site_name, tenant=tenant, app=app) + site1 <- try(get_sharepoint_site(site_name=site_name, tenant=tenant, app=app), silent=TRUE) + if(inherits(site1, "try-error")) + skip("SharePoint tests skipped: service not available") expect_is(site1, "ms_site") expect_identical(site1$properties$displayName, site_name) site2 <- get_sharepoint_site(site_url=site_url, tenant=tenant, app=app) - expect_is(site1, "ms_site") + expect_is(site2, "ms_site") expect_identical(site1$properties$webUrl, site_url) site3 <- get_sharepoint_site(site_id=site_id, tenant=tenant, app=app) - expect_is(site1, "ms_site") + expect_is(site3, "ms_site") expect_identical(site1$properties$id, site_id) expect_identical(site1$properties, site2$properties) expect_identical(site2$properties, site3$properties) + + sites <- list_sharepoint_sites() + expect_is(sites, "list") + expect_true(all(sapply(sites, inherits, "ms_site"))) }) test_that("SharePoint methods work", { - gr <- AzureGraph::ms_graph$new(token=tok) - testsite <- try(gr$call_graph_endpoint(file.path("sites", site_id)), silent=TRUE) - if(inherits(testsite, "try-error")) - skip("SharePoint tests skipped: service not available") - site <- get_sharepoint_site(site_name, tenant=tenant, app=app) expect_is(site, "ms_site") diff --git a/tests/testthat/test04_teams.R b/tests/testthat/test04_teams.R new file mode 100644 index 0000000..551bbc4 --- /dev/null +++ b/tests/testthat/test04_teams.R @@ -0,0 +1,77 @@ +tenant <- Sys.getenv("AZ_TEST_TENANT_ID") +app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID") +team_name <- Sys.getenv("AZ_TEST_TEAM_NAME") +team_id <- Sys.getenv("AZ_TEST_TEAM_ID") +channel_name <- Sys.getenv("AZ_TEST_CHANNEL_NAME") +channel_id <- Sys.getenv("AZ_TEST_CHANNEL_ID") + +if(tenant == "" || app == "" || team_name == "" || team_id == "" || channel_name == "" || channel_id == "") + skip("Teams tests skipped: Microsoft Graph credentials not set") + +if(!interactive()) + skip("Teams tests skipped: must be in interactive session") + +tok <- try(AzureAuth::get_azure_token( + c("https://graph.microsoft.com/.default", + "openid", + "offline_access"), + tenant=tenant, app=app, version=2), + silent=TRUE) +if(inherits(tok, "try-error")) + skip("Teams tests skipped: no access to tenant") + +test_that("Teams client works", +{ + expect_error(get_team(team_name=team_name, team_id=team_id, tenant=tenant, app=app)) + + team1 <- try(get_team(team_name=team_name, tenant=tenant, app=app), silent=TRUE) + if(inherits(team1, "try-error")) + skip("SharePoint tests skipped: service not available") + + expect_is(team1, "ms_team") + expect_identical(team1$properties$displayName, team_name) + + team2 <- get_team(team_id=team_id, tenant=tenant, app=app) + expect_is(team2, "ms_team") + expect_identical(team1$properties$id, team_id) + + teams <- list_teams() + expect_is(teams, "list") + expect_true(all(sapply(teams, inherits, "ms_team"))) +}) + +test_that("Teams methods work", +{ + team <- get_team(team_name, tenant=tenant, app=app) + expect_is(team, "ms_team") + + # drive -- functionality tested in test02 + drives <- team$list_drives() + expect_is(drives, "list") + expect_true(all(sapply(drives, inherits, "ms_drive"))) + + drv <- team$get_drive() + expect_is(drv, "ms_drive") + + grp <- team$get_group() + expect_is(grp, "az_group") + + site <- team$get_sharepoint_site() + expect_is(site, "ms_site") + + # channels + chans <- team$list_channels() + expect_is(chans, "list") + expect_true(all(sapply(chans, inherits, "ms_channel"))) + + expect_error(team$get_channel(channel_name, channel_id)) + + chan0 <- team$get_channel() + expect_is(chan0, "ms_channel") + + chan1 <- team$get_channel(channel_name=channel_name) + expect_is(chan1, "ms_channel") + + chan2 <- team$get_channel(channel_id=channel_id) + expect_is(chan2, "ms_channel") +}) diff --git a/tests/testthat/test04a_channel.R b/tests/testthat/test04a_channel.R new file mode 100644 index 0000000..1c58238 --- /dev/null +++ b/tests/testthat/test04a_channel.R @@ -0,0 +1,76 @@ +tenant <- Sys.getenv("AZ_TEST_TENANT_ID") +app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID") +team_name <- Sys.getenv("AZ_TEST_TEAM_NAME") +team_id <- Sys.getenv("AZ_TEST_TEAM_ID") +channel_name <- Sys.getenv("AZ_TEST_CHANNEL_NAME") +channel_id <- Sys.getenv("AZ_TEST_CHANNEL_ID") + +if(tenant == "" || app == "" || team_name == "" || team_id == "" || channel_name == "" || channel_id == "") + skip("Channel tests skipped: Microsoft Graph credentials not set") + +if(!interactive()) + skip("Channel tests skipped: must be in interactive session") + +tok <- try(AzureAuth::get_azure_token( + c("https://graph.microsoft.com/.default", + "openid", + "offline_access"), + tenant=tenant, app=app, version=2), + silent=TRUE) +if(inherits(tok, "try-error")) + skip("Channel tests skipped: no access to tenant") + +test_that("Channel methods work", +{ + team <- get_team(team_id=team_id, tenant=tenant, app=app) + expect_is(team, "ms_team") + + chan <- team$get_channel(channel_name=channel_name) + expect_is(chan, "ms_channel") + + lst <- chan$list_messages() + expect_is(lst, "list") + expect_true(all(sapply(lst, inherits, "ms_chat_message"))) + + msg_body <- sprintf("Test message: %s", make_name(5)) + msg <- chan$send_message(msg_body) + expect_is(msg, "ms_chat_message") + + msg2_body <- sprintf("