discourse_api 0.45.0 → 0.48.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -17
  3. data/.gitignore +0 -19
  4. data/CHANGELOG.md +39 -0
  5. data/discourse_api.gemspec +4 -4
  6. data/examples/bookmark_topic.rb +15 -0
  7. data/examples/category.rb +3 -0
  8. data/examples/invite_users.rb +21 -3
  9. data/examples/notifications.rb +18 -0
  10. data/examples/topic_lists.rb +3 -0
  11. data/lib/discourse_api/api/categories.rb +37 -11
  12. data/lib/discourse_api/api/dashboard.rb +2 -2
  13. data/lib/discourse_api/api/groups.rb +15 -4
  14. data/lib/discourse_api/api/invite.rb +67 -2
  15. data/lib/discourse_api/api/notifications.rb +5 -2
  16. data/lib/discourse_api/api/private_messages.rb +10 -3
  17. data/lib/discourse_api/api/search.rb +1 -1
  18. data/lib/discourse_api/api/site_settings.rb +3 -3
  19. data/lib/discourse_api/api/topics.rb +30 -2
  20. data/lib/discourse_api/api/users.rb +4 -0
  21. data/lib/discourse_api/client.rb +13 -1
  22. data/lib/discourse_api/error.rb +3 -0
  23. data/lib/discourse_api/single_sign_on.rb +6 -6
  24. data/lib/discourse_api/version.rb +1 -1
  25. data/spec/discourse_api/api/categories_spec.rb +90 -0
  26. data/spec/discourse_api/api/groups_spec.rb +29 -0
  27. data/spec/discourse_api/api/invite_spec.rb +123 -0
  28. data/spec/discourse_api/api/private_messages_spec.rb +4 -4
  29. data/spec/discourse_api/api/search_spec.rb +2 -2
  30. data/spec/discourse_api/api/topics_spec.rb +69 -3
  31. data/spec/discourse_api/api/users_spec.rb +14 -0
  32. data/spec/discourse_api/client_spec.rb +21 -0
  33. data/spec/fixtures/notification_success.json +3 -0
  34. data/spec/fixtures/retrieve_invite.json +116 -0
  35. data/spec/fixtures/top.json +108 -0
  36. data/spec/fixtures/topic_posts.json +1 -0
  37. metadata +17 -7
@@ -15,8 +15,7 @@ module DiscourseApi
15
15
  #NONCE_EXPIRY_TIME = 10.minutes # minutes is a rails method and is causing an error. Is this needed in the api?
16
16
 
17
17
  attr_accessor(*ACCESSORS)
18
- attr_accessor :custom_fields
19
- attr_writer :sso_secret, :sso_url
18
+ attr_writer :custom_fields, :sso_secret, :sso_url
20
19
 
21
20
  def self.sso_secret
22
21
  raise RuntimeError, "sso_secret not implemented on class, be sure to set it on instance"
@@ -27,7 +26,6 @@ module DiscourseApi
27
26
  end
28
27
 
29
28
  def self.parse_hash(payload)
30
- payload
31
29
  sso = new
32
30
 
33
31
  sso.sso_secret = payload.delete(:sso_secret)
@@ -40,11 +38,13 @@ module DiscourseApi
40
38
  if BOOLS.include? k
41
39
  val = ["true", "false"].include?(val) ? val == "true" : nil
42
40
  end
43
- val = Array(val) if ARRAYS.include?(k) && !val.nil?
41
+ val = val.split(",") if ARRAYS.include?(k) && !val.nil?
44
42
  sso.send("#{k}=", val)
45
43
  end
44
+
46
45
  # Set custom_fields
47
46
  sso.custom_fields = payload[:custom_fields]
47
+
48
48
  # Add custom_fields (old format)
49
49
  payload.each do |k, v|
50
50
  if field = k[/^custom\.(.+)$/, 1]
@@ -79,7 +79,7 @@ module DiscourseApi
79
79
  if BOOLS.include? k
80
80
  val = ["true", "false"].include?(val) ? val == "true" : nil
81
81
  end
82
- val = Array(val) if ARRAYS.include?(k) && !val.nil?
82
+ val = val.split(",") if ARRAYS.include?(k) && !val.nil?
83
83
  sso.send("#{k}=", val)
84
84
  end
85
85
 
@@ -127,7 +127,7 @@ module DiscourseApi
127
127
 
128
128
  ACCESSORS.each do |k|
129
129
  next if (val = send k) == nil
130
- payload[k] = val
130
+ payload[k] = val
131
131
  end
132
132
 
133
133
  if @custom_fields
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module DiscourseApi
3
- VERSION = "0.45.0"
3
+ VERSION = "0.48.0"
4
4
  end
@@ -28,6 +28,25 @@ describe DiscourseApi::API::Categories do
28
28
  end
29
29
  end
30
30
 
31
+ describe "#categories_full" do
32
+ before do
33
+ stub_get("#{host}/categories.json")
34
+ .to_return(body: fixture("categories.json"), headers: { content_type: "application/json" })
35
+ end
36
+
37
+ it "requests the correct resource" do
38
+ subject.categories
39
+ expect(a_get("#{host}/categories.json")).to have_been_made
40
+ end
41
+
42
+ it "returns the entire categories response" do
43
+ categories = subject.categories_full
44
+ expect(categories).to be_a Hash
45
+ expect(categories).to have_key 'category_list'
46
+ expect(categories).to have_key 'featured_users'
47
+ end
48
+ end
49
+
31
50
  describe '#category_latest_topics' do
32
51
  before do
33
52
  stub_get("#{host}/c/category-slug/l/latest.json")
@@ -40,6 +59,20 @@ describe DiscourseApi::API::Categories do
40
59
  end
41
60
  end
42
61
 
62
+ describe '#category_latest_topics_full' do
63
+ before do
64
+ stub_get("#{host}/c/category-slug/l/latest.json")
65
+ .to_return(body: fixture("category_latest_topics.json"), headers: { content_type: "application/json" })
66
+ end
67
+
68
+ it "returns the entire latest topics in a category response" do
69
+ latest_topics = subject.category_latest_topics_full(category_slug: 'category-slug')
70
+ expect(latest_topics).to be_a Hash
71
+ expect(latest_topics).to have_key 'topic_list'
72
+ expect(latest_topics).to have_key 'users'
73
+ end
74
+ end
75
+
43
76
  describe '#category_top_topics' do
44
77
  before do
45
78
  stub_get("#{host}/c/category-slug/l/top.json")
@@ -55,6 +88,23 @@ describe DiscourseApi::API::Categories do
55
88
  end
56
89
  end
57
90
 
91
+ describe '#category_top_topics_full' do
92
+ before do
93
+ stub_get("#{host}/c/category-slug/l/top.json")
94
+ .to_return(
95
+ body: fixture("category_topics.json"),
96
+ headers: { content_type: "application/json" }
97
+ )
98
+ end
99
+
100
+ it "returns the entire top topics in a category response" do
101
+ topics = subject.category_top_topics_full('category-slug')
102
+ expect(topics).to be_a Hash
103
+ expect(topics).to have_key 'topic_list'
104
+ expect(topics).to have_key 'users'
105
+ end
106
+ end
107
+
58
108
  describe '#category_new_topics' do
59
109
  before do
60
110
  stub_get("#{host}/c/category-slug/l/new.json")
@@ -70,6 +120,23 @@ describe DiscourseApi::API::Categories do
70
120
  end
71
121
  end
72
122
 
123
+ describe '#category_new_topics_full' do
124
+ before do
125
+ stub_get("#{host}/c/category-slug/l/new.json")
126
+ .to_return(
127
+ body: fixture("category_topics.json"),
128
+ headers: { content_type: "application/json" }
129
+ )
130
+ end
131
+
132
+ it "returns the new topics in a category" do
133
+ topics = subject.category_new_topics_full('category-slug')
134
+ expect(topics).to be_a Hash
135
+ expect(topics).to have_key 'topic_list'
136
+ expect(topics).to have_key 'users'
137
+ end
138
+ end
139
+
73
140
  describe '#category_new_category' do
74
141
  before do
75
142
  stub_post("#{host}/categories")
@@ -85,4 +152,27 @@ describe DiscourseApi::API::Categories do
85
152
  end
86
153
  end
87
154
 
155
+ describe "#category_set_user_notification" do
156
+ before do
157
+ stub_post("#{host}/category/1/notifications").to_return(body: fixture("notification_success.json"), headers: { content_type: "application/json" })
158
+ end
159
+
160
+ it "makes the post request" do
161
+ response = subject.category_set_user_notification(id: 1, notification_level: 3)
162
+ expect(a_post("#{host}/category/1/notifications")).to have_been_made
163
+ expect(response['success']).to eq('OK')
164
+ end
165
+ end
166
+
167
+ describe "#category_set_user_notification_level" do
168
+ before do
169
+ stub_post("#{host}/category/1/notifications").to_return(body: fixture("notification_success.json"), headers: { content_type: "application/json" })
170
+ end
171
+
172
+ it "makes the post request" do
173
+ response = subject.category_set_user_notification_level(1, notification_level: 3)
174
+ expect(a_post("#{host}/category/1/notifications").with(body: "notification_level=3")).to have_been_made
175
+ expect(response['success']).to eq('OK')
176
+ end
177
+ end
88
178
  end
@@ -91,6 +91,35 @@ describe DiscourseApi::API::Groups do
91
91
  end
92
92
  end
93
93
 
94
+ describe "add owners" do
95
+ let(:url) { "#{host}/admin/groups/123/owners.json" }
96
+
97
+ before do
98
+ stub_put(url)
99
+ end
100
+
101
+ it "makes the member an owner" do
102
+ subject.group_add_owners(123, usernames: "sam")
103
+ params = escape_params("group[usernames]" => "sam")
104
+ expect(a_request(:put, "#{host}/admin/groups/123/owners.json").
105
+ with(body: params)
106
+ ).to have_been_made
107
+ end
108
+ end
109
+
110
+ describe "remove owners" do
111
+ let(:url) { "#{host}/admin/groups/123/owners.json?group%5Busernames%5D=sam" }
112
+
113
+ before do
114
+ stub_delete(url)
115
+ end
116
+
117
+ it "removes the owner role from the group member" do
118
+ subject.group_remove_owners(123, usernames: "sam")
119
+ expect(a_delete(url)).to have_been_made
120
+ end
121
+ end
122
+
94
123
  describe "group members" do
95
124
  it "list members" do
96
125
  stub_get("#{host}/groups/mygroup/members.json?limit=100&offset=0").to_return(body: fixture("members_0.json"), headers: { content_type: "application/json" })
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe DiscourseApi::API::Invite do
5
+ subject { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }
6
+
7
+ describe "#invite_user" do
8
+ before do
9
+ stub_post("#{host}/invites").to_return(body: fixture("topic_invite_user.json"), headers: { content_type: "application/json" })
10
+ end
11
+
12
+ it "requests the correct resource" do
13
+ subject.invite_user(email: "fake_user@example.com", group_ids: "41,42")
14
+ expect(a_post("#{host}/invites")).to have_been_made
15
+ end
16
+
17
+ it "returns success" do
18
+ response = subject.invite_user(email: "fake_user@example.com", group_ids: "41,42")
19
+ expect(response).to be_a Hash
20
+ expect(response['success']).to be_truthy
21
+ end
22
+ end
23
+
24
+ describe "#update_invite" do
25
+ before do
26
+ stub_put("#{host}/invites/27").to_return(body: fixture("topic_invite_user.json"), headers: { content_type: "application/json" })
27
+ end
28
+
29
+ it "updates invite" do
30
+ subject.update_invite(27, email: "namee@example.com")
31
+ expect(a_put("#{host}/invites/27")).to have_been_made
32
+ end
33
+ end
34
+
35
+ describe "#retrieve_invite" do
36
+ before do
37
+ stub_get("#{host}/invites/retrieve.json?email=foo@bar.com").to_return(body: fixture("retrieve_invite.json"), headers: { content_type: "application/json" })
38
+ end
39
+
40
+ it "requests the correct resource" do
41
+ subject.retrieve_invite(email: "foo@bar.com")
42
+ expect(a_get("#{host}/invites/retrieve.json?email=foo@bar.com")).to have_been_made
43
+ end
44
+
45
+ it "returns the requested topics" do
46
+ invites = subject.retrieve_invite(email: "foo@bar.com")
47
+ expect(invites).to be_an Hash
48
+ end
49
+
50
+ it "returns the requested invite" do
51
+ invites = subject.retrieve_invite(email: "foo@bar.com")
52
+ expect(invites["email"]).to eq("foo@bar.com")
53
+ expect(invites).to have_key("invite_key")
54
+ end
55
+ end
56
+
57
+ describe "#destroy_all_expired_invites" do
58
+ let(:url) { "#{host}/invites/destroy-all-expired" }
59
+
60
+ before do
61
+ stub_post(url)
62
+ .to_return(
63
+ body: '{"success": "OK"}',
64
+ headers: { content_type: "application/json" }
65
+ )
66
+ end
67
+
68
+ it "destroys all expired invites" do
69
+ subject.destroy_all_expired_invites
70
+ expect(a_post(url)).to have_been_made
71
+ end
72
+ end
73
+
74
+ describe "#resend_all_invites" do
75
+ let(:url) { "#{host}/invites/reinvite-all" }
76
+
77
+ before do
78
+ stub_post(url)
79
+ .to_return(
80
+ body: '{"success": "OK"}',
81
+ headers: { content_type: "application/json" }
82
+ )
83
+ end
84
+
85
+ it "resends all invites" do
86
+ subject.resend_all_invites
87
+ expect(a_post(url)).to have_been_made
88
+ end
89
+ end
90
+
91
+ describe "#resend_invite" do
92
+ let(:url) { "#{host}/invites/reinvite" }
93
+
94
+ before do
95
+ stub_post(url)
96
+ .to_return(
97
+ body: '{"success": "OK"}',
98
+ headers: { content_type: "application/json" }
99
+ )
100
+ end
101
+
102
+ it "resends invite" do
103
+ subject.resend_invite("foo@bar.com")
104
+ expect(a_post(url)).to have_been_made
105
+ end
106
+ end
107
+
108
+ describe "#destroy_invite" do
109
+ let(:url) { "#{host}/invites?id=27" }
110
+
111
+ before do
112
+ stub_delete(url).to_return(
113
+ body: '{"success": "OK"}',
114
+ headers: { content_type: "application/json" }
115
+ )
116
+ end
117
+
118
+ it "destroys the invite" do
119
+ subject.destroy_invite(27)
120
+ expect(a_delete(url)).to have_been_made
121
+ end
122
+ end
123
+ end
@@ -36,19 +36,19 @@ describe DiscourseApi::API::PrivateMessages do
36
36
  end
37
37
  end
38
38
 
39
- describe '#create_private_message' do
39
+ describe '#create_pm' do
40
40
  before do
41
41
  stub_post("#{host}/posts")
42
- subject.create_private_message(
42
+ subject.create_pm(
43
43
  title: "Confidential: Hello World!",
44
44
  raw: "This is the raw markdown for my private message",
45
- target_usernames: "user1,user2"
45
+ target_recipients: "user1,user2"
46
46
  )
47
47
  end
48
48
 
49
49
  it "makes a create private message request" do
50
50
  expect(a_post("#{host}/posts").with(body:
51
- 'archetype=private_message&raw=This+is+the+raw+markdown+for+my+private+message&target_usernames=user1%2Cuser2&title=Confidential%3A+Hello+World%21')
51
+ 'archetype=private_message&raw=This+is+the+raw+markdown+for+my+private+message&target_recipients=user1%2Cuser2&title=Confidential%3A+Hello+World%21')
52
52
  ).to have_been_made
53
53
  end
54
54
  end
@@ -6,12 +6,12 @@ describe DiscourseApi::API::Search do
6
6
 
7
7
  describe "#search" do
8
8
  before do
9
- stub_get("#{host}/search/query").with(query: { term: "test" }).to_return(body: fixture("search.json"), headers: { content_type: "application/json" })
9
+ stub_get("#{host}/search").with(query: { q: "test" }).to_return(body: fixture("search.json"), headers: { content_type: "application/json" })
10
10
  end
11
11
 
12
12
  it "requests the correct resource" do
13
13
  subject.search("test")
14
- expect(a_get("#{host}/search/query").with(query: { term: "test" })).to have_been_made
14
+ expect(a_get("#{host}/search").with(query: { q: "test" })).to have_been_made
15
15
  end
16
16
 
17
17
  it "returns the requested search" do
@@ -15,18 +15,18 @@ describe DiscourseApi::API::Topics do
15
15
  end
16
16
  end
17
17
 
18
- describe "#invite_user_to_topic" do
18
+ describe "#invite_to_topic" do
19
19
  before do
20
20
  stub_post("#{host}/t/12/invite").to_return(body: fixture("topic_invite_user.json"), headers: { content_type: "application/json" })
21
21
  end
22
22
 
23
23
  it "requests the correct resource" do
24
- subject.invite_user_to_topic(email: "fake_user@example.com", topic_id: 12)
24
+ subject.invite_to_topic(12, email: "fake_user@example.com")
25
25
  expect(a_post("#{host}/t/12/invite")).to have_been_made
26
26
  end
27
27
 
28
28
  it "returns success" do
29
- response = subject.invite_user_to_topic(email: "fake_user@example.com", topic_id: 12)
29
+ response = subject.invite_to_topic(12, email: "fake_user@example.com")
30
30
  expect(response).to be_a Hash
31
31
  expect(response['success']).to be_truthy
32
32
  end
@@ -55,6 +55,29 @@ describe DiscourseApi::API::Topics do
55
55
  end
56
56
  end
57
57
 
58
+ describe "#top_topics" do
59
+ before do
60
+ stub_get("#{host}/top.json").to_return(body: fixture("top.json"), headers: { content_type: "application/json" })
61
+ end
62
+
63
+ it "requests the correct resource" do
64
+ subject.top_topics
65
+ expect(a_get("#{host}/top.json")).to have_been_made
66
+ end
67
+
68
+ it "returns the requested topics" do
69
+ topics = subject.top_topics
70
+ expect(topics).to be_an Array
71
+ expect(topics.first).to be_a Hash
72
+ end
73
+
74
+ it "can take a hash param" do
75
+ topics = subject.top_topics({})
76
+ expect(topics).to be_an Array
77
+ expect(topics.first).to be_a Hash
78
+ end
79
+ end
80
+
58
81
  describe "#new_topics" do
59
82
  before do
60
83
  stub_get("#{host}/new.json").to_return(body: fixture("new.json"), headers: { content_type: "application/json" })
@@ -147,6 +170,14 @@ describe DiscourseApi::API::Topics do
147
170
  expect(body['post_stream']['posts']).to be_an Array
148
171
  expect(body['post_stream']['posts'].first).to be_a Hash
149
172
  end
173
+
174
+ it "can retrieve a topic posts' raw attribute" do
175
+ body = subject.topic_posts(57, [123], { include_raw: true })
176
+ expect(body).to be_a Hash
177
+ expect(body['post_stream']['posts']).to be_an Array
178
+ expect(body['post_stream']['posts'].first).to be_a Hash
179
+ expect(body['post_stream']['posts'].first['raw']).to be_an Array
180
+ end
150
181
  end
151
182
 
152
183
  describe "#create_topic_with_tags" do
@@ -166,4 +197,39 @@ describe DiscourseApi::API::Topics do
166
197
  end
167
198
  end
168
199
 
200
+ describe "#topic_set_user_notification_level" do
201
+ before do
202
+ stub_post("#{host}/t/1/notifications").to_return(body: fixture("notification_success.json"), headers: { content_type: "application/json" })
203
+ end
204
+
205
+ it "makes the post request" do
206
+ response = subject.topic_set_user_notification_level(1, notification_level: 3)
207
+ expect(a_post("#{host}/t/1/notifications").with(body: "notification_level=3")).to have_been_made
208
+ expect(response['success']).to eq('OK')
209
+ end
210
+ end
211
+
212
+ describe "#bookmark_topic" do
213
+ before do
214
+ stub_put("#{host}/t/1/bookmark.json").to_return(body: "", headers: { content_type: "application/json" })
215
+ end
216
+
217
+ it "makes the put request" do
218
+ response = subject.bookmark_topic(1)
219
+ expect(a_put("#{host}/t/1/bookmark.json")).to have_been_made
220
+ expect(response.body).to eq(nil)
221
+ end
222
+ end
223
+
224
+ describe "#remove_topic_bookmark" do
225
+ before do
226
+ stub_put("#{host}/t/1/remove_bookmarks.json").to_return(body: "", headers: { content_type: "application/json" })
227
+ end
228
+
229
+ it "makes the put request" do
230
+ response = subject.remove_topic_bookmark(1)
231
+ expect(a_put("#{host}/t/1/remove_bookmarks.json")).to have_been_made
232
+ expect(response.body).to eq(nil)
233
+ end
234
+ end
169
235
  end
@@ -306,6 +306,20 @@ describe DiscourseApi::API::Users do
306
306
  end
307
307
  end
308
308
 
309
+ describe "#anonymize" do
310
+ before do
311
+ url = "#{host}/admin/users/11/anonymize"
312
+ stub_put(url).to_return(body: '', status: 200)
313
+ end
314
+
315
+ it "makes the correct put request" do
316
+ result = subject.anonymize(11)
317
+ url = "#{host}/admin/users/11/anonymize"
318
+ expect(a_put(url)).to have_been_made
319
+ expect(result.status).to eq(200)
320
+ end
321
+ end
322
+
309
323
  describe "#delete_user" do
310
324
  before do
311
325
  url = "#{host}/admin/users/11.json?delete_posts=true"
@@ -28,6 +28,27 @@ describe DiscourseApi::Client do
28
28
  end
29
29
  end
30
30
 
31
+ describe "#timeout" do
32
+ context 'custom timeout' do
33
+ it "is set to Faraday connection" do
34
+ expect(subject.send(:connection).options.timeout).to eq(30)
35
+ end
36
+ end
37
+
38
+ context 'default timeout' do
39
+ it "is set to Faraday connection" do
40
+ subject.timeout = 25
41
+ expect(subject.send(:connection).options.timeout).to eq(25)
42
+ end
43
+ end
44
+
45
+ it "raises DiscourseApi::Timeout" do
46
+ stub_get("#{host}/t/1.json").to_timeout
47
+
48
+ expect { subject.topic(1) }.to raise_error(DiscourseApi::Timeout)
49
+ end
50
+ end
51
+
31
52
  describe "#api_key" do
32
53
  it "is publically accessible" do
33
54
  subject.api_key = "test_d7fd0429940"
@@ -0,0 +1,3 @@
1
+ {
2
+ "success": "OK"
3
+ }
@@ -0,0 +1,116 @@
1
+ {
2
+ "id": 26,
3
+ "invite_key": "CGUHjNC4Na",
4
+ "link": "http://localhost:3000/invites/CGUHjNC4Na",
5
+ "email": "foo@bar.com",
6
+ "emailed": true,
7
+ "custom_message": null,
8
+ "created_at": "2021-05-07T20:48:14.278Z",
9
+ "updated_at": "2021-05-07T20:48:14.278Z",
10
+ "expires_at": "2021-06-06T20:48:14.278Z",
11
+ "expired": false,
12
+ "topics": [
13
+ {
14
+ "id": 1,
15
+ "title": "About the Site Feedback category",
16
+ "fancy_title": "About the Site Feedback category",
17
+ "slug": "about-the-site-feedback-category",
18
+ "posts_count": 1
19
+ }
20
+ ],
21
+ "groups": [
22
+ {
23
+ "id": 1,
24
+ "automatic": true,
25
+ "name": "admins",
26
+ "display_name": "admins",
27
+ "user_count": 1,
28
+ "mentionable_level": 0,
29
+ "messageable_level": 0,
30
+ "visibility_level": 1,
31
+ "primary_group": false,
32
+ "title": null,
33
+ "grant_trust_level": null,
34
+ "incoming_email": null,
35
+ "has_messages": true,
36
+ "flair_url": null,
37
+ "flair_bg_color": null,
38
+ "flair_color": null,
39
+ "bio_raw": "",
40
+ "bio_cooked": null,
41
+ "bio_excerpt": null,
42
+ "public_admission": false,
43
+ "public_exit": false,
44
+ "allow_membership_requests": false,
45
+ "full_name": null,
46
+ "default_notification_level": 3,
47
+ "membership_request_template": null,
48
+ "members_visibility_level": 0,
49
+ "can_see_members": true,
50
+ "can_admin_group": true,
51
+ "publish_read_state": false
52
+ },
53
+ {
54
+ "id": 2,
55
+ "automatic": true,
56
+ "name": "moderators",
57
+ "display_name": "moderators",
58
+ "user_count": 1,
59
+ "mentionable_level": 0,
60
+ "messageable_level": 99,
61
+ "visibility_level": 1,
62
+ "primary_group": false,
63
+ "title": null,
64
+ "grant_trust_level": null,
65
+ "incoming_email": null,
66
+ "has_messages": true,
67
+ "flair_url": null,
68
+ "flair_bg_color": null,
69
+ "flair_color": null,
70
+ "bio_raw": null,
71
+ "bio_cooked": null,
72
+ "bio_excerpt": null,
73
+ "public_admission": false,
74
+ "public_exit": false,
75
+ "allow_membership_requests": false,
76
+ "full_name": null,
77
+ "default_notification_level": 2,
78
+ "membership_request_template": null,
79
+ "members_visibility_level": 0,
80
+ "can_see_members": true,
81
+ "can_admin_group": true,
82
+ "publish_read_state": false
83
+ },
84
+ {
85
+ "id": 3,
86
+ "automatic": true,
87
+ "name": "staff",
88
+ "display_name": "staff",
89
+ "user_count": 2,
90
+ "mentionable_level": 0,
91
+ "messageable_level": 0,
92
+ "visibility_level": 1,
93
+ "primary_group": false,
94
+ "title": null,
95
+ "grant_trust_level": null,
96
+ "incoming_email": null,
97
+ "has_messages": false,
98
+ "flair_url": null,
99
+ "flair_bg_color": null,
100
+ "flair_color": null,
101
+ "bio_raw": null,
102
+ "bio_cooked": null,
103
+ "bio_excerpt": null,
104
+ "public_admission": false,
105
+ "public_exit": false,
106
+ "allow_membership_requests": false,
107
+ "full_name": null,
108
+ "default_notification_level": 3,
109
+ "membership_request_template": null,
110
+ "members_visibility_level": 0,
111
+ "can_see_members": true,
112
+ "can_admin_group": true,
113
+ "publish_read_state": false
114
+ }
115
+ ]
116
+ }