├── .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
8 |
9 | Logo by Zane Dax
10 | [@StarTrek_Lt](https://x.com/startrek_lt)
11 |
12 |
13 | [](https://CRAN.R-project.org/package=meetupr)
14 | [](https://rladies.r-universe.dev/meetupr)
15 | [](https://opensource.org/licenses/MIT)
16 | [](https://github.com/rladies/meetupr/actions/workflows/R-CMD-check.yaml)
17 | [](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
6 |
7 | Logo by Zane Dax
8 | [@StarTrek_Lt](https://x.com/startrek_lt)
9 |
10 |
11 |
12 | [](https://CRAN.R-project.org/package=meetupr)
14 | [](https://rladies.r-universe.dev/meetupr)
16 | [](https://opensource.org/licenses/MIT)
18 | [](https://github.com/rladies/meetupr/actions/workflows/R-CMD-check.yaml)
19 | [](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 |
--------------------------------------------------------------------------------