├── .github ├── .gitignore ├── ISSUE_TEMPLATE.md ├── workflows │ ├── lint.yaml │ ├── R-CMD-check-novcr.yaml │ ├── pkgdown.yaml │ ├── R-CMD-check.yaml │ └── test-coverage.yaml ├── unused-workflows │ └── render-readme.yaml └── CONTRIBUTING.md ├── man ├── .gitignore ├── figures │ └── logo.png ├── meetup_sitrep.Rd ├── get_group.Rd ├── get_self.Rd ├── local_meetupr_debug.Rd ├── get_event.Rd ├── get_event_comments.Rd ├── meetup_query.Rd ├── meetup_schema_queries.Rd ├── meetup_schema_search.Rd ├── meetup_schema.Rd ├── get_event_rsvps.Rd ├── meetup_schema_mutations.Rd ├── get_group_members.Rd ├── find_topics.Rd ├── meetup_auth.Rd ├── find_groups.Rd ├── meetup_req.Rd ├── meetup_schema_type.Rd ├── get_group_events.Rd ├── meetup_client.Rd ├── meetupr-package.Rd ├── meetup_auth_status.Rd ├── meetup_keys.Rd ├── get_pro.Rd └── meetup_ci.Rd ├── man-roxygen ├── id.R ├── token.R ├── schema.R ├── extra_graphql.R ├── urlname.R ├── status.R ├── date_after.R ├── date_before.R ├── max_results.R ├── verbose.R ├── use_oauth.R ├── client_name.R └── handle_multiples.R ├── .gitattributes ├── LICENSE ├── R ├── sysdata.rda ├── zzz.R ├── deprecated.R ├── meetupr-package.R ├── find.R ├── get-pro.R ├── get-self.R ├── graphql-builders.R ├── graphql-extractors.R ├── get-event.R └── api.R ├── vignettes ├── .gitignore └── _vcr │ ├── graphql-debug-errors.yml │ ├── introsp-multiple-queries.yml │ ├── graphql-manual-pagination.yml │ ├── meetupr-sitrep.yml │ ├── meetupr-verify-sitrep.yml │ ├── introsp-nested-data.yml │ ├── meetupr-group-members.yml │ ├── introsp-custom-query.yml │ ├── meetupr-event-details.yml │ ├── meetupr-group-info.yml │ └── graphql-list-columns.yml ├── tests ├── testthat.R └── testthat │ ├── helper.R │ ├── test-deprecated.R │ ├── _vcr │ ├── get_self.yml │ ├── get_self_basic.yml │ ├── get_event_rsvps_max.yml │ ├── find_topics_multiples.yml │ ├── find_groups_datetime.yml │ ├── find_groups_correct.yml │ ├── get_event.yml │ └── get_group.yml │ ├── _snaps │ ├── get_self.md │ ├── get_event.md │ └── get_group.md │ ├── test-get_pro.R │ ├── test-graphql-builders.R │ └── test-get_event.R ├── pkgdown └── favicon │ ├── favicon.ico │ ├── favicon-96x96.png │ ├── apple-touch-icon.png │ ├── web-app-manifest-192x192.png │ ├── web-app-manifest-512x512.png │ └── site.webmanifest ├── CRAN-SUBMISSION ├── .gitignore ├── .lintr ├── data-raw └── built-ins.R ├── codecov.yml ├── inst ├── WORDLIST ├── graphql │ ├── get_self.graphql │ ├── find_topics.graphql │ ├── get_group_members.graphql │ ├── get_event_rsvps.graphql │ ├── get_group.graphql │ ├── get_event.graphql │ ├── get_pro_groups.graphql │ ├── find_groups.graphql │ ├── get_group_events.graphql │ ├── introspection.graphql │ └── get_pro_events.graphql └── _vcr │ ├── get_self.yml │ ├── find_topics.yml │ ├── get_event.yml │ └── get_group.yml ├── .Rbuildignore ├── meetupr.Rproj ├── cran-comments.md ├── LICENSE.md ├── NAMESPACE ├── _pkgdown.yml ├── DESCRIPTION ├── README.Rmd ├── NEWS.md └── README.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /man/.gitignore: -------------------------------------------------------------------------------- 1 | get_boards.Rd 2 | -------------------------------------------------------------------------------- /man-roxygen/id.R: -------------------------------------------------------------------------------- 1 | #' @param id Required event ID 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | tests/fixtures/**/* -diff 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2025 2 | COPYRIGHT HOLDER: R-Ladies Global 3 | -------------------------------------------------------------------------------- /man-roxygen/token.R: -------------------------------------------------------------------------------- 1 | #' @param token Meetup token 2 | #' 3 | -------------------------------------------------------------------------------- /R/sysdata.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rladies/meetupr/HEAD/R/sysdata.rda -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | .httr-oauth 2 | *.html 3 | *.R 4 | 5 | /.quarto/ 6 | *_files/ 7 | -------------------------------------------------------------------------------- /man-roxygen/schema.R: -------------------------------------------------------------------------------- 1 | #' @param schema The schema object obtained from `meetup_schema()`. 2 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rladies/meetupr/HEAD/man/figures/logo.png -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(meetupr) 3 | 4 | test_check("meetupr") 5 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | # nocov start 2 | .onLoad <- function(...) { 3 | S7::methods_register() 4 | } 5 | # nocov end 6 | -------------------------------------------------------------------------------- /man-roxygen/extra_graphql.R: -------------------------------------------------------------------------------- 1 | #' @param extra_graphql A graphql object. Extra objects to return 2 | #' 3 | -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rladies/meetupr/HEAD/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /CRAN-SUBMISSION: -------------------------------------------------------------------------------- 1 | Version: 0.3.0 2 | Date: 2025-10-20 10:43:48 UTC 3 | SHA: 395e7c39794735125ee3c3dc61ef79f0a0a55dba 4 | -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rladies/meetupr/HEAD/pkgdown/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rladies/meetupr/HEAD/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /man-roxygen/urlname.R: -------------------------------------------------------------------------------- 1 | #' @param urlname Character. The name of the group as indicated in the 2 | #' \url{https://www.meetup.com/} url. 3 | -------------------------------------------------------------------------------- /pkgdown/favicon/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rladies/meetupr/HEAD/pkgdown/favicon/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /pkgdown/favicon/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rladies/meetupr/HEAD/pkgdown/favicon/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /man-roxygen/status.R: -------------------------------------------------------------------------------- 1 | #' @param status Character. The status of the events to retrieve. 2 | #' One or more of "active", "past", "draft", "cancelled". 3 | -------------------------------------------------------------------------------- /man-roxygen/date_after.R: -------------------------------------------------------------------------------- 1 | #' @param date_after Datetime string in "YYYY-MM-DDTHH:MM:SSZ" (ISO8601) format. Events occurring after this date/time will be returned. 2 | -------------------------------------------------------------------------------- /man-roxygen/date_before.R: -------------------------------------------------------------------------------- 1 | #' @param date_before Datetime string in "YYYY-MM-DDTHH:MM:SSZ" (ISO8601) format. Events occurring before this date/time will be returned. 2 | -------------------------------------------------------------------------------- /man-roxygen/max_results.R: -------------------------------------------------------------------------------- 1 | #' @param max_results Maximum number of results to return. If set to NULL, 2 | #' will return all available results (may take a long time). 3 | -------------------------------------------------------------------------------- /man-roxygen/verbose.R: -------------------------------------------------------------------------------- 1 | #' @param verbose logical; do you want informative messages? `TRUE` by default in 2 | #' interactive sessions. Can be toggled by the `meetupr.verbose` option. 3 | -------------------------------------------------------------------------------- /man-roxygen/use_oauth.R: -------------------------------------------------------------------------------- 1 | #' @param use_oauth Logical. Should meetupe use OAuth access? Defaults to TRUE, 2 | #' but can be overridden if access to the legacy API key authorization is 3 | #' needed. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | inst/doc 6 | vignettes/*.R 7 | vignettes/*.html 8 | .DS_Store 9 | docs 10 | figure/ 11 | 12 | /.quarto/ 13 | meetupr 14 | .Renviron 15 | -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: linters_with_defaults( 2 | object_usage_linter = NULL, 3 | defaults = default_linters 4 | ) 5 | encoding: "UTF-8" 6 | exclusions: list( 7 | "vignettes/rladies.Rmd" 8 | ) 9 | -------------------------------------------------------------------------------- /man-roxygen/client_name.R: -------------------------------------------------------------------------------- 1 | #' @param client_name A string representing the name of the client. By 2 | #' default, it is set to `"meetupr"` and retrieved from the 3 | #' `MEETUP_CLIENT_NAME` environment variable. 4 | -------------------------------------------------------------------------------- /data-raw/built-ins.R: -------------------------------------------------------------------------------- 1 | meetupr_client <- list( 2 | id = "sr9210v6ncscjo0h0ruc0ptqfj", 3 | secret = "fe44mm84e8btl7o5is5ctbg6fb" 4 | ) 5 | 6 | usethis::use_data( 7 | meetupr_client, 8 | internal = TRUE, 9 | overwrite = TRUE 10 | ) 11 | -------------------------------------------------------------------------------- /man-roxygen/handle_multiples.R: -------------------------------------------------------------------------------- 1 | #' @param handle_multiples Character. How to handle multiple matches. One of 2 | #' "list" or "first", or "error". 3 | #' If "list", return a list-column with all matches. 4 | #' If "first", return only the first match. 5 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | Athanasia 2 | CMD 3 | Codecov 4 | D'Agostino 5 | DDTHH 6 | Dax 7 | De 8 | GraphQL 9 | Keyring 10 | Mierzwa 11 | OAuth 12 | ORCID 13 | Queiroz 14 | RSVPs 15 | SSZ 16 | Sulima 17 | Wordcloud 18 | YYYY 19 | deauthorize 20 | graphql 21 | httr 22 | keyring 23 | lagos 24 | lon 25 | misconfiguration 26 | organiser 27 | pre 28 | rladies 29 | summarised 30 | tibble 31 | tibbles 32 | vcr 33 | wordcloud 34 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^cran-comments\.md$ 4 | ^NEWS\.md$ 5 | ^\.travis\.yml$ 6 | ^README-.*\.png$ 7 | ^README\.Rmd$ 8 | ^man-roxygen$ 9 | ^\.github$ 10 | ^CODE_OF_CONDUCT\.md$ 11 | ^_pkgdown\.yml$ 12 | ^docs$ 13 | ^pkgdown$ 14 | ^codecov\.yml$ 15 | ^LICENSE\.md$ 16 | figure/ 17 | .lintr 18 | vignettes/rladies* 19 | vignettes/_vcr/rladies-* 20 | vignettes/figures/rladies 21 | ^CRAN-SUBMISSION$ 22 | data-raw/ 23 | .quarto 24 | -------------------------------------------------------------------------------- /inst/graphql/get_self.graphql: -------------------------------------------------------------------------------- 1 | query GetSelf{ 2 | self { 3 | id 4 | name 5 | email 6 | isOrganizer 7 | isLeader 8 | isMemberPlusSubscriber 9 | isProOrganizer 10 | adminProNetworks { 11 | id 12 | name 13 | } 14 | bio 15 | city 16 | country 17 | state 18 | lat 19 | lon 20 | startDate 21 | preferredLocale 22 | memberUrl 23 | << extra_graphql >> 24 | } 25 | } -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | library(vcr) 2 | library(withr) 3 | 4 | invisible( 5 | vcr::vcr_configure() 6 | ) 7 | 8 | local_clean_backend <- function(env = parent.frame()) { 9 | # Clear any existing cached backend 10 | .meetupr_env$keyring_backend <- NULL 11 | 12 | # Ensure cleanup after test 13 | withr::defer( 14 | { 15 | .meetupr_env$keyring_backend <- NULL 16 | }, 17 | envir = env 18 | ) 19 | } 20 | 21 | event_id <- "103349942" 22 | -------------------------------------------------------------------------------- /inst/graphql/find_topics.graphql: -------------------------------------------------------------------------------- 1 | query findTopics( 2 | $query: String!, 3 | $first: Int = 1000, 4 | $after: String 5 | ) { 6 | suggestTopics( 7 | query: $query 8 | first: $first 9 | after: $after 10 | ) { 11 | pageInfo { 12 | hasNextPage 13 | endCursor 14 | } 15 | totalCount 16 | edges { 17 | node { 18 | id 19 | name 20 | urlkey 21 | description 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /meetupr.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace,vignette 22 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## R CMD check results 2 | 3 | 0 errors | 0 warnings | 1 note 4 | 5 | * This is a new release. 6 | * Note explanation: 7 | * Possibly misspelled GraphQL (24:59): this is correct official spelling 8 | * Passes on github actions: 9 | - { os: macos-latest, r: "release" } 10 | - { os: windows-latest, r: "release" } 11 | - { os: ubuntu-latest, r: "devel", http-user-agent: "release" } 12 | - { os: ubuntu-latest, r: "release" } 13 | - { os: ubuntu-latest, r: "oldrel-1" } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please briefly describe your problem and what output you expect. If you have a question, please don't use this form. Instead, ask on or . 2 | 3 | Please include a minimal reproducible example (AKA a reprex). If you've never heard of a [reprex](http://reprex.tidyverse.org/) before, start by reading . 4 | 5 | --- 6 | 7 | Brief description of the problem 8 | 9 | ```r 10 | # insert reprex here 11 | ``` 12 | -------------------------------------------------------------------------------- /pkgdown/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /tests/testthat/test-deprecated.R: -------------------------------------------------------------------------------- 1 | test_that(".fetch_results throws deprecation error", { 2 | expect_warning( 3 | .fetch_results() 4 | ) 5 | }) 6 | 7 | test_that("meetup_call throws deprecation error", { 8 | expect_warning( 9 | meetup_call() 10 | ) 11 | }) 12 | 13 | test_that(".quick_fetch throws deprecation error", { 14 | expect_warning( 15 | .quick_fetch() 16 | ) 17 | }) 18 | 19 | test_that("get_meetup_comments warns about using get_event_comments", { 20 | expect_error( 21 | get_meetup_comments() 22 | ) 23 | }) 24 | -------------------------------------------------------------------------------- /man/meetup_sitrep.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sitrep.R 3 | \name{meetup_sitrep} 4 | \alias{meetup_sitrep} 5 | \title{Show meetupr authentication status} 6 | \usage{ 7 | meetup_sitrep() 8 | } 9 | \value{ 10 | Invisibly returns a list with authentication status details. 11 | } 12 | \description{ 13 | This function checks the authentication status for the Meetup API. 14 | It provides feedback on whether credentials are configured correctly, 15 | tests API connectivity, and shows available authentication methods. 16 | } 17 | \examples{ 18 | meetup_sitrep() 19 | 20 | } 21 | -------------------------------------------------------------------------------- /vignettes/_vcr/graphql-debug-errors.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"\n query {\n groupByUrlname(urlname: \"rladies-san-francisco\") 7 | {\n id\n name\n }\n }","variables":{}}' 8 | response: 9 | status: 200 10 | headers: 11 | content-type: application/json;charset=utf-8 12 | accept-ranges: bytes 13 | date: Sun, 19 Oct 2025 21:43:20 GMT 14 | content-length: '76' 15 | body: 16 | string: '{"data":{"groupByUrlname":{"id":"5190472","name":"R-Ladies San Francisco"}}}' 17 | recorded_at: 2025-10-19 21:43:20 18 | recorded_with: VCR-vcr/2.0.0 19 | -------------------------------------------------------------------------------- /inst/graphql/get_group_members.graphql: -------------------------------------------------------------------------------- 1 | query members( 2 | $urlname: String! 3 | $cursor: String 4 | $first: Int = 1000 5 | ) { 6 | groupByUrlname(urlname: $urlname) { 7 | id 8 | name 9 | memberships(after: $cursor, first: $first) { 10 | pageInfo { 11 | hasNextPage 12 | endCursor 13 | } 14 | totalCount 15 | edges { 16 | node { 17 | id 18 | name 19 | memberUrl 20 | memberPhoto { 21 | baseUrl 22 | } 23 | } 24 | metadata { 25 | status 26 | role 27 | joinTime 28 | lastAccessTime 29 | } 30 | } 31 | } 32 | << extra_graphql >> 33 | } 34 | } -------------------------------------------------------------------------------- /man/get_group.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get-group.R 3 | \name{get_group} 4 | \alias{get_group} 5 | \title{Get detailed information about a Meetup group} 6 | \usage{ 7 | get_group(urlname) 8 | } 9 | \arguments{ 10 | \item{urlname}{The URL name of the Meetup group (e.g., "rladies-lagos")} 11 | } 12 | \value{ 13 | A list containing detailed information about the Meetup group 14 | } 15 | \description{ 16 | Get detailed information about a Meetup group 17 | } 18 | \examples{ 19 | \dontshow{ 20 | vcr::insert_example_cassette("get_group", package = "meetupr") 21 | meetupr:::mock_if_no_auth() 22 | } 23 | get_group("rladies-lagos") 24 | \dontshow{ 25 | vcr::eject_cassette() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /man/get_self.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get-self.R 3 | \name{get_self} 4 | \alias{get_self} 5 | \title{Get information about the authenticated user} 6 | \usage{ 7 | get_self() 8 | } 9 | \value{ 10 | A list containing user information 11 | } 12 | \description{ 13 | Retrieves detailed information about the currently authenticated Meetup user, 14 | including basic profile data, account type, 15 | subscription status, and API access permissions. 16 | } 17 | \examples{ 18 | \dontshow{ 19 | vcr::insert_example_cassette("get_self", package = "meetupr") 20 | meetupr:::mock_if_no_auth() 21 | } 22 | user <- get_self() 23 | cat("Hello", user$name, "!") 24 | \dontshow{ 25 | vcr::eject_cassette() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /inst/graphql/get_event_rsvps.graphql: -------------------------------------------------------------------------------- 1 | query eventRSVPs( 2 | $id: ID! 3 | $cursor: String 4 | $first: Int = 1000 5 | ) { 6 | event(id: $id) { 7 | id 8 | title 9 | dateTime 10 | rsvps(after: $cursor, first: $first) { 11 | pageInfo { 12 | hasNextPage 13 | endCursor 14 | } 15 | totalCount 16 | edges { 17 | cursor 18 | node { 19 | id 20 | member { 21 | id 22 | name 23 | bio 24 | memberUrl 25 | memberPhoto { 26 | baseUrl 27 | } 28 | organizedGroupCount 29 | } 30 | guestsCount 31 | status 32 | } 33 | } 34 | } 35 | << extra_graphql >> 36 | } 37 | } -------------------------------------------------------------------------------- /man/local_meetupr_debug.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{local_meetupr_debug} 4 | \alias{local_meetupr_debug} 5 | \title{Temporarily enable debug mode} 6 | \usage{ 7 | local_meetupr_debug(level = 1, env = parent.frame()) 8 | } 9 | \arguments{ 10 | \item{level}{Debug level: 1 for on, 0 for off} 11 | 12 | \item{env}{The environment to use for scoping} 13 | } 14 | \value{ 15 | The old debug value (invisibly) 16 | } 17 | \description{ 18 | Temporarily enable debug mode 19 | } 20 | \examples{ 21 | \dontrun{ 22 | # Within a function or test 23 | local_meetupr_debug(1) 24 | # Debug output enabled for remainder of scope 25 | 26 | # Manual cleanup 27 | old <- local_meetupr_debug(1, env = emptyenv()) 28 | # ... code with debugging ... 29 | Sys.setenv(MEETUPR_DEBUG = old) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /man/get_event.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get-event.R 3 | \name{get_event} 4 | \alias{get_event} 5 | \title{Get information for a specified event} 6 | \usage{ 7 | get_event(id, extra_graphql = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{id}{Required event ID} 11 | 12 | \item{extra_graphql}{A graphql object. Extra objects to return} 13 | 14 | \item{...}{Should be empty. Used for parameter expansion} 15 | } 16 | \value{ 17 | A meetup_event object with information about the specified event 18 | } 19 | \description{ 20 | Get information for a specified event 21 | } 22 | \examples{ 23 | \dontshow{ 24 | vcr::insert_example_cassette("get_event", package = "meetupr") 25 | meetupr:::mock_if_no_auth() 26 | } 27 | event <- get_event(id = "103349942") 28 | \dontshow{ 29 | vcr::eject_cassette() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /inst/graphql/get_group.graphql: -------------------------------------------------------------------------------- 1 | query getGroup($urlname: String!) { 2 | groupByUrlname(urlname: $urlname) { 3 | id 4 | name 5 | description 6 | urlname 7 | link 8 | city 9 | country 10 | timezone 11 | foundedDate 12 | stats { 13 | memberCounts { 14 | all 15 | } 16 | } 17 | organizer { 18 | id 19 | name 20 | } 21 | keyGroupPhoto { 22 | baseUrl 23 | } 24 | topicCategory { 25 | id 26 | name 27 | } 28 | events( 29 | first: 1, 30 | filter: { 31 | status: [ACTIVE, PAST, CANCELLED, DRAFT] 32 | } 33 | ) { 34 | totalCount 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /man/get_event_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get-event.R 3 | \name{get_event_comments} 4 | \alias{get_event_comments} 5 | \title{Get the comments for a specified event} 6 | \usage{ 7 | get_event_comments(id, ..., extra_graphql = NULL) 8 | } 9 | \arguments{ 10 | \item{id}{Required event ID} 11 | 12 | \item{...}{Should be empty. Used for parameter expansion} 13 | 14 | \item{extra_graphql}{A graphql object. Extra objects to return} 15 | } 16 | \value{ 17 | A tibble with the following columns: 18 | \itemize{ 19 | \item id 20 | \item comment 21 | \item created 22 | \item like_count 23 | \item member_id 24 | \item member_name 25 | \item link 26 | } 27 | } 28 | \description{ 29 | Get the comments for a specified event 30 | } 31 | \examples{ 32 | \dontrun{ 33 | comments <- get_event_comments(id = "103349942") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /inst/graphql/get_event.graphql: -------------------------------------------------------------------------------- 1 | query getEvent($id: ID!) { 2 | event(id: $id) { 3 | id 4 | title 5 | eventUrl 6 | createdTime 7 | status 8 | dateTime 9 | duration 10 | description 11 | 12 | group { 13 | id 14 | name 15 | urlname 16 | } 17 | 18 | venues { 19 | id 20 | name 21 | address 22 | city 23 | state 24 | postalCode 25 | country 26 | lat 27 | lon 28 | venueType 29 | } 30 | 31 | rsvps(first: 1) { 32 | totalCount 33 | } 34 | 35 | featuredEventPhoto { 36 | baseUrl 37 | } 38 | 39 | feeSettings { 40 | required 41 | amount 42 | currency 43 | accepts 44 | refundPolicy { 45 | notes 46 | days 47 | } 48 | } 49 | 50 | << extra_graphql >> 51 | } 52 | } -------------------------------------------------------------------------------- /man/meetup_query.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/api.R 3 | \name{meetup_query} 4 | \alias{meetup_query} 5 | \title{Execute GraphQL query} 6 | \usage{ 7 | meetup_query(graphql, ..., .envir = parent.frame()) 8 | } 9 | \arguments{ 10 | \item{graphql}{GraphQL query string} 11 | 12 | \item{...}{Variables to pass to query} 13 | 14 | \item{.envir}{Environment for error handling} 15 | } 16 | \value{ 17 | The response from the GraphQL API as a list. 18 | } 19 | \description{ 20 | This function executes a GraphQL query with the provided variables. 21 | It validates the variables, constructs the request, 22 | and handles any errors returned by the GraphQL API. 23 | } 24 | \examples{ 25 | \dontrun{ 26 | query <- " 27 | query GetUser($id: ID!) { 28 | user(id: $id) { 29 | id 30 | name 31 | } 32 | }" 33 | meetup_query(graphql = query, id = "12345") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | 8 | name: lint.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::lintr, local::. 27 | needs: lint 28 | 29 | - name: Lint 30 | run: lintr::lint_package() 31 | shell: Rscript {0} 32 | env: 33 | LINTR_ERROR_ON_LINT: true 34 | -------------------------------------------------------------------------------- /inst/graphql/get_pro_groups.graphql: -------------------------------------------------------------------------------- 1 | query proGroups( 2 | $urlname: ID!, 3 | $first: Int = 1000, 4 | $cursor: String 5 | ) { 6 | proNetwork(urlname: $urlname) { 7 | groupsSearch(input: { 8 | after: $cursor, 9 | first: $first 10 | }) { 11 | totalCount 12 | pageInfo { 13 | hasNextPage 14 | endCursor 15 | } 16 | edges { 17 | node { 18 | id 19 | name 20 | urlname 21 | description 22 | lat 23 | lon 24 | city 25 | state 26 | country 27 | membershipMetadata { 28 | status 29 | } 30 | memberships { 31 | totalCount 32 | } 33 | foundedDate 34 | proJoinDate 35 | timezone 36 | joinMode 37 | who: customMemberLabel 38 | isPrivate 39 | } 40 | } 41 | } 42 | << extra_graphql >> 43 | } 44 | } -------------------------------------------------------------------------------- /R/deprecated.R: -------------------------------------------------------------------------------- 1 | .fetch_results <- function(...) { 2 | lifecycle::deprecate_warn( 3 | "0.3.0", 4 | ".fetch_results()", 5 | "meetup_query()", 6 | details = "The REST API is no longer supported. 7 | Use GraphQL functions instead." 8 | ) 9 | } 10 | 11 | meetup_call <- function(...) { 12 | lifecycle::deprecate_warn( 13 | "0.3.0", 14 | "meetup_call()", 15 | "meetup_query()", 16 | details = "The REST API is no longer supported. 17 | Use GraphQL functions instead." 18 | ) 19 | } 20 | 21 | .quick_fetch <- function(...) { 22 | lifecycle::deprecate_warn( 23 | "0.3.0", 24 | ".quick_fetch()", 25 | "meetup_query()", 26 | details = "The REST API is no longer supported. 27 | Use GraphQL functions instead." 28 | ) 29 | } 30 | 31 | get_meetup_comments <- function(...) { 32 | lifecycle::deprecate_stop( 33 | "0.3.0", 34 | "get_meetup_comments()", 35 | NULL, 36 | details = "Comments are no longer supported in the Meetup API." 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /inst/graphql/find_groups.graphql: -------------------------------------------------------------------------------- 1 | query findGroups( 2 | $query: String! 3 | $cursor: String 4 | $first: Int = 1000 5 | $lat: Float = 0.0 6 | $lon: Float = 0.0 7 | $radius: Float = 100000000.0 8 | $categoryId: ID 9 | $topicCategoryId: ID 10 | ) { 11 | groupSearch( 12 | after: $cursor 13 | first: $first 14 | filter: { 15 | query: $query 16 | lat: $lat 17 | lon: $lon 18 | radius: $radius 19 | categoryId: $categoryId 20 | topicCategoryId: $topicCategoryId 21 | } 22 | ) { 23 | pageInfo { 24 | hasNextPage 25 | endCursor 26 | } 27 | totalCount 28 | edges { 29 | node { 30 | id 31 | name 32 | urlname 33 | city 34 | state 35 | country 36 | lat 37 | lon 38 | memberships { 39 | totalCount 40 | } 41 | foundedDate 42 | timezone 43 | joinMode 44 | isPrivate 45 | membershipMetadata { 46 | status 47 | } 48 | } 49 | } 50 | << extra_graphql >> 51 | } 52 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2025 R-Ladies Global 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vignettes/_vcr/introsp-multiple-queries.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"\nquery GetMultipleThings($groupUrlname: String!, $eventId: 7 | ID!) {\n # First query\n group: groupByUrlname(urlname: $groupUrlname) {\n name\n memberCount\n }\n \n # 8 | Second query\n event: event(id: $eventId) {\n title\n dateTime\n }\n \n # 9 | Third query\n self {\n name\n email\n }\n}\n","variables":{"groupUrlname":"rladies-lagos","eventId":"103349942"}}' 10 | response: 11 | status: 200 12 | headers: 13 | content-type: application/json;charset=utf-8 14 | accept-ranges: bytes 15 | date: Sun, 19 Oct 2025 21:43:21 GMT 16 | content-length: '226' 17 | body: 18 | string: '{"errors":[{"message":"Validation error (FieldUndefined@[groupByUrlname/memberCount]) 19 | : Field ''memberCount'' in type ''Group'' is undefined","locations":[{"line":6,"column":5}],"extensions":{"classification":"ValidationError"}}]}' 20 | recorded_at: 2025-10-19 21:43:21 21 | recorded_with: VCR-vcr/2.0.0 22 | -------------------------------------------------------------------------------- /vignettes/_vcr/graphql-manual-pagination.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"\n query GetEvents($urlname: String!, $after: String) {\n groupByUrlname(urlname: 7 | $urlname) {\n pastEvents(input: {first: 20, after: $after}) {\n pageInfo 8 | {\n hasNextPage\n endCursor\n }\n edges {\n node 9 | {\n id\n title\n dateTime\n }\n }\n }\n }\n }","variables":{"urlname":"rladies-san-francisco"}}' 10 | response: 11 | status: 200 12 | headers: 13 | content-type: application/json;charset=utf-8 14 | accept-ranges: bytes 15 | date: Sun, 19 Oct 2025 21:43:19 GMT 16 | content-length: '224' 17 | body: 18 | string: '{"errors":[{"message":"Validation error (FieldUndefined@[groupByUrlname/pastEvents]) 19 | : Field ''pastEvents'' in type ''Group'' is undefined","locations":[{"line":4,"column":7}],"extensions":{"classification":"ValidationError"}}]}' 20 | recorded_at: 2025-10-19 21:43:19 21 | recorded_with: VCR-vcr/2.0.0 22 | -------------------------------------------------------------------------------- /man/meetup_schema_queries.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/graphql-introspection.R 3 | \name{meetup_schema_queries} 4 | \alias{meetup_schema_queries} 5 | \title{Explore available query fields in the Meetup GraphQL API} 6 | \usage{ 7 | meetup_schema_queries(schema = meetup_schema()) 8 | } 9 | \arguments{ 10 | \item{schema}{The schema object obtained from \code{meetup_schema()}.} 11 | } 12 | \value{ 13 | A tibble with details about each query field, including: 14 | \describe{ 15 | \item{field_name}{Name of the query field} 16 | \item{description}{Human-readable description of the field} 17 | \item{args_count}{Number of arguments the field accepts} 18 | \item{return_type}{The GraphQL type returned by this field} 19 | } 20 | } 21 | \description{ 22 | This function retrieves the root-level query fields available in the Meetup 23 | GraphQL API. These are the entry points for data fetching (e.g., 24 | \code{groupByUrlname}, \code{event}, etc.). 25 | } 26 | \examples{ 27 | \dontrun{ 28 | # List all available queries 29 | queries <- meetup_schema_queries() 30 | 31 | # Find group-related queries 32 | queries |> 33 | dplyr::filter(grepl("group", field_name, ignore.case = TRUE)) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /man/meetup_schema_search.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/graphql-introspection.R 3 | \name{meetup_schema_search} 4 | \alias{meetup_schema_search} 5 | \title{Search for types in the Meetup GraphQL API schema} 6 | \usage{ 7 | meetup_schema_search(pattern, schema = meetup_schema()) 8 | } 9 | \arguments{ 10 | \item{pattern}{A string pattern to search for in type names and descriptions. 11 | The search is case-insensitive.} 12 | 13 | \item{schema}{The schema object obtained from \code{meetup_schema()}.} 14 | } 15 | \value{ 16 | A tibble with details about matching types: 17 | \describe{ 18 | \item{type_name}{Name of the type} 19 | \item{kind}{GraphQL kind (OBJECT, ENUM, INTERFACE, etc.)} 20 | \item{description}{Human-readable description} 21 | \item{field_count}{Number of fields in the type} 22 | } 23 | } 24 | \description{ 25 | This function searches across all types in the schema by name or description. 26 | Useful for discovering what data structures are 27 | available (e.g., Event, Group, Venue, Member). 28 | } 29 | \examples{ 30 | \dontrun{ 31 | # Find all event-related types 32 | meetup_schema_search("event") 33 | 34 | # Find location-related types 35 | meetup_schema_search("location") 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /man/meetup_schema.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/graphql-introspection.R 3 | \name{meetup_schema} 4 | \alias{meetup_schema} 5 | \title{Introspect the Meetup GraphQL API schema} 6 | \usage{ 7 | meetup_schema(asis = FALSE) 8 | } 9 | \arguments{ 10 | \item{asis}{Logical; if TRUE, returns the raw response from the API as JSON. 11 | If FALSE (default), returns the parsed schema object.} 12 | } 13 | \value{ 14 | If \code{asis} is FALSE (default), the parsed schema object with nested 15 | lists containing query types, mutation types, and type definitions. If 16 | \code{asis} is TRUE, a JSON string representation of the schema. 17 | } 18 | \description{ 19 | This function performs an introspection query on the Meetup GraphQL API to 20 | retrieve the full schema details, including available query types, mutation 21 | types, and type definitions. 22 | } 23 | \examples{ 24 | \dontshow{ 25 | vcr::insert_example_cassette("meetup_schema", package = "meetupr") 26 | meetupr:::mock_if_no_auth() 27 | } 28 | # Get the full schema 29 | schema <- meetup_schema() 30 | 31 | # Explore what's available 32 | names(schema) 33 | 34 | # Get as JSON for external tools 35 | schema_json <- meetup_schema(asis = TRUE) 36 | \dontshow{ 37 | vcr::eject_cassette() 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /man/get_event_rsvps.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get-event.R 3 | \name{get_event_rsvps} 4 | \alias{get_event_rsvps} 5 | \title{Get the RSVPs for a specified event} 6 | \usage{ 7 | get_event_rsvps( 8 | id, 9 | max_results = NULL, 10 | handle_multiples = "list", 11 | extra_graphql = NULL, 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{id}{Required event ID} 17 | 18 | \item{max_results}{Maximum number of results to return. If set to NULL, 19 | will return all available results (may take a long time).} 20 | 21 | \item{handle_multiples}{Character. How to handle multiple matches. One of 22 | "list" or "first", or "error". 23 | If "list", return a list-column with all matches. 24 | If "first", return only the first match.} 25 | 26 | \item{extra_graphql}{A graphql object. Extra objects to return} 27 | 28 | \item{...}{Should be empty. Used for parameter expansion} 29 | } 30 | \value{ 31 | A tibble with the RSVPs for the specified event 32 | } 33 | \description{ 34 | Get the RSVPs for a specified event 35 | } 36 | \examples{ 37 | \dontshow{ 38 | vcr::insert_example_cassette("get_event_rsvps", package = "meetupr") 39 | meetupr:::mock_if_no_auth() 40 | } 41 | rsvps <- get_event_rsvps(id = "103349942") 42 | \dontshow{ 43 | vcr::eject_cassette() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /man/meetup_schema_mutations.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/graphql-introspection.R 3 | \name{meetup_schema_mutations} 4 | \alias{meetup_schema_mutations} 5 | \title{Explore available mutations in the Meetup GraphQL API} 6 | \usage{ 7 | meetup_schema_mutations(schema = meetup_schema()) 8 | } 9 | \arguments{ 10 | \item{schema}{The schema object obtained from \code{meetup_schema()}.} 11 | } 12 | \value{ 13 | A tibble with details about each mutation, including: 14 | \describe{ 15 | \item{field_name}{Name of the mutation} 16 | \item{description}{Human-readable description} 17 | \item{args_count}{Number of arguments the mutation accepts} 18 | \item{return_type}{The GraphQL type returned after mutation} 19 | } 20 | If no mutations are available, returns a tibble with a message. 21 | } 22 | \description{ 23 | This function retrieves the mutation operations available in the Meetup 24 | GraphQL API. Mutations are operations that modify data on the server (create, 25 | update, delete). 26 | } 27 | \examples{ 28 | \dontrun{ 29 | # List all available mutations 30 | mutations <- meetup_schema_mutations() 31 | 32 | # Check if mutations are supported 33 | if (nrow(mutations) > 0 && !"message" \%in\% names(mutations)) { 34 | print(mutations$field_name) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /inst/_vcr/get_self.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query GetSelf{\n self {\n id\n name\n email\n isOrganizer\n isLeader\n isMemberPlusSubscriber \n isProOrganizer\n adminProNetworks {\n id\n name\n }\n bio 7 | \n city \n country \n state \n lat \n lon \n startDate \n preferredLocale 8 | \n memberUrl \n \n }\n}","variables":{}}' 9 | response: 10 | status: 200 11 | headers: 12 | content-type: application/json;charset=utf-8 13 | accept-ranges: bytes 14 | date: Wed, 17 Sep 2025 15:11:00 GMT 15 | content-length: '490' 16 | body: 17 | string: '{"data":{"self":{"id":"251470805","name":"R-Ladies Global","email":"meetup@rladies.org","isOrganizer":true,"isLeader":true,"isMemberPlusSubscriber":false,"isProOrganizer":true,"adminProNetworks":[{"id":"559792434039549952","name":"R-Ladies"}],"bio":"R-Ladies 18 | Global. https://rladies.org","city":"San Francisco","country":"us","state":"CA","lat":37.78,"lon":-122.42,"startDate":"2018-04-01T22:15:11-04:00","preferredLocale":"en-US","memberUrl":"https://www.meetup.com/members/251470805/"}}}' 19 | recorded_at: 2025-09-17 15:11:00 20 | recorded_with: VCR-vcr/2.0.0 21 | -------------------------------------------------------------------------------- /tests/testthat/_vcr/get_self.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query GetSelf{\n self {\n id\n name\n email\n isOrganizer\n isLeader\n isMemberPlusSubscriber \n isProOrganizer\n adminProNetworks {\n id\n name\n }\n bio 7 | \n city \n country \n state \n lat \n lon \n startDate \n preferredLocale 8 | \n memberUrl \n \n }\n}","variables":{}}' 9 | response: 10 | status: 200 11 | headers: 12 | content-type: application/json;charset=utf-8 13 | accept-ranges: bytes 14 | date: Sun, 19 Oct 2025 21:21:01 GMT 15 | content-length: '490' 16 | body: 17 | string: '{"data":{"self":{"id":"251470805","name":"R-Ladies Global","email":"meetup@rladies.org","isOrganizer":true,"isLeader":true,"isMemberPlusSubscriber":false,"isProOrganizer":true,"adminProNetworks":[{"id":"559792434039549952","name":"R-Ladies"}],"bio":"R-Ladies 18 | Global. https://rladies.org","city":"San Francisco","country":"us","state":"CA","lat":37.78,"lon":-122.42,"startDate":"2018-04-01T22:15:11-04:00","preferredLocale":"en-US","memberUrl":"https://www.meetup.com/members/251470805/"}}}' 19 | recorded_at: 2025-10-19 21:21:01 20 | recorded_with: VCR-vcr/2.0.0 21 | -------------------------------------------------------------------------------- /vignettes/_vcr/meetupr-sitrep.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query GetSelf{\n self {\n id\n name\n email\n isOrganizer\n isLeader\n isMemberPlusSubscriber \n isProOrganizer\n adminProNetworks {\n id\n name\n }\n bio 7 | \n city \n country \n state \n lat \n lon \n startDate \n preferredLocale 8 | \n memberUrl \n \n }\n}","variables":{}}' 9 | response: 10 | status: 200 11 | headers: 12 | content-type: application/json;charset=utf-8 13 | accept-ranges: bytes 14 | date: Sun, 19 Oct 2025 21:43:22 GMT 15 | content-length: '490' 16 | body: 17 | string: '{"data":{"self":{"id":"251470805","name":"R-Ladies Global","email":"meetup@rladies.org","isOrganizer":true,"isLeader":true,"isMemberPlusSubscriber":false,"isProOrganizer":true,"adminProNetworks":[{"id":"559792434039549952","name":"R-Ladies"}],"bio":"R-Ladies 18 | Global. https://rladies.org","city":"San Francisco","country":"us","state":"CA","lat":37.78,"lon":-122.42,"startDate":"2018-04-01T22:15:11-04:00","preferredLocale":"en-US","memberUrl":"https://www.meetup.com/members/251470805/"}}}' 19 | recorded_at: 2025-10-19 21:43:22 20 | recorded_with: VCR-vcr/2.0.0 21 | -------------------------------------------------------------------------------- /tests/testthat/_vcr/get_self_basic.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query GetSelf{\n self {\n id\n name\n email\n isOrganizer\n isLeader\n isMemberPlusSubscriber \n isProOrganizer\n adminProNetworks {\n id\n name\n }\n bio 7 | \n city \n country \n state \n lat \n lon \n startDate \n preferredLocale 8 | \n memberUrl \n \n }\n}","variables":{}}' 9 | response: 10 | status: 200 11 | headers: 12 | content-type: application/json;charset=utf-8 13 | accept-ranges: bytes 14 | date: Sun, 19 Oct 2025 21:21:01 GMT 15 | content-length: '490' 16 | body: 17 | string: '{"data":{"self":{"id":"251470805","name":"R-Ladies Global","email":"meetup@rladies.org","isOrganizer":true,"isLeader":true,"isMemberPlusSubscriber":false,"isProOrganizer":true,"adminProNetworks":[{"id":"559792434039549952","name":"R-Ladies"}],"bio":"R-Ladies 18 | Global. https://rladies.org","city":"San Francisco","country":"us","state":"CA","lat":37.78,"lon":-122.42,"startDate":"2018-04-01T22:15:11-04:00","preferredLocale":"en-US","memberUrl":"https://www.meetup.com/members/251470805/"}}}' 19 | recorded_at: 2025-10-19 21:21:01 20 | recorded_with: VCR-vcr/2.0.0 21 | -------------------------------------------------------------------------------- /vignettes/_vcr/meetupr-verify-sitrep.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query GetSelf{\n self {\n id\n name\n email\n isOrganizer\n isLeader\n isMemberPlusSubscriber \n isProOrganizer\n adminProNetworks {\n id\n name\n }\n bio 7 | \n city \n country \n state \n lat \n lon \n startDate \n preferredLocale 8 | \n memberUrl \n \n }\n}","variables":{}}' 9 | response: 10 | status: 200 11 | headers: 12 | content-type: application/json;charset=utf-8 13 | accept-ranges: bytes 14 | date: Sun, 19 Oct 2025 21:43:22 GMT 15 | content-length: '490' 16 | body: 17 | string: '{"data":{"self":{"id":"251470805","name":"R-Ladies Global","email":"meetup@rladies.org","isOrganizer":true,"isLeader":true,"isMemberPlusSubscriber":false,"isProOrganizer":true,"adminProNetworks":[{"id":"559792434039549952","name":"R-Ladies"}],"bio":"R-Ladies 18 | Global. https://rladies.org","city":"San Francisco","country":"us","state":"CA","lat":37.78,"lon":-122.42,"startDate":"2018-04-01T22:15:11-04:00","preferredLocale":"en-US","memberUrl":"https://www.meetup.com/members/251470805/"}}}' 19 | recorded_at: 2025-10-19 21:43:22 20 | recorded_with: VCR-vcr/2.0.0 21 | -------------------------------------------------------------------------------- /man/get_group_members.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get-group.R 3 | \name{get_group_members} 4 | \alias{get_group_members} 5 | \title{Get the members from a meetup group} 6 | \usage{ 7 | get_group_members( 8 | urlname, 9 | max_results = NULL, 10 | handle_multiples = "list", 11 | extra_graphql = NULL, 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{urlname}{Character. The name of the group as indicated in the 17 | \url{https://www.meetup.com/} url.} 18 | 19 | \item{max_results}{Maximum number of results to return. If set to NULL, 20 | will return all available results (may take a long time).} 21 | 22 | \item{handle_multiples}{Character. How to handle multiple matches. One of 23 | "list" or "first", or "error". 24 | If "list", return a list-column with all matches. 25 | If "first", return only the first match.} 26 | 27 | \item{extra_graphql}{A graphql object. Extra objects to return} 28 | 29 | \item{...}{Should be empty. Used for parameter expansion} 30 | } 31 | \value{ 32 | A tibble with group members 33 | } 34 | \description{ 35 | Get the members from a meetup group 36 | } 37 | \examples{ 38 | \dontshow{ 39 | vcr::insert_example_cassette("get_group_members", package = "meetupr") 40 | meetupr:::mock_if_no_auth() 41 | } 42 | get_group_members("rladies-lagos") 43 | \dontshow{ 44 | vcr::eject_cassette() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,meetup_event) 4 | S3method(print,meetup_group) 5 | S3method(print,meetup_user) 6 | export(find_groups) 7 | export(find_topics) 8 | export(get_event) 9 | export(get_event_comments) 10 | export(get_event_rsvps) 11 | export(get_group) 12 | export(get_group_events) 13 | export(get_group_members) 14 | export(get_pro_events) 15 | export(get_pro_groups) 16 | export(get_self) 17 | export(local_meetupr_debug) 18 | export(meetup_auth) 19 | export(meetup_auth_status) 20 | export(meetup_ci_load) 21 | export(meetup_ci_setup) 22 | export(meetup_client) 23 | export(meetup_key_delete) 24 | export(meetup_key_get) 25 | export(meetup_key_set) 26 | export(meetup_query) 27 | export(meetup_req) 28 | export(meetup_schema) 29 | export(meetup_schema_mutations) 30 | export(meetup_schema_queries) 31 | export(meetup_schema_search) 32 | export(meetup_schema_type) 33 | export(meetup_sitrep) 34 | importFrom(base64enc,base64decode) 35 | importFrom(base64enc,base64encode) 36 | importFrom(cli,cli_abort) 37 | importFrom(cli,cli_alert_info) 38 | importFrom(cli,cli_alert_success) 39 | importFrom(cli,cli_bullets) 40 | importFrom(cli,cli_code) 41 | importFrom(cli,cli_h1) 42 | importFrom(cli,cli_h2) 43 | importFrom(cli,cli_h3) 44 | importFrom(clipr,clipr_available) 45 | importFrom(clipr,write_clip) 46 | importFrom(httr2,oauth_cache_path) 47 | importFrom(rlang,is_installed) 48 | -------------------------------------------------------------------------------- /vignettes/_vcr/introsp-nested-data.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"\nquery {\n group(urlname: \"rladies-lagos\") {\n upcomingEvents(input: 7 | {first: 5}) {\n edges {\n node {\n title\n venue 8 | {\n name\n city\n }\n }\n }\n }\n }\n}\n","variables":{}}' 9 | response: 10 | status: 200 11 | headers: 12 | content-type: application/json;charset=utf-8 13 | accept-ranges: bytes 14 | date: Sun, 19 Oct 2025 21:43:21 GMT 15 | content-length: '579' 16 | body: 17 | string: '{"errors":[{"message":"Validation error (MissingFieldArgument@[group]) 18 | : Missing field argument ''id''","locations":[{"line":3,"column":3}],"extensions":{"classification":"ValidationError"}},{"message":"Validation 19 | error (UnknownArgument@[group]) : Unknown field argument ''urlname''","locations":[{"line":3,"column":9}],"extensions":{"classification":"ValidationError"}},{"message":"Validation 20 | error (FieldUndefined@[group/upcomingEvents]) : Field ''upcomingEvents'' in 21 | type ''Group'' is undefined","locations":[{"line":4,"column":5}],"extensions":{"classification":"ValidationError"}}]}' 22 | recorded_at: 2025-10-19 21:43:21 23 | recorded_with: VCR-vcr/2.0.0 24 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | template: 2 | bootstrap: 5 3 | url: http://rladies.org/meetupr/ 4 | 5 | logo: 6 | image: man/figures/logo.png 7 | href: https://rladies.org/meetupr 8 | 9 | navbar: 10 | components: 11 | articles: 12 | text: Tutorials 13 | href: articles/index.html 14 | news: 15 | text: News 16 | href: news/index.html 17 | 18 | articles: 19 | - title: Get Started 20 | contents: meetupr 21 | - title: Advanced Topics 22 | contents: 23 | - graphql 24 | - introspection 25 | - advanced-auth 26 | - title: Use Cases 27 | contents: rladies 28 | 29 | reference: 30 | - title: Authentication 31 | desc: Functions for authenticating and verification 32 | contents: 33 | - meetup_sitrep 34 | - contains("auth") 35 | - starts_with("has") 36 | - meetup_client 37 | - meetup_keys 38 | - contains("_ci") 39 | - title: Retrieving data and working with data 40 | desc: | 41 | Functions for retrieving data 42 | contents: 43 | - starts_with("get") 44 | - starts_with("find") 45 | - title: Query functions 46 | desc: Functions enabling querying of data 47 | contents: 48 | - meetup_req 49 | - meetup_query 50 | - title: Introspection 51 | desc: Functions for introspection and debugging 52 | contents: 53 | - starts_with("meetup_schema") 54 | - contains("debug") 55 | -------------------------------------------------------------------------------- /man/find_topics.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/find.R 3 | \name{find_topics} 4 | \alias{find_topics} 5 | \title{Find topics on Meetup} 6 | \usage{ 7 | find_topics( 8 | query, 9 | max_results = 200, 10 | handle_multiples = "list", 11 | extra_graphql = NULL, 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{query}{A string query to search for topics.} 17 | 18 | \item{max_results}{Maximum number of results to return. If set to NULL, 19 | will return all available results (may take a long time).} 20 | 21 | \item{handle_multiples}{Character. How to handle multiple matches. One of 22 | "list" or "first", or "error". 23 | If "list", return a list-column with all matches. 24 | If "first", return only the first match.} 25 | 26 | \item{extra_graphql}{A graphql object. Extra objects to return} 27 | 28 | \item{...}{Used for parameter expansion, must be empty.} 29 | } 30 | \value{ 31 | A data frame of topics matching the search query. 32 | } 33 | \description{ 34 | Search for topics on Meetup using a query string. 35 | This function allows you to find topics that match your search criteria. 36 | } 37 | \examples{ 38 | \dontshow{ 39 | vcr::insert_example_cassette("find_topics", package = "meetupr") 40 | meetupr:::mock_if_no_auth() 41 | } 42 | find_topics("R", max_results = 10) 43 | find_topics("Data Science", max_results = 5) 44 | \dontshow{ 45 | vcr::eject_cassette() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/get_self.md: -------------------------------------------------------------------------------- 1 | # print.meetup_user outputs full data correctly 2 | 3 | Code 4 | print.meetup_user(user) 5 | Message 6 | 7 | -- Meetup User: -- 8 | 9 | * ID: user123 10 | * Name: John Doe 11 | * Email: john@example.com 12 | 13 | -- Roles: 14 | * Organizer: Yes 15 | * Leader: No 16 | * Pro Organizer: Yes 17 | * Member Plus: No 18 | * Pro API Access: Yes 19 | 20 | -- Location: 21 | * City: New York 22 | * Country: USA 23 | 24 | # print.meetup_user handles missing optional fields 25 | 26 | Code 27 | print.meetup_user(user) 28 | Message 29 | 30 | -- Meetup User: -- 31 | 32 | * ID: user123 33 | * Name: John Doe 34 | 35 | -- Roles: 36 | * Organizer: No 37 | * Leader: No 38 | * Pro Organizer: No 39 | * Member Plus: No 40 | 41 | -- Location: 42 | 43 | # print.meetup_user handles partial location data 44 | 45 | Code 46 | print.meetup_user(user) 47 | Message 48 | 49 | -- Meetup User: -- 50 | 51 | * ID: user123 52 | * Name: John Doe 53 | 54 | -- Roles: 55 | * Organizer: Yes 56 | * Leader: Yes 57 | * Pro Organizer: No 58 | * Member Plus: Yes 59 | * Pro API Access: No 60 | 61 | -- Location: 62 | * City: Los Angeles 63 | 64 | -------------------------------------------------------------------------------- /.github/unused-workflows/render-readme.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/master/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | paths: 6 | - 'README.Rmd' 7 | - 'man/rmd-fragments/**' 8 | - '.github/workflows/render-readme.yaml' 9 | 10 | name: render-rmarkdown 11 | 12 | jobs: 13 | render-rmarkdown: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 17 | steps: 18 | - name: Checkout repo 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | 23 | - uses: r-lib/actions/setup-pandoc@v1 24 | 25 | - uses: r-lib/actions/setup-r@v1 26 | 27 | - uses: r-lib/actions/setup-r-dependencies@v1 28 | with: 29 | cache-version: readme-1 30 | extra-packages: | 31 | devtools 32 | rmarkdown 33 | 34 | - name: Render Rmarkdown files 35 | shell: Rscript {0} 36 | run: | 37 | devtools::build_readme() 38 | 39 | - name: Commit results 40 | run: | 41 | git config --local user.name "$GITHUB_ACTOR" 42 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 43 | git commit ${RMD_PATH[*]/.Rmd/.md} -m 'Re-build Rmarkdown files' || echo "No changes to commit" 44 | git push origin || echo "No changes to commit" 45 | -------------------------------------------------------------------------------- /inst/graphql/get_group_events.graphql: -------------------------------------------------------------------------------- 1 | query groupEvents( 2 | $urlname: String! 3 | $cursor: String 4 | $first: Int = 1000 5 | $status: [EventStatus!] 6 | $date_after: DateTime 7 | $date_before: DateTime 8 | ) { 9 | groupByUrlname(urlname: $urlname) { 10 | id 11 | name 12 | events( 13 | after: $cursor 14 | first: $first 15 | filter: { 16 | status: $status 17 | afterDateTime: $date_after 18 | beforeDateTime: $date_before 19 | } 20 | ) { 21 | pageInfo { 22 | hasNextPage 23 | endCursor 24 | } 25 | totalCount 26 | edges { 27 | cursor 28 | node { 29 | ...eventFields 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | fragment eventFields on Event { 37 | id 38 | title 39 | eventUrl 40 | createdTime 41 | status 42 | dateTime 43 | duration 44 | description 45 | 46 | group { 47 | id 48 | name 49 | urlname 50 | } 51 | 52 | venues { 53 | id 54 | name 55 | address 56 | city 57 | state 58 | postalCode 59 | country 60 | lat 61 | lon 62 | venueType 63 | } 64 | 65 | rsvps(first: 1) { 66 | totalCount 67 | } 68 | 69 | featuredEventPhoto { 70 | baseUrl 71 | } 72 | 73 | feeSettings { 74 | required 75 | amount 76 | currency 77 | accepts 78 | refundPolicy { 79 | notes 80 | days 81 | } 82 | } 83 | 84 | << extra_graphql >> 85 | } -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check-novcr.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | # uncomment when https://github.com/ropensci/vcr/issues/549 is resolved 9 | # schedule: 10 | # - cron: "30 2 * * 0" 11 | 12 | name: R-cmd-check (no VCR) 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | R-CMD-check: 18 | runs-on: ubuntu-latest 19 | name: R-cmd-check (no VCR) 20 | 21 | env: 22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 23 | R_KEEP_PKG_SOURCE: yes 24 | "meetupr:token": ${{ secrets.meetupr_token }} 25 | "meetupr:token_file": ${{ secrets.meetupr_token_file }} 26 | VCR_TURN_OFF: true 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - uses: r-lib/actions/setup-pandoc@v2 32 | 33 | - uses: r-lib/actions/setup-r@v2 34 | with: 35 | r-version: "release" 36 | http-user-agent: "release" 37 | use-public-rspm: true 38 | 39 | - uses: r-lib/actions/setup-r-dependencies@v2 40 | with: 41 | extra-packages: | 42 | any::rcmdcheck 43 | any::knitr 44 | needs: check 45 | 46 | - uses: r-lib/actions/check-r-package@v2 47 | with: 48 | upload-snapshots: true 49 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 50 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | 11 | name: pkgdown.yaml 12 | 13 | permissions: read-all 14 | 15 | jobs: 16 | pkgdown: 17 | runs-on: ubuntu-latest 18 | # Only restrict concurrency for non-PR jobs 19 | concurrency: 20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 21 | env: 22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 23 | permissions: 24 | contents: write 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: r-lib/actions/setup-pandoc@v2 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | extra-packages: any::pkgdown, local::. 37 | needs: website 38 | 39 | - name: Build site 40 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 41 | shell: Rscript {0} 42 | 43 | - name: Deploy to GitHub pages 🚀 44 | if: github.event_name != 'pull_request' 45 | uses: JamesIves/github-pages-deploy-action@v4.5.0 46 | with: 47 | clean: false 48 | branch: gh-pages 49 | folder: docs 50 | -------------------------------------------------------------------------------- /man/meetup_auth.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/auth.R 3 | \name{meetup_auth} 4 | \alias{meetup_auth} 5 | \alias{meetup_deauth} 6 | \title{Meetup API Authentication} 7 | \usage{ 8 | meetup_auth(...) 9 | 10 | meetup_deauth( 11 | client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr"), 12 | clear_keyring = TRUE 13 | ) 14 | } 15 | \arguments{ 16 | \item{...}{Additional arguments to \code{meetup_client()}.} 17 | 18 | \item{client_name}{A string representing the name of the client. By 19 | default, it is set to \code{"meetupr"} and retrieved from the 20 | \code{MEETUP_CLIENT_NAME} environment variable.} 21 | 22 | \item{clear_keyring}{A logical value indicating whether to clear 23 | the associated keyring entries. Defaults to \code{TRUE}.} 24 | } 25 | \value{ 26 | Nothing. Outputs messages indicating the result of the 27 | process. 28 | } 29 | \description{ 30 | Functions to manage authentication with the Meetup API. 31 | Includes functions to authenticate, 32 | and deauthorize by removing cached credentials. 33 | } 34 | \section{Functions}{ 35 | \itemize{ 36 | \item \code{meetup_auth()}: Authenticate and display the 37 | authenticated user's name. 38 | 39 | \item \code{meetup_deauth()}: Remove cached authentication 40 | for the Meetup API client. 41 | 42 | }} 43 | \examples{ 44 | \dontrun{ 45 | meetup_auth() 46 | 47 | # Default deauthorization 48 | meetup_deauth() 49 | 50 | # Deauthorization with a custom client name 51 | meetup_deauth(client_name = "custom_client") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /inst/graphql/introspection.graphql: -------------------------------------------------------------------------------- 1 | query IntrospectionQuery { 2 | __schema { 3 | queryType { name } 4 | mutationType { name } 5 | subscriptionType { name } 6 | types { 7 | ...FullType 8 | } 9 | } 10 | } 11 | 12 | fragment FullType on __Type { 13 | kind 14 | name 15 | description 16 | fields(includeDeprecated: true) { 17 | name 18 | description 19 | args { 20 | ...InputValue 21 | } 22 | type { 23 | ...TypeRef 24 | } 25 | isDeprecated 26 | deprecationReason 27 | } 28 | inputFields { 29 | ...InputValue 30 | } 31 | interfaces { 32 | ...TypeRef 33 | } 34 | enumValues(includeDeprecated: true) { 35 | name 36 | description 37 | isDeprecated 38 | deprecationReason 39 | } 40 | possibleTypes { 41 | ...TypeRef 42 | } 43 | } 44 | 45 | fragment InputValue on __InputValue { 46 | name 47 | description 48 | type { ...TypeRef } 49 | defaultValue 50 | } 51 | 52 | fragment TypeRef on __Type { 53 | kind 54 | name 55 | ofType { 56 | kind 57 | name 58 | ofType { 59 | kind 60 | name 61 | ofType { 62 | kind 63 | name 64 | ofType { 65 | kind 66 | name 67 | ofType { 68 | kind 69 | name 70 | ofType { 71 | kind 72 | name 73 | ofType { 74 | kind 75 | name 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /inst/graphql/get_pro_events.graphql: -------------------------------------------------------------------------------- 1 | query proEvents( 2 | $urlname: ID!, 3 | $first: Int = 1000, 4 | $cursor: String 5 | $date_after: DateTime 6 | $date_before: DateTime 7 | $status: [String!] 8 | ) { 9 | proNetwork(urlname: $urlname) { 10 | eventsSearch( 11 | input: { 12 | after: $cursor, 13 | first: $first, 14 | filter: { 15 | eventDateMax: $date_before 16 | eventDateMin: $date_after 17 | status: $status 18 | } 19 | } 20 | ) { 21 | totalCount 22 | pageInfo { 23 | hasNextPage 24 | endCursor 25 | } 26 | edges { 27 | node { 28 | id 29 | title 30 | description 31 | status 32 | dateTime 33 | duration 34 | eventUrl 35 | featuredEventPhoto { 36 | id 37 | baseUrl 38 | highResUrl 39 | standardUrl 40 | thumbUrl 41 | } 42 | rsvps { 43 | yesCount 44 | waitlistCount 45 | totalCount 46 | } 47 | venues { 48 | id 49 | name 50 | address 51 | city 52 | state 53 | country 54 | lat 55 | lon 56 | postalCode 57 | venueType 58 | } 59 | group { 60 | name 61 | urlname 62 | } 63 | << extra_graphql >> 64 | } 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /man/find_groups.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/find.R 3 | \name{find_groups} 4 | \alias{find_groups} 5 | \title{Find groups using text-based search} 6 | \usage{ 7 | find_groups( 8 | query, 9 | topic_id = NULL, 10 | category_id = NULL, 11 | max_results = 200, 12 | handle_multiples = "list", 13 | extra_graphql = NULL, 14 | ... 15 | ) 16 | } 17 | \arguments{ 18 | \item{query}{Character string to search for groups} 19 | 20 | \item{topic_id}{Numeric ID of a topic to filter groups by} 21 | 22 | \item{category_id}{Numeric ID of a category to filter groups by} 23 | 24 | \item{max_results}{Maximum number of results to return. If set to NULL, 25 | will return all available results (may take a long time).} 26 | 27 | \item{handle_multiples}{Character. How to handle multiple matches. One of 28 | "list" or "first", or "error". 29 | If "list", return a list-column with all matches. 30 | If "first", return only the first match.} 31 | 32 | \item{extra_graphql}{A graphql object. Extra objects to return} 33 | 34 | \item{...}{Should be empty. Used for parameter expansion} 35 | } 36 | \value{ 37 | A tibble with group information 38 | } 39 | \description{ 40 | Search for groups on Meetup using a text query. This function allows 41 | you to find groups that match your search criteria. 42 | } 43 | \examples{ 44 | \dontshow{ 45 | vcr::insert_example_cassette("find_groups", package = "meetupr") 46 | meetupr:::mock_if_no_auth() 47 | } 48 | groups <- find_groups("R-Ladies") 49 | \dontshow{ 50 | vcr::eject_cassette() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/get_event.md: -------------------------------------------------------------------------------- 1 | # print.meetup_event snapshot test 2 | 3 | Code 4 | print(event) 5 | Message 6 | 7 | -- Meetup Event -- 8 | 9 | * ID: "103349942" 10 | * Title: Ecosystem GIS & Community Building 11 | * Status: "PAST" 12 | * Date/Time: 2013-02-18T18:30:00-05:00 13 | * Duration: PT2H 14 | * RSVPs: 97 15 | 16 | -- Group: 17 | * Data Visualization DC ("data-visualization-dc") 18 | 19 | -- Venue: 20 | * Name: Browsermedia/NClud 21 | * Location: Washington, DC, us 22 | 23 | 24 | 25 | --- 26 | 27 | Code 28 | print(event) 29 | Message 30 | 31 | -- Meetup Event -- 32 | 33 | * ID: "103349942" 34 | * Title: Ecosystem GIS & Community Building 35 | * Status: "PAST" 36 | * Date/Time: 2013-02-18T18:30:00-05:00 37 | * Duration: PT2H 38 | * RSVPs: 97 39 | 40 | -- Group: 41 | * Data Visualization DC ("data-visualization-dc") 42 | 43 | -- Venue: 44 | * Name: Browsermedia/NClud 45 | * Location: Washington, DC, us 46 | 47 | 48 | 49 | # process_event_data adds correct class 50 | 51 | Code 52 | print(result) 53 | Message 54 | 55 | -- Meetup Event -- 56 | 57 | * ID: "123" 58 | * Title: Test Event 59 | * Status: "ACTIVE" 60 | 61 | -- Fee: 62 | * 10 USD 63 | 64 | -------------------------------------------------------------------------------- /R/meetupr-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | "_PACKAGE" 3 | 4 | # Global variable bindings for R CMD check 5 | utils::globalVariables(c( 6 | # attendee_mappings 7 | "id", 8 | "country", 9 | "venues_country", 10 | "field_name" 11 | )) 12 | 13 | # nocov start 14 | #' Knit vignettes 15 | #' 16 | #' This function processes all R Markdown files in the `vignettes` directory, 17 | #' knitting them into HTML format. It also handles the copying of any 18 | #' generated figures to the appropriate location within the `vignettes/static` 19 | #' directory. After processing, it cleans up any temporary files created during 20 | #' the knitting process. 21 | #' The function is intended for internal use in knitting the vignettes 22 | #' during development and get record vcr 23 | #' cassettes. 24 | #' @return A list containing a summary of the knitting process, including the 25 | #' names of the processed files. 26 | #' @keywords internal 27 | #' @noRd 28 | knit_vignettes <- function() { 29 | proc <- list.files( 30 | "vignettes", 31 | "Rmd$", 32 | full.names = TRUE 33 | ) 34 | 35 | lapply(proc, function(x) { 36 | fig_path <- "static" 37 | knitr::knit( 38 | x, 39 | gsub("\\.Rmd$", ".html", x) 40 | ) 41 | imgs <- list.files(fig_path, full.names = TRUE) 42 | sapply(imgs, function(x) { 43 | file.copy( 44 | x, 45 | file.path("vignettes", fig_path, basename(x)), 46 | overwrite = TRUE 47 | ) 48 | }) 49 | invisible(unlink(fig_path, recursive = TRUE)) 50 | }) 51 | 52 | list( 53 | "Knit vignettes", 54 | sapply(proc, basename) 55 | ) 56 | } # nocov end 57 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | name: R-CMD-check.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | R-CMD-check: 14 | runs-on: ${{ matrix.config.os }} 15 | 16 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | config: 22 | - { os: macos-latest, r: "release" } 23 | - { os: windows-latest, r: "release" } 24 | - { os: ubuntu-latest, r: "devel", http-user-agent: "release" } 25 | - { os: ubuntu-latest, r: "release" } 26 | - { os: ubuntu-latest, r: "oldrel-1" } 27 | 28 | env: 29 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 30 | R_KEEP_PKG_SOURCE: yes 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - uses: r-lib/actions/setup-pandoc@v2 36 | 37 | - uses: r-lib/actions/setup-r@v2 38 | with: 39 | r-version: ${{ matrix.config.r }} 40 | http-user-agent: ${{ matrix.config.http-user-agent }} 41 | use-public-rspm: true 42 | 43 | - uses: r-lib/actions/setup-r-dependencies@v2 44 | with: 45 | extra-packages: | 46 | any::rcmdcheck 47 | any::knitr 48 | needs: check 49 | 50 | - uses: r-lib/actions/check-r-package@v2 51 | with: 52 | upload-snapshots: true 53 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 54 | -------------------------------------------------------------------------------- /man/meetup_req.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/api.R 3 | \name{meetup_req} 4 | \alias{meetup_req} 5 | \title{Create and Configure a Meetup API Request} 6 | \usage{ 7 | meetup_req(rate_limit = 500/60, cache = TRUE, ...) 8 | } 9 | \arguments{ 10 | \item{rate_limit}{A numeric value specifying the maximum number of requests} 11 | 12 | \item{cache}{A logical value indicating whether to cache the OAuth token 13 | on disk. Defaults to \code{TRUE}.} 14 | 15 | \item{...}{Additional arguments passed to \code{\link[=meetup_client]{meetup_client()}} for setting up 16 | the OAuth client.} 17 | } 18 | \value{ 19 | A \code{httr2} request object pre-configured to 20 | interact with the Meetup API. 21 | } 22 | \description{ 23 | This function prepares and configures an HTTP request for interacting with 24 | the Meetup API. It allows the user to authenticate via OAuth, specify the 25 | use of caching, and set custom client configuration. 26 | } 27 | \details{ 28 | This function constructs an HTTP POST request directed to the Meetup API 29 | and applies appropriate OAuth headers for authentication. The function 30 | is prepared to support caching and provides flexibility for client 31 | customization with the \code{...} parameter. The implementation is currently 32 | commented out and would require activation for functionality. 33 | } 34 | \examples{ 35 | \dontrun{ 36 | # Example 1: Basic request with caching enabled 37 | req <- meetup_req(cache = TRUE) 38 | 39 | # Example 2: Request with custom client ID and secret 40 | req <- meetup_req( 41 | cache = FALSE, 42 | client_id = "your_client_id", 43 | client_secret = "your_client_secret" 44 | ) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /man/meetup_schema_type.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/graphql-introspection.R 3 | \name{meetup_schema_type} 4 | \alias{meetup_schema_type} 5 | \title{Get fields for a specific type in the Meetup GraphQL API schema} 6 | \usage{ 7 | meetup_schema_type(type_name, schema = meetup_schema(), ...) 8 | } 9 | \arguments{ 10 | \item{type_name}{The name of the type for which to retrieve fields (e.g., 11 | "Event", "Group", "Member").} 12 | 13 | \item{schema}{The schema object obtained from \code{meetup_schema()}.} 14 | 15 | \item{...}{Additional arguments passed to \code{grepl()} for type name matching 16 | (e.g., \code{ignore.case = TRUE}).} 17 | } 18 | \value{ 19 | A tibble with details about the fields: 20 | \describe{ 21 | \item{field_name}{Name of the field} 22 | \item{description}{Human-readable description} 23 | \item{type}{GraphQL type of the field} 24 | \item{deprecated}{Logical indicating if field is deprecated} 25 | } 26 | If the type is not found, throws an error. If multiple types match, returns 27 | a tibble of matching type names. If the type has no fields, returns a 28 | message. 29 | } 30 | \description{ 31 | This function retrieves detailed information about 32 | all fields available on a 33 | specific GraphQL type. Use this to discover what 34 | data you can query from types 35 | like Event, Group, or Member. 36 | } 37 | \examples{ 38 | \dontrun{ 39 | # Get all fields on the Event type 40 | event_fields <- meetup_schema_type("Event") 41 | 42 | # Find deprecated fields 43 | event_fields |> 44 | dplyr::filter(deprecated) 45 | 46 | # Pass cached schema to avoid repeated introspection 47 | schema <- meetup_schema() 48 | group_fields <- meetup_schema_type("Group", schema = schema) 49 | venue_fields <- meetup_schema_type("Venue", schema = schema) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /man/get_group_events.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get-group.R 3 | \name{get_group_events} 4 | \alias{get_group_events} 5 | \title{Get the events from a meetup group} 6 | \usage{ 7 | get_group_events( 8 | urlname, 9 | status = NULL, 10 | date_before = NULL, 11 | date_after = NULL, 12 | max_results = NULL, 13 | handle_multiples = "list", 14 | extra_graphql = NULL, 15 | ... 16 | ) 17 | } 18 | \arguments{ 19 | \item{urlname}{Character. The name of the group as indicated in the 20 | \url{https://www.meetup.com/} url.} 21 | 22 | \item{status}{Character vector of event statuses to retrieve.} 23 | 24 | \item{date_before}{Datetime string in "YYYY-MM-DDTHH:MM:SSZ" (ISO8601) format. Events occurring before this date/time will be returned.} 25 | 26 | \item{date_after}{Datetime string in "YYYY-MM-DDTHH:MM:SSZ" (ISO8601) format. Events occurring after this date/time will be returned.} 27 | 28 | \item{max_results}{Maximum number of results to return. If set to NULL, 29 | will return all available results (may take a long time).} 30 | 31 | \item{handle_multiples}{Character. How to handle multiple matches. One of 32 | "list" or "first", or "error". 33 | If "list", return a list-column with all matches. 34 | If "first", return only the first match.} 35 | 36 | \item{extra_graphql}{A graphql object. Extra objects to return} 37 | 38 | \item{...}{Should be empty. Used for parameter expansion} 39 | } 40 | \value{ 41 | A tibble with the events for the specified group 42 | } 43 | \description{ 44 | Get the events from a meetup group 45 | } 46 | \examples{ 47 | \dontshow{ 48 | vcr::insert_example_cassette("get_group_events", package = "meetupr") 49 | meetupr:::mock_if_no_auth() 50 | } 51 | get_group_events("rladies-lagos", "past") 52 | get_group_events( 53 | "rladies-lagos", 54 | status = "past", 55 | date_before = "2023-01-01T12:00:00Z" 56 | ) 57 | \dontshow{ 58 | vcr::eject_cassette() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/testthat/test-get_pro.R: -------------------------------------------------------------------------------- 1 | test_that("get_pro_groups() works", { 2 | mock_if_no_auth() 3 | vcr::local_cassette("get_pro_groups") 4 | # Limit results to reduce fixture size 5 | groups <- get_pro_groups(urlname = "rladies", max_results = 10) 6 | expect_s3_class(groups, "data.frame") 7 | expect_gt(nrow(groups), 5) 8 | expect_lte(nrow(groups), 10) 9 | }) 10 | 11 | test_that("get_pro_events() works", { 12 | mock_if_no_auth() 13 | vcr::local_cassette("get_pro_events") 14 | skip_if_not(is_self_pro(), "Skipping Pro tests") 15 | # Limit results to reduce fixture size 16 | events <- get_pro_events( 17 | urlname = "rladies", 18 | status = "cancelled", 19 | max_results = 5 20 | ) 21 | expect_s3_class(events, "data.frame") 22 | expect_gt(nrow(events), 0) 23 | expect_lte(nrow(events), 5) 24 | }) 25 | 26 | test_that("get_pro_events() warns for non-Pro organizers", { 27 | mock_if_no_auth() 28 | vcr::local_cassette("get_pro_events_non_pro") 29 | local_mocked_bindings( 30 | is_self_pro = function() FALSE 31 | ) 32 | expect_warning( 33 | # Limit results to reduce fixture size 34 | get_pro_events(urlname = "rladies", max_results = 5), 35 | "The authenticated user must have Pro access" 36 | ) 37 | }) 38 | 39 | test_that("is_self_pro returns TRUE for Pro organizers", { 40 | mock_resp <- list(data = list(self = list(isProOrganizer = TRUE))) 41 | local_mocked_bindings( 42 | meetup_query = function(...) mock_resp, 43 | meetup_auth_status = function(...) TRUE 44 | ) 45 | expect_true(is_self_pro()) 46 | }) 47 | 48 | test_that("is_self_pro returns FALSE for non-Pro organizers", { 49 | mock_resp <- list(data = list(self = list(isProOrganizer = FALSE))) 50 | local_mocked_bindings( 51 | meetup_query = function(...) mock_resp 52 | ) 53 | expect_false(is_self_pro()) 54 | 55 | local_mocked_bindings( 56 | meetup_auth_status = function(...) FALSE 57 | ) 58 | expect_false(is_self_pro()) 59 | }) 60 | -------------------------------------------------------------------------------- /man/meetup_client.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/auth.R 3 | \name{meetup_client} 4 | \alias{meetup_client} 5 | \title{Create a Meetup OAuth Client} 6 | \usage{ 7 | meetup_client( 8 | client_id = NULL, 9 | client_secret = NULL, 10 | client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr"), 11 | ... 12 | ) 13 | } 14 | \arguments{ 15 | \item{client_id}{A string representing the Meetup client ID. By default, 16 | it is retrieved from the \code{meetup:client_id} environment variable.} 17 | 18 | \item{client_secret}{A string representing the Meetup client secret. By 19 | default, it is retrieved from the \code{meetup:client_secret} environment 20 | variable.} 21 | 22 | \item{client_name}{A string representing the name of the client. By 23 | default, it is set to \code{"meetupr"} and retrieved from the 24 | \code{MEETUP_CLIENT_NAME} environment variable.} 25 | 26 | \item{...}{Additional arguments passed to the \code{httr2::oauth_client} function.} 27 | } 28 | \value{ 29 | An OAuth client object created with the \code{httr2::oauth_client} 30 | function. This client can be used to handle authentication with the 31 | Meetup API. 32 | } 33 | \description{ 34 | This function initializes and returns an OAuth client for authenticating 35 | with the Meetup API. It requires the Meetup client ID and secret, which 36 | can be passed as arguments or retrieved from environment variables. 37 | } 38 | \details{ 39 | If the \code{client_id} or \code{client_secret} parameters are empty, the function 40 | will throw an error prompting you to set the \code{meetup:client_id} and 41 | \code{meetup:client_secret} environment variables. 42 | } 43 | \examples{ 44 | \dontrun{ 45 | # Example 1: Using environment variables to set credentials 46 | client <- meetup_client() 47 | 48 | # Example 2: Passing client ID and secret as arguments 49 | client <- meetup_client( 50 | client_id = "your_client_id", 51 | client_secret = "your_client_secret" 52 | ) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /man/meetupr-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/meetupr-package.R 3 | \docType{package} 4 | \name{meetupr-package} 5 | \alias{meetupr} 6 | \alias{meetupr-package} 7 | \title{meetupr: Access Meetup Data} 8 | \description{ 9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 10 | 11 | Provides programmatic access to the 'Meetup' 'GraphQL' API (\url{https://www.meetup.com/api/schema/}), enabling users to retrieve information about groups, events, and members from 'Meetup' (\url{https://www.meetup.com/}). Supports authentication via 'OAuth2' and includes functions for common queries and data manipulation tasks. 12 | } 13 | \seealso{ 14 | Useful links: 15 | \itemize{ 16 | \item \url{https://rladies.org/meetupr/} 17 | \item Report bugs at \url{https://github.com/rladies/meetupr/issues} 18 | } 19 | 20 | } 21 | \author{ 22 | \strong{Maintainer}: Athanasia Mo Mowinckel \email{a.m.mowinckel@psykologi.uio.no} (\href{https://orcid.org/0000-0002-5756-0223}{ORCID}) 23 | 24 | Authors: 25 | \itemize{ 26 | \item Erin LeDell \email{oss@ledell.org} 27 | \item Olga Mierzwa-Sulima \email{olga@rladies.org} 28 | \item Lucy D'Agostino McGowan \email{lucy@rladies.org} 29 | \item Claudia Vitolo 30 | } 31 | 32 | Other contributors: 33 | \itemize{ 34 | \item Gabriela De Queiroz \email{gabriela@rladies.org} [contributor] 35 | \item Michael Beigelmacher [contributor] 36 | \item Augustina Ragwitz \email{augustina.ragwitz@ibm.com} [contributor] 37 | \item Greg Sutcliffe \email{github@emeraldverie.org} [contributor] 38 | \item Rick Pack \email{rickeyhp@gmail.com} [contributor] 39 | \item Ben Ubah \email{ubah.ben22@gmail.com} [contributor] 40 | \item Maëlle Salmon \email{maelle.salmon@yahoo.se} (\href{https://orcid.org/0000-0002-2815-0399}{ORCID}) [contributor] 41 | \item Barret Schloerke \email{barret@rstudio.com} (\href{https://orcid.org/0000-0001-9986-114X}{ORCID}) [contributor] 42 | \item R-Ladies Global [copyright holder] 43 | } 44 | 45 | } 46 | \keyword{internal} 47 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | 8 | name: test-coverage.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | test-coverage: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: r-lib/actions/setup-r@v2 22 | with: 23 | use-public-rspm: true 24 | 25 | - uses: r-lib/actions/setup-r-dependencies@v2 26 | with: 27 | extra-packages: any::covr, any::xml2 28 | needs: coverage 29 | 30 | - name: Test coverage 31 | run: | 32 | cov <- covr::package_coverage( 33 | quiet = FALSE, 34 | clean = FALSE, 35 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 36 | ) 37 | print(cov) 38 | covr::to_cobertura(cov) 39 | shell: Rscript {0} 40 | 41 | - uses: codecov/codecov-action@v5 42 | with: 43 | # Fail if error if not on PR, or if on PR and token is given 44 | fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} 45 | files: ./cobertura.xml 46 | plugins: noop 47 | disable_search: true 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | 50 | - name: Show testthat output 51 | if: always() 52 | run: | 53 | ## -------------------------------------------------------------------- 54 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 55 | shell: bash 56 | 57 | - name: Upload test results 58 | if: failure() 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: coverage-test-failures 62 | path: ${{ runner.temp }}/package 63 | -------------------------------------------------------------------------------- /man/meetup_auth_status.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/auth.R 3 | \name{meetup_auth_status} 4 | \alias{meetup_auth_status} 5 | \alias{has_auth} 6 | \title{Check Authentication Status for Meetup API} 7 | \usage{ 8 | meetup_auth_status( 9 | client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr"), 10 | silent = FALSE 11 | ) 12 | 13 | has_auth(client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr")) 14 | } 15 | \arguments{ 16 | \item{client_name}{A string representing the name of the client. By 17 | default, it is set to \code{"meetupr"} and retrieved from the 18 | \code{MEETUP_CLIENT_NAME} environment variable.} 19 | 20 | \item{silent}{A \code{logical} indicating whether to suppress output 21 | messages. Defaults to \code{FALSE}.} 22 | } 23 | \value{ 24 | logical. \code{TRUE} if a valid token 25 | is found, \code{FALSE} otherwise. 26 | If \code{silent} is \code{FALSE}, the function outputs status messages. 27 | } 28 | \description{ 29 | This function verifies if a user is 30 | authenticated to interact with the Meetup 31 | API by checking the existence of token 32 | cache files in the specified directory. 33 | } 34 | \details{ 35 | The function checks the \code{httr2} OAuth cache directory for encrypted 36 | token files (\code{.rds.enc}) associated with the specified client. Based on 37 | the results, it provides feedback about the authentication status. Multiple 38 | tokens in the cache directory trigger a warning, while a missing token or 39 | cache directory result in an error message. 40 | } 41 | \section{Functions}{ 42 | \itemize{ 43 | \item \code{has_auth()}: Check if authenticated 44 | to Meetup API. Uses silent mode. 45 | 46 | }} 47 | \examples{ 48 | \dontrun{ 49 | # Check authentication status with default client name 50 | status <- meetup_auth_status() 51 | 52 | # Check authentication status with a specific client name 53 | status <- meetup_auth_status(client_name = "custom_client") 54 | 55 | # Suppress output messages 56 | status <- meetup_auth_status(silent = TRUE) 57 | } 58 | 59 | } 60 | \seealso{ 61 | \code{\link[httr2]{oauth_cache_path}}, \code{\link[cli]{cli_alert}} 62 | } 63 | -------------------------------------------------------------------------------- /man/meetup_keys.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/credentials.R 3 | \name{meetup_keys} 4 | \alias{meetup_keys} 5 | \alias{meetup_key_set} 6 | \alias{meetup_key_get} 7 | \alias{meetup_key_delete} 8 | \title{Manage API keys in system keyring} 9 | \usage{ 10 | meetup_key_set( 11 | key, 12 | value, 13 | client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr") 14 | ) 15 | 16 | meetup_key_get( 17 | key, 18 | client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr"), 19 | error = TRUE 20 | ) 21 | 22 | meetup_key_delete( 23 | key, 24 | client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr") 25 | ) 26 | } 27 | \arguments{ 28 | \item{key}{Character string indicating the key name to store/retrieve. 29 | Default is \code{"token"}. Valid options are \code{"client_id"}, \code{"client_secret"}, 30 | \code{"token"}, and \code{"token_file"}.} 31 | 32 | \item{value}{Character string with the value to store. If 33 | \code{NULL} (default), 34 | prompts for interactive input.} 35 | 36 | \item{client_name}{A string representing the name of the client. By 37 | default, it is set to \code{"meetupr"} and retrieved from the 38 | \code{MEETUP_CLIENT_NAME} environment variable.} 39 | 40 | \item{error}{Logical. If \code{TRUE} (default), raises an 41 | error when key not found. 42 | If \code{FALSE}, returns \code{NULL}.} 43 | } 44 | \value{ 45 | \itemize{ 46 | \item \code{meetup_key_set()}: Returns \code{TRUE} invisibly on success 47 | \item \code{meetup_key_get()}: Returns the key value, or \code{NULL} 48 | if not found and \code{error = FALSE} 49 | } 50 | } 51 | \description{ 52 | Store and retrieve keys securely using the system keyring. 53 | Typically used for storing OAuth tokens and credentials 54 | for the meetupr package. 55 | } 56 | \section{Functions}{ 57 | \itemize{ 58 | \item \code{meetup_key_set()}: Store a key in the system keyring 59 | 60 | \item \code{meetup_key_get()}: Retrieve a key from the system keyring 61 | 62 | \item \code{meetup_key_delete()}: Delete a key in the system keyring 63 | 64 | }} 65 | \examples{ 66 | \dontrun{ 67 | meetup_key_set("token", "my-access-token") 68 | meetup_key_set("client_id") 69 | 70 | meetup_key_get("token") 71 | meetup_key_get("missing_key", error = FALSE) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to meetupr 2 | 3 | This outlines how to propose a change to meetupr. For more detailed 4 | info about contributing to this, and other tidyverse packages, please see the 5 | [**development contributing guide**](https://rstd.io/tidy-contrib). 6 | 7 | ### Fixing typos 8 | 9 | Small typos or grammatical errors in documentation may be edited directly using 10 | the GitHub web interface, so long as the changes are made in the _source_ file. 11 | 12 | * YES: you edit a roxygen comment in a `.R` file below `R/`. 13 | * NO: you edit an `.Rd` file below `man/`. 14 | 15 | ### Prerequisites 16 | 17 | Before you make a substantial pull request, you should always file an issue and 18 | make sure someone from the team agrees that it’s a problem. If you’ve found a 19 | bug, create an associated issue and illustrate the bug with a minimal 20 | [reprex](https://www.tidyverse.org/help/#reprex). 21 | 22 | ### Pull request process 23 | 24 | * We recommend that you create a Git branch for each pull request (PR). 25 | * Look at the Travis build status before and after making changes. 26 | The `README` should contain badges for any continuous integration services used 27 | by the package. 28 | * New code should follow the tidyverse [style guide](http://style.tidyverse.org). 29 | You can use the [styler](https://CRAN.R-project.org/package=styler) package to 30 | apply these styles, but please don't restyle code that has nothing to do with 31 | your PR. 32 | * We use [roxygen2](https://cran.r-project.org/package=roxygen2), with 33 | [Markdown syntax](https://cran.r-project.org/web/packages/roxygen2/vignettes/markdown.html), 34 | for documentation. 35 | * We use [testthat](https://cran.r-project.org/package=testthat). Contributions 36 | with test cases included are easier to accept. 37 | * For user-facing changes, add a bullet to the top of `NEWS.md` below the current 38 | development version header describing the changes made followed by your GitHub 39 | username, and links to relevant issue(s)/PR(s). 40 | 41 | ### Code of Conduct 42 | 43 | Please note that this project is released with a [Contributor Code of 44 | Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to 45 | abide by its terms. 46 | 47 | ### See tidyverse [development contributing guide](https://rstd.io/tidy-contrib) for further details. 48 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Type: Package 2 | Package: meetupr 3 | Title: Access Meetup Data 4 | Version: 0.3.0 5 | Authors@R: c( 6 | person("Athanasia Mo", "Mowinckel", , "a.m.mowinckel@psykologi.uio.no", role = c("aut", "cre"), 7 | comment = c(ORCID = "0000-0002-5756-0223")), 8 | person("Erin", "LeDell", , "oss@ledell.org", role = "aut"), 9 | person("Olga", "Mierzwa-Sulima", , "olga@rladies.org", role = "aut"), 10 | person("Lucy", "D'Agostino McGowan", , "lucy@rladies.org", role = "aut"), 11 | person("Claudia", "Vitolo", role = "aut"), 12 | person("Gabriela", "De Queiroz", , "gabriela@rladies.org", role = "ctb"), 13 | person("Michael", "Beigelmacher", role = "ctb"), 14 | person("Augustina", "Ragwitz", , "augustina.ragwitz@ibm.com", role = "ctb"), 15 | person("Greg", "Sutcliffe", , "github@emeraldverie.org", role = "ctb"), 16 | person("Rick", "Pack", , "rickeyhp@gmail.com", role = "ctb"), 17 | person("Ben", "Ubah", , "ubah.ben22@gmail.com", role = "ctb"), 18 | person("Maëlle", "Salmon", , "maelle.salmon@yahoo.se", role = "ctb", 19 | comment = c(ORCID = "0000-0002-2815-0399")), 20 | person("Barret", "Schloerke", , "barret@rstudio.com", role = "ctb", 21 | comment = c(ORCID = "0000-0001-9986-114X")), 22 | person("R-Ladies Global", role = "cph") 23 | ) 24 | Description: Provides programmatic access to the 'Meetup' 'GraphQL' API 25 | (), enabling users to retrieve 26 | information about groups, events, and members from 'Meetup' 27 | (). Supports authentication via 'OAuth2' and 28 | includes functions for common queries and data manipulation tasks. 29 | License: MIT + file LICENSE 30 | Depends: 31 | R (>= 4.2) 32 | Imports: 33 | base64enc, 34 | countrycode, 35 | cli, 36 | clipr, 37 | dplyr, 38 | glue, 39 | httr2, 40 | jsonlite, 41 | keyring, 42 | lifecycle, 43 | purrr, 44 | rlang, 45 | rlist, 46 | S7, 47 | tools, 48 | withr 49 | Suggests: 50 | covr, 51 | ggplot2, 52 | ggwordcloud, 53 | httpuv, 54 | knitr, 55 | rmarkdown, 56 | testthat, 57 | tidyr, 58 | tidytext, 59 | vcr 60 | VignetteBuilder: 61 | knitr 62 | Config/testthat/edition: 3 63 | Config/testthat/parallel: true 64 | Encoding: UTF-8 65 | Roxygen: list(markdown = TRUE) 66 | RoxygenNote: 7.3.2 67 | URL: https://rladies.org/meetupr/ 68 | BugReports: https://github.com/rladies/meetupr/issues 69 | -------------------------------------------------------------------------------- /man/get_pro.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get-pro.R 3 | \name{get_pro} 4 | \alias{get_pro} 5 | \alias{get_pro_groups} 6 | \alias{get_pro_events} 7 | \title{Retrieve information about Meetup Pro networks, 8 | including groups and events.} 9 | \usage{ 10 | get_pro_groups( 11 | urlname, 12 | max_results = NULL, 13 | handle_multiples = "list", 14 | extra_graphql = NULL, 15 | ... 16 | ) 17 | 18 | get_pro_events( 19 | urlname, 20 | status = NULL, 21 | date_before = date_before, 22 | date_after = date_after, 23 | max_results = NULL, 24 | handle_multiples = "list", 25 | extra_graphql = NULL, 26 | ... 27 | ) 28 | } 29 | \arguments{ 30 | \item{urlname}{Character. The name of the group as indicated in the 31 | \url{https://www.meetup.com/} url.} 32 | 33 | \item{max_results}{Maximum number of results to return. If set to NULL, 34 | will return all available results (may take a long time).} 35 | 36 | \item{handle_multiples}{Character. How to handle multiple matches. One of 37 | "list" or "first", or "error". 38 | If "list", return a list-column with all matches. 39 | If "first", return only the first match.} 40 | 41 | \item{extra_graphql}{A graphql object. Extra objects to return} 42 | 43 | \item{...}{Should be empty. Used for parameter expansion} 44 | 45 | \item{status}{Which status the events should have.} 46 | 47 | \item{date_before}{Datetime string in "YYYY-MM-DDTHH:MM:SSZ" (ISO8601) format. Events occurring before this date/time will be returned.} 48 | 49 | \item{date_after}{Datetime string in "YYYY-MM-DDTHH:MM:SSZ" (ISO8601) format. Events occurring after this date/time will be returned.} 50 | } 51 | \value{ 52 | tibble with pro network information 53 | 54 | A tibble with meetup pro information 55 | } 56 | \description{ 57 | Meetup Pro is a premium service for organizations 58 | managing multiple Meetup groups. 59 | This functionality allows you to access details about 60 | the groups within a Pro network 61 | and the events they host. 62 | } 63 | \section{Functions}{ 64 | \itemize{ 65 | \item \code{get_pro_groups()}: retrieve groups in a pro network 66 | 67 | \item \code{get_pro_events()}: retrieve events from a pro network 68 | 69 | }} 70 | \examples{ 71 | \dontshow{ 72 | vcr::insert_example_cassette("get_pro", package = "meetupr") 73 | meetupr:::mock_if_no_auth() 74 | } 75 | urlname <- "rladies" 76 | members <- get_pro_groups(urlname) 77 | 78 | upcoming_events <- get_pro_events(urlname, "upcoming", max_results = 5) 79 | \dontshow{ 80 | vcr::eject_cassette() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/get_group.md: -------------------------------------------------------------------------------- 1 | # print.meetup_group outputs full data correctly 2 | 3 | Code 4 | print.meetup_group(group) 5 | Message 6 | 7 | -- Meetup Group: -- 8 | 9 | * Name: Tech Enthusiasts 10 | * URL: tech-enthusiasts 11 | * Link: http://meetup.com/tech-enthusiasts 12 | * Location: San Francisco, USA 13 | * Timezone: PST 14 | * Founded: January 01, 2020 15 | 16 | -- Statistics: 17 | * Members: 500 18 | * Total Events: 100 19 | 20 | -- Organizer: 21 | * Name: Jane Doe 22 | * Category: Technology 23 | 24 | -- Description: 25 | A group for tech lovers 26 | 27 | # print.meetup_group handles missing optional fields 28 | 29 | Code 30 | print.meetup_group(group) 31 | Message 32 | 33 | -- Meetup Group: -- 34 | 35 | * Name: Beginner Coders 36 | * URL: beginner-coders 37 | * Link: http://meetup.com/beginner-coders 38 | * Timezone: EST 39 | * Founded: June 15, 2021 40 | 41 | -- Statistics: 42 | * Members: 200 43 | * Total Events: 20 44 | 45 | -- Description: 46 | No description available. 47 | 48 | # print.meetup_group handles long descriptions 49 | 50 | Code 51 | print.meetup_group(group) 52 | Message 53 | 54 | -- Meetup Group: -- 55 | 56 | * Name: History Lovers 57 | * URL: history-lovers 58 | * Link: http://meetup.com/history-lovers 59 | * Location: Boston, USA 60 | * Timezone: EST 61 | * Founded: September 20, 2019 62 | 63 | -- Statistics: 64 | * Members: 1,000 65 | * Total Events: 50 66 | 67 | -- Organizer: 68 | * Name: John Smith 69 | * Category: Education 70 | 71 | -- Description: 72 | This is a great group for history enthusiasts. This is a great group for 73 | history enthusiasts. This is a great group for history enthusiasts. This is a 74 | great group for history enthusiasts. This is a... 75 | 76 | # print.meetup_group handles edge case with location parts 77 | 78 | Code 79 | print.meetup_group(group) 80 | Message 81 | 82 | -- Meetup Group: -- 83 | 84 | * Name: Science Gurus 85 | * URL: science-gurus 86 | * Link: http://meetup.com/science-gurus 87 | * Location: USA 88 | * Timezone: CST 89 | * Founded: March 01, 2018 90 | 91 | -- Statistics: 92 | * Members: 300 93 | * Total Events: 30 94 | 95 | -- Organizer: 96 | * Name: Sarah Lee 97 | 98 | -- Description: 99 | Discussing science topics 100 | 101 | -------------------------------------------------------------------------------- /R/find.R: -------------------------------------------------------------------------------- 1 | #' Find groups using text-based search 2 | #' 3 | #' Search for groups on Meetup using a text query. This function allows 4 | #' you to find groups that match your search criteria. 5 | #' 6 | #' @param query Character string to search for groups 7 | #' @param topic_id Numeric ID of a topic to filter groups by 8 | #' @param category_id Numeric ID of a category to filter groups by 9 | #' @template max_results 10 | #' @template handle_multiples 11 | #' @template extra_graphql 12 | #' @param ... Should be empty. Used for parameter expansion 13 | #' @return A tibble with group information 14 | #' @examples 15 | #' \dontshow{ 16 | #' vcr::insert_example_cassette("find_groups", package = "meetupr") 17 | #' meetupr:::mock_if_no_auth() 18 | #' } 19 | #' groups <- find_groups("R-Ladies") 20 | #' \dontshow{ 21 | #' vcr::eject_cassette() 22 | #' } 23 | #' @export 24 | find_groups <- function( 25 | query, 26 | topic_id = NULL, 27 | category_id = NULL, 28 | max_results = 200, 29 | handle_multiples = "list", 30 | extra_graphql = NULL, 31 | ... 32 | ) { 33 | rlang::check_dots_empty() 34 | 35 | std_query <- standard_query( 36 | "find_groups", 37 | "data.groupSearch" 38 | ) 39 | 40 | execute( 41 | std_query, 42 | query = query, 43 | categoryId = category_id, 44 | topicCategoryId = topic_id, 45 | first = max_results, 46 | max_results = max_results, 47 | handle_multiples = handle_multiples, 48 | extra_graphql = extra_graphql 49 | ) |> 50 | process_datetime_fields("founded_date") 51 | } 52 | 53 | 54 | #' Find topics on Meetup 55 | #' 56 | #' Search for topics on Meetup using a query string. 57 | #' This function allows you to find topics that match your search criteria. 58 | #' 59 | #' @param query A string query to search for topics. 60 | #' @template max_results 61 | #' @template handle_multiples 62 | #' @template extra_graphql 63 | #' @param ... Used for parameter expansion, must be empty. 64 | #' @return A data frame of topics matching the search query. 65 | #' @examples 66 | #' \dontshow{ 67 | #' vcr::insert_example_cassette("find_topics", package = "meetupr") 68 | #' meetupr:::mock_if_no_auth() 69 | #' } 70 | #' find_topics("R", max_results = 10) 71 | #' find_topics("Data Science", max_results = 5) 72 | #' \dontshow{ 73 | #' vcr::eject_cassette() 74 | #' } 75 | #' @export 76 | find_topics <- function( 77 | query, 78 | max_results = 200, 79 | handle_multiples = "list", 80 | extra_graphql = NULL, 81 | ... 82 | ) { 83 | rlang::check_dots_empty() 84 | 85 | std_query <- standard_query( 86 | "find_topics", 87 | "data.suggestTopics" 88 | ) 89 | 90 | execute( 91 | std_query, 92 | query = query, 93 | first = max_results, 94 | max_results = max_results, 95 | handle_multiples = handle_multiples, 96 | extra_graphql = extra_graphql 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /tests/testthat/_vcr/get_event_rsvps_max.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query eventRSVPs(\n $id: ID!\n $cursor: String\n $first: 7 | Int = 1000\n) {\n event(id: $id) {\n id\n title\n dateTime\n rsvps(after: 8 | $cursor, first: $first) {\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n edges 9 | {\n cursor\n node {\n id\n member {\n id\n name\n bio\n memberUrl\n memberPhoto 10 | {\n baseUrl\n }\n organizedGroupCount\n }\n guestsCount\n status\n }\n }\n }\n \n }\n}","variables":{"id":"103349942","first":5}}' 11 | response: 12 | status: 200 13 | headers: 14 | content-type: application/json;charset=utf-8 15 | accept-ranges: bytes 16 | date: Sun, 19 Oct 2025 21:21:00 GMT 17 | content-length: '1868' 18 | body: 19 | string: '{"data":{"event":{"id":"103349942","title":"Ecosystem GIS & Community 20 | Building","dateTime":"2013-02-18T18:30:00-05:00","rsvps":{"pageInfo":{"hasNextPage":true,"endCursor":"NA=="},"totalCount":97,"edges":[{"cursor":"MA==","node":{"id":"683104152","member":{"id":"12251810","name":"Sean 21 | Moore Gonzalez","bio":"Background in the defense industry with focus on physics, 22 | machine learning, communications, and AI. General goal to connect and promote 23 | data scientists.","memberUrl":"https://www.meetup.com/members/12251810/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"},"organizedGroupCount":0},"guestsCount":0,"status":"YES"}},{"cursor":"MQ==","node":{"id":"683108312","member":{"id":"482161","name":"Harlan 24 | Harris","bio":"","memberUrl":"https://www.meetup.com/members/482161/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"},"organizedGroupCount":0},"guestsCount":0,"status":"YES"}},{"cursor":"Mg==","node":{"id":"683109202","member":{"id":"6382386","name":"Harnam 25 | Rai","bio":"","memberUrl":"https://www.meetup.com/members/6382386/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"},"organizedGroupCount":0},"guestsCount":0,"status":"YES"}},{"cursor":"Mw==","node":{"id":"683114012","member":{"id":"70383262","name":"Dario 26 | Rivera","bio":"","memberUrl":"https://www.meetup.com/members/70383262/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"},"organizedGroupCount":0},"guestsCount":0,"status":"YES"}},{"cursor":"NA==","node":{"id":"683114892","member":{"id":"8783792","name":"Aaron 27 | M","bio":"","memberUrl":"https://www.meetup.com/members/8783792/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"},"organizedGroupCount":0},"guestsCount":0,"status":"YES"}}]}}}}' 28 | recorded_at: 2025-10-19 21:21:00 29 | recorded_with: VCR-vcr/2.0.0 30 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | format: gfm 3 | --- 4 | 5 | 6 | 7 | # meetupr Meetupr hex logo by Zane Dax @StarTrek_Lt 8 | 9 | Logo by Zane Dax 10 | [@StarTrek_Lt](https://x.com/startrek_lt) 11 | 12 | 13 | [![CRAN status](https://www.r-pkg.org/badges/version/meetupr)](https://CRAN.R-project.org/package=meetupr) 14 | [![meetupr status badge](https://rladies.r-universe.dev/meetupr/badges/version)](https://rladies.r-universe.dev/meetupr) 15 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 16 | [![R-CMD-check](https://github.com/rladies/meetupr/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/rladies/meetupr/actions/workflows/R-CMD-check.yaml) 17 | [![Codecov test 18 | coverage](https://codecov.io/gh/rladies/meetupr/graph/badge.svg)](https://app.codecov.io/gh/rladies/meetupr) 19 | 20 | 21 | 22 | R interface to the Meetup GraphQL API 23 | 24 | ## Installation 25 | 26 | Install the CRAN version: 27 | 28 | ```r 29 | install.packages("meetupr") 30 | ``` 31 | 32 | To install the development version from R-universe: 33 | 34 | ```r 35 | install.packages( 36 | 'meetupr', 37 | repos = c( 38 | 'https://rladies.r-universe.dev', 39 | 'https://cloud.r-project.org' 40 | ) 41 | ) 42 | ``` 43 | 44 | or from GitHub: 45 | 46 | ```r 47 | # install.packages("remotes") 48 | remotes::install_github("rladies/meetupr") 49 | ``` 50 | 51 | ## Authentication 52 | 53 | meetupr uses OAuth 2.0 for authentication with the Meetup API. The first 54 | time you run a meetupr function, you’ll be prompted to authorize the 55 | application in your browser. Your token will be cached for future 56 | sessions. 57 | 58 | ## Usage 59 | 60 | ### Get group events 61 | 62 | ```{r} 63 | library(meetupr) 64 | 65 | events <- get_group_events("rladies-san-francisco", "past") 66 | ``` 67 | 68 | ### Get group members 69 | 70 | ```{r} 71 | members <- get_group_members("rladies-san-francisco") 72 | head(members) 73 | ``` 74 | 75 | ### Search for groups 76 | 77 | ```{r} 78 | groups <- find_groups("R-Ladies") |> 79 | dplyr::arrange(desc(founded_date)) 80 | ``` 81 | 82 | ### Pro network access 83 | 84 | For Meetup Pro networks, note that user needs to be a **pro** network organiser to access the data. 85 | 86 | ``` r 87 | # Get all groups in a pro network 88 | pro_groups <- get_pro_groups("rladies") 89 | 90 | # Get events from a pro network 91 | pro_events <- get_pro_events("rladies", max_results = 10) 92 | ``` 93 | 94 | ## Contributing 95 | 96 | We welcome contributions! 97 | Please see the [contribution guidelines](https://github.com/rladies/meetupr/blob/main/.github/CONTRIBUTING.md). 98 | 99 | ## Code of Conduct 100 | 101 | Please note that this project is released with a [Contributor Code of 102 | Conduct](https://github.com/rladies/.github/blob/master/CODE_OF_CONDUCT.md). 103 | By contributing to this project, you agree to abide by its terms. 104 | -------------------------------------------------------------------------------- /man/meetup_ci.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/auth.R 3 | \name{meetup_ci} 4 | \alias{meetup_ci} 5 | \alias{meetup_ci_setup} 6 | \alias{meetup_ci_load} 7 | \title{Meetup API CI Authentication} 8 | \usage{ 9 | meetup_ci_setup(client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr")) 10 | 11 | meetup_ci_load(client_name = Sys.getenv("MEETUP_CLIENT_NAME", "meetupr")) 12 | } 13 | \arguments{ 14 | \item{client_name}{A string representing the name of the client. By 15 | default, it is set to \code{"meetupr"} and retrieved from the 16 | \code{MEETUP_CLIENT_NAME} environment variable.} 17 | } 18 | \value{ 19 | \itemize{ 20 | \item \code{meetup_setup_ci()}: Returns the encoded base64 authentication token 21 | invisibly. 22 | \item \code{meetup_load_ci()}: Returns \code{TRUE} invisibly if the token was successfully 23 | loaded. 24 | } 25 | } 26 | \description{ 27 | Functions to manage Meetup API authentication in Continuous Integration (CI) 28 | environments. \code{meetup_setup_ci()} prepares authentication credentials for CI 29 | use by encoding tokens, while \code{meetup_load_ci()} loads and decodes those 30 | credentials in the CI environment. 31 | } 32 | \details{ 33 | \subsection{Setting up CI Authentication}{ 34 | 35 | \code{meetup_setup_ci()} performs the following steps: 36 | \itemize{ 37 | \item Checks if the user has authenticated with the Meetup API 38 | \item Reads the existing token file from the OAuth cache directory 39 | \item Encodes the token file contents into a base64 string 40 | \item Stores credentials in the keyring with service name "meetupr" 41 | \item Provides guidance for setting environment variables in CI 42 | \item Copies the encoded token to the clipboard (if available) 43 | } 44 | } 45 | 46 | \subsection{Loading CI Authentication}{ 47 | 48 | \code{meetup_load_ci()} requires the following to be stored in the keyring 49 | (typically populated from environment variables in CI): 50 | \itemize{ 51 | \item \code{token}: The base64-encoded token string 52 | (service: "meetupr", username: "token") 53 | \item \code{token_file}: The name of the token file 54 | (service: "meetupr", username: "token_file") 55 | } 56 | 57 | The decoded token is saved in the OAuth cache directory at: 58 | \code{{oauth_cache_path}/{client_name}/{token_file}} 59 | } 60 | 61 | \subsection{Environment Variables for CI}{ 62 | 63 | When using the environment backend (automatically selected when keyring 64 | support is unavailable, such as on CRAN or headless CI systems), credentials 65 | are stored as: 66 | \itemize{ 67 | \item \code{meetupr:token} - The base64-encoded token 68 | \item \code{meetupr:token_file} - The token filename 69 | } 70 | } 71 | } 72 | \section{Functions}{ 73 | \itemize{ 74 | \item \code{meetup_ci_setup()}: Setup authentication for CI environments 75 | 76 | \item \code{meetup_ci_load()}: Load authentication from CI environment 77 | 78 | }} 79 | \examples{ 80 | \dontrun{ 81 | # Setup CI authentication (run locally): 82 | meetup_setup_ci() 83 | 84 | # In your CI pipeline, load the credentials: 85 | meetup_load_ci() 86 | 87 | # Custom client name: 88 | meetup_setup_ci(client_name = "my_custom_client") 89 | meetup_load_ci(client_name = "my_custom_client") 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /R/get-pro.R: -------------------------------------------------------------------------------- 1 | #' Retrieve information about Meetup Pro networks, 2 | #' including groups and events. 3 | #' 4 | #' Meetup Pro is a premium service for organizations 5 | #' managing multiple Meetup groups. 6 | #' This functionality allows you to access details about 7 | #' the groups within a Pro network 8 | #' and the events they host. 9 | #' 10 | #' @template urlname 11 | #' @template max_results 12 | #' @template handle_multiples 13 | #' @template date_before 14 | #' @template date_after 15 | #' @param ... Should be empty. Used for parameter expansion 16 | #' @template extra_graphql 17 | #' @param status Which status the events should have. 18 | #' @return tibble with pro network information 19 | #' 20 | #' @examples 21 | #' \dontshow{ 22 | #' vcr::insert_example_cassette("get_pro", package = "meetupr") 23 | #' meetupr:::mock_if_no_auth() 24 | #' } 25 | #' urlname <- "rladies" 26 | #' members <- get_pro_groups(urlname) 27 | #' 28 | #' upcoming_events <- get_pro_events(urlname, "upcoming", max_results = 5) 29 | #' \dontshow{ 30 | #' vcr::eject_cassette() 31 | #' } 32 | #' @name get_pro 33 | #' @return A tibble with meetup pro information 34 | NULL 35 | 36 | #' @export 37 | #' @describeIn get_pro retrieve groups in a pro network 38 | get_pro_groups <- function( 39 | urlname, 40 | max_results = NULL, 41 | handle_multiples = "list", 42 | extra_graphql = NULL, 43 | ... 44 | ) { 45 | rlang::check_dots_empty() 46 | execute( 47 | standard_query( 48 | "get_pro_groups", 49 | "data.proNetwork.groupsSearch" 50 | ), 51 | urlname = urlname, 52 | first = max_results, 53 | max_results = max_results, 54 | handle_multiples = handle_multiples, 55 | extra_graphql = extra_graphql 56 | ) |> 57 | process_datetime_fields(c("founded_date", "pro_join_date")) |> 58 | dplyr::mutate( 59 | country = get_country_code(country) 60 | ) 61 | } 62 | 63 | #' @export 64 | #' @describeIn get_pro retrieve events from a pro network 65 | get_pro_events <- function( 66 | urlname, 67 | status = NULL, 68 | date_before = date_before, 69 | date_after = date_after, 70 | max_results = NULL, 71 | handle_multiples = "list", 72 | extra_graphql = NULL, 73 | ... 74 | ) { 75 | rlang::check_dots_empty() 76 | 77 | if (!is_self_pro()) { 78 | cli::cli_warn( 79 | "The authenticated user must have Pro 80 | access to retrieve Network event data." 81 | ) 82 | } 83 | 84 | execute( 85 | standard_query( 86 | "get_pro_events", 87 | "data.proNetwork.eventsSearch" 88 | ), 89 | urlname = urlname, 90 | first = max_results, 91 | max_results = max_results, 92 | status = validate_event_status(status, pro = TRUE), 93 | handle_multiples = handle_multiples, 94 | extra_graphql = extra_graphql 95 | ) |> 96 | process_datetime_fields(c("date_time", "pro_join_date")) 97 | } 98 | 99 | #' Check if the authenticated user has Pro access 100 | #' @keywords internal 101 | #' @noRd 102 | is_self_pro <- function() { 103 | if (!meetup_auth_status(silent = TRUE)) { 104 | return(FALSE) 105 | } 106 | resp <- meetup_query( 107 | " 108 | query { self { 109 | isProOrganizer 110 | } } 111 | " 112 | ) 113 | resp$data$self$isProOrganizer 114 | } 115 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | ## meetupr 0.3.0 2 | 3 | - Updated to use new Meetup schema from February 2025 4 | - Switched to using the `httr2` package for making API requests instead of `httr`. 5 | - Uses s7 classes for internal query objects instead of lists. 6 | - Added functions: 7 | - `meetup_query()` - Run custom queries against the Meetup API. 8 | - `meetup_sitrep()` - Get information about your API connection status. 9 | - `meetup_schema()` - Get information about the Meetup API query options. 10 | - added deprecation warnings for `get_meetup_comments()`. 11 | - Added new vignettes 12 | - Expanded test suite 13 | - Uses vcr in examples and vignettes in addition to tests 14 | 15 | 16 | ## meetupr 0.2.0 17 | 18 | ### Breaking changes 19 | 20 | * All mentions of and arguments related to API keys have been removed as the Meetup API no longer supports authentication with an API key. 21 | 22 | ### New features 23 | 24 | * Added automatic rate limiting based on the response headers. 25 | * Added support for non-interactive use. 26 | * Added functions for getting pro events and groups. 27 | 28 | ### Internals 29 | 30 | * Renamed `api_method` to `api_path` in internal function, `.fetch_results()`, as it's less confusing. 31 | 32 | ## meetupr 0.1.1 33 | * Added `get_event_rsvps()` function. Contribution by Michael Beigelmacher: https://github.com/rladies/meetupr/pull/19 34 | 35 | ## meetupr 0.1.0 36 | 37 | * Added `NEWS.md` file. 38 | 39 | ### BREAKING CHANGE 40 | 41 | Updated `get_events()`, `get_boards()`, and `get_group_members()` to output a tibble with summarised information. The raw content previously output by these functions can be found in the `resource` column of each output tibble. 42 | 43 | ### BREAKING CHANGES 44 | Changed the name of `get_meetup_attendees()` and `get_meetup_comments()` to `get_comments()` and `get_attendees()` for distinction (all other `get_*` functions get something about a group, not a specific event from that group). Also updated the output of these functions from lists to tibbles. The raw content previously output by these functions can be found in the `resource` column of each output tibble. 45 | 46 | * Officially deprecated the `get_meetup_attendees()` and `get_meetup_comments()` functions. 47 | * Added a bunch of fields to the `get_events()` output. 48 | * Added ability to pass in a vector of statuses for `event_status` in addition to a single string. 49 | * Added `find_groups()` function to get list of groups using text-based search. 50 | * Added short vignette to demonstrate the use of the new `find_groups()` function. 51 | * Added `...` option to `.quick_fetch()` and `.fetch_results()` (in `internals.R`) to use any parameter in the `GET` request. 52 | * Removed `LazyData = TRUE` from DESCRIPTION file (this is not needed because there is no dataset shipped within the package). 53 | * Added `.get_api_key()` internal function which is used inside `.fetch_results()` so now if `api_key = NULL` it will automatically populate that variable with the `MEETUP_KEY` environment variable, if available. 54 | * Added a printout of how many results are returned for a query so users will understand why it's taking a while for the function to finish executing. 55 | * Renamed `api_params` to `api_method` in internal function, `.fetch_results()`, since that's the official name for what that argument represents. 56 | * Added several new columns to the `get_group_members()` result tibble (e.g. bio, city, country, lat, lon, etc) 57 | * Added a References section in the R docs for each function which includes a link to the official Meetup API documentation for each endpoint. 58 | 59 | 60 | ## meetupr 0.0.1 61 | 62 | * Initial release. 63 | -------------------------------------------------------------------------------- /R/get-self.R: -------------------------------------------------------------------------------- 1 | #' Get information about the authenticated user 2 | #' 3 | #' Retrieves detailed information about the currently authenticated Meetup user, 4 | #' including basic profile data, account type, 5 | #' subscription status, and API access permissions. 6 | #' 7 | #' @return A list containing user information 8 | #' @export 9 | #' @examples 10 | #' \dontshow{ 11 | #' vcr::insert_example_cassette("get_self", package = "meetupr") 12 | #' meetupr:::mock_if_no_auth() 13 | #' } 14 | #' user <- get_self() 15 | #' cat("Hello", user$name, "!") 16 | #' \dontshow{ 17 | #' vcr::eject_cassette() 18 | #' } 19 | get_self <- function() { 20 | execute( 21 | meetup_template_query( 22 | template_path("get_self"), 23 | "", 24 | "data.self", 25 | process_data = process_self_data 26 | ), 27 | extra_graphql = NULL 28 | ) 29 | } 30 | 31 | #' Extract Location Information 32 | #' @keywords internal 33 | #' @noRd 34 | extract_location_info <- function(user_data) { 35 | list( 36 | city = user_data$city, 37 | state = user_data$state, 38 | country = get_country_code(user_data$country), 39 | lat = user_data$lat, 40 | lon = user_data$lon 41 | ) 42 | } 43 | 44 | #' Extract Profile Information 45 | #' @keywords internal 46 | #' @noRd 47 | extract_profile_info <- function(user_data) { 48 | list( 49 | bio = user_data$bio, 50 | member_url = user_data$memberUrl, 51 | join_time = user_data$startDate, 52 | preferred_locale = user_data$preferredLocale 53 | ) 54 | } 55 | 56 | #' Determine Pro Status 57 | #' @keywords internal 58 | #' @noRd 59 | determine_pro_status <- function(user_data) { 60 | has_pro_organizer <- user_data$isProOrganizer %||% FALSE 61 | has_admin_networks <- !is.null(user_data$adminProNetworks) && 62 | length(user_data$adminProNetworks) > 0 63 | 64 | has_pro_organizer || has_admin_networks 65 | } 66 | 67 | #' @export 68 | print.meetup_user <- function(x, ...) { 69 | cli::cli_h2("Meetup User:") 70 | cli::cli_li("ID: {x$id}") 71 | cli::cli_li("Name: {x$name}") 72 | if (!is.null(x$email)) { 73 | cli::cli_li("Email: {x$email}") 74 | } 75 | 76 | cli::cli_h3("Roles:") 77 | cli::cli_li("Organizer: {ifelse(x$is_organizer, 'Yes', 'No')}") 78 | cli::cli_li("Leader: {ifelse(x$is_leader, 'Yes', 'No')}") 79 | cli::cli_li("Pro Organizer: {ifelse(x$is_pro_organizer, 'Yes', 'No')}") 80 | cli::cli_li("Member Plus: {ifelse(x$is_member_plus_subscriber, 'Yes', 'No')}") 81 | 82 | if (!is.na(x$has_pro_access)) { 83 | cli::cli_li("Pro API Access: {ifelse(x$has_pro_access, 'Yes', 'No')}") 84 | } 85 | 86 | if (!is.null(x$location)) { 87 | cli::cli_h3("Location:") 88 | if (!is.null(x$location$city)) { 89 | cli::cli_li("City: {x$location$city}") 90 | } 91 | if (!is.null(x$location$country)) { 92 | cli::cli_li("Country: {x$location$country}") 93 | } 94 | } 95 | 96 | invisible(x) 97 | } 98 | 99 | #' Self query template 100 | #' @keywords internal 101 | #' @noRd 102 | process_self_data <- function(data, ...) { 103 | if (length(data) == 0 || is.null(data[[1]])) { 104 | cli::cli_abort("No user data returned from self query") 105 | } 106 | pro_status <- determine_pro_status(data) 107 | 108 | structure( 109 | list( 110 | id = data$id, 111 | name = data$name, 112 | email = data$email, 113 | is_organizer = data$isOrganizer %||% FALSE, 114 | is_leader = data$isLeader %||% FALSE, 115 | is_member_plus_subscriber = data$isMemberPlusSubscriber %||% 116 | FALSE, 117 | is_pro_organizer = data$isProOrganizer %||% FALSE, 118 | has_pro_access = pro_status, 119 | location = extract_location_info(data), 120 | profile = extract_profile_info(data), 121 | raw = data 122 | ), 123 | class = c("meetup_user", "list") 124 | ) 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # meetupr Meetupr hex logo by Zane Dax @StarTrek_Lt 6 | 7 | Logo by Zane Dax 8 | [@StarTrek_Lt](https://x.com/startrek_lt) 9 | 10 | 11 | 12 | [![CRAN 13 | status](https://www.r-pkg.org/badges/version/meetupr.png)](https://CRAN.R-project.org/package=meetupr) 14 | [![meetupr status 15 | badge](https://rladies.r-universe.dev/meetupr/badges/version.png)](https://rladies.r-universe.dev/meetupr) 16 | [![License: 17 | MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 18 | [![R-CMD-check](https://github.com/rladies/meetupr/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/rladies/meetupr/actions/workflows/R-CMD-check.yaml) 19 | [![Codecov test 20 | coverage](https://codecov.io/gh/rladies/meetupr/graph/badge.svg)](https://app.codecov.io/gh/rladies/meetupr) 21 | 22 | 23 | 24 | R interface to the Meetup GraphQL API 25 | 26 | ## Installation 27 | 28 | Install the CRAN version: 29 | 30 | ``` r 31 | install.packages("meetupr") 32 | ``` 33 | 34 | To install the development version from R-universe: 35 | 36 | ``` r 37 | install.packages( 38 | 'meetupr', 39 | repos = c( 40 | 'https://rladies.r-universe.dev', 41 | 'https://cloud.r-project.org' 42 | ) 43 | ) 44 | ``` 45 | 46 | or from GitHub: 47 | 48 | ``` r 49 | # install.packages("remotes") 50 | remotes::install_github("rladies/meetupr") 51 | ``` 52 | 53 | ## Authentication 54 | 55 | meetupr uses OAuth 2.0 for authentication with the Meetup API. The first 56 | time you run a meetupr function, you’ll be prompted to authorize the 57 | application in your browser. Your token will be cached for future 58 | sessions. 59 | 60 | ## Usage 61 | 62 | ### Get group events 63 | 64 | ``` r 65 | library(meetupr) 66 | 67 | events <- get_group_events("rladies-san-francisco", "past") 68 | ``` 69 | 70 | ### Get group members 71 | 72 | ``` r 73 | members <- get_group_members("rladies-san-francisco") 74 | head(members) 75 | ``` 76 | 77 | # A tibble: 6 × 4 78 | id name member_url member_photo_url 79 | 80 | 1 14534094 Gabriela de Queiroz https://www.meetup.com/members/… https://secure-… 81 | 2 64513952 T. Libman https://www.meetup.com/members/… https://secure-… 82 | 3 25902562 Maggie L. https://www.meetup.com/members/… https://secure-… 83 | 4 2412055 Marsee Henon https://www.meetup.com/members/… https://secure-… 84 | 5 11509157 Jessica Montoya https://www.meetup.com/members/… https://secure-… 85 | 6 2920822 Benay Dara-Abrams https://www.meetup.com/members/… https://secure-… 86 | 87 | ### Search for groups 88 | 89 | ``` r 90 | groups <- find_groups("R-Ladies") |> 91 | dplyr::arrange(desc(founded_date)) 92 | ``` 93 | 94 | ### Pro network access 95 | 96 | For Meetup Pro networks, note that user needs to be a **pro** network 97 | organiser to access the data. 98 | 99 | ``` r 100 | # Get all groups in a pro network 101 | pro_groups <- get_pro_groups("rladies") 102 | 103 | # Get events from a pro network 104 | pro_events <- get_pro_events("rladies", max_results = 10) 105 | ``` 106 | 107 | ## Contributing 108 | 109 | We welcome contributions! Please see the [contribution 110 | guidelines](https://github.com/rladies/meetupr/blob/main/.github/CONTRIBUTING.md). 111 | 112 | ## Code of Conduct 113 | 114 | Please note that this project is released with a [Contributor Code of 115 | Conduct](https://github.com/rladies/.github/blob/master/CODE_OF_CONDUCT.md). 116 | By contributing to this project, you agree to abide by its terms. 117 | -------------------------------------------------------------------------------- /tests/testthat/test-graphql-builders.R: -------------------------------------------------------------------------------- 1 | test_that("meetup_template_query creates a meetup_template object", { 2 | obj <- meetup_template_query( 3 | template = "template", 4 | page_info_path = "path.to.pageInfo", 5 | edges_path = "path.to.edges" 6 | ) 7 | 8 | expect_true(S7::S7_inherits(obj, S7::S7_object)) 9 | expect_equal(obj@template, "template") 10 | expect_equal(obj@page_info_path, "path.to.pageInfo") 11 | expect_equal(obj@edges_path, "path.to.edges") 12 | }) 13 | 14 | test_that("execute handles no data returned", { 15 | local_mocked_bindings( 16 | execute_from_template = function(...) list(data = NULL) 17 | ) 18 | obj <- meetup_template_query("template", "path.to.pageInfo", "path.to.edges") 19 | result <- execute(obj) 20 | expect_s3_class(result, "tbl") 21 | }) 22 | 23 | test_that("execute respects max_results", { 24 | local_mocked_bindings( 25 | execute_from_template = function(...) { 26 | list( 27 | data = list( 28 | edges = list( 29 | list(node = "a"), 30 | list(node = "b"), 31 | list(node = "c") 32 | ) 33 | ) 34 | ) 35 | }, 36 | extract_at_path = function(object, response) response$data$edges 37 | ) 38 | obj <- meetup_template_query("template", "data.pageInfo", "data.edges") 39 | result <- execute(obj, max_results = 2) 40 | expect_equal(nrow(result), 2) 41 | expect_equal(result$node, c("a", "b")) 42 | }) 43 | 44 | test_that("execute stops on static cursor", { 45 | local_mocked_bindings( 46 | execute_from_template = function(...) { 47 | list( 48 | data = list( 49 | pageInfo = list(hasNextPage = TRUE, endCursor = "cursor1"), 50 | edges = list(list(node = "a")) 51 | ) 52 | ) 53 | }, 54 | extract_at_path = function(object, response) response$data$edges 55 | ) 56 | obj <- meetup_template_query("template", "path.to.pageInfo", "path.to.edges") 57 | result <- execute(obj) 58 | expect_equal(result$node, c("a")) 59 | }) 60 | 61 | test_that("extract_at_path returns appropriate data", { 62 | response <- list( 63 | data = list( 64 | edges = list( 65 | list(node = "a"), 66 | list(node = "b") 67 | ) 68 | ) 69 | ) 70 | obj <- meetup_template_query("template", "path.to.pageInfo", "data.edges") 71 | result <- extract_at_path(obj, response) 72 | expect_equal(result, list("a", "b")) 73 | }) 74 | 75 | test_that("extract_at_path handles non-standard edges", { 76 | response <- list(data = list(edges = list("a", "b"))) 77 | obj <- meetup_template_query("template", "path.to.pageInfo", "data.edges") 78 | result <- extract_at_path(obj, response) 79 | expect_equal(result, list("a", "b")) 80 | }) 81 | 82 | test_that("get_cursor returns correct cursor", { 83 | response <- list( 84 | data = list(pageInfo = list(hasNextPage = TRUE, endCursor = "cursor1")) 85 | ) 86 | obj <- meetup_template_query("template", "data.pageInfo", "path.to.edges") 87 | cursor <- get_cursor(obj, response) 88 | expect_equal(cursor, list(cursor = "cursor1")) 89 | }) 90 | 91 | test_that("get_cursor returns NULL if no next page", { 92 | response <- list(data = list(pageInfo = list(hasNextPage = FALSE))) 93 | obj <- meetup_template_query("template", "data.pageInfo", "path.to.edges") 94 | cursor <- get_cursor(obj, response) 95 | expect_null(cursor) 96 | }) 97 | 98 | test_that("parse_path_to_pluck splits path correctly", { 99 | path <- "data.pageInfo.endCursor" 100 | result <- parse_path_to_pluck(path) 101 | expect_equal(result, c("data", "pageInfo", "endCursor")) 102 | }) 103 | 104 | test_that("standard_query constructs correctly", { 105 | result <- standard_query("template", "base.path") 106 | expect_true(S7::S7_inherits(result, S7::S7_object)) 107 | 108 | expect_equal(result@template, "") 109 | expect_equal(result@page_info_path, "base.path.pageInfo") 110 | expect_equal(result@edges_path, "base.path.edges") 111 | }) 112 | -------------------------------------------------------------------------------- /R/graphql-builders.R: -------------------------------------------------------------------------------- 1 | #' S7 class for representing GraphQL query configurations 2 | #' @keywords internal 3 | #' @noRd 4 | meetup_template <- S7::new_class( 5 | "meetup_template", 6 | properties = list( 7 | template = S7::class_character, 8 | page_info_path = S7::class_character, 9 | edges_path = S7::class_character, 10 | process_data = S7::class_function 11 | ) 12 | ) 13 | 14 | #' Constructor for meetup_template 15 | #' @keywords internal 16 | #' @noRd 17 | meetup_template_query <- function( 18 | template, 19 | page_info_path, 20 | edges_path, 21 | process_data = process_graphql_list 22 | ) { 23 | meetup_template( 24 | template = template, 25 | page_info_path = page_info_path, 26 | edges_path = edges_path, 27 | process_data = process_data 28 | ) 29 | } 30 | 31 | #' Execute a meetup_template 32 | #' @keywords internal 33 | #' @noRd 34 | execute <- S7::new_generic("execute", "object") 35 | 36 | S7::method(execute, meetup_template) <- function( 37 | object, 38 | max_results = NULL, 39 | handle_multiples = "list", 40 | extra_graphql = NULL, 41 | ..., 42 | .progress = TRUE 43 | ) { 44 | all_data <- list() 45 | cursor <- NULL 46 | previous_cursor <- NULL 47 | page_count <- 0 48 | 49 | repeat { 50 | page_count <- page_count + 1 51 | 52 | response <- execute_from_template( 53 | object@template, 54 | ..., 55 | cursor = cursor, 56 | extra_graphql = extra_graphql 57 | ) 58 | 59 | current_data <- extract_at_path(object, response) 60 | if (length(current_data) == 0) { 61 | break 62 | } 63 | 64 | all_data <- c(all_data, current_data) 65 | 66 | # Check if we've hit max_results 67 | if (!is.null(max_results) && length(all_data) >= max_results) { 68 | break 69 | } 70 | 71 | cursor_info <- get_cursor(object, response) 72 | if (is.null(cursor_info)) { 73 | break 74 | } 75 | 76 | # Prevent infinite loops 77 | new_cursor <- cursor_info$cursor 78 | if (!is.null(previous_cursor) && new_cursor == previous_cursor) { 79 | break 80 | } 81 | 82 | previous_cursor <- cursor 83 | cursor <- new_cursor 84 | } 85 | 86 | # Trim to max_results if specified 87 | if (!is.null(max_results) && length(all_data) > max_results) { 88 | all_data <- all_data[1:max_results] 89 | } 90 | object@process_data(all_data, handle_multiples) 91 | } 92 | 93 | #' Parse dot-separated path to pluck arguments 94 | #' @keywords internal 95 | #' @noRd 96 | parse_path_to_pluck <- function(path) { 97 | strsplit(path, "\\.")[[1]] 98 | } 99 | 100 | # Generic path extraction 101 | extract_at_path <- S7::new_generic("extract_at_path", c("object", "response")) 102 | 103 | S7::method(extract_at_path, list(meetup_template, S7::class_any)) <- function( 104 | object, 105 | response 106 | ) { 107 | edges_parts <- strsplit(object@edges_path, "\\.")[[1]] 108 | edges <- purrr::pluck(response, !!!edges_parts) 109 | if (!is.null(edges) && length(edges) > 0) { 110 | if (!any(sapply(edges, function(x) "node" %in% names(x)))) { 111 | return(edges) 112 | } 113 | return( 114 | lapply(edges, `[[`, "node") 115 | ) 116 | } 117 | list() 118 | } 119 | 120 | # Pagination using stored path 121 | get_cursor <- S7::new_generic("get_cursor", c("object", "response")) 122 | 123 | S7::method(get_cursor, list(meetup_template, S7::class_any)) <- function( 124 | object, 125 | response 126 | ) { 127 | page_info_parts <- strsplit(object@page_info_path, "\\.")[[1]] 128 | if (length(page_info_parts) == 0) { 129 | return(NULL) 130 | } 131 | page_info <- purrr::pluck(response, !!!page_info_parts) 132 | 133 | if (!is.null(page_info) && page_info$hasNextPage) { 134 | return( 135 | list(cursor = page_info$endCursor) 136 | ) 137 | } 138 | NULL 139 | } 140 | 141 | # For common patterns 142 | standard_query <- function(template, base_path) { 143 | meetup_template_query( 144 | template = template_path(template), 145 | page_info_path = paste0( 146 | base_path, 147 | ".pageInfo" 148 | ), 149 | edges_path = paste0( 150 | base_path, 151 | ".edges" 152 | ) 153 | ) 154 | } 155 | -------------------------------------------------------------------------------- /tests/testthat/test-get_event.R: -------------------------------------------------------------------------------- 1 | test_that("get_event_rsvps gets data correctly", { 2 | mock_if_no_auth() 3 | vcr::local_cassette("get_event_rsvps") 4 | result <- get_event_rsvps(id = event_id) 5 | expect_s3_class(result, "tbl_df") 6 | expect_true(nrow(result) > 0) 7 | }) 8 | 9 | test_that("get_event_comments() returns empty", { 10 | expect_warning( 11 | comments <- get_event_comments(id = event_id), 12 | "no longer available" 13 | ) 14 | expect_s3_class(comments, "data.frame") 15 | expect_equal(ncol(comments), 7) 16 | expect_equal(nrow(comments), 0) 17 | }) 18 | 19 | test_that("get_event_comments returns warning and empty tibble", { 20 | withr::local_tempdir() 21 | expect_warning( 22 | result <- get_event_comments(id = "103349942"), 23 | "Event comments functionality has been removed" 24 | ) 25 | 26 | expect_s3_class(result, "tbl_df") 27 | expect_equal(nrow(result), 0) 28 | expect_true(all( 29 | names(result) %in% 30 | c( 31 | "id", 32 | "comment", 33 | "created", 34 | "like_count", 35 | "member_id", 36 | "member_name", 37 | "link" 38 | ) 39 | )) 40 | }) 41 | 42 | test_that("get_event_rsvps works with handle_multiples parameter", { 43 | mock_if_no_auth() 44 | vcr::local_cassette("get_event_rsvps") 45 | 46 | result <- get_event_rsvps( 47 | id = event_id, 48 | handle_multiples = "first" 49 | ) 50 | 51 | expect_s3_class(result, "tbl_df") 52 | }) 53 | 54 | test_that("get_event_rsvps respects max_results", { 55 | mock_if_no_auth() 56 | vcr::local_cassette("get_event_rsvps_max") 57 | 58 | result <- get_event_rsvps( 59 | id = event_id, 60 | max_results = 5 61 | ) 62 | 63 | expect_s3_class(result, "tbl_df") 64 | expect_lte(nrow(result), 5) 65 | }) 66 | 67 | test_that("get_event retrieves event data", { 68 | mock_if_no_auth() 69 | vcr::local_cassette("get_event") 70 | 71 | result <- get_event(id = event_id) 72 | 73 | expect_s3_class(result, "meetup_event") 74 | expect_s3_class(result, "list") 75 | expect_true("id" %in% names(result)) 76 | expect_true("title" %in% names(result)) 77 | expect_true("eventUrl" %in% names(result)) 78 | expect_true("status" %in% names(result)) 79 | }) 80 | 81 | test_that("get_event returns proper structure", { 82 | mock_if_no_auth() 83 | vcr::local_cassette("get_event") 84 | 85 | result <- get_event(id = event_id) 86 | 87 | # Check required fields exist 88 | expect_true(!is.null(result$id)) 89 | expect_true(!is.null(result$title)) 90 | 91 | # Check structure 92 | expect_type(result, "list") 93 | expect_true(inherits(result, "meetup_event")) 94 | }) 95 | 96 | test_that("print.meetup_event snapshot test", { 97 | mock_if_no_auth() 98 | vcr::local_cassette("get_event") 99 | 100 | event <- get_event(id = event_id) 101 | 102 | expect_snapshot(print(event)) 103 | }) 104 | 105 | test_that("print.meetup_event snapshot test", { 106 | mock_if_no_auth() 107 | vcr::local_cassette("get_event") 108 | 109 | event <- get_event(id = event_id) 110 | 111 | expect_snapshot(print(event)) 112 | }) 113 | 114 | test_that("print.meetup_event returns invisibly", { 115 | mock_if_no_auth() 116 | vcr::local_cassette("get_event") 117 | 118 | event <- get_event(id = event_id) 119 | 120 | result <- withVisible(print(event)) 121 | expect_false(result$visible) 122 | expect_identical(result$value, event) 123 | }) 124 | 125 | test_that("process_event_data handles missing data", { 126 | expect_error( 127 | process_event_data(list()), 128 | "No event data returned" 129 | ) 130 | 131 | expect_error( 132 | process_event_data(NULL), 133 | "No event data returned" 134 | ) 135 | }) 136 | 137 | test_that("process_event_data adds correct class", { 138 | mock_data <- list( 139 | id = "123", 140 | title = "Test Event", 141 | status = "ACTIVE", 142 | feeSettings = list( 143 | required = TRUE, 144 | amount = 10, 145 | currency = "USD" 146 | ) 147 | ) 148 | 149 | result <- process_event_data(mock_data) 150 | expect_snapshot(print(result)) 151 | expect_s3_class(result, "meetup_event") 152 | expect_equal(result$id, "123") 153 | expect_match(result$title, "Test Event") 154 | }) 155 | -------------------------------------------------------------------------------- /inst/_vcr/find_topics.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query findTopics(\n $query: String!, \n $first: Int = 1000, 7 | \n $after: String\n ) {\n suggestTopics(\n query: $query\n first: 8 | $first\n after: $after\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n edges 9 | {\n node {\n id\n name\n urlkey\n description\n }\n }\n }\n}","variables":{"query":"R","first":10}}' 10 | response: 11 | status: 200 12 | headers: 13 | content-type: application/json;charset=utf-8 14 | accept-ranges: bytes 15 | date: Wed, 17 Sep 2025 15:05:26 GMT 16 | content-length: '1756' 17 | body: 18 | string: '{"data":{"suggestTopics":{"pageInfo":{"hasNextPage":true,"endCursor":"eyJ2YWx1ZXMiOlt7InR5cGUiOiJEb3VibGUiLCJ2YWx1ZSI6Mi44MDIxODgyfSx7InR5cGUiOiJTdHJpbmciLCJ2YWx1ZSI6IjU4NDkwMi1lbl9VUyJ9XX0="},"totalCount":10000,"edges":[{"node":{"id":"109937","name":"Board 19 | Games","urlkey":"board-games","description":null}},{"node":{"id":"24384","name":"Consciousness","urlkey":"consciousness","description":"Meet 20 | other local people who are interested in the Science of Consciousness. Discuss 21 | the recent steps that have been made in the science of consciousness and the 22 | new paradigm that''s emerging as a result."}},{"node":{"id":"21137","name":"Recreational 23 | Sports","urlkey":"recreational-sports","description":"Meet other people interested 24 | in Recreational Sports!"}},{"node":{"id":"35073","name":"Business Referral 25 | Networking","urlkey":"business-referral-networking","description":"Meet other 26 | local people interested in Business Referral Networking: gather to network, 27 | share advice, and exchange ideas!"}},{"node":{"id":"35170","name":"Hiking","urlkey":"hiking","description":"Meet 28 | other local hiking enthusiasts! All those who are dedicated to hiking excursions 29 | near and far, with a focus on leaving no trace of litter on our lands."}},{"node":{"id":"43699","name":"Reading","urlkey":"reading","description":null}},{"node":{"id":"19285","name":"Real 30 | Estate Investors","urlkey":"real-estate-investors","description":null}},{"node":{"id":"73492","name":"Real 31 | Estate Investing","urlkey":"real-estate-investing","description":null}},{"node":{"id":"16714","name":"Tabletop 32 | Role Playing and Board Games","urlkey":"tabletop-role-playing-and-board-games","description":null}},{"node":{"id":"584902","name":"Machine 33 | Learning","urlkey":"machine-learning","description":null}}]}}}' 34 | recorded_at: 2025-09-17 15:05:26 35 | - request: 36 | method: POST 37 | uri: https://api.meetup.com/gql-ext 38 | body: 39 | string: '{"query":"query findTopics(\n $query: String!, \n $first: Int = 1000, 40 | \n $after: String\n ) {\n suggestTopics(\n query: $query\n first: 41 | $first\n after: $after\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n edges 42 | {\n node {\n id\n name\n urlkey\n description\n }\n }\n }\n}","variables":{"query":"Data 43 | Science","first":5}}' 44 | response: 45 | status: 200 46 | headers: 47 | content-type: application/json;charset=utf-8 48 | accept-ranges: bytes 49 | date: Wed, 17 Sep 2025 15:05:27 GMT 50 | content-length: '699' 51 | body: 52 | string: '{"data":{"suggestTopics":{"pageInfo":{"hasNextPage":true,"endCursor":"eyJ2YWx1ZXMiOlt7InR5cGUiOiJEb3VibGUiLCJ2YWx1ZSI6MTQuODczNDMyfSx7InR5cGUiOiJTdHJpbmciLCJ2YWx1ZSI6IjE3MzMzMy1lbl9VUyJ9XX0="},"totalCount":558,"edges":[{"node":{"id":"1401412","name":"Data 53 | Vault Database Modeling","urlkey":"data-vault-database-modeling","description":null}},{"node":{"id":"1413652","name":"Data 54 | Warehouse","urlkey":"data-warehouse","description":null}},{"node":{"id":"95904","name":"Data 55 | Privacy","urlkey":"data-privacy","description":null}},{"node":{"id":"480642","name":"Data","urlkey":"data","description":null}},{"node":{"id":"173333","name":"Data 56 | Governance","urlkey":"data-governance","description":null}}]}}}' 57 | recorded_at: 2025-09-17 15:05:27 58 | recorded_with: VCR-vcr/2.0.0 59 | -------------------------------------------------------------------------------- /vignettes/_vcr/meetupr-group-members.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query members(\n $urlname: String!\n $cursor: String\n $first: 7 | Int = 1000\n) {\n groupByUrlname(urlname: $urlname) {\n id\n name\n memberships(after: 8 | $cursor, first: $first) {\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n edges 9 | {\n node {\n id\n name\n memberUrl\n memberPhoto 10 | {\n baseUrl\n }\n }\n metadata {\n status\n role\n joinTime\n lastAccessTime\n }\n }\n }\n \n }\n}","variables":{"urlname":"rladies-lagos","first":10}}' 11 | response: 12 | status: 200 13 | headers: 14 | content-type: application/json;charset=utf-8 15 | accept-ranges: bytes 16 | date: Sun, 19 Oct 2025 21:43:24 GMT 17 | content-length: '3270' 18 | body: 19 | string: '{"data":{"groupByUrlname":{"id":"32612004","name":"R-Ladies Lagos","memberships":{"pageInfo":{"hasNextPage":true,"endCursor":"MTU2NjI4OTI5NjAwMDoyNTM5ODY1MDE="},"totalCount":880,"edges":[{"node":{"id":"251470805","name":"R-Ladies 20 | Global","memberUrl":"https://www.meetup.com/members/251470805/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"}},"metadata":{"status":"LEADER","role":"ORGANIZER","joinTime":"2019-08-16T09:45:21+01:00","lastAccessTime":"2025-09-10T12:35:45+01:00"}},{"node":{"id":"225839690","name":"EYITAYO 21 | ALIMI","memberUrl":"https://www.meetup.com/members/225839690/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"}},"metadata":{"status":"LEADER","role":"COORGANIZER","joinTime":"2019-08-16T15:33:34+01:00","lastAccessTime":"2025-03-28T21:56:10+01:00"}},{"node":{"id":"236466773","name":"Adedamilola 22 | Adekanye","memberUrl":"https://www.meetup.com/members/236466773/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"}},"metadata":{"status":"ACTIVE","role":"MEMBER","joinTime":"2019-08-16T16:00:28+01:00","lastAccessTime":"2019-08-21T01:57:32+01:00"}},{"node":{"id":"255806325","name":"Helen","memberUrl":"https://www.meetup.com/members/255806325/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"}},"metadata":{"status":"ACTIVE","role":"MEMBER","joinTime":"2019-08-16T17:24:28+01:00","lastAccessTime":"2022-03-18T04:45:12+01:00"}},{"node":{"id":"287781170","name":"Alvin","memberUrl":"https://www.meetup.com/members/287781170/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"}},"metadata":{"status":"ACTIVE","role":"MEMBER","joinTime":"2019-08-17T10:59:48+01:00","lastAccessTime":"2019-08-17T10:59:48+01:00"}},{"node":{"id":"266271010","name":"ijeoma 23 | benson","memberUrl":"https://www.meetup.com/members/266271010/","memberPhoto":null},"metadata":{"status":"ACTIVE","role":"MEMBER","joinTime":"2019-08-18T04:39:52+01:00","lastAccessTime":"2019-08-18T04:39:52+01:00"}},{"node":{"id":"284993544","name":"Ochuko","memberUrl":"https://www.meetup.com/members/284993544/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"}},"metadata":{"status":"ACTIVE","role":"MEMBER","joinTime":"2019-08-18T06:50:46+01:00","lastAccessTime":"2019-09-16T04:45:34+01:00"}},{"node":{"id":"249638660","name":"Folajimi 24 | Aroloye","memberUrl":"https://www.meetup.com/members/249638660/","memberPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-member/"}},"metadata":{"status":"ACTIVE","role":"MEMBER","joinTime":"2019-08-19T05:11:39+01:00","lastAccessTime":"2019-12-01T07:07:04+01:00"}},{"node":{"id":"287607204","name":"Olaniyi 25 | ayomide recheal ","memberUrl":"https://www.meetup.com/members/287607204/","memberPhoto":null},"metadata":{"status":"ACTIVE","role":"MEMBER","joinTime":"2019-08-19T09:25:11+01:00","lastAccessTime":"2019-08-23T11:33:21+01:00"}},{"node":{"id":"253986501","name":"Ofure 26 | Ughu","memberUrl":"https://www.meetup.com/members/253986501/","memberPhoto":null},"metadata":{"status":"ACTIVE","role":"MEMBER","joinTime":"2019-08-20T04:21:36+01:00","lastAccessTime":"2019-08-20T04:21:36+01:00"}}]}}}}' 27 | recorded_at: 2025-10-19 21:43:24 28 | recorded_with: VCR-vcr/2.0.0 29 | -------------------------------------------------------------------------------- /tests/testthat/_vcr/find_topics_multiples.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query findTopics(\n $query: String!, \n $first: Int = 1000, 7 | \n $after: String\n ) {\n suggestTopics(\n query: $query\n first: 8 | $first\n after: $after\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n edges 9 | {\n node {\n id\n name\n urlkey\n description\n }\n }\n }\n}","variables":{"query":"AI","first":8}}' 10 | response: 11 | status: 200 12 | headers: 13 | content-type: application/json;charset=utf-8 14 | accept-ranges: bytes 15 | date: Sun, 19 Oct 2025 21:21:07 GMT 16 | content-length: '1268' 17 | body: 18 | string: '{"data":{"suggestTopics":{"pageInfo":{"hasNextPage":true,"endCursor":"eyJ2YWx1ZXMiOlt7InR5cGUiOiJEb3VibGUiLCJ2YWx1ZSI6Mi42MzAyNDJ9LHsidHlwZSI6IlN0cmluZyIsInZhbHVlIjoiNDc2NjgyLWVuX1VTIn1dfQ=="},"totalCount":383,"edges":[{"node":{"id":"128849","name":"Plein 19 | Air Painting","urlkey":"plein-air-painting","description":null}},{"node":{"id":"67937","name":"Cosplay","urlkey":"cosplay","description":"Meet 20 | other local costume players to talk about the art of cosplay: making yourself 21 | look like you don''t belong in this reality."}},{"node":{"id":"33633","name":"Flying 22 | Airplanes","urlkey":"flying-airplanes","description":null}},{"node":{"id":"127757","name":"Artificial 23 | Intelligence Machine Learning Robotics","urlkey":"artificial-intelligence-machine-learning-robotics","description":null}},{"node":{"id":"90164","name":"AI 24 | and Society","urlkey":"ai-and-society","description":null}},{"node":{"id":"1311042","name":"Multi-Rotor 25 | Aircraft","urlkey":"multi-rotor-aircraft","description":null}},{"node":{"id":"506892","name":"Airbnb","urlkey":"airbnb","description":null}},{"node":{"id":"476682","name":"Aikido","urlkey":"aikido","description":"Meet 26 | other local Aikidoka to get off the mat for some fun and relaxation as well 27 | as learn about people from surrounding dojos."}}]}}}' 28 | recorded_at: 2025-10-19 21:21:07 29 | - request: 30 | method: POST 31 | uri: https://api.meetup.com/gql-ext 32 | body: 33 | string: '{"query":"query findTopics(\n $query: String!, \n $first: Int = 1000, 34 | \n $after: String\n ) {\n suggestTopics(\n query: $query\n first: 35 | $first\n after: $after\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n edges 36 | {\n node {\n id\n name\n urlkey\n description\n }\n }\n }\n}","variables":{"query":"AI","first":8}}' 37 | response: 38 | status: 200 39 | headers: 40 | content-type: application/json;charset=utf-8 41 | accept-ranges: bytes 42 | date: Sun, 19 Oct 2025 21:21:07 GMT 43 | content-length: '1268' 44 | body: 45 | string: '{"data":{"suggestTopics":{"pageInfo":{"hasNextPage":true,"endCursor":"eyJ2YWx1ZXMiOlt7InR5cGUiOiJEb3VibGUiLCJ2YWx1ZSI6Mi42MzAyNDJ9LHsidHlwZSI6IlN0cmluZyIsInZhbHVlIjoiNDc2NjgyLWVuX1VTIn1dfQ=="},"totalCount":383,"edges":[{"node":{"id":"128849","name":"Plein 46 | Air Painting","urlkey":"plein-air-painting","description":null}},{"node":{"id":"67937","name":"Cosplay","urlkey":"cosplay","description":"Meet 47 | other local costume players to talk about the art of cosplay: making yourself 48 | look like you don''t belong in this reality."}},{"node":{"id":"33633","name":"Flying 49 | Airplanes","urlkey":"flying-airplanes","description":null}},{"node":{"id":"127757","name":"Artificial 50 | Intelligence Machine Learning Robotics","urlkey":"artificial-intelligence-machine-learning-robotics","description":null}},{"node":{"id":"90164","name":"AI 51 | and Society","urlkey":"ai-and-society","description":null}},{"node":{"id":"1311042","name":"Multi-Rotor 52 | Aircraft","urlkey":"multi-rotor-aircraft","description":null}},{"node":{"id":"506892","name":"Airbnb","urlkey":"airbnb","description":null}},{"node":{"id":"476682","name":"Aikido","urlkey":"aikido","description":"Meet 53 | other local Aikidoka to get off the mat for some fun and relaxation as well 54 | as learn about people from surrounding dojos."}}]}}}' 55 | recorded_at: 2025-10-19 21:21:07 56 | recorded_with: VCR-vcr/2.0.0 57 | -------------------------------------------------------------------------------- /vignettes/_vcr/introsp-custom-query.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"\nquery GetEventDetails($eventId: ID!) {\n event(id: $eventId) 7 | {\n id\n title\n description\n dateTime\n duration\n \n # 8 | Nested group information\n group {\n name\n urlname\n city\n }\n\n venues 9 | {\n name\n address\n city\n state\n postalCode\n country\n lat\n lon\n venueType\n }\n }\n}\n","variables":{"eventId":"103349942"}}' 10 | response: 11 | status: 200 12 | headers: 13 | content-type: application/json;charset=utf-8 14 | accept-ranges: bytes 15 | date: Sun, 19 Oct 2025 22:22:53 GMT 16 | content-length: '3322' 17 | body: 18 | string: '{"data":{"event":{"id":"103349942","title":"Ecosystem GIS & Community 19 | Building","description":"Hello New DVDC Members!\n\nIs Data Visualization 20 | a science or an art? Our goal is to create great events where everyones'' 21 | experiences reflect that sentiment, and we are beginning a great speaker in 22 | Joseph Sexton (http://www.terpconnect.umd.edu/~jsexton/). Joe will give us 23 | a glimpse of his department''s unique global ecological datasets, with stunning 24 | visuals, and what goes into unparalleled GIS data. Joe''s datasets include 25 | DISCO (Dynamic Impervious Surface Cover Observations) and global 30-m resolution 26 | continuous fields of tree cover from 2000 - 2005. Elizabeth Lyon will show 27 | us the next evolution of Wikipedia with interactive map visualization by MapStory.\n\nWe 28 | are excited to have NClud host the event, they have a great aesthetic downtown 29 | space that facilitates networking as well as presentations, and as with all 30 | Data Community DC (http://www.meetup.com/Data-Visualization-DC/events/103349942/www.datacommunitydc.org) 31 | events we cap the night with Data Drinks.\n\nAgenda\n\n6:30pm -- Networking 32 | and Refreshments 7:00pm -- Introduction & Announcements 7:15pm -- Presentation 33 | #1 7:45pm -- Readjust & Announcements 7:55pm -- Presentation #2 8:25pm -- 34 | Q&A - Discussion 8:40pm -- Data Drinks Bios\n\nJoseph Sexton (http://www.terpconnect.umd.edu/~jsexton/)\n\nJoe 35 | is a research professor at the University of Maryland whose research focuses 36 | on ecosystem dynamics and the remote sensing methods required to monitor landscape 37 | changes over time. He developes statistical and ecological analyses for the 38 | Global Forest Cover Change Project (http://glcf.umiacs.umd.edu/research/portal/gfcc/index.shtml), 39 | a joint project of the Global Land Cover Facility (http://www.glcf.umd.edu/index.shtml), 40 | NASA''s Goddard Space Flight Center (http://www.nasa.gov/centers/goddard/home/index.html), 41 | and South Dakota State University (http://globalmonitoring.sdstate.edu/). 42 | The project is mining more than thirty years of Landsat images to map changes 43 | in Earth''s forest cover from 1975 to 2005. Joe also contributes ecological, 44 | statistical, and remote sensing expertise to collaborative studies of urban 45 | heat islands, threatened and endangered species habitat, tropical deforestation, 46 | climate effects on boreal biomes, and urban growth.\n\nElizabeth Lyon\n\nLiz 47 | is a Geographer with the U.S. Army Corps of Engineers. She leads innovative 48 | research and technology development in social media, social sciences, and 49 | geospatial technology. Currently she works on building technical capacity 50 | for spatial narrative development. Elizabeth volunteers her time in Washington 51 | DC growing the geospatial community. She is currently completing her Ph.D. 52 | in Geography at George Mason University. She has a Certificate in Computational 53 | Social Science from George Mason University, a Masters of Science in Geography 54 | from University of Illinois and a bachelor’s degree in Economics and French 55 | from Augustana College.","dateTime":"2013-02-18T18:30:00-05:00","duration":"PT2H","group":{"name":"Data 56 | Visualization DC","urlname":"data-visualization-dc","city":"Washington"},"venues":[{"name":"Browsermedia/NClud","address":"1203 57 | 19th Street","city":"Washington","state":"DC","postalCode":"20036","country":"us","lat":38.905802,"lon":-77.043432,"venueType":""}]}}}' 58 | recorded_at: 2025-10-19 22:22:54 59 | recorded_with: VCR-vcr/2.0.0 60 | -------------------------------------------------------------------------------- /tests/testthat/_vcr/find_groups_datetime.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query findGroups(\n $query: String!\n $cursor: String\n $first: 7 | Int = 1000\n $lat: Float = 0.0\n $lon: Float = 0.0\n $radius: Float = 100000000.0\n $categoryId: 8 | ID\n $topicCategoryId: ID\n) {\n groupSearch(\n after: $cursor\n first: 9 | $first\n filter: {\n query: $query\n lat: $lat\n lon: $lon\n radius: 10 | $radius\n categoryId: $categoryId\n topicCategoryId: $topicCategoryId\n }\n ) 11 | {\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n edges 12 | {\n node {\n id\n name\n urlname\n city\n state\n country\n lat\n lon\n memberships 13 | {\n totalCount\n }\n foundedDate\n timezone\n joinMode\n isPrivate\n membershipMetadata 14 | {\n status\n }\n }\n }\n \n }\n}","variables":{"query":"JavaScript","first":10}}' 15 | response: 16 | status: 200 17 | headers: 18 | content-type: application/json;charset=utf-8 19 | accept-ranges: bytes 20 | date: Sun, 19 Oct 2025 21:21:03 GMT 21 | content-length: '3327' 22 | body: 23 | string: '{"data":{"groupSearch":{"pageInfo":{"hasNextPage":true,"endCursor":"MTA="},"totalCount":10,"edges":[{"node":{"id":"38154603","name":"MG.js","urlname":"monchengladbach-open-source-intelligence-meetup-group","city":"Mönchengladbach","state":"","country":"de","lat":51.2,"lon":6.42,"memberships":{"totalCount":21},"foundedDate":"2025-08-20T15:16:54+02:00","timezone":"Europe/Berlin","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"5844892","name":"Uruguay 24 | Javascript Meetup Group","urlname":"uruguayjs","city":"Montevideo","state":"","country":"uy","lat":-34.87,"lon":-56.17,"memberships":{"totalCount":2115},"foundedDate":"2012-11-14T10:55:56-02:00","timezone":"America/Montevideo","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"34960918","name":"JavaScript 25 | en Español","urlname":"javascript-en-espanol","city":"Nashville","state":"TN","country":"us","lat":36.17,"lon":-86.78,"memberships":{"totalCount":4},"foundedDate":"2021-03-28T01:18:37-05:00","timezone":"America/Chicago","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"5179202","name":"MedellínJS","urlname":"MedellinJS","city":"Medellín","state":"","country":"co","lat":6.25,"lon":-75.59,"memberships":{"totalCount":8695},"foundedDate":"2012-09-30T19:32:31-05:00","timezone":"America/Bogota","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"33189726","name":"Javascript 26 | Malaysia","urlname":"Javascript-Malaysia","city":"Kuala Lumpur","state":"","country":"my","lat":3.16,"lon":101.71,"memberships":{"totalCount":1325},"foundedDate":"2020-01-10T23:36:23+08:00","timezone":"Asia/Kuala_Lumpur","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"18210750","name":"Javascript-G","urlname":"Javascript-G","city":"Chennai","state":"","country":"in","lat":13.09,"lon":80.27,"memberships":{"totalCount":247},"foundedDate":"2014-11-24T00:28:22+05:30","timezone":"Asia/Kolkata","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"1546066","name":"Triangle 27 | JavaScript","urlname":"Triangle-JavaScript","city":"Durham","state":"NC","country":"us","lat":35.92,"lon":-78.92,"memberships":{"totalCount":2852},"foundedDate":"2009-10-25T00:58:08-04:00","timezone":"America/New_York","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"29661071","name":"LimaJS","urlname":"LimaJS","city":"Lima","state":"","country":"pe","lat":-12.07,"lon":-77.05,"memberships":{"totalCount":4715},"foundedDate":"2018-08-29T10:33:27-05:00","timezone":"America/Lima","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"25021192","name":"Kuala 28 | Lumpur JavaScript Meetup","urlname":"Kuala-Lumpur-JavaScript-Meetup","city":"Kuala 29 | Lumpur","state":"","country":"my","lat":3.16,"lon":101.71,"memberships":{"totalCount":91},"foundedDate":"2017-07-17T05:37:40+08:00","timezone":"Asia/Kuala_Lumpur","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"10269972","name":"JavaScript 30 | Zagreb","urlname":"JavaScript-Zagreb","city":"Zagreb","state":"","country":"hr","lat":45.8,"lon":15.97,"memberships":{"totalCount":1929},"foundedDate":"2013-09-15T08:26:04+02:00","timezone":"Europe/Belgrade","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}}]}}}' 31 | recorded_at: 2025-10-19 21:21:03 32 | recorded_with: VCR-vcr/2.0.0 33 | -------------------------------------------------------------------------------- /tests/testthat/_vcr/find_groups_correct.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query findGroups(\n $query: String!\n $cursor: String\n $first: 7 | Int = 1000\n $lat: Float = 0.0\n $lon: Float = 0.0\n $radius: Float = 100000000.0\n $categoryId: 8 | ID\n $topicCategoryId: ID\n) {\n groupSearch(\n after: $cursor\n first: 9 | $first\n filter: {\n query: $query\n lat: $lat\n lon: $lon\n radius: 10 | $radius\n categoryId: $categoryId\n topicCategoryId: $topicCategoryId\n }\n ) 11 | {\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n edges 12 | {\n node {\n id\n name\n urlname\n city\n state\n country\n lat\n lon\n memberships 13 | {\n totalCount\n }\n foundedDate\n timezone\n joinMode\n isPrivate\n membershipMetadata 14 | {\n status\n }\n }\n }\n \n }\n}","variables":{"query":"R-Ladies","first":10}}' 15 | response: 16 | status: 200 17 | headers: 18 | content-type: application/json;charset=utf-8 19 | accept-ranges: bytes 20 | date: Sun, 19 Oct 2025 21:21:02 GMT 21 | content-length: '3441' 22 | body: 23 | string: '{"data":{"groupSearch":{"pageInfo":{"hasNextPage":true,"endCursor":"MTA="},"totalCount":10,"edges":[{"node":{"id":"38219424","name":"R 24 | gRanada","urlname":"r-granada-es","city":"Granada","state":"","country":"es","lat":37.17,"lon":-3.59,"memberships":{"totalCount":17},"foundedDate":"2025-10-09T03:38:12+02:00","timezone":"Europe/Madrid","joinMode":"OPEN","isPrivate":false,"membershipMetadata":null}},{"node":{"id":"24820200","name":"R-Ladies 25 | CDMX","urlname":"rladies-cdmx","city":"México City","state":"","country":"mx","lat":19.43,"lon":-99.14,"memberships":{"totalCount":2933},"foundedDate":"2017-07-03T23:36:23-05:00","timezone":"America/Mexico_City","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}},{"node":{"id":"32767673","name":"R-Ladies 26 | Pachuca","urlname":"rladies-pachuca","city":"Pachuca","state":"","country":"mx","lat":20.12,"lon":-98.76,"memberships":{"totalCount":38},"foundedDate":"2019-09-15T05:15:37-05:00","timezone":"America/Mexico_City","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}},{"node":{"id":"33041212","name":"R-Ladies 27 | Al-Khartum","urlname":"rladies-al-khartum","city":"al-Khartum","state":"","country":"sd","lat":15.58,"lon":32.52,"memberships":{"totalCount":110},"foundedDate":"2019-11-23T13:15:46+02:00","timezone":"Africa/Khartoum","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}},{"node":{"id":"35897820","name":"R-Ladies 28 | Gaborone","urlname":"rladies-gaborone","city":"Gaborone","state":"","country":"bw","lat":-24.65,"lon":25.91,"memberships":{"totalCount":1123},"foundedDate":"2021-09-25T11:42:27+02:00","timezone":"Africa/Maputo","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}},{"node":{"id":"28441250","name":"R-Ladies 29 | Bariloche","urlname":"rladies-bariloche","city":"San Carlos de Bariloche","state":"","country":"ar","lat":-41.14,"lon":-71.32,"memberships":{"totalCount":582},"foundedDate":"2018-05-09T22:22:15-03:00","timezone":"America/Argentina/Cordoba","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}},{"node":{"id":"20378903","name":"R-Ladies 30 | Melbourne","urlname":"rladies-melbourne","city":"Melbourne","state":"","country":"au","lat":-37.81,"lon":144.96,"memberships":{"totalCount":2683},"foundedDate":"2016-09-01T20:31:57+10:00","timezone":"Australia/Melbourne","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}},{"node":{"id":"28706674","name":"R-Ladies 31 | Niterói","urlname":"rladies-niteroi","city":"Niterói","state":"","country":"br","lat":-22.9,"lon":-43.13,"memberships":{"totalCount":1160},"foundedDate":"2018-06-04T19:31:17-03:00","timezone":"America/Sao_Paulo","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}},{"node":{"id":"31829495","name":"R-Ladies 32 | Goiânia","urlname":"rladies-goiania","city":"Goiania","state":"","country":"br","lat":-16.72,"lon":-49.26,"memberships":{"totalCount":613},"foundedDate":"2019-05-06T12:59:50-03:00","timezone":"America/Sao_Paulo","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}},{"node":{"id":"33361775","name":"R-Ladies 33 | Ha Noi","urlname":"rladies-ha-noi","city":"Ha Noi","state":"","country":"vn","lat":21.03,"lon":105.84,"memberships":{"totalCount":123},"foundedDate":"2020-02-16T13:43:44+07:00","timezone":"Asia/Ho_Chi_Minh","joinMode":"OPEN","isPrivate":false,"membershipMetadata":{"status":"LEADER"}}}]}}}' 34 | recorded_at: 2025-10-19 21:21:02 35 | recorded_with: VCR-vcr/2.0.0 36 | -------------------------------------------------------------------------------- /inst/_vcr/get_event.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query getEvent($id: ID!) {\n event(id: $id) {\n id\n title\n eventUrl\n createdTime\n status\n dateTime\n duration\n description\n \n group 7 | {\n id\n name\n urlname\n }\n \n venues {\n id\n name\n address\n city\n state\n postalCode\n country\n lat\n lon\n venueType\n }\n \n rsvps(first: 8 | 1) {\n totalCount\n }\n \n featuredEventPhoto {\n baseUrl\n }\n \n feeSettings 9 | {\n required\n amount\n currency\n accepts\n refundPolicy 10 | {\n notes\n days\n }\n }\n \n \n }\n}","variables":{"id":"103349942"}}' 11 | response: 12 | status: 200 13 | headers: 14 | content-type: application/json;charset=utf-8 15 | accept-ranges: bytes 16 | date: Sun, 19 Oct 2025 16:54:23 GMT 17 | content-length: '3538' 18 | body: 19 | string: '{"data":{"event":{"id":"103349942","title":"Ecosystem GIS & Community 20 | Building","eventUrl":"https://www.meetup.com/data-visualization-dc/events/103349942/","createdTime":"2013-02-06T14:01:17-05:00","status":"PAST","dateTime":"2013-02-18T18:30:00-05:00","duration":"PT2H","description":"Hello 21 | New DVDC Members!\n\nIs Data Visualization a science or an art? Our goal is 22 | to create great events where everyones'' experiences reflect that sentiment, 23 | and we are beginning a great speaker in Joseph Sexton (http://www.terpconnect.umd.edu/~jsexton/). 24 | Joe will give us a glimpse of his department''s unique global ecological datasets, 25 | with stunning visuals, and what goes into unparalleled GIS data. Joe''s datasets 26 | include DISCO (Dynamic Impervious Surface Cover Observations) and global 30-m 27 | resolution continuous fields of tree cover from 2000 - 2005. Elizabeth Lyon 28 | will show us the next evolution of Wikipedia with interactive map visualization 29 | by MapStory.\n\nWe are excited to have NClud host the event, they have a great 30 | aesthetic downtown space that facilitates networking as well as presentations, 31 | and as with all Data Community DC (http://www.meetup.com/Data-Visualization-DC/events/103349942/www.datacommunitydc.org) 32 | events we cap the night with Data Drinks.\n\nAgenda\n\n6:30pm -- Networking 33 | and Refreshments 7:00pm -- Introduction & Announcements 7:15pm -- Presentation 34 | #1 7:45pm -- Readjust & Announcements 7:55pm -- Presentation #2 8:25pm -- 35 | Q&A - Discussion 8:40pm -- Data Drinks Bios\n\nJoseph Sexton (http://www.terpconnect.umd.edu/~jsexton/)\n\nJoe 36 | is a research professor at the University of Maryland whose research focuses 37 | on ecosystem dynamics and the remote sensing methods required to monitor landscape 38 | changes over time. He developes statistical and ecological analyses for the 39 | Global Forest Cover Change Project (http://glcf.umiacs.umd.edu/research/portal/gfcc/index.shtml), 40 | a joint project of the Global Land Cover Facility (http://www.glcf.umd.edu/index.shtml), 41 | NASA''s Goddard Space Flight Center (http://www.nasa.gov/centers/goddard/home/index.html), 42 | and South Dakota State University (http://globalmonitoring.sdstate.edu/). 43 | The project is mining more than thirty years of Landsat images to map changes 44 | in Earth''s forest cover from 1975 to 2005. Joe also contributes ecological, 45 | statistical, and remote sensing expertise to collaborative studies of urban 46 | heat islands, threatened and endangered species habitat, tropical deforestation, 47 | climate effects on boreal biomes, and urban growth.\n\nElizabeth Lyon\n\nLiz 48 | is a Geographer with the U.S. Army Corps of Engineers. She leads innovative 49 | research and technology development in social media, social sciences, and 50 | geospatial technology. Currently she works on building technical capacity 51 | for spatial narrative development. Elizabeth volunteers her time in Washington 52 | DC growing the geospatial community. She is currently completing her Ph.D. 53 | in Geography at George Mason University. She has a Certificate in Computational 54 | Social Science from George Mason University, a Masters of Science in Geography 55 | from University of Illinois and a bachelor’s degree in Economics and French 56 | from Augustana College.","group":{"id":"6957082","name":"Data Visualization 57 | DC","urlname":"data-visualization-dc"},"venues":[{"id":"10975182","name":"Browsermedia/NClud","address":"1203 58 | 19th Street","city":"Washington","state":"DC","postalCode":"20036","country":"us","lat":38.905802,"lon":-77.043432,"venueType":""}],"rsvps":{"totalCount":97},"featuredEventPhoto":null,"feeSettings":null}}}' 59 | recorded_at: 2025-10-19 16:54:23 60 | recorded_with: VCR-vcr/2.0.0 61 | -------------------------------------------------------------------------------- /tests/testthat/_vcr/get_event.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query getEvent($id: ID!) {\n event(id: $id) {\n id\n title\n eventUrl\n createdTime\n status\n dateTime\n duration\n description\n \n group 7 | {\n id\n name\n urlname\n }\n \n venues {\n id\n name\n address\n city\n state\n postalCode\n country\n lat\n lon\n venueType\n }\n \n rsvps(first: 8 | 1) {\n totalCount\n }\n \n featuredEventPhoto {\n baseUrl\n }\n \n feeSettings 9 | {\n required\n amount\n currency\n accepts\n refundPolicy 10 | {\n notes\n days\n }\n }\n \n \n }\n}","variables":{"id":"103349942"}}' 11 | response: 12 | status: 200 13 | headers: 14 | content-type: application/json;charset=utf-8 15 | accept-ranges: bytes 16 | date: Sun, 19 Oct 2025 21:21:00 GMT 17 | content-length: '3538' 18 | body: 19 | string: '{"data":{"event":{"id":"103349942","title":"Ecosystem GIS & Community 20 | Building","eventUrl":"https://www.meetup.com/data-visualization-dc/events/103349942/","createdTime":"2013-02-06T14:01:17-05:00","status":"PAST","dateTime":"2013-02-18T18:30:00-05:00","duration":"PT2H","description":"Hello 21 | New DVDC Members!\n\nIs Data Visualization a science or an art? Our goal is 22 | to create great events where everyones'' experiences reflect that sentiment, 23 | and we are beginning a great speaker in Joseph Sexton (http://www.terpconnect.umd.edu/~jsexton/). 24 | Joe will give us a glimpse of his department''s unique global ecological datasets, 25 | with stunning visuals, and what goes into unparalleled GIS data. Joe''s datasets 26 | include DISCO (Dynamic Impervious Surface Cover Observations) and global 30-m 27 | resolution continuous fields of tree cover from 2000 - 2005. Elizabeth Lyon 28 | will show us the next evolution of Wikipedia with interactive map visualization 29 | by MapStory.\n\nWe are excited to have NClud host the event, they have a great 30 | aesthetic downtown space that facilitates networking as well as presentations, 31 | and as with all Data Community DC (http://www.meetup.com/Data-Visualization-DC/events/103349942/www.datacommunitydc.org) 32 | events we cap the night with Data Drinks.\n\nAgenda\n\n6:30pm -- Networking 33 | and Refreshments 7:00pm -- Introduction & Announcements 7:15pm -- Presentation 34 | #1 7:45pm -- Readjust & Announcements 7:55pm -- Presentation #2 8:25pm -- 35 | Q&A - Discussion 8:40pm -- Data Drinks Bios\n\nJoseph Sexton (http://www.terpconnect.umd.edu/~jsexton/)\n\nJoe 36 | is a research professor at the University of Maryland whose research focuses 37 | on ecosystem dynamics and the remote sensing methods required to monitor landscape 38 | changes over time. He developes statistical and ecological analyses for the 39 | Global Forest Cover Change Project (http://glcf.umiacs.umd.edu/research/portal/gfcc/index.shtml), 40 | a joint project of the Global Land Cover Facility (http://www.glcf.umd.edu/index.shtml), 41 | NASA''s Goddard Space Flight Center (http://www.nasa.gov/centers/goddard/home/index.html), 42 | and South Dakota State University (http://globalmonitoring.sdstate.edu/). 43 | The project is mining more than thirty years of Landsat images to map changes 44 | in Earth''s forest cover from 1975 to 2005. Joe also contributes ecological, 45 | statistical, and remote sensing expertise to collaborative studies of urban 46 | heat islands, threatened and endangered species habitat, tropical deforestation, 47 | climate effects on boreal biomes, and urban growth.\n\nElizabeth Lyon\n\nLiz 48 | is a Geographer with the U.S. Army Corps of Engineers. She leads innovative 49 | research and technology development in social media, social sciences, and 50 | geospatial technology. Currently she works on building technical capacity 51 | for spatial narrative development. Elizabeth volunteers her time in Washington 52 | DC growing the geospatial community. She is currently completing her Ph.D. 53 | in Geography at George Mason University. She has a Certificate in Computational 54 | Social Science from George Mason University, a Masters of Science in Geography 55 | from University of Illinois and a bachelor’s degree in Economics and French 56 | from Augustana College.","group":{"id":"6957082","name":"Data Visualization 57 | DC","urlname":"data-visualization-dc"},"venues":[{"id":"10975182","name":"Browsermedia/NClud","address":"1203 58 | 19th Street","city":"Washington","state":"DC","postalCode":"20036","country":"us","lat":38.905802,"lon":-77.043432,"venueType":""}],"rsvps":{"totalCount":97},"featuredEventPhoto":null,"feeSettings":null}}}' 59 | recorded_at: 2025-10-19 21:21:00 60 | recorded_with: VCR-vcr/2.0.0 61 | -------------------------------------------------------------------------------- /vignettes/_vcr/meetupr-event-details.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query getEvent($id: ID!) {\n event(id: $id) {\n id\n title\n eventUrl\n createdTime\n status\n dateTime\n duration\n description\n \n group 7 | {\n id\n name\n urlname\n }\n \n venues {\n id\n name\n address\n city\n state\n postalCode\n country\n lat\n lon\n venueType\n }\n \n rsvps(first: 8 | 1) {\n totalCount\n }\n \n featuredEventPhoto {\n baseUrl\n }\n \n feeSettings 9 | {\n required\n amount\n currency\n accepts\n refundPolicy 10 | {\n notes\n days\n }\n }\n \n \n }\n}","variables":{"id":"103349942"}}' 11 | response: 12 | status: 200 13 | headers: 14 | content-type: application/json;charset=utf-8 15 | accept-ranges: bytes 16 | date: Sun, 19 Oct 2025 21:43:24 GMT 17 | content-length: '3538' 18 | body: 19 | string: '{"data":{"event":{"id":"103349942","title":"Ecosystem GIS & Community 20 | Building","eventUrl":"https://www.meetup.com/data-visualization-dc/events/103349942/","createdTime":"2013-02-06T14:01:17-05:00","status":"PAST","dateTime":"2013-02-18T18:30:00-05:00","duration":"PT2H","description":"Hello 21 | New DVDC Members!\n\nIs Data Visualization a science or an art? Our goal is 22 | to create great events where everyones'' experiences reflect that sentiment, 23 | and we are beginning a great speaker in Joseph Sexton (http://www.terpconnect.umd.edu/~jsexton/). 24 | Joe will give us a glimpse of his department''s unique global ecological datasets, 25 | with stunning visuals, and what goes into unparalleled GIS data. Joe''s datasets 26 | include DISCO (Dynamic Impervious Surface Cover Observations) and global 30-m 27 | resolution continuous fields of tree cover from 2000 - 2005. Elizabeth Lyon 28 | will show us the next evolution of Wikipedia with interactive map visualization 29 | by MapStory.\n\nWe are excited to have NClud host the event, they have a great 30 | aesthetic downtown space that facilitates networking as well as presentations, 31 | and as with all Data Community DC (http://www.meetup.com/Data-Visualization-DC/events/103349942/www.datacommunitydc.org) 32 | events we cap the night with Data Drinks.\n\nAgenda\n\n6:30pm -- Networking 33 | and Refreshments 7:00pm -- Introduction & Announcements 7:15pm -- Presentation 34 | #1 7:45pm -- Readjust & Announcements 7:55pm -- Presentation #2 8:25pm -- 35 | Q&A - Discussion 8:40pm -- Data Drinks Bios\n\nJoseph Sexton (http://www.terpconnect.umd.edu/~jsexton/)\n\nJoe 36 | is a research professor at the University of Maryland whose research focuses 37 | on ecosystem dynamics and the remote sensing methods required to monitor landscape 38 | changes over time. He developes statistical and ecological analyses for the 39 | Global Forest Cover Change Project (http://glcf.umiacs.umd.edu/research/portal/gfcc/index.shtml), 40 | a joint project of the Global Land Cover Facility (http://www.glcf.umd.edu/index.shtml), 41 | NASA''s Goddard Space Flight Center (http://www.nasa.gov/centers/goddard/home/index.html), 42 | and South Dakota State University (http://globalmonitoring.sdstate.edu/). 43 | The project is mining more than thirty years of Landsat images to map changes 44 | in Earth''s forest cover from 1975 to 2005. Joe also contributes ecological, 45 | statistical, and remote sensing expertise to collaborative studies of urban 46 | heat islands, threatened and endangered species habitat, tropical deforestation, 47 | climate effects on boreal biomes, and urban growth.\n\nElizabeth Lyon\n\nLiz 48 | is a Geographer with the U.S. Army Corps of Engineers. She leads innovative 49 | research and technology development in social media, social sciences, and 50 | geospatial technology. Currently she works on building technical capacity 51 | for spatial narrative development. Elizabeth volunteers her time in Washington 52 | DC growing the geospatial community. She is currently completing her Ph.D. 53 | in Geography at George Mason University. She has a Certificate in Computational 54 | Social Science from George Mason University, a Masters of Science in Geography 55 | from University of Illinois and a bachelor’s degree in Economics and French 56 | from Augustana College.","group":{"id":"6957082","name":"Data Visualization 57 | DC","urlname":"data-visualization-dc"},"venues":[{"id":"10975182","name":"Browsermedia/NClud","address":"1203 58 | 19th Street","city":"Washington","state":"DC","postalCode":"20036","country":"us","lat":38.905802,"lon":-77.043432,"venueType":""}],"rsvps":{"totalCount":97},"featuredEventPhoto":null,"feeSettings":null}}}' 59 | recorded_at: 2025-10-19 21:43:24 60 | recorded_with: VCR-vcr/2.0.0 61 | -------------------------------------------------------------------------------- /R/graphql-extractors.R: -------------------------------------------------------------------------------- 1 | #' Convert GraphQL response list to tibble using list_flatten + bind_rows 2 | #' @param dlist List of GraphQL response items 3 | #' @param handle_multiples How to handle duplicate fields: "first" (keep first 4 | #' value), "list" (combine into list columns) 5 | #' @return tibble with flattened structure 6 | #' @keywords internal 7 | #' @noRd 8 | process_graphql_list <- function( 9 | dlist, 10 | handle_multiples = "list" 11 | ) { 12 | if (length(dlist) == 0) { 13 | return(dplyr::tibble()) 14 | } 15 | 16 | handle_multiples <- match.arg( 17 | handle_multiples, 18 | c("first", "list") 19 | ) 20 | 21 | handle_fun <- switch( 22 | handle_multiples, 23 | list = multiples_to_listcol, 24 | first = multiples_keep_first 25 | ) 26 | 27 | # Flatten each item completely 28 | lapply(dlist, function(x) { 29 | rlist::list.flatten(x) 30 | }) |> 31 | silent_bind_rows() |> 32 | handle_fun() |> 33 | clean_field_names() 34 | } 35 | 36 | silent_bind_rows <- function(...) { 37 | suppressMessages(dplyr::bind_rows(...)) 38 | } 39 | 40 | #' Combine duplicate columns (those with ..1, ..2 suffixes) into list columns 41 | #' @param df tibble with potential duplicate columns 42 | #' @return tibble with list columns for duplicates 43 | #' @keywords internal 44 | #' @noRd 45 | multiples_to_listcol <- function(df) { 46 | column_names <- names(df) 47 | pattern <- "\\.{3}\\d+$" 48 | base_names <- unique(gsub(pattern, "", column_names)) 49 | 50 | new_data <- list() 51 | 52 | for (base_name in base_names) { 53 | matching_cols <- column_names[startsWith(column_names, base_name)] 54 | 55 | if (length(matching_cols) == 1 && matching_cols == base_name) { 56 | new_data[[base_name]] <- df[[base_name]] 57 | } else { 58 | suffix_pattern <- sprintf( 59 | "%s(%s)?$", 60 | escape_regex(base_name), 61 | pattern 62 | ) 63 | matching_cols <- matching_cols[grepl(suffix_pattern, matching_cols)] 64 | 65 | ordered_cols <- sort(matching_cols) 66 | if (base_name %in% matching_cols) { 67 | ordered_cols <- c( 68 | base_name, 69 | sort(matching_cols[matching_cols != base_name]) 70 | ) 71 | } 72 | 73 | list_values <- purrr::pmap(df[ordered_cols], function(...) { 74 | values <- unname(list(...)) 75 | # Remove NA values 76 | non_na_values <- values[!is.na(values)] 77 | if (length(non_na_values) == 0) { 78 | return(list()) 79 | } else if (length(non_na_values) == 1) { 80 | return(non_na_values[[1]]) 81 | } 82 | non_na_values 83 | }) 84 | 85 | new_data[[base_name]] <- list_values 86 | } 87 | } 88 | 89 | dplyr::as_tibble(new_data) 90 | } 91 | 92 | #' Keep only the first value for duplicate columns 93 | #' @param df tibble with potential duplicate columns 94 | #' @return tibble with only first values 95 | #' @keywords internal 96 | #' @noRd 97 | multiples_keep_first <- function(df) { 98 | column_names <- names(df) 99 | pattern <- "\\.{3}\\d+$" 100 | base_names <- unique(gsub(pattern, "", column_names)) 101 | 102 | new_data <- list() 103 | 104 | for (base_name in base_names) { 105 | # Find all columns that match this base name 106 | matching_cols <- column_names[startsWith(column_names, base_name)] 107 | 108 | if (base_name %in% matching_cols) { 109 | # Keep the base name (no suffix) 110 | new_data[[base_name]] <- df[[base_name]] 111 | } else { 112 | # Take the first numbered one 113 | first_col <- sort(matching_cols)[1] 114 | new_data[[base_name]] <- df[[first_col]] 115 | } 116 | } 117 | 118 | dplyr::as_tibble(new_data) 119 | } 120 | 121 | #' Escape special regex characters 122 | #' @param string String to escape 123 | #' @return Escaped string 124 | #' @keywords internal 125 | #' @noRd 126 | escape_regex <- function(string) { 127 | gsub("([.|()\\^{}+$*?]|\\[|\\])", "\\\\\\1", string) 128 | } 129 | 130 | #' Clean up field names from GraphQL format to R conventions 131 | #' @param df tibble with GraphQL field names 132 | #' @return tibble with cleaned names 133 | #' @keywords internal 134 | #' @noRd 135 | clean_field_names <- function(df) { 136 | old_names <- names(df) 137 | names(df) <- make.names(old_names, unique = TRUE) |> 138 | sapply(clean_field_name) 139 | 140 | df 141 | } 142 | 143 | #' Clean a single field name 144 | #' @param name Field name to clean 145 | #' @return Cleaned field name 146 | #' @keywords internal 147 | #' @noRd 148 | clean_field_name <- function(name) { 149 | name |> 150 | # Convert camelCase to snake_case 151 | gsub("([a-z])([A-Z])", "\\1_\\2", x = _) |> 152 | # Replace dots and dashes with underscores 153 | gsub("[.-]+", "_", x = _) |> 154 | tolower() |> 155 | # Collapse multiple underscores 156 | gsub("__+", "_", x = _) |> 157 | # Clean up redundant suffixes 158 | gsub("_total_count$", "_count", x = _) |> 159 | gsub("_base_url$", "_url", x = _) |> 160 | gsub("_metadata_", "_", x = _) |> 161 | # Remove duplicate words (e.g., "group_group" -> "group") 162 | gsub("(\\w+)_\\1(?=_|$)", "\\1", x = _, perl = TRUE) 163 | } 164 | -------------------------------------------------------------------------------- /inst/_vcr/get_group.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query getGroup($urlname: String!) {\n groupByUrlname(urlname: 7 | $urlname) {\n id\n name\n description\n urlname\n link\n city\n country\n timezone\n foundedDate\n stats 8 | {\n memberCounts {\n all\n }\n }\n organizer 9 | {\n id\n name\n }\n keyGroupPhoto {\n baseUrl\n }\n topicCategory 10 | {\n id\n name\n }\n events(\n first: 11 | 1, \n filter: { \n status: [ACTIVE, PAST, CANCELLED, 12 | DRAFT] \n }\n ) {\n totalCount\n }\n }\n}","variables":{"urlname":"rladies-lagos"}}' 13 | response: 14 | status: 200 15 | headers: 16 | content-type: application/json;charset=utf-8 17 | accept-ranges: bytes 18 | date: Wed, 17 Sep 2025 15:06:09 GMT 19 | content-length: '3754' 20 | body: 21 | string: '{"data":{"groupByUrlname":{"id":"32612004","name":"R-Ladies Lagos","description":"R-Ladies 22 | is a world-wide organization to promote gender diversity in the R community.\nR-Ladies 23 | welcomes members of all R proficiency levels, whether you''re a new or aspiring 24 | R user, or an experienced R programmer interested in mentoring, networking 25 | & expert upskilling. Our community is designed to develop our members'' R 26 | skills & knowledge through social, collaborative learning & sharing. Supporting 27 | minority identity access to STEM skills & careers, the Free Software Movement, 28 | and contributing to the global R community!\nThis is a local chapter of R-Ladies 29 | Global, an organisation that promotes gender diversity in the R community 30 | worldwide. We are pro-actively inclusive of queer, trans, and all minority 31 | identities, with additional sensitivity to intersectional identities. Our 32 | priority is to provide a safe community space for anyone identifying as a 33 | minority gender who is interested in and/or working with R. As a founding 34 | principle, there is no cost or charge to participate in any of our R-Ladies 35 | communities around the world.\nWe are part of Global R-Ladies group. You can 36 | access our presentations, R scripts, and Projects on our Github account and 37 | follow us on twitter to stay up to date about R-Ladies news!\nWebsite: [https://www.rladies.org](https://www.rladies.org)\nTwitter: 38 | @RLadiesGlobal ( [https://twitter.com/RLadiesGlobal](https://twitter.com/RLadiesGlobal) 39 | )\nGithub: [https://github.com/rladies](https://github.com/rladies)\nP.S. 40 | Community Policies & Code of Conduct:\nThe leadership, mentoring & teaching 41 | roles within this Community are held exclusively by minority genders (majority 42 | gender speakers may be allowed/invited as one-off guests in exceptional circumstances 43 | at the leadership team''s discretion). Due to unexpected demand, we have opened 44 | learning participation to all genders, dependent on initial and on-going vetting 45 | by the leadership team. However, the stated priority of the R-Ladies communities 46 | is the development & support specifically of those identifying as a minority 47 | gender, and we, therefore, reserve the right to guard this interest through 48 | whatever measures the leadership team deems appropriate. Anyone involved with 49 | R-Ladies is expected to fully respect each other, the mandate of this community, 50 | and the goodwill on which R-Ladies is founded, or face expulsion/a penalty 51 | of any form, at the discretion of the leadership team.\nFull community guidelines 52 | are found here: [https://github.com/rladies/starter-kit/wiki](https://github.com/rladies/starter-kit/wiki)\nP.P.S. 53 | Photos, Films and all other media/recordings:\nPhotos, Films, and all other 54 | media/recordings: photographs and/or video/other media will be taken at events 55 | held by this community. By taking part in an R-Ladies event you grant the 56 | community organizers full rights to use the images resulting from the photography/video 57 | filming/media, and any reproductions or adaptations of the images for publicity, 58 | fundraising or other purposes to help achieve the community’s aims. This might 59 | include (but is not limited to), the right to use them in their printed and 60 | online publicity, social media, press releases and funding applications. If 61 | you do not wish to be recorded in these media please inform a community organizer.","urlname":"rladies-lagos","link":"https://www.meetup.com/rladies-lagos","city":"Lagos","country":"ng","timezone":"Africa/Lagos","foundedDate":"2019-08-16T09:45:21+01:00","stats":{"memberCounts":{"all":866}},"organizer":{"id":"251470805","name":"R-Ladies 62 | Global"},"keyGroupPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-events/"},"topicCategory":{"id":"546","name":"Technology"},"events":{"totalCount":13}}}}' 63 | recorded_at: 2025-09-17 15:06:09 64 | recorded_with: VCR-vcr/2.0.0 65 | -------------------------------------------------------------------------------- /tests/testthat/_vcr/get_group.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query getGroup($urlname: String!) {\n groupByUrlname(urlname: 7 | $urlname) {\n id\n name\n description\n urlname\n link\n city\n country\n timezone\n foundedDate\n stats 8 | {\n memberCounts {\n all\n }\n }\n organizer 9 | {\n id\n name\n }\n keyGroupPhoto {\n baseUrl\n }\n topicCategory 10 | {\n id\n name\n }\n events(\n first: 11 | 1, \n filter: { \n status: [ACTIVE, PAST, CANCELLED, 12 | DRAFT] \n }\n ) {\n totalCount\n }\n }\n}","variables":{"urlname":"rladies-lagos"}}' 13 | response: 14 | status: 200 15 | headers: 16 | content-type: application/json;charset=utf-8 17 | accept-ranges: bytes 18 | date: Sun, 19 Oct 2025 21:21:01 GMT 19 | content-length: '3754' 20 | body: 21 | string: '{"data":{"groupByUrlname":{"id":"32612004","name":"R-Ladies Lagos","description":"R-Ladies 22 | is a world-wide organization to promote gender diversity in the R community.\nR-Ladies 23 | welcomes members of all R proficiency levels, whether you''re a new or aspiring 24 | R user, or an experienced R programmer interested in mentoring, networking 25 | & expert upskilling. Our community is designed to develop our members'' R 26 | skills & knowledge through social, collaborative learning & sharing. Supporting 27 | minority identity access to STEM skills & careers, the Free Software Movement, 28 | and contributing to the global R community!\nThis is a local chapter of R-Ladies 29 | Global, an organisation that promotes gender diversity in the R community 30 | worldwide. We are pro-actively inclusive of queer, trans, and all minority 31 | identities, with additional sensitivity to intersectional identities. Our 32 | priority is to provide a safe community space for anyone identifying as a 33 | minority gender who is interested in and/or working with R. As a founding 34 | principle, there is no cost or charge to participate in any of our R-Ladies 35 | communities around the world.\nWe are part of Global R-Ladies group. You can 36 | access our presentations, R scripts, and Projects on our Github account and 37 | follow us on twitter to stay up to date about R-Ladies news!\nWebsite: [https://www.rladies.org](https://www.rladies.org)\nTwitter: 38 | @RLadiesGlobal ( [https://twitter.com/RLadiesGlobal](https://twitter.com/RLadiesGlobal) 39 | )\nGithub: [https://github.com/rladies](https://github.com/rladies)\nP.S. 40 | Community Policies & Code of Conduct:\nThe leadership, mentoring & teaching 41 | roles within this Community are held exclusively by minority genders (majority 42 | gender speakers may be allowed/invited as one-off guests in exceptional circumstances 43 | at the leadership team''s discretion). Due to unexpected demand, we have opened 44 | learning participation to all genders, dependent on initial and on-going vetting 45 | by the leadership team. However, the stated priority of the R-Ladies communities 46 | is the development & support specifically of those identifying as a minority 47 | gender, and we, therefore, reserve the right to guard this interest through 48 | whatever measures the leadership team deems appropriate. Anyone involved with 49 | R-Ladies is expected to fully respect each other, the mandate of this community, 50 | and the goodwill on which R-Ladies is founded, or face expulsion/a penalty 51 | of any form, at the discretion of the leadership team.\nFull community guidelines 52 | are found here: [https://github.com/rladies/starter-kit/wiki](https://github.com/rladies/starter-kit/wiki)\nP.P.S. 53 | Photos, Films and all other media/recordings:\nPhotos, Films, and all other 54 | media/recordings: photographs and/or video/other media will be taken at events 55 | held by this community. By taking part in an R-Ladies event you grant the 56 | community organizers full rights to use the images resulting from the photography/video 57 | filming/media, and any reproductions or adaptations of the images for publicity, 58 | fundraising or other purposes to help achieve the community’s aims. This might 59 | include (but is not limited to), the right to use them in their printed and 60 | online publicity, social media, press releases and funding applications. If 61 | you do not wish to be recorded in these media please inform a community organizer.","urlname":"rladies-lagos","link":"https://www.meetup.com/rladies-lagos","city":"Lagos","country":"ng","timezone":"Africa/Lagos","foundedDate":"2019-08-16T09:45:21+01:00","stats":{"memberCounts":{"all":880}},"organizer":{"id":"251470805","name":"R-Ladies 62 | Global"},"keyGroupPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-events/"},"topicCategory":{"id":"546","name":"Technology"},"events":{"totalCount":13}}}}' 63 | recorded_at: 2025-10-19 21:21:01 64 | recorded_with: VCR-vcr/2.0.0 65 | -------------------------------------------------------------------------------- /vignettes/_vcr/meetupr-group-info.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query getGroup($urlname: String!) {\n groupByUrlname(urlname: 7 | $urlname) {\n id\n name\n description\n urlname\n link\n city\n country\n timezone\n foundedDate\n stats 8 | {\n memberCounts {\n all\n }\n }\n organizer 9 | {\n id\n name\n }\n keyGroupPhoto {\n baseUrl\n }\n topicCategory 10 | {\n id\n name\n }\n events(\n first: 11 | 1, \n filter: { \n status: [ACTIVE, PAST, CANCELLED, 12 | DRAFT] \n }\n ) {\n totalCount\n }\n }\n}","variables":{"urlname":"rladies-lagos"}}' 13 | response: 14 | status: 200 15 | headers: 16 | content-type: application/json;charset=utf-8 17 | accept-ranges: bytes 18 | date: Sun, 19 Oct 2025 21:43:23 GMT 19 | content-length: '3754' 20 | body: 21 | string: '{"data":{"groupByUrlname":{"id":"32612004","name":"R-Ladies Lagos","description":"R-Ladies 22 | is a world-wide organization to promote gender diversity in the R community.\nR-Ladies 23 | welcomes members of all R proficiency levels, whether you''re a new or aspiring 24 | R user, or an experienced R programmer interested in mentoring, networking 25 | & expert upskilling. Our community is designed to develop our members'' R 26 | skills & knowledge through social, collaborative learning & sharing. Supporting 27 | minority identity access to STEM skills & careers, the Free Software Movement, 28 | and contributing to the global R community!\nThis is a local chapter of R-Ladies 29 | Global, an organisation that promotes gender diversity in the R community 30 | worldwide. We are pro-actively inclusive of queer, trans, and all minority 31 | identities, with additional sensitivity to intersectional identities. Our 32 | priority is to provide a safe community space for anyone identifying as a 33 | minority gender who is interested in and/or working with R. As a founding 34 | principle, there is no cost or charge to participate in any of our R-Ladies 35 | communities around the world.\nWe are part of Global R-Ladies group. You can 36 | access our presentations, R scripts, and Projects on our Github account and 37 | follow us on twitter to stay up to date about R-Ladies news!\nWebsite: [https://www.rladies.org](https://www.rladies.org)\nTwitter: 38 | @RLadiesGlobal ( [https://twitter.com/RLadiesGlobal](https://twitter.com/RLadiesGlobal) 39 | )\nGithub: [https://github.com/rladies](https://github.com/rladies)\nP.S. 40 | Community Policies & Code of Conduct:\nThe leadership, mentoring & teaching 41 | roles within this Community are held exclusively by minority genders (majority 42 | gender speakers may be allowed/invited as one-off guests in exceptional circumstances 43 | at the leadership team''s discretion). Due to unexpected demand, we have opened 44 | learning participation to all genders, dependent on initial and on-going vetting 45 | by the leadership team. However, the stated priority of the R-Ladies communities 46 | is the development & support specifically of those identifying as a minority 47 | gender, and we, therefore, reserve the right to guard this interest through 48 | whatever measures the leadership team deems appropriate. Anyone involved with 49 | R-Ladies is expected to fully respect each other, the mandate of this community, 50 | and the goodwill on which R-Ladies is founded, or face expulsion/a penalty 51 | of any form, at the discretion of the leadership team.\nFull community guidelines 52 | are found here: [https://github.com/rladies/starter-kit/wiki](https://github.com/rladies/starter-kit/wiki)\nP.P.S. 53 | Photos, Films and all other media/recordings:\nPhotos, Films, and all other 54 | media/recordings: photographs and/or video/other media will be taken at events 55 | held by this community. By taking part in an R-Ladies event you grant the 56 | community organizers full rights to use the images resulting from the photography/video 57 | filming/media, and any reproductions or adaptations of the images for publicity, 58 | fundraising or other purposes to help achieve the community’s aims. This might 59 | include (but is not limited to), the right to use them in their printed and 60 | online publicity, social media, press releases and funding applications. If 61 | you do not wish to be recorded in these media please inform a community organizer.","urlname":"rladies-lagos","link":"https://www.meetup.com/rladies-lagos","city":"Lagos","country":"ng","timezone":"Africa/Lagos","foundedDate":"2019-08-16T09:45:21+01:00","stats":{"memberCounts":{"all":880}},"organizer":{"id":"251470805","name":"R-Ladies 62 | Global"},"keyGroupPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-events/"},"topicCategory":{"id":"546","name":"Technology"},"events":{"totalCount":13}}}}' 63 | recorded_at: 2025-10-19 21:43:23 64 | recorded_with: VCR-vcr/2.0.0 65 | -------------------------------------------------------------------------------- /R/get-event.R: -------------------------------------------------------------------------------- 1 | #' Get information for a specified event 2 | #' 3 | #' @param id Required event ID 4 | #' @param ... Should be empty. Used for parameter expansion 5 | #' @template extra_graphql 6 | #' @return A meetup_event object with information about the specified event 7 | #' 8 | #' @examples 9 | #' \dontshow{ 10 | #' vcr::insert_example_cassette("get_event", package = "meetupr") 11 | #' meetupr:::mock_if_no_auth() 12 | #' } 13 | #' event <- get_event(id = "103349942") 14 | #' \dontshow{ 15 | #' vcr::eject_cassette() 16 | #' } 17 | #' @export 18 | get_event <- function( 19 | id, 20 | extra_graphql = NULL, 21 | ... 22 | ) { 23 | rlang::check_dots_empty() 24 | 25 | result <- execute( 26 | meetup_template_query( 27 | template = template_path("get_event"), 28 | page_info_path = "data.event.pageInfo", 29 | edges_path = "data.event", 30 | process_data = process_event_data 31 | ), 32 | id = id, 33 | extra_graphql = extra_graphql 34 | ) 35 | 36 | result 37 | } 38 | 39 | #' Process event data into meetup_event object 40 | #' @keywords internal 41 | #' @noRd 42 | process_event_data <- function(data, ...) { 43 | if (length(data) == 0 || is.null(data[[1]])) { 44 | cli::cli_abort("No event data returned") 45 | } 46 | 47 | # Just add the class to the existing list 48 | structure( 49 | data, 50 | class = c("meetup_event", "list") 51 | ) 52 | } 53 | 54 | #' @export 55 | print.meetup_event <- function(x, ...) { 56 | cli::cli_h2("Meetup Event") 57 | 58 | cli::cli_li("ID: {.val {x$id}}") 59 | cli::cli_li("Title: {.strong {x$title}}") 60 | cli::cli_li("Status: {.val {x$status}}") 61 | 62 | if (!is.null(x$dateTime)) { 63 | cli::cli_li("Date/Time: {x$dateTime}") 64 | } 65 | 66 | if (!is.null(x$duration)) { 67 | cli::cli_li("Duration: {x$duration}") 68 | } 69 | 70 | if (!is.null(x$rsvps$totalCount)) { 71 | cli::cli_li("RSVPs: {x$rsvps$totalCount}") 72 | } 73 | 74 | if (!is.null(x$group)) { 75 | cli::cli_h3("Group:") 76 | cli::cli_li("{x$group$name} ({.val {x$group$urlname}})") 77 | } 78 | 79 | if (!is.null(x$venues) && length(x$venues) > 0) { 80 | venue <- x$venues[[1]] 81 | cli::cli_h3("Venue:") 82 | if (!is.null(venue$name)) { 83 | cli::cli_li("Name: {venue$name}") 84 | } 85 | location_parts <- c(venue$city, venue$state, venue$country) 86 | location <- paste( 87 | location_parts[!sapply(location_parts, is.null)], 88 | collapse = ", " 89 | ) 90 | if (nzchar(location)) { 91 | cli::cli_li("Location: {location}") 92 | } 93 | } 94 | 95 | if (!is.null(x$feeSettings) && isTRUE(x$feeSettings$required)) { 96 | cli::cli_h3("Fee:") 97 | cli::cli_li("{x$feeSettings$amount} {x$feeSettings$currency}") 98 | } 99 | 100 | if (!is.null(x$eventUrl)) { 101 | cli::cli_text("") 102 | cli::cli_text("{.url {x$eventUrl}}") 103 | } 104 | 105 | invisible(x) 106 | } 107 | 108 | #' Get the RSVPs for a specified event 109 | #' 110 | #' @param id Required event ID 111 | #' @param ... Should be empty. Used for parameter expansion 112 | #' @template max_results 113 | #' @template handle_multiples 114 | #' @template extra_graphql 115 | #' @return A tibble with the RSVPs for the specified event 116 | #' 117 | #' @examples 118 | #' \dontshow{ 119 | #' vcr::insert_example_cassette("get_event_rsvps", package = "meetupr") 120 | #' meetupr:::mock_if_no_auth() 121 | #' } 122 | #' rsvps <- get_event_rsvps(id = "103349942") 123 | #' \dontshow{ 124 | #' vcr::eject_cassette() 125 | #' } 126 | #' @export 127 | get_event_rsvps <- function( 128 | id, 129 | max_results = NULL, 130 | handle_multiples = "list", 131 | extra_graphql = NULL, 132 | ... 133 | ) { 134 | rlang::check_dots_empty() 135 | 136 | execute( 137 | standard_query( 138 | "get_event_rsvps", 139 | "data.event.rsvps" 140 | ), 141 | id = id, 142 | first = max_results, 143 | max_results = max_results, 144 | handle_multiples = handle_multiples, 145 | extra_graphql = extra_graphql 146 | ) 147 | } 148 | 149 | #' Get the comments for a specified event 150 | #' 151 | #' @param id Required event ID 152 | #' @param ... Should be empty. Used for parameter expansion 153 | #' @template extra_graphql 154 | #' @return A tibble with the following columns: 155 | #' * id 156 | #' * comment 157 | #' * created 158 | #' * like_count 159 | #' * member_id 160 | #' * member_name 161 | #' * link 162 | #' @examples 163 | #' \dontrun{ 164 | #' comments <- get_event_comments(id = "103349942") 165 | #' } 166 | #' @export 167 | get_event_comments <- function( 168 | id, 169 | ..., 170 | extra_graphql = NULL 171 | ) { 172 | rlang::check_dots_empty() 173 | 174 | cli::cli_warn(c( 175 | "!" = "Event comments functionality has been 176 | removed from the current Meetup GraphQL API.", 177 | "i" = "The 'comments' field is no longer available 178 | on the Event type.", 179 | "i" = "This function returns an empty tibble for 180 | backwards compatibility.", 181 | "i" = "Comment mutations may still work, but 182 | querying comments is not supported." 183 | )) 184 | 185 | dplyr::tibble( 186 | id = character(0), 187 | comment = character(0), 188 | created = character(0), 189 | like_count = integer(0), 190 | member_id = character(0), 191 | member_name = character(0), 192 | link = character(0) 193 | ) 194 | } 195 | -------------------------------------------------------------------------------- /R/api.R: -------------------------------------------------------------------------------- 1 | #' Meetup API Prefix 2 | #' 3 | #' @keywords internal 4 | #' @noRd 5 | meetup_api_prefix <- function() { 6 | Sys.getenv( 7 | "MEETUP_API_URL", 8 | "https://api.meetup.com/gql-ext" 9 | ) 10 | } 11 | 12 | #' Create and Configure a Meetup API Request 13 | #' 14 | #' This function prepares and configures an HTTP request for interacting with 15 | #' the Meetup API. It allows the user to authenticate via OAuth, specify the 16 | #' use of caching, and set custom client configuration. 17 | #' 18 | #' @param rate_limit A numeric value specifying the maximum number of requests 19 | #' @param cache A logical value indicating whether to cache the OAuth token 20 | #' on disk. Defaults to `TRUE`. 21 | #' @param ... Additional arguments passed to [meetup_client()] for setting up 22 | #' the OAuth client. 23 | #' 24 | #' @return A `httr2` request object pre-configured to 25 | #' interact with the Meetup API. 26 | #' 27 | #' @examples 28 | #' \dontrun{ 29 | #' # Example 1: Basic request with caching enabled 30 | #' req <- meetup_req(cache = TRUE) 31 | #' 32 | #' # Example 2: Request with custom client ID and secret 33 | #' req <- meetup_req( 34 | #' cache = FALSE, 35 | #' client_id = "your_client_id", 36 | #' client_secret = "your_client_secret" 37 | #' ) 38 | #' } 39 | #' 40 | #' @details 41 | #' This function constructs an HTTP POST request directed to the Meetup API 42 | #' and applies appropriate OAuth headers for authentication. The function 43 | #' is prepared to support caching and provides flexibility for client 44 | #' customization with the `...` parameter. The implementation is currently 45 | #' commented out and would require activation for functionality. 46 | #' 47 | #' @export 48 | meetup_req <- function(rate_limit = 500 / 60, cache = TRUE, ...) { 49 | meetup_api_prefix() |> 50 | httr2::request() |> 51 | httr2::req_headers( 52 | "Content-Type" = "application/json" 53 | ) |> 54 | httr2::req_error(body = handle_api_error) |> 55 | httr2::req_oauth_auth_code( 56 | client = meetup_client(...), 57 | auth_url = "https://secure.meetup.com/oauth2/authorize", 58 | redirect_uri = "http://localhost:1410", 59 | cache_disk = cache 60 | ) |> 61 | httr2::req_throttle(rate = rate_limit) 62 | } 63 | 64 | #' Execute GraphQL query 65 | #' 66 | #' This function executes a GraphQL query with the provided variables. 67 | #' It validates the variables, constructs the request, 68 | #' and handles any errors returned by the GraphQL API. 69 | #' @param graphql GraphQL query string 70 | #' @param ... Variables to pass to query 71 | #' @param .envir Environment for error handling 72 | #' @return The response from the GraphQL API as a list. 73 | #' @examples 74 | #' \dontrun{ 75 | #' query <- " 76 | #' query GetUser($id: ID!) { 77 | #' user(id: $id) { 78 | #' id 79 | #' name 80 | #' } 81 | #' }" 82 | #' meetup_query(graphql = query, id = "12345") 83 | #' } 84 | #' @export 85 | meetup_query <- function( 86 | graphql, 87 | ..., 88 | .envir = parent.frame() 89 | ) { 90 | variables <- rlang::list2(...) |> 91 | purrr::compact() 92 | validate_graphql_variables(variables) 93 | 94 | req <- build_request( 95 | graphql, 96 | variables 97 | ) 98 | 99 | resp <- req |> 100 | httr2::req_perform() |> 101 | httr2::resp_body_json() 102 | 103 | if (!is.null(resp$errors)) { 104 | cli::cli_abort( 105 | c( 106 | "Failed to execute GraphQL query.", 107 | sapply(resp$errors, function(e) { 108 | gsub("\\{", "{{", gsub("\\}", "}}", e$message)) 109 | }) 110 | ), 111 | .envir = .envir 112 | ) 113 | } 114 | 115 | resp 116 | } 117 | 118 | #' Build a GraphQL Request 119 | #' This function constructs an HTTP request for a GraphQL query, 120 | #' including the query and variables in the request body. 121 | #' @param query A character string containing the GraphQL query. 122 | #' @param variables A named list of variables to include with the query. 123 | #' @return A `httr2` request object ready to be sent. 124 | #' @noRd 125 | #' @keywords internal 126 | build_request <- function( 127 | graphql, 128 | variables = list() 129 | ) { 130 | # Ensure variables is always a proper object, not an array 131 | if (length(variables) == 0 || is.null(variables)) { 132 | variables <- structure( 133 | list(), 134 | names = character(0) 135 | ) 136 | } 137 | 138 | # Construct body once 139 | body_list <- list( 140 | query = graphql, 141 | variables = variables 142 | ) 143 | 144 | # Debug the request body if enabled 145 | if (check_debug_mode()) { 146 | body <- jsonlite::toJSON( 147 | body_list, 148 | auto_unbox = TRUE, 149 | pretty = TRUE 150 | ) |> 151 | strsplit("\n|\\\\n") |> 152 | unlist() 153 | cli::cli_alert_info("DEBUG: JSON to be sent:") 154 | cli::cli_code(body) 155 | } 156 | 157 | meetup_req() |> 158 | httr2::req_body_json(body_list, auto_unbox = TRUE) 159 | } 160 | 161 | #' Handle API Error 162 | #' 163 | #' This function processes the error response from the API 164 | #' and extracts meaningful error messages. 165 | #' 166 | #' @param resp The response object from the API request. 167 | #' @return A character string containing the error message. 168 | #' @keywords internal 169 | #' @noRd 170 | handle_api_error <- function(resp) { 171 | error_data <- httr2::resp_body_json(resp) 172 | if (!is.null(error_data$errors)) { 173 | messages <- sapply(error_data$errors, function(err) err$message) 174 | paste("Meetup API errors:", paste(messages, collapse = "; ")) 175 | } else { 176 | "Unknown Meetup API error" 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /vignettes/_vcr/graphql-list-columns.yml: -------------------------------------------------------------------------------- 1 | http_interactions: 2 | - request: 3 | method: POST 4 | uri: https://api.meetup.com/gql-ext 5 | body: 6 | string: '{"query":"query groupEvents(\n $urlname: String!\n $cursor: String\n $first: 7 | Int = 1000\n $status: [EventStatus!]\n $date_after: DateTime\n $date_before: 8 | DateTime\n) {\n groupByUrlname(urlname: $urlname) {\n id\n name\n events(\n after: 9 | $cursor\n first: $first\n filter: {\n status: $status\n afterDateTime: 10 | $date_after\n beforeDateTime: $date_before\n }\n ) {\n pageInfo 11 | {\n hasNextPage\n endCursor\n }\n totalCount\n edges 12 | {\n cursor\n node {\n ...eventFields\n }\n }\n }\n }\n}\n\nfragment 13 | eventFields on Event {\n id\n title\n eventUrl\n createdTime\n status\n dateTime\n duration\n description\n \n group 14 | {\n id\n name\n urlname\n }\n \n venues {\n id\n name\n address\n city\n state\n postalCode\n country\n lat\n lon\n venueType\n }\n \n rsvps(first: 15 | 1) {\n totalCount\n }\n \n featuredEventPhoto {\n baseUrl\n }\n \n feeSettings 16 | {\n required\n amount\n currency\n accepts\n refundPolicy {\n notes\n days\n }\n }\n \n \n featuredEventPhoto 17 | {\n highResUrl\n }\n \n}","variables":{"urlname":"rladies-lagos","status":["ACTIVE","AUTOSCHED","AUTOSCHED_CANCELLED","AUTOSCHED_DRAFT","AUTOSCHED_FINISHED","BLOCKED","CANCELLED","CANCELLED_PERM","DRAFT","PAST","PENDING","PROPOSED","TEMPLATE"],"first":3}}' 18 | response: 19 | status: 200 20 | headers: 21 | content-type: application/json;charset=utf-8 22 | accept-ranges: bytes 23 | date: Sun, 19 Oct 2025 22:22:47 GMT 24 | content-length: '3548' 25 | body: 26 | string: '{"data":{"groupByUrlname":{"id":"32612004","name":"R-Ladies Lagos","events":{"pageInfo":{"hasNextPage":true,"endCursor":"MjY3ODA0ODE0OjE1ODIzNTg0MDAwMDA="},"totalCount":14,"edges":[{"cursor":"MjY0MTU2MDYxOjE1NjkwNTI4MDAwMDA=","node":{"id":"264156061","title":"satRday 27 | Lagos + The Launch of RLadies Lagos.","eventUrl":"https://www.meetup.com/rladies-lagos/events/264156061/","createdTime":"2019-08-20T07:54:50-04:00","status":"PAST","dateTime":"2019-09-21T09:00:00+01:00","duration":"PT7H","description":"satRday-Lagos 28 | is an event aimed at getting you started with the R programing language.\n\nDo 29 | you love data or you are thinking of kick-starting your career in Technology 30 | by learning a programing language, then this is for you.\n\nDo you do statistics,digital 31 | media listening and analysis, data visualization, data engineering, data management,data 32 | sciences animation, economics and more. Let''s show you how R would improve 33 | your projects.\n\nThis event is strictly hands-on so bring your laptops and 34 | devices along .\n\nSee you there .","group":{"id":"32612004","name":"R-Ladies 35 | Lagos","urlname":"rladies-lagos"},"venues":[{"id":"26632228","name":"Civil 36 | Engineering Building, ","address":"YABATECH,","city":"Lagos","state":"al","postalCode":"meetup1","country":"ng","lat":6.652131,"lon":3.271455,"venueType":""}],"rsvps":{"totalCount":26},"featuredEventPhoto":null,"feeSettings":null}},{"cursor":"MjY2NzU3MjY1OjE1NzU3MDU2MDAwMDA=","node":{"id":"266757265","title":"Data 37 | Mining using R","eventUrl":"https://www.meetup.com/rladies-lagos/events/266757265/","createdTime":"2019-11-26T12:00:29-05:00","status":"PAST","dateTime":"2019-12-07T09:00:00+01:00","duration":"PT7H","description":"Hi 38 | there R Users, it is time again for our physical meetup. It promises to packed 39 | but very interesting time.\n\nAgenda\n\n* Project presentation from the last 40 | online meeting\n*Hands-on projects for this month\n* Preparation for upcoming 41 | conferences\n- Women in Analytics 2020 (Columbus)\n- useR 2020 (St. Louis)\n*R-Ladies 42 | Lagos 2020 Outlook\n\nCome with your computers, extension boxes and data visualization 43 | ideas.","group":{"id":"32612004","name":"R-Ladies Lagos","urlname":"rladies-lagos"},"venues":[{"id":"26750374","name":"Sweets 44 | And Pastries Ltd","address":"76 Ogudu Rd","city":"Lagos","state":"LA","postalCode":"100242","country":"ng","lat":6.57837,"lon":3.387718,"venueType":""}],"rsvps":{"totalCount":14},"featuredEventPhoto":null,"feeSettings":null}},{"cursor":"MjY3ODA0ODE0OjE1ODIzNTg0MDAwMDA=","node":{"id":"267804814","title":"Getting 45 | started with animated data in R","eventUrl":"https://www.meetup.com/rladies-lagos/events/267804814/","createdTime":"2020-01-10T07:56:39-05:00","status":"PAST","dateTime":"2020-02-22T09:00:00+01:00","duration":"PT7H","description":"Hi 46 | there R Users, it is time again for our first physical meetup of the year 47 | 2020.\n\nAgenda\n\n*2019 in Review\n*Homework presentation\n*Getting started 48 | with data animation in R\n*Summer training.\n\nCome with your computers, extension 49 | boxes, and data - maker ideas.","group":{"id":"32612004","name":"R-Ladies 50 | Lagos","urlname":"rladies-lagos"},"venues":[{"id":"26750374","name":"Sweets 51 | And Pastries Ltd","address":"76 Ogudu Rd","city":"Lagos","state":"LA","postalCode":"100242","country":"ng","lat":6.57837,"lon":3.387718,"venueType":""}],"rsvps":{"totalCount":15},"featuredEventPhoto":{"baseUrl":"https://secure-content.meetupstatic.com/images/classic-events/","highResUrl":"https://secure.meetupstatic.com/photos/event/b/6/9/2/highres_488026738.jpeg"},"feeSettings":null}}]}}}}' 52 | recorded_at: 2025-10-19 22:22:47 53 | recorded_with: VCR-vcr/2.0.0 54 | --------------------------------------------------------------------------------