circle-so 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +145 -0
- data/lib/circle-so.rb +5 -0
- data/lib/circle_so/client.rb +147 -0
- data/lib/circle_so/errors.rb +39 -0
- data/lib/circle_so/resources/access_groups.rb +21 -0
- data/lib/circle_so/resources/base.rb +15 -0
- data/lib/circle_so/resources/chat_preferences.rb +9 -0
- data/lib/circle_so/resources/comments.rb +12 -0
- data/lib/circle_so/resources/community.rb +10 -0
- data/lib/circle_so/resources/community_members.rb +32 -0
- data/lib/circle_so/resources/community_segments.rb +13 -0
- data/lib/circle_so/resources/course_lessons.rb +16 -0
- data/lib/circle_so/resources/course_sections.rb +13 -0
- data/lib/circle_so/resources/direct_uploads.rb +9 -0
- data/lib/circle_so/resources/embeds.rb +10 -0
- data/lib/circle_so/resources/event_attendees.rb +11 -0
- data/lib/circle_so/resources/events.rb +14 -0
- data/lib/circle_so/resources/flagged_contents.rb +10 -0
- data/lib/circle_so/resources/forms.rb +13 -0
- data/lib/circle_so/resources/gamification.rb +9 -0
- data/lib/circle_so/resources/images.rb +12 -0
- data/lib/circle_so/resources/invitation_links.rb +11 -0
- data/lib/circle_so/resources/member_tags.rb +13 -0
- data/lib/circle_so/resources/messages.rb +9 -0
- data/lib/circle_so/resources/page_profile_fields.rb +9 -0
- data/lib/circle_so/resources/posts.rb +15 -0
- data/lib/circle_so/resources/profile_fields.rb +11 -0
- data/lib/circle_so/resources/search.rb +9 -0
- data/lib/circle_so/resources/space_group_members.rb +15 -0
- data/lib/circle_so/resources/space_groups.rb +13 -0
- data/lib/circle_so/resources/space_members.rb +15 -0
- data/lib/circle_so/resources/spaces.rb +14 -0
- data/lib/circle_so/resources/tagged_members.rb +13 -0
- data/lib/circle_so/resources/topics.rb +13 -0
- data/lib/circle_so/token.rb +38 -0
- data/lib/circle_so/version.rb +5 -0
- data/lib/circle_so.rb +44 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f9168b54a09c87d1ed7adc9abdd8946b030d06e984ecef02b43eb8f201745e2e
|
|
4
|
+
data.tar.gz: '01888d6cfe6da5d686c47c5ab750a14c628fa025c09dac1445ea0b02d63905f9'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5ede5321b909824ba1e0c16167f554fed5f791dd6defe4689678355e4b401fe98ecafbbb02fd23bfb36af500acfe542b17b952bb60c690fffe401d597faeef95
|
|
7
|
+
data.tar.gz: 41f64f4e5007116a4a552967f0acb701f457d19ff17f1beeb7b0a766f9179b44144e3e5224b8090767d4766f76a67d10ba24eea04aca7d7df1b6f76386aa3967
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-08-06
|
|
4
|
+
|
|
5
|
+
- Initial release.
|
|
6
|
+
- Full coverage of the Circle.so Admin API v2 (68 endpoints across 30
|
|
7
|
+
resources): community, community members, access groups, spaces, space
|
|
8
|
+
groups, space/space-group membership, posts, comments, events, event
|
|
9
|
+
attendees, courses (lessons, sections, progress), community segments,
|
|
10
|
+
member tags, tagged members, topics, forms, flagged contents, invitation
|
|
11
|
+
links, messages, direct uploads, embeds, images, profile fields, page
|
|
12
|
+
profile fields, gamification leaderboard, advanced search, chat
|
|
13
|
+
preferences.
|
|
14
|
+
- Classified errors (`CircleSo::APIError` subclasses) including the
|
|
15
|
+
404-HTML "wrong path" guard.
|
|
16
|
+
- Three-state token version detection (`CircleSo.detect_token_version`).
|
|
17
|
+
- Zero runtime dependencies.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lawrence Lin
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# circle-so
|
|
2
|
+
|
|
3
|
+
Zero-dependency Ruby client for the [Circle.so](https://circle.so) Admin API v2 — full coverage of all 68 endpoints ([official docs](https://api.circle.so/)): members, spaces, space groups, access groups, posts, comments, events, courses, segments, tags, and more, with classified errors and token version detection.
|
|
4
|
+
|
|
5
|
+
> Unofficial. Not affiliated with Circle Internet Services, Inc.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem install circle-so
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or in your Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "circle-so"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Ruby ≥ 3.1. No runtime dependencies (built on `net/http`).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "circle_so"
|
|
25
|
+
|
|
26
|
+
client = CircleSo::Client.new(token: ENV["CIRCLE_SO_TOKEN"])
|
|
27
|
+
|
|
28
|
+
community = client.community.show
|
|
29
|
+
|
|
30
|
+
# Invite a member (idempotent — re-inviting an existing email does not
|
|
31
|
+
# create a duplicate or re-send the invitation email):
|
|
32
|
+
result = client.community_members.invite(email: "user@example.com", name: "User Name")
|
|
33
|
+
result["community_member"]["id"]
|
|
34
|
+
|
|
35
|
+
# Access groups (member identified by email, group id in the URL):
|
|
36
|
+
client.access_groups.list
|
|
37
|
+
client.access_groups.add_member(group_id, email: "user@example.com")
|
|
38
|
+
client.access_groups.remove_member(group_id, email: "user@example.com")
|
|
39
|
+
|
|
40
|
+
# Spaces / space groups (id goes in the request BODY, even for DELETE):
|
|
41
|
+
client.space_members.add(space_id: 123, email: "user@example.com")
|
|
42
|
+
client.space_group_members.add(space_group_id: 456, email: "user@example.com")
|
|
43
|
+
|
|
44
|
+
# Posts, events, courses, ...
|
|
45
|
+
client.posts.list(space_id: 123)
|
|
46
|
+
client.events.create(name: "Launch party", space_id: 123)
|
|
47
|
+
client.course_lessons.update_progress(lesson_id: 1, user_email: "user@example.com")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
All responses are parsed JSON (`Hash`/`Array` with string keys), exactly as Circle returns them.
|
|
51
|
+
|
|
52
|
+
## Resources
|
|
53
|
+
|
|
54
|
+
Every Admin API v2 endpoint is covered:
|
|
55
|
+
|
|
56
|
+
| `client.` … | Methods |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `access_groups` | `list`, `create`, `update`, `archive`, `unarchive`, `members`, `member`, `add_member`, `remove_member` |
|
|
59
|
+
| `chat_preferences` | `update` |
|
|
60
|
+
| `comments` | `list`, `create`, `show`, `delete` |
|
|
61
|
+
| `community` | `show`, `update` |
|
|
62
|
+
| `community_members` | `list`, `search`, `show`, `update`, `invite`/`create`, `deactivate`, `delete`, `ban`, `access_groups`, `spaces` |
|
|
63
|
+
| `community_segments` | `list`, `create`, `update`, `delete`, `duplicate` |
|
|
64
|
+
| `course_lessons` | `list`, `create`, `show`, `update`, `delete`, `update_progress` |
|
|
65
|
+
| `course_sections` | `list`, `create`, `show`, `update`, `delete` |
|
|
66
|
+
| `direct_uploads` | `create` |
|
|
67
|
+
| `embeds` | `create`, `show` |
|
|
68
|
+
| `event_attendees` | `list`, `add`, `remove` |
|
|
69
|
+
| `events` | `list`, `create`, `show`, `update`, `delete`, `duplicate` |
|
|
70
|
+
| `flagged_contents` | `list`, `create` |
|
|
71
|
+
| `forms` | `list`, `show`, `update`, `delete`, `duplicate` |
|
|
72
|
+
| `gamification` | `leaderboard` |
|
|
73
|
+
| `images` | `create_post`, `delete_post`, `duplicate_post` |
|
|
74
|
+
| `invitation_links` | `list`, `delete`, `revoke` |
|
|
75
|
+
| `member_tags` | `list`, `create`, `show`, `update`, `delete` |
|
|
76
|
+
| `messages` | `create` |
|
|
77
|
+
| `page_profile_fields` | `list` |
|
|
78
|
+
| `posts` | `list`, `create`, `show`, `update`, `delete`, `summary`, `unfollow` |
|
|
79
|
+
| `profile_fields` | `list`, `archive`, `unarchive` |
|
|
80
|
+
| `search` | `advanced` |
|
|
81
|
+
| `space_group_members` | `list`, `show`, `add`, `remove` |
|
|
82
|
+
| `space_groups` | `list`, `create`, `show`, `update`, `delete` |
|
|
83
|
+
| `space_members` | `list`, `show`, `add`, `remove` |
|
|
84
|
+
| `spaces` | `list`, `create`, `show`, `update`, `delete`, `ai_summaries` |
|
|
85
|
+
| `tagged_members` | `list`, `show`, `add`, `remove` |
|
|
86
|
+
| `topics` | `list`, `create`, `show`, `update`, `delete` |
|
|
87
|
+
|
|
88
|
+
List endpoints accept query params as keyword arguments (`page:`, `per_page:`, filters). Create/update endpoints accept the request body as keyword arguments. There is also a low-level escape hatch: `client.get(path, params)`, `client.post(path, body)`, etc.
|
|
89
|
+
|
|
90
|
+
## Error handling
|
|
91
|
+
|
|
92
|
+
Non-2xx responses raise a subclass of `CircleSo::APIError` with a classified `kind`, the HTTP `status`, and the parsed response `body`:
|
|
93
|
+
|
|
94
|
+
| class | kind | meaning |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `CircleSo::UnauthorizedError` | `:unauthorized` | 401 — wrong/invalid token (or a v1 token on the v2 API) |
|
|
97
|
+
| `CircleSo::ForbiddenError` | `:forbidden` | 403 — plan no longer allows the endpoint |
|
|
98
|
+
| `CircleSo::NotFoundError` | `:not_found` | 404 with a JSON body — missing record/param |
|
|
99
|
+
| `CircleSo::WrongPathError` | `:wrong_path` | 404 with an **HTML** body — the route does not exist (bug guard) |
|
|
100
|
+
| `CircleSo::RateLimitedError` | `:rate_limited` | 429 |
|
|
101
|
+
| `CircleSo::ServerError` | `:server_error` | 5xx |
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
begin
|
|
105
|
+
client.access_groups.add_member(group_id, email: email)
|
|
106
|
+
rescue CircleSo::UnauthorizedError
|
|
107
|
+
# pause syncing, prompt the user to re-connect
|
|
108
|
+
rescue CircleSo::APIError => e
|
|
109
|
+
logger.error("Circle #{e.status} (#{e.kind}): #{e.message}")
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The `wrong_path` distinction matters: Circle returns 404 **HTML** for routes that don't exist and 404 **JSON** for missing records. The client tells them apart via `Content-Type` so a typo'd endpoint doesn't masquerade as "record not found".
|
|
114
|
+
|
|
115
|
+
## Token version detection
|
|
116
|
+
|
|
117
|
+
Circle issues v1 (Professional plan) and v2 (Business plan) admin tokens that look identical. A v1 token on the v2 API returns a plain 401, so use the three-state probe to give users an actionable error:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
CircleSo.detect_token_version(token) # => :v2, :v1, or :invalid
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## API quirks this client encodes
|
|
124
|
+
|
|
125
|
+
Learned from a live integration (the test fixtures under `spec/fixtures` were captured against the real API):
|
|
126
|
+
|
|
127
|
+
- **Asymmetric membership endpoints.** Access groups take the group id in the URL and the member's *email* in the body (`POST /access_groups/:id/community_members`). Spaces and space groups instead use flat endpoints (`/space_members`, `/space_group_members`) with the id in the body — even for `DELETE`.
|
|
128
|
+
- **Members are keyed by email**, not member id, for all membership operations.
|
|
129
|
+
- **Adds/removes are idempotent.** Re-adding returns the same success response; you can apply a full expected set without querying current state first.
|
|
130
|
+
- **Space group membership cascades** to all spaces inside the group.
|
|
131
|
+
- **First `invite` sends Circle's invitation email**; subsequent calls for the same email are no-ops (the response message says "already a member").
|
|
132
|
+
- **`course_lessons` update uses `PATCH`** while every other update uses `PUT`; `tagged_members.remove` identifies the record via query params.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bundle install
|
|
138
|
+
bundle exec rake spec # runs the suite with 100% line-coverage enforcement
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Tests stub Circle at the network boundary with [WebMock](https://github.com/bblimke/webmock), replaying real response bodies captured from the live Admin API — no credentials needed, fully deterministic.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
data/lib/circle-so.rb
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module CircleSo
|
|
8
|
+
# HTTP client for the Circle.so Admin API v2.
|
|
9
|
+
#
|
|
10
|
+
# client = CircleSo::Client.new(token: ENV["CIRCLE_SO_TOKEN"])
|
|
11
|
+
# client.community.show
|
|
12
|
+
# client.community_members.invite(email: "user@example.com")
|
|
13
|
+
#
|
|
14
|
+
# Zero runtime dependencies — built on Net::HTTP. All responses are parsed
|
|
15
|
+
# JSON (Hash/Array with string keys). Non-2xx responses raise a subclass of
|
|
16
|
+
# CircleSo::APIError (see errors.rb for the classification).
|
|
17
|
+
class Client
|
|
18
|
+
BASE_URL = "https://app.circle.so/api/admin/v2"
|
|
19
|
+
|
|
20
|
+
attr_reader :base_url
|
|
21
|
+
|
|
22
|
+
def initialize(token:, base_url: BASE_URL, open_timeout: 10, read_timeout: 60)
|
|
23
|
+
raise ArgumentError, "token is required" if token.nil? || token.empty?
|
|
24
|
+
|
|
25
|
+
@token = token
|
|
26
|
+
@base_url = base_url.chomp("/")
|
|
27
|
+
@open_timeout = open_timeout
|
|
28
|
+
@read_timeout = read_timeout
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# -- resources ----------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def access_groups = @access_groups ||= Resources::AccessGroups.new(self)
|
|
34
|
+
def chat_preferences = @chat_preferences ||= Resources::ChatPreferences.new(self)
|
|
35
|
+
def comments = @comments ||= Resources::Comments.new(self)
|
|
36
|
+
def community = @community ||= Resources::Community.new(self)
|
|
37
|
+
def community_members = @community_members ||= Resources::CommunityMembers.new(self)
|
|
38
|
+
def community_segments = @community_segments ||= Resources::CommunitySegments.new(self)
|
|
39
|
+
def course_lessons = @course_lessons ||= Resources::CourseLessons.new(self)
|
|
40
|
+
def course_sections = @course_sections ||= Resources::CourseSections.new(self)
|
|
41
|
+
def direct_uploads = @direct_uploads ||= Resources::DirectUploads.new(self)
|
|
42
|
+
def embeds = @embeds ||= Resources::Embeds.new(self)
|
|
43
|
+
def event_attendees = @event_attendees ||= Resources::EventAttendees.new(self)
|
|
44
|
+
def events = @events ||= Resources::Events.new(self)
|
|
45
|
+
def flagged_contents = @flagged_contents ||= Resources::FlaggedContents.new(self)
|
|
46
|
+
def forms = @forms ||= Resources::Forms.new(self)
|
|
47
|
+
def gamification = @gamification ||= Resources::Gamification.new(self)
|
|
48
|
+
def images = @images ||= Resources::Images.new(self)
|
|
49
|
+
def invitation_links = @invitation_links ||= Resources::InvitationLinks.new(self)
|
|
50
|
+
def member_tags = @member_tags ||= Resources::MemberTags.new(self)
|
|
51
|
+
def messages = @messages ||= Resources::Messages.new(self)
|
|
52
|
+
def page_profile_fields = @page_profile_fields ||= Resources::PageProfileFields.new(self)
|
|
53
|
+
def posts = @posts ||= Resources::Posts.new(self)
|
|
54
|
+
def profile_fields = @profile_fields ||= Resources::ProfileFields.new(self)
|
|
55
|
+
def search = @search ||= Resources::Search.new(self)
|
|
56
|
+
def space_group_members = @space_group_members ||= Resources::SpaceGroupMembers.new(self)
|
|
57
|
+
def space_groups = @space_groups ||= Resources::SpaceGroups.new(self)
|
|
58
|
+
def space_members = @space_members ||= Resources::SpaceMembers.new(self)
|
|
59
|
+
def spaces = @spaces ||= Resources::Spaces.new(self)
|
|
60
|
+
def tagged_members = @tagged_members ||= Resources::TaggedMembers.new(self)
|
|
61
|
+
def topics = @topics ||= Resources::Topics.new(self)
|
|
62
|
+
|
|
63
|
+
# -- low-level requests (public escape hatch) ---------------------------
|
|
64
|
+
|
|
65
|
+
def get(path, params = {}) = request(Net::HTTP::Get, path, params: params)
|
|
66
|
+
def post(path, body = nil, params = {}) = request(Net::HTTP::Post, path, body: body, params: params)
|
|
67
|
+
def put(path, body = nil, params = {}) = request(Net::HTTP::Put, path, body: body, params: params)
|
|
68
|
+
def patch(path, body = nil, params = {}) = request(Net::HTTP::Patch, path, body: body, params: params)
|
|
69
|
+
def delete(path, body = nil, params = {}) = request(Net::HTTP::Delete, path, body: body, params: params)
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def request(verb, path, params: {}, body: nil)
|
|
74
|
+
uri = build_uri(path, params)
|
|
75
|
+
req = verb.new(uri)
|
|
76
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
77
|
+
req["Accept"] = "application/json"
|
|
78
|
+
req["User-Agent"] = "circle-so ruby gem #{CircleSo::VERSION}"
|
|
79
|
+
if body
|
|
80
|
+
req["Content-Type"] = "application/json"
|
|
81
|
+
req.body = JSON.generate(body)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
res = http_for(uri).request(req)
|
|
85
|
+
handle_response(res, verb, path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_uri(path, params)
|
|
89
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
90
|
+
compact = params.reject { |_k, v| v.nil? }
|
|
91
|
+
uri.query = [uri.query, URI.encode_www_form(compact)].compact.reject(&:empty?).join("&") unless compact.empty?
|
|
92
|
+
uri
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def http_for(uri)
|
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
97
|
+
http.use_ssl = uri.scheme == "https"
|
|
98
|
+
http.open_timeout = @open_timeout
|
|
99
|
+
http.read_timeout = @read_timeout
|
|
100
|
+
http
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_response(res, verb, path)
|
|
104
|
+
status = res.code.to_i
|
|
105
|
+
json = json_response?(res)
|
|
106
|
+
parsed = json ? parse_json(res.body) : res.body
|
|
107
|
+
|
|
108
|
+
return parsed if (200..299).cover?(status)
|
|
109
|
+
|
|
110
|
+
# Circle returns 404 with an HTML body when the route does not exist
|
|
111
|
+
# (vs 404 JSON for a missing record) — surface that as a distinct error
|
|
112
|
+
# so a typo'd endpoint doesn't masquerade as "record not found".
|
|
113
|
+
method_name = verb.name.split("::").last.upcase
|
|
114
|
+
if status == 404 && !json
|
|
115
|
+
raise WrongPathError.new(
|
|
116
|
+
"Circle returned 404 HTML for #{method_name} #{path} (route does not exist)",
|
|
117
|
+
status: 404, body: res.body, kind: :wrong_path
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
message = (parsed.is_a?(Hash) && parsed["message"]) || "#{method_name} #{path} failed with status #{status}"
|
|
122
|
+
klass, kind = classify_status(status)
|
|
123
|
+
raise klass.new(message, status: status, body: parsed, kind: kind)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def json_response?(res)
|
|
127
|
+
(res["content-type"] || "").include?("application/json")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_json(body)
|
|
131
|
+
body.nil? || body.empty? ? nil : JSON.parse(body)
|
|
132
|
+
rescue JSON::ParserError
|
|
133
|
+
body
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def classify_status(status)
|
|
137
|
+
case status
|
|
138
|
+
when 401 then [UnauthorizedError, :unauthorized]
|
|
139
|
+
when 403 then [ForbiddenError, :forbidden]
|
|
140
|
+
when 404 then [NotFoundError, :not_found]
|
|
141
|
+
when 429 then [RateLimitedError, :rate_limited]
|
|
142
|
+
when 500..599 then [ServerError, :server_error]
|
|
143
|
+
else [APIError, :unknown]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised for any non-2xx response from the Circle API. Carries the HTTP
|
|
7
|
+
# status, the parsed response body (Hash when JSON, String otherwise), and
|
|
8
|
+
# a classification kind mirroring the status:
|
|
9
|
+
#
|
|
10
|
+
# :unauthorized 401 — wrong/invalid token (or a v1 token on the v2 API)
|
|
11
|
+
# :forbidden 403 — plan no longer allows the endpoint
|
|
12
|
+
# :not_found 404 with a JSON body — missing record/param
|
|
13
|
+
# :wrong_path 404 with an HTML body — the route does not exist
|
|
14
|
+
# :rate_limited 429
|
|
15
|
+
# :server_error 5xx
|
|
16
|
+
# :unknown anything else
|
|
17
|
+
class APIError < Error
|
|
18
|
+
attr_reader :status, :body, :kind
|
|
19
|
+
|
|
20
|
+
def initialize(message, status:, body: nil, kind: :unknown)
|
|
21
|
+
super(message)
|
|
22
|
+
@status = status
|
|
23
|
+
@body = body
|
|
24
|
+
@kind = kind
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class UnauthorizedError < APIError; end
|
|
29
|
+
class ForbiddenError < APIError; end
|
|
30
|
+
class NotFoundError < APIError; end
|
|
31
|
+
|
|
32
|
+
# Circle returns 404 with an HTML body when the route itself does not exist
|
|
33
|
+
# (as opposed to 404 JSON for a missing record). Getting this error means
|
|
34
|
+
# the request hit a non-existent endpoint — a bug, not missing data.
|
|
35
|
+
class WrongPathError < APIError; end
|
|
36
|
+
|
|
37
|
+
class RateLimitedError < APIError; end
|
|
38
|
+
class ServerError < APIError; end
|
|
39
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class AccessGroups < Base
|
|
6
|
+
def list(**params) = client.get("/access_groups", params)
|
|
7
|
+
def create(**attrs) = client.post("/access_groups", attrs)
|
|
8
|
+
def update(id, **attrs) = client.put("/access_groups/#{id}", attrs)
|
|
9
|
+
# DELETE archives the access group (Circle keeps the record).
|
|
10
|
+
def archive(id) = client.delete("/access_groups/#{id}")
|
|
11
|
+
def unarchive(id) = client.patch("/access_groups/#{id}/unarchive")
|
|
12
|
+
|
|
13
|
+
# Membership: the group id goes in the URL, the member is identified by
|
|
14
|
+
# email (not member id) in the body/query.
|
|
15
|
+
def members(access_group_id, **params) = client.get("/access_groups/#{access_group_id}/community_members", params)
|
|
16
|
+
def member(access_group_id, email:) = client.get("/access_groups/#{access_group_id}/community_member", {email: email})
|
|
17
|
+
def add_member(access_group_id, email:, **attrs) = client.post("/access_groups/#{access_group_id}/community_members", {email: email, **attrs})
|
|
18
|
+
def remove_member(access_group_id, email:) = client.delete("/access_groups/#{access_group_id}/community_members", {email: email})
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class Comments < Base
|
|
6
|
+
def list(**params) = client.get("/comments", params)
|
|
7
|
+
def create(**attrs) = client.post("/comments", attrs)
|
|
8
|
+
def show(id) = client.get("/comments/#{id}")
|
|
9
|
+
def delete(id) = client.delete("/comments/#{id}")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class CommunityMembers < Base
|
|
6
|
+
def list(**params) = client.get("/community_members", params)
|
|
7
|
+
def search(**params) = client.get("/community_members/search", params)
|
|
8
|
+
def show(id) = client.get("/community_members/#{id}")
|
|
9
|
+
def update(id, **attrs) = client.put("/community_members/#{id}", attrs)
|
|
10
|
+
|
|
11
|
+
# Create/invite a member by email. Idempotent: re-calling for an
|
|
12
|
+
# existing email returns the same member (no duplicate, no re-sent
|
|
13
|
+
# invitation email). The response "message" says "already a member"
|
|
14
|
+
# in that case. First creation triggers Circle's invitation email.
|
|
15
|
+
def invite(email:, **attrs) = client.post("/community_members", {email: email, **attrs})
|
|
16
|
+
alias_method :create, :invite
|
|
17
|
+
|
|
18
|
+
# Deactivate the member (removes community access, keeps the record).
|
|
19
|
+
def deactivate(id) = client.delete("/community_members/#{id}")
|
|
20
|
+
# Permanently delete the member. Note: Circle models this as a PUT.
|
|
21
|
+
def delete(id) = client.put("/community_members/#{id}/delete_member")
|
|
22
|
+
def ban(id) = client.put("/community_members/#{id}/ban_member")
|
|
23
|
+
|
|
24
|
+
# Access groups this member belongs to.
|
|
25
|
+
def access_groups(id, **params) = client.get("/community_members/#{id}/access_groups", params)
|
|
26
|
+
|
|
27
|
+
# Spaces a member belongs to. Filter with community_member_id: or
|
|
28
|
+
# user_email: (query params).
|
|
29
|
+
def spaces(**params) = client.get("/community_member_spaces", params)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class CommunitySegments < Base
|
|
6
|
+
def list(**params) = client.get("/community_segments", params)
|
|
7
|
+
def create(**attrs) = client.post("/community_segments", attrs)
|
|
8
|
+
def update(id, **attrs) = client.put("/community_segments/#{id}", attrs)
|
|
9
|
+
def delete(id) = client.delete("/community_segments/#{id}")
|
|
10
|
+
def duplicate(id) = client.post("/community_segments/#{id}/duplicate")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class CourseLessons < Base
|
|
6
|
+
def list(**params) = client.get("/course_lessons", params)
|
|
7
|
+
def create(**attrs) = client.post("/course_lessons", attrs)
|
|
8
|
+
def show(id) = client.get("/course_lessons/#{id}")
|
|
9
|
+
# Note: Circle uses PATCH here (course sections use PUT).
|
|
10
|
+
def update(id, **attrs) = client.patch("/course_lessons/#{id}", attrs)
|
|
11
|
+
def delete(id) = client.delete("/course_lessons/#{id}")
|
|
12
|
+
|
|
13
|
+
def update_progress(**attrs) = client.put("/course_lesson_progress", attrs)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class CourseSections < Base
|
|
6
|
+
def list(**params) = client.get("/course_sections", params)
|
|
7
|
+
def create(**attrs) = client.post("/course_sections", attrs)
|
|
8
|
+
def show(id) = client.get("/course_sections/#{id}")
|
|
9
|
+
def update(id, **attrs) = client.put("/course_sections/#{id}", attrs)
|
|
10
|
+
def delete(id) = client.delete("/course_sections/#{id}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class EventAttendees < Base
|
|
6
|
+
def list(**params) = client.get("/event_attendees", params)
|
|
7
|
+
def add(**attrs) = client.post("/event_attendees", attrs)
|
|
8
|
+
def remove(**attrs) = client.delete("/event_attendees", attrs)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class Events < Base
|
|
6
|
+
def list(**params) = client.get("/events", params)
|
|
7
|
+
def create(**attrs) = client.post("/events", attrs)
|
|
8
|
+
def show(id) = client.get("/events/#{id}")
|
|
9
|
+
def update(id, **attrs) = client.put("/events/#{id}", attrs)
|
|
10
|
+
def delete(id) = client.delete("/events/#{id}")
|
|
11
|
+
def duplicate(id, space_id:, **attrs) = client.post("/spaces/#{space_id}/events/#{id}/duplicate", attrs)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class Forms < Base
|
|
6
|
+
def list(**params) = client.get("/forms", params)
|
|
7
|
+
def show(id) = client.get("/forms/#{id}")
|
|
8
|
+
def update(id, **attrs) = client.put("/forms/#{id}", attrs)
|
|
9
|
+
def delete(id) = client.delete("/forms/#{id}")
|
|
10
|
+
def duplicate(id) = client.post("/forms/#{id}/duplicate")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
# Image posts inside a space (/spaces/{space_id}/images/posts).
|
|
6
|
+
class Images < Base
|
|
7
|
+
def create_post(space_id, **attrs) = client.post("/spaces/#{space_id}/images/posts", attrs)
|
|
8
|
+
def delete_post(space_id, id) = client.delete("/spaces/#{space_id}/images/posts/#{id}")
|
|
9
|
+
def duplicate_post(space_id, id) = client.post("/spaces/#{space_id}/images/posts/#{id}/duplicate")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class InvitationLinks < Base
|
|
6
|
+
def list(**params) = client.get("/invitation_links", params)
|
|
7
|
+
def delete(id) = client.delete("/invitation_links/#{id}")
|
|
8
|
+
def revoke(id) = client.patch("/invitation_links/#{id}/revoke")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class MemberTags < Base
|
|
6
|
+
def list(**params) = client.get("/member_tags", params)
|
|
7
|
+
def create(**attrs) = client.post("/member_tags", attrs)
|
|
8
|
+
def show(id) = client.get("/member_tags/#{id}")
|
|
9
|
+
def update(id, **attrs) = client.put("/member_tags/#{id}", attrs)
|
|
10
|
+
def delete(id) = client.delete("/member_tags/#{id}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class Posts < Base
|
|
6
|
+
def list(**params) = client.get("/posts", params)
|
|
7
|
+
def create(**attrs) = client.post("/posts", attrs)
|
|
8
|
+
def show(id) = client.get("/posts/#{id}")
|
|
9
|
+
def update(id, **attrs) = client.put("/posts/#{id}", attrs)
|
|
10
|
+
def delete(id) = client.delete("/posts/#{id}")
|
|
11
|
+
def summary(post_id) = client.get("/posts/#{post_id}/summary")
|
|
12
|
+
def unfollow(post_id, community_member_id:) = client.delete("/posts/#{post_id}/post_followers", nil, {community_member_id: community_member_id})
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class ProfileFields < Base
|
|
6
|
+
def list(**params) = client.get("/profile_fields", params)
|
|
7
|
+
def archive(id) = client.put("/profile_fields/#{id}/archive")
|
|
8
|
+
def unarchive(id) = client.put("/profile_fields/#{id}/unarchive")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
# Space group membership uses flat endpoints: the space_group_id goes in
|
|
6
|
+
# the request body (even for DELETE), not the URL. Members are identified
|
|
7
|
+
# by email. Adding a member to a space group cascades into all its spaces.
|
|
8
|
+
class SpaceGroupMembers < Base
|
|
9
|
+
def list(**params) = client.get("/space_group_members", params)
|
|
10
|
+
def show(space_group_id:, email:) = client.get("/space_group_member", {space_group_id: space_group_id, email: email})
|
|
11
|
+
def add(space_group_id:, email:, **attrs) = client.post("/space_group_members", {space_group_id: space_group_id, email: email, **attrs})
|
|
12
|
+
def remove(space_group_id:, email:) = client.delete("/space_group_members", {space_group_id: space_group_id, email: email})
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class SpaceGroups < Base
|
|
6
|
+
def list(**params) = client.get("/space_groups", params)
|
|
7
|
+
def create(**attrs) = client.post("/space_groups", attrs)
|
|
8
|
+
def show(id) = client.get("/space_groups/#{id}")
|
|
9
|
+
def update(id, **attrs) = client.put("/space_groups/#{id}", attrs)
|
|
10
|
+
def delete(id) = client.delete("/space_groups/#{id}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
# Space membership uses flat endpoints: the space_id goes in the request
|
|
6
|
+
# body (even for DELETE), not the URL. Members are identified by email.
|
|
7
|
+
# Adds/removes are idempotent.
|
|
8
|
+
class SpaceMembers < Base
|
|
9
|
+
def list(**params) = client.get("/space_members", params)
|
|
10
|
+
def show(space_id:, email:) = client.get("/space_member", {space_id: space_id, email: email})
|
|
11
|
+
def add(space_id:, email:, **attrs) = client.post("/space_members", {space_id: space_id, email: email, **attrs})
|
|
12
|
+
def remove(space_id:, email:) = client.delete("/space_members", {space_id: space_id, email: email})
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class Spaces < Base
|
|
6
|
+
def list(**params) = client.get("/spaces", params)
|
|
7
|
+
def create(**attrs) = client.post("/spaces", attrs)
|
|
8
|
+
def show(id) = client.get("/spaces/#{id}")
|
|
9
|
+
def update(id, **attrs) = client.put("/spaces/#{id}", attrs)
|
|
10
|
+
def delete(id) = client.delete("/spaces/#{id}")
|
|
11
|
+
def ai_summaries(space_id, **params) = client.get("/spaces/#{space_id}/ai_summaries", params)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class TaggedMembers < Base
|
|
6
|
+
def list(**params) = client.get("/tagged_members", params)
|
|
7
|
+
def show(id) = client.get("/tagged_members/#{id}")
|
|
8
|
+
def add(**attrs) = client.post("/tagged_members", attrs)
|
|
9
|
+
# Removal identifies the record by query params, not the body.
|
|
10
|
+
def remove(user_email:, member_tag_id:) = client.delete("/tagged_members", nil, {user_email: user_email, member_tag_id: member_tag_id})
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CircleSo
|
|
4
|
+
module Resources
|
|
5
|
+
class Topics < Base
|
|
6
|
+
def list(**params) = client.get("/topics", params)
|
|
7
|
+
def create(**attrs) = client.post("/topics", attrs)
|
|
8
|
+
def show(id) = client.get("/topics/#{id}")
|
|
9
|
+
def update(id, **attrs) = client.put("/topics/#{id}", attrs)
|
|
10
|
+
def delete(id) = client.delete("/topics/#{id}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module CircleSo
|
|
7
|
+
# Circle issues two admin token types that look identical but hit different
|
|
8
|
+
# APIs:
|
|
9
|
+
#
|
|
10
|
+
# :v2 Business-plan token — works against /api/admin/v2
|
|
11
|
+
# :v1 Professional-plan token — only /api/v1 (no access groups)
|
|
12
|
+
# :invalid not a usable token
|
|
13
|
+
#
|
|
14
|
+
# A v1 token on a v2 endpoint returns a plain HTTP 401, indistinguishable
|
|
15
|
+
# from an invalid token — so probe v2 first, then v1, to give users an
|
|
16
|
+
# actionable three-state answer.
|
|
17
|
+
module Token
|
|
18
|
+
BASE_URL_V1 = "https://app.circle.so/api/v1"
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def detect_version(token, base_url_v2: Client::BASE_URL, base_url_v1: BASE_URL_V1)
|
|
23
|
+
return :v2 if probe(base_url_v2, "Bearer #{token}")
|
|
24
|
+
return :v1 if probe(base_url_v1, "Token #{token}")
|
|
25
|
+
|
|
26
|
+
:invalid
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def probe(base_url, authorization)
|
|
30
|
+
uri = URI.parse("#{base_url.chomp("/")}/community_members?per_page=1")
|
|
31
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
32
|
+
http.use_ssl = uri.scheme == "https"
|
|
33
|
+
res = http.request(Net::HTTP::Get.new(uri).tap { |r| r["Authorization"] = authorization })
|
|
34
|
+
(200..299).cover?(res.code.to_i)
|
|
35
|
+
end
|
|
36
|
+
private_class_method :probe
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/circle_so.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "circle_so/version"
|
|
4
|
+
require_relative "circle_so/errors"
|
|
5
|
+
require_relative "circle_so/resources/base"
|
|
6
|
+
require_relative "circle_so/resources/access_groups"
|
|
7
|
+
require_relative "circle_so/resources/chat_preferences"
|
|
8
|
+
require_relative "circle_so/resources/comments"
|
|
9
|
+
require_relative "circle_so/resources/community"
|
|
10
|
+
require_relative "circle_so/resources/community_members"
|
|
11
|
+
require_relative "circle_so/resources/community_segments"
|
|
12
|
+
require_relative "circle_so/resources/course_lessons"
|
|
13
|
+
require_relative "circle_so/resources/course_sections"
|
|
14
|
+
require_relative "circle_so/resources/direct_uploads"
|
|
15
|
+
require_relative "circle_so/resources/embeds"
|
|
16
|
+
require_relative "circle_so/resources/event_attendees"
|
|
17
|
+
require_relative "circle_so/resources/events"
|
|
18
|
+
require_relative "circle_so/resources/flagged_contents"
|
|
19
|
+
require_relative "circle_so/resources/forms"
|
|
20
|
+
require_relative "circle_so/resources/gamification"
|
|
21
|
+
require_relative "circle_so/resources/images"
|
|
22
|
+
require_relative "circle_so/resources/invitation_links"
|
|
23
|
+
require_relative "circle_so/resources/member_tags"
|
|
24
|
+
require_relative "circle_so/resources/messages"
|
|
25
|
+
require_relative "circle_so/resources/page_profile_fields"
|
|
26
|
+
require_relative "circle_so/resources/posts"
|
|
27
|
+
require_relative "circle_so/resources/profile_fields"
|
|
28
|
+
require_relative "circle_so/resources/search"
|
|
29
|
+
require_relative "circle_so/resources/space_group_members"
|
|
30
|
+
require_relative "circle_so/resources/space_groups"
|
|
31
|
+
require_relative "circle_so/resources/space_members"
|
|
32
|
+
require_relative "circle_so/resources/spaces"
|
|
33
|
+
require_relative "circle_so/resources/tagged_members"
|
|
34
|
+
require_relative "circle_so/resources/topics"
|
|
35
|
+
require_relative "circle_so/client"
|
|
36
|
+
require_relative "circle_so/token"
|
|
37
|
+
|
|
38
|
+
module CircleSo
|
|
39
|
+
# Convenience constructor: CircleSo.client(token: "...")
|
|
40
|
+
def self.client(**options) = Client.new(**options)
|
|
41
|
+
|
|
42
|
+
# Three-state token probe: :v2, :v1, or :invalid. See CircleSo::Token.
|
|
43
|
+
def self.detect_token_version(token, **options) = Token.detect_version(token, **options)
|
|
44
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: circle-so
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Lawrence Lin
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Zero-dependency Ruby client for the Circle.so Admin API v2 — members,
|
|
13
|
+
spaces, space groups, access groups, posts, events, courses and more, with classified
|
|
14
|
+
errors and token version detection.
|
|
15
|
+
email:
|
|
16
|
+
- lawrence@kaik.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/circle-so.rb
|
|
25
|
+
- lib/circle_so.rb
|
|
26
|
+
- lib/circle_so/client.rb
|
|
27
|
+
- lib/circle_so/errors.rb
|
|
28
|
+
- lib/circle_so/resources/access_groups.rb
|
|
29
|
+
- lib/circle_so/resources/base.rb
|
|
30
|
+
- lib/circle_so/resources/chat_preferences.rb
|
|
31
|
+
- lib/circle_so/resources/comments.rb
|
|
32
|
+
- lib/circle_so/resources/community.rb
|
|
33
|
+
- lib/circle_so/resources/community_members.rb
|
|
34
|
+
- lib/circle_so/resources/community_segments.rb
|
|
35
|
+
- lib/circle_so/resources/course_lessons.rb
|
|
36
|
+
- lib/circle_so/resources/course_sections.rb
|
|
37
|
+
- lib/circle_so/resources/direct_uploads.rb
|
|
38
|
+
- lib/circle_so/resources/embeds.rb
|
|
39
|
+
- lib/circle_so/resources/event_attendees.rb
|
|
40
|
+
- lib/circle_so/resources/events.rb
|
|
41
|
+
- lib/circle_so/resources/flagged_contents.rb
|
|
42
|
+
- lib/circle_so/resources/forms.rb
|
|
43
|
+
- lib/circle_so/resources/gamification.rb
|
|
44
|
+
- lib/circle_so/resources/images.rb
|
|
45
|
+
- lib/circle_so/resources/invitation_links.rb
|
|
46
|
+
- lib/circle_so/resources/member_tags.rb
|
|
47
|
+
- lib/circle_so/resources/messages.rb
|
|
48
|
+
- lib/circle_so/resources/page_profile_fields.rb
|
|
49
|
+
- lib/circle_so/resources/posts.rb
|
|
50
|
+
- lib/circle_so/resources/profile_fields.rb
|
|
51
|
+
- lib/circle_so/resources/search.rb
|
|
52
|
+
- lib/circle_so/resources/space_group_members.rb
|
|
53
|
+
- lib/circle_so/resources/space_groups.rb
|
|
54
|
+
- lib/circle_so/resources/space_members.rb
|
|
55
|
+
- lib/circle_so/resources/spaces.rb
|
|
56
|
+
- lib/circle_so/resources/tagged_members.rb
|
|
57
|
+
- lib/circle_so/resources/topics.rb
|
|
58
|
+
- lib/circle_so/token.rb
|
|
59
|
+
- lib/circle_so/version.rb
|
|
60
|
+
homepage: https://github.com/linyiru/circle-so
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/linyiru/circle-so
|
|
65
|
+
source_code_uri: https://github.com/linyiru/circle-so
|
|
66
|
+
changelog_uri: https://github.com/linyiru/circle-so/blob/main/CHANGELOG.md
|
|
67
|
+
bug_tracker_uri: https://github.com/linyiru/circle-so/issues
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 3.1.0
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 3.6.9
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Ruby client for the Circle.so Admin API v2
|
|
86
|
+
test_files: []
|