Full Code of discourse/discourse_api for AI

main 6e5e71b692bb cached
139 files
306.0 KB
93.2k tokens
249 symbols
1 requests
Download .txt
Showing preview only (338K chars total). Download the full file or copy to clipboard to get everything.
Repository: discourse/discourse_api
Branch: main
Commit: 6e5e71b692bb
Files: 139
Total size: 306.0 KB

Directory structure:
gitextract_ygzwukcj/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .rubocop.yml
├── .streerc
├── CHANGELOG.md
├── Gemfile
├── Guardfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── config_example.yml
├── discourse_api.gemspec
├── examples/
│   ├── backups.rb
│   ├── badges.rb
│   ├── bookmark_topic.rb
│   ├── category.rb
│   ├── change_topic_status.rb
│   ├── create_private_message.rb
│   ├── create_topic.rb
│   ├── create_update_category.rb
│   ├── create_user.rb
│   ├── dashboard.rb
│   ├── disposable_invite_tokens.rb
│   ├── example.rb
│   ├── group_set_user_notification_level.rb
│   ├── groups.rb
│   ├── invite_users.rb
│   ├── manage_api_keys.rb
│   ├── notifications.rb
│   ├── polls.rb
│   ├── post_action.rb
│   ├── search.rb
│   ├── sent_private_messages.rb
│   ├── sso.rb
│   ├── topic_lists.rb
│   ├── update_user.rb
│   └── upload_file.rb
├── lib/
│   ├── discourse_api/
│   │   ├── api/
│   │   │   ├── api_key.rb
│   │   │   ├── backups.rb
│   │   │   ├── badges.rb
│   │   │   ├── categories.rb
│   │   │   ├── dashboard.rb
│   │   │   ├── email.rb
│   │   │   ├── groups.rb
│   │   │   ├── invite.rb
│   │   │   ├── notifications.rb
│   │   │   ├── params.rb
│   │   │   ├── polls.rb
│   │   │   ├── posts.rb
│   │   │   ├── private_messages.rb
│   │   │   ├── search.rb
│   │   │   ├── site_settings.rb
│   │   │   ├── sso.rb
│   │   │   ├── tags.rb
│   │   │   ├── topics.rb
│   │   │   ├── uploads.rb
│   │   │   ├── user_actions.rb
│   │   │   └── users.rb
│   │   ├── client.rb
│   │   ├── error.rb
│   │   ├── example_helper.rb
│   │   ├── single_sign_on.rb
│   │   └── version.rb
│   └── discourse_api.rb
└── spec/
    ├── discourse_api/
    │   ├── api/
    │   │   ├── api_key_spec.rb
    │   │   ├── backups_spec.rb
    │   │   ├── badges_spec.rb
    │   │   ├── categories_spec.rb
    │   │   ├── email_spec.rb
    │   │   ├── groups_spec.rb
    │   │   ├── invite_spec.rb
    │   │   ├── notifications_spec.rb
    │   │   ├── params_spec.rb
    │   │   ├── polls_spec.rb
    │   │   ├── posts_spec.rb
    │   │   ├── private_messages_spec.rb
    │   │   ├── search_spec.rb
    │   │   ├── site_settings_spec.rb
    │   │   ├── sso_spec.rb
    │   │   ├── topics_spec.rb
    │   │   ├── uploads_spec.rb
    │   │   ├── user_actions_spec.rb
    │   │   └── users_spec.rb
    │   ├── client_spec.rb
    │   └── single_sign_on_spec.rb
    ├── fixtures/
    │   ├── admin_user.json
    │   ├── api_key.json
    │   ├── backups.json
    │   ├── badges.json
    │   ├── categories.json
    │   ├── category_latest_topics.json
    │   ├── category_topics.json
    │   ├── create_topic_with_tags.json
    │   ├── email_list_all.json
    │   ├── email_settings.json
    │   ├── group.json
    │   ├── groups.json
    │   ├── hot.json
    │   ├── latest.json
    │   ├── list_api_keys.json
    │   ├── members_0.json
    │   ├── members_1.json
    │   ├── members_2.json
    │   ├── new.json
    │   ├── notification_success.json
    │   ├── notifications.json
    │   ├── polls_toggle_status.json
    │   ├── polls_vote.json
    │   ├── polls_voters.json
    │   ├── post.json
    │   ├── post_action_users.json
    │   ├── posts_before.json
    │   ├── posts_latest.json
    │   ├── private_messages.json
    │   ├── regenerate_api_key.json
    │   ├── replies.json
    │   ├── replies_and_topics.json
    │   ├── retrieve_invite.json
    │   ├── search.json
    │   ├── top.json
    │   ├── topic.json
    │   ├── topic_invite_user.json
    │   ├── topic_posts.json
    │   ├── topics_created_by.json
    │   ├── update_trust_level.json
    │   ├── upload_avatar.json
    │   ├── upload_file.json
    │   ├── user.json
    │   ├── user_activate_success.json
    │   ├── user_badges.json
    │   ├── user_create_success.json
    │   ├── user_grant_admin.json
    │   ├── user_grant_moderator.json
    │   ├── user_list.json
    │   ├── user_log_out_success.json
    │   ├── user_update_avatar_success.json
    │   ├── user_update_user.json
    │   └── user_update_username.json
    └── spec_helper.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
          bundler-cache: true

      - name: Rubocop
        run: bundle exec rubocop

      - name: syntax_tree
        if: ${{ !cancelled() }}
        run: |
          set -E
          bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake')

  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]

    steps:
      - uses: actions/checkout@v4

      - name: Setup ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
          bundler-cache: true

      - name: Tests
        run: bundle exec rake test

  publish:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: [lint, test]
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Release Gem
        uses: discourse/publish-rubygems-action@v3
        env:
          RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
          GIT_EMAIL: team@discourse.org
          GIT_NAME: discoursebot


================================================
FILE: .gitignore
================================================
Gemfile.lock
coverage
/config.yml


================================================
FILE: .rubocop.yml
================================================
inherit_gem:
  rubocop-discourse: stree-compat.yml

Discourse/Plugins:
  Enabled: false


================================================
FILE: .streerc
================================================
--print-width=100
--plugins=plugin/trailing_comma,plugin/disable_auto_ternary


================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [2.1.0] - 2025-12-02
### Changed
- Add support for `external_id` in topic creation
### Added
- Add get topic url by `external_id`

## [2.0.1] - 2023-06-09
### Removed
- Invite admin method removed

## [2.0.0] - 2023-05-26
### Changed
- The package now requires ruby 2.7+
- The package now requires faraday 2.7+

## [1.1.0] - 2022-07-05
### Changed
- `DiscourseApi::SingleSignOn.parse` now raises `DiscourseApi::SingleSignOn::ParseError` (inherits from `RuntimeError` to preserve backward compatibility) instead of `RuntimeError` when there's a signature mismatch.
- `DiscourseApi::SingleSignOn.parse` now raises `DiscourseApi::SingleSignOn::MissingConfigError` (also inherits from `RuntimeError`) if `sso_secret` or `sso_url` are missing.

## [1.0.0] - 2022-05-01
### Changed
- The package now requires ruby 2.6+

## [0.48.1] - 2022-04-13
### Added
- New attributes for Discourse Connect (aka SSO)

## [0.48.0] - 2022-01-28
### Added
- `group_add_owners` method (#239)
- `group_remove_owners` method (#239)
- `anonymize` method (#241)

### Changed
- `DiscourseApi::Timeout` error now inherits from `DiscourseApi::Error` (#240)
- `DiscourseApi::SingleSignOn#groups` now returns an array of strings where each string is a group name, rather than an array with a single string that contains all the groups comma-concatenated (#243)

## [0.47.0] - 2021-07-19
### Added
- Update invite method
- Retrieve invite method
- Destroy all expired invites method
- Destroy invite method
- Resend all invites method
- Resend invite method
- Pass params to get notifications API

### Deprecated
- `invite_user_to_topic` has been deprecated, use `invite_to_topic` instead.
- `create_private_message` has been deprecated, use `create_pm` instead.

## [0.46.0] - 2021-04-12
### Added
- Allow bookmarking topics
- Add timeout to requests
- Add params to get_topic_posts

## [0.45.1] - 2021-03-11
### Added
- Fetch global top topics
- Allow setting topic notifications
- Return full category response

### Changed
- Use new search endpoint

## [0.45.0] - 2021-01-15
### Added
- Tag configuration in create_category/update_category
- Topic#change_owner
- Support passing approved to #create_user
### Changed
- API key methods use the latest endpoints

## [0.44.0] - 2020-11-13
### Fixed
- Updated `show_tag` method to use new route
### Removed
- Support for Ruby 2.3 and 2.4

## [0.43.1] - 2020-11-04
### Fixed
- Tagged version 0.43.0 got pushed without commit due to new master branch
  protections in github. No, code changes here just making sure tags align with
  commits.

## [0.43.0] - 2020-11-04
### Added
- Add pagination to list groups endpoint
### Deprecated
- `change_topic_status` has been deprecated, use `update_topic_status` instead.

## [0.42.0] - 2020-07-09
### Added
- Create topics with tags

## [0.41.0] - 2020-06-17
### Added
- Add basic auth support
### Fixed
- Fix SSO custom field prefixes
### Removed
- Obsolete api key endpoints

## [0.40.0] - 2020-05-07
### Fixed
- Add missing attributes to `sync_sso`
### Added
- Add delete category method

## [0.39.3] - 2020-04-30
### Fixed
- Add `reviewable_by_group_name` to categories

## [0.39.2] - 2020-04-30
### Fixed
- Add `members_visibility_level` to group

## [0.39.1] - 2020-03-27
### Fixed
- Ensure released gem version matches this commit

## [0.39.0] - 2020-03-27
### Added
- Get latest posts across topics via posts.json
- Allow  more options parameters when creating a category
- Don't require topic slug when updating topic status
- Example files now read config.yml file when present for client settings
### Fixed
- Issue with `topic_posts` and frozen strings
- Fixed some topic and category methods

## [0.38.0] - 2019-10-18
### Added
- Allow setting locale in SingleSignOn
- Optional param to group members to include owners as well as members

## [0.37.0] - 2019-09-23
### Added
- user-badges endpoint for full badges list
- expanded list of allowed messages
- grant/revoke moderation

## [0.36.0] - 2019-07-18
### Added
- Added poll methods
### Fixed
- Updated create topic example
- Fixed capitalization for header auth keys

## [0.35.0] - 2019-05-15
### Added
- Added `custom_fields` param to create/update category
- Added `frozen_string_literal: true` to all the files
- Added rubocop and all the changes that went along with it
### Fixed
- Allow `api_username` to be changed for an initialized client
- Update many of the `/users` routes to use the `/u` route
### Changed
- Changed `update_trust_level` to follow consistent method param syntax where
  you specify the id first followed by params

## [0.34.0] - 2019-04-02
### Added
- Header based authentication
### Removed
- Query param based authentication

## [0.33.0] - 2019-03-04
### Added
- Added a new method to update a users notification level in a category

## [0.32.0] - 2019-02-13
### Added
- Added a new method to update a users notification level in a group

## [0.31.0] - 2019-02-07
### Added
- Added `deactivate` method
- Added 201 and 204 as valid POST responses

## [0.30.0] - 2018-12-19
### Added
- Add params hash to `list_users`

## [0.29.0] - 2018-12-07
### Added
- Add `add_groups` and `remove_groups` to `sync_sso`

## [0.28.2] - 2018-11-26
### Fixed
- Updated arguments for suspending a user

## [0.28.1] - 2018-10-26
### Fixed
- Fixed non-URI chars in `check_username` method

## [0.28.0] - 2018-10-23
### Added
- Added `check_username` method

## [0.27.0] - 2018-09-14
### Added
- Added `site_settings_update` method

## [0.26.0] - 2018-09-10
### Added
- Added user `user_actions` endpoint so you can retrieve `user_replies` and
  `user_topics_and_replies`

## [0.25.0] - 2018-08-15
### Added
- Added ability to rescue certain error classes and inspect the response object

## [0.24.0] - 2018-05-30
### Added
- Added support for custom `user_fields` when creating a user

## [0.23.1] - 2018-05-24
### Fixed
- Can now change `api_username` without creating a new client

## [0.23.0] - 2018-05-24
### Added
- Added `delete_user` method

## [0.22.0] - 2018-05-04
### Added
- Support for subfolder paths

## [0.21.0] - 2018-04-23
### Fixed
- Update GET groups api route
- Update PUT groups api route

## [0.20.0] - 2017-12-13
### Added
- Add base error class
### Fixed
- Update SSO

## [0.19.0] - 2017-11-22
### Added
- Added optional `create_post` params

## [0.18.0] - 2017-10-17
### Added
- Added `update_group` API call
### Fixed
- Fixed params for create groups endpoint
- Fixed invite token API endpoint

## [0.17.0] - 2017-06-29
### Added
- Add title to SSO sync

## [0.16.1] - 2017-06-23
### Fixed
- `user_sso` should use `user_id` instead of `username`
- `upload_file` should also include optional `user_id` param

## [0.16.0] - 2017-05-14
### Added
- added `upload_file`
### Removed
- removed `upload_post_image`

## [0.15.0] - 2017-04-12
### Added
- added the ability to create private messages

## [0.14.1] - 2016-12-20
### Fixed
- allow for rack 2.0+ versions so that it doesn't clash with rails.

## [0.14.0] - 2016-10-30
### Added
- improved error responses by adding `NotFoundError`, `UnprocessableEntity`, and `TooManyRequests`
- added `delete_post` method

## [0.13.0] - 2016-10-09
### Added
- added `update_category`
- added `upload_post_image`

## [0.12.0] - 2016-10-06
### Added
- add endpoint for `/admin/users/{id}/suspend`
- add endpoint for `/admin/users/{id}/unsuspend`

## [0.11.0] - 2016-09-03
### Fixed
- add destination folder to backup download
- `post_action_users`

### Added
- `change_topic_status`
- set username of topic on creation

## [0.10.1] - 2016-05-04
### Fixed
- raise an error if search is empty
- fix /category path to be just /c
- return errors for category_latest_topics if there are some

## [0.10.0] - 2016-04-19
### Added
- group_members: Allows you to retrieve more than 100 users with pagination (offset &
  limit)
### Fixed
- Deprecation warning with SimpleCov
- updated rack dependency and added ruby 2.3 to travis config

## [0.9.1] - 2016-03-23
### Fixed
- topic and post like/flag need to use `:id`

## [0.9.0] - 2016-03-22
### Added
- can now like/flag topics and posts

## [0.8.1] - 2016-03-03
### Fixed
- enable use of discourse_api to make unauthenticated requests to discourse
  endpoints like /categories and /topics

## [0.8.0] - 2016-02-28
### Added
- get stats from admin dashboard
- get only stat totals from admin dashboard

## [0.7.0] - 2015-12-09
### Added
- get user by external_id

## [0.6.2] - 2015-12-02
### Fixed
- `API::Params` will not work correctly when both optional and defaults are
  specified

## [0.6.1] - 2015-11-28
### Fixed
- typo in topic_posts method

## [0.6.0] - 2015-11-27
### Added
- get posts in topic by an array of id's

## [0.5.1] - 2015-11-21
### Fixed
- remove puts statement

## [0.5.0] - 2015-11-21
### Added
- get latest category topics by page

## [0.4.0] - 2015-01-15
### Added
- generate an api key for a user
- revoke an api key for a user
- update user trust level
- grant user badge

## [0.3.6] - 2015-01-11
### Added
- list badges
- view email settings
- list emails sent
- list badges by user
- be able to specify SSL connection settings
- list api keys generated
- list backups created

## [0.3.5] - 2015-01-06
### Added
- Can now get a list of users by type: active, new, staff, etc.
- `client.category_latest_posts("category-slug")` endpoint

## [0.1.2] - 2014-05-11
- Release


================================================
FILE: Gemfile
================================================
# frozen_string_literal: true
source "https://rubygems.org"

# Specify your gem's dependencies in discourse_api.gemspec
gemspec


================================================
FILE: Guardfile
================================================
# frozen_string_literal: true
guard :rspec do
  watch(%r{^spec/.+_spec\.rb$})
  watch(%r{^lib/(.+)\.rb$})     { |m| "spec/lib/#{m[1]}_spec.rb" }
  watch('spec/spec_helper.rb')  { 'spec' }
end


================================================
FILE: LICENSE.txt
================================================
Copyright (c) 2014 Civilized Discourse Construction Kit, Inc.

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# DiscourseApi

The Discourse API gem allows you to consume the Discourse API

## Installation

Add this line to your application's Gemfile:

    gem 'discourse_api'

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install discourse_api

## Usage

Over time this project intends to have a full Discourse API. At the moment there are only a
few endpoints available:

```ruby
client = DiscourseApi::Client.new("http://try.discourse.org")
client.api_key = "YOUR_API_KEY"
client.api_username = "YOUR_USERNAME"

client.ssl(...)                                 #=> specify SSL connection settings if needed

# Topic endpoints
client.latest_topics                            #=> Gets a list of the latest topics
client.new_topics                               #=> Gets a list of new topics
client.topics_by("sam")                         #=> Gets a list of topics created by user "sam"
client.topic(57)                                #=> Gets the topic with id 57

# Search endpoint
client.search("sandbox")                        #=> Gets a list of topics that match "sandbox"

# Categories endpoint
client.categories                               #=> Gets a list of categories
client.category_latest_topics(category_slug: "lounge")  #=> Gets a list of latest topics in a category

# SSO endpoint
client.sync_sso(                                #=> Synchronizes the SSO record
  sso_secret: "discourse_sso_rocks",
  name: "Test Name",
  username: "test_name",
  email: "name@example.com",
  external_id: "2",
  custom_fields: {
    field_1: 'potato'
  }
)

# Private messages
client.private_messages("test_user")            #=> Gets a list of private messages received by "test_user"
client.sent_private_messages("test_user")       #=> Gets a list of private messages sent by "test_user"
client.create_private_message(                  #=> Creates a private messages by api_username user
  title: "Confidential: Hello World!",
  raw: "This is the raw markdown for my private message",
  target_usernames: "user1,user2"
)

```

You can handle some basic errors by rescuing from certain error classes and inspecting the response object passed to those errors:

```ruby
begin
  client.create_group({ name: 'NO' })
rescue DiscourseApi::UnprocessableEntity => error
  # `body` is something like `{ errors: ["Name must be at least 3 characters"] }`
  # This outputs "Name must be at least 3 characters"
  puts error.response.body['errors'].first
end
```

Check out [lib/discourse_api/error.rb](lib/discourse_api/error.rb) and [lib/discourse_api/client.rb](lib/discourse_api/client.rb)'s `handle_error` method for the types of errors raised by the API.

If your forum has a basic HTTP authentication enabled, set user and password:

```ruby
client.basic_auth = {
  user: "test",
  password: "secret"
}
```

## Contributing

1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request

## Testing

1. Install discourse locally
2. Inside of your discourse directory, run: `bundle exec rake db:api_test_seed`
3. Start discourse: `bundle exec rails s`
4. Install bundler in the discourse_api directory, run `gem install bundler`
5. Inside of your discourse_api directory, run: `bundle exec rspec spec/`


================================================
FILE: Rakefile
================================================
# frozen_string_literal: true
require 'bundler/gem_tasks'

require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)

require 'rubocop/rake_task'
RuboCop::RakeTask.new(:rubocop)

task test: :spec
task lint: :rubocop
task default: [:spec, :lint]


================================================
FILE: config_example.yml
================================================
# Specify your environment by making a copy of this file to create a file in your root directory called config.yml with your environment settings.

# host e.g. http://localhost:3000 or https://discourse.my_domain.com
host: YOUR_HOST_NAME

# api user (can effect results returned, e.g. .categories method returns only the categories your api_username can see)
# create a new client with when changing api user
api_username: YOUR_API_USERNAME

# api key from Discourse admin panel /admin/api/keys
api_key: YOUR_API_KEY


================================================
FILE: discourse_api.gemspec
================================================
# frozen_string_literal: true

lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) if !$LOAD_PATH.include?(lib)
require "discourse_api/version"

Gem::Specification.new do |spec|
  spec.name = "discourse_api"
  spec.version = DiscourseApi::VERSION
  spec.authors = ["Sam Saffron", "John Paul Ashenfelter", "Michael Herold", "Blake Erickson"]
  spec.email = %w[
    sam.saffron@gmail.com
    john@ashenfelter.com
    michael.j.herold@gmail.com
    o.blakeerickson@gmail.com
  ]
  spec.description = "Discourse API"
  spec.summary = "Allows access to the Discourse API"
  spec.homepage = "http://github.com/discourse/discourse_api"
  spec.license = "MIT"

  spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
  spec.require_paths = ["lib"]

  spec.add_runtime_dependency "faraday", "~> 2.7"
  spec.add_runtime_dependency "faraday-follow_redirects"
  spec.add_runtime_dependency "faraday-multipart"
  spec.add_runtime_dependency "rack", ">= 1.6"

  spec.add_development_dependency "bundler", "~> 2.0"
  spec.add_development_dependency "guard", "~> 2.14"
  spec.add_development_dependency "guard-rspec", "~> 4.7"
  spec.add_development_dependency "rake", ">= 12.3.3"
  spec.add_development_dependency "rb-inotify", "~> 0.9"
  spec.add_development_dependency "rspec", "~> 3.4"
  spec.add_development_dependency "simplecov", "~> 0.11"
  spec.add_development_dependency "webmock", "~> 3.0"
  spec.add_development_dependency "rubocop-discourse", "= 3.8.1"
  spec.add_development_dependency "syntax_tree", "~> 6.2.0"

  spec.required_ruby_version = ">= 2.7.0"
end


================================================
FILE: examples/backups.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# get list of backup files
puts client.backups()

# create backup
puts client.create_backup()

# restore backup
puts client.restore_backup("backup_file_name.tar.gz")

# download backup
puts client.download_backup("backup_file_name.tar.gz", "/tmp/")


================================================
FILE: examples/badges.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# get badges
puts client.badges

# get badges for a user
puts client.user_badges("test-user")

# Grants a badge to a user.
puts client.grant_user_badge(badge_id: 9, username: "test-user", reason: "Really nice person")

# Creates a new badge

###
# Required params:
#   :name, :badge_type_id (gold: 1, bronze: 3, silver: 2)
# Optional params:
#   :description, :allow_title, :multiple_grant, :icon, :listable,
#   :target_posts, :query, :enabled, :auto_revoke, :badge_grouping_id,
#   :trigger, :show_posts, :image, :long_description
###

puts client.create_badge(name: "Shiny new badge", badge_type_id: 1)


================================================
FILE: examples/bookmark_topic.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# Bookmark topic
puts client.bookmark_topic(1418)

# Remove bookmark from topic
puts client.remove_topic_bookmark(1418)


================================================
FILE: examples/category.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# get categories
puts client.categories()

# get sub categories for parent category with id 2
puts client.categories(parent_category_id: 2)

# get the full categories response
puts client.categories_full()

# List topics in a category
category_topics = client.category_latest_topics(category_slug: "test-category")
puts category_topics

# List topics in a category paged
category_topics_paged = client.category_latest_topics(category_slug: "test-category", page: "5")
puts category_topics_paged

# update category notification_level
update_response =
  client.category_set_user_notification(
    id: "test-id",
    notification_level: "test-notification-level",
  )
puts update_response


================================================
FILE: examples/change_topic_status.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

response =
  client.create_topic(
    category: 1,
    skip_validations: true,
    auto_track: false,
    title: "Concert Master: A new way to choose",
    raw: "This is the raw markdown for my post",
  )

# get topic_id from response
topic_id = response["topic_id"]

##
# available options (guessing from reading discourse source)
# status can be: ['autoclose', 'closed', 'archived', 'disabled', 'visible']
# enabled can be: [true, false]
##

# lock topic (note: api_username determines user that is performing action)
params = { status: "closed", enabled: true, api_username: "YOUR USERNAME/USERS USERNAME" }
client.change_topic_status(topic_id, params)

# unlock topic (note: api_username determines user that is performing action)
params = { status: "closed", enabled: false, api_username: "YOUR USERNAME/USERS USERNAME" }
client.change_topic_status(topic_id, params)


================================================
FILE: examples/create_private_message.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

client.create_private_message(
  title: "Confidential: Hello World!",
  raw: "This is the raw markdown for my private message",
  target_usernames: "user1,user2",
)


================================================
FILE: examples/create_topic.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

client.create_topic(
  category: 1,
  skip_validations: true,
  auto_track: false,
  title: "Concert Master: A new way to choose",
  raw: "This is the raw markdown for my post",
)

# create Poll topic
client.create_topic(
  category: 2,
  skip_validations: false,
  auto_track: false,
  title: "Your Favorite Color?",
  raw: "[poll name=color]\n- Green\n- Blue\n- Red\n[/poll]",
)

# Create Topic with Tags
client.create_topic(
  category: 1,
  skip_validations: true,
  auto_track: false,
  title: "Concert Master: A new way to choose",
  raw: "This is the raw markdown for my post",
  tags: %w[asdf fdsa],
)


================================================
FILE: examples/create_update_category.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

###
# Required category params:
#   :name, :color, :text_color
# Optional category params:
#   :slug, :permissions, :auto_close_hours, :auto_close_based_on_last_post, :position, :email_in,
#   :email_in_allow_strangers, :logo_url, :background_url, :allow_badges, :topic_template
###

# Create category
new_category = client.create_category(name: "Test Category", color: "AB9364", text_color: "FFFFFF")
puts "Created category: " + new_category.to_s

# Update category
response =
  client.update_category(
    id: new_category["id"],
    name: "The Best Test Category",
    slug: "the-best-test-category",
    color: "0E76BD",
    text_color: "000000",
  )
puts "Updated category: " + response.to_s


================================================
FILE: examples/create_user.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# create user
user =
  client.create_user(
    name: "Bruce Wayne",
    email: "bruce@wayne.com",
    username: "batman",
    password: "WhySoSerious",
  )

# activate user
client.activate(user["user_id"])


================================================
FILE: examples/dashboard.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# get all dashboard status as json
puts client.get_dashboard_stats

# get hash of some dashboard total value
puts client.get_dashboard_stats_totals
# sample output: {"users"=>9, "topics"=>230, "posts"=>441}


================================================
FILE: examples/disposable_invite_tokens.rb
================================================
# frozen_string_literal: true
# requires this plugin => https://github.com/discourse/discourse-invite-tokens

require "csv"

$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# fetch email-less invite tokens
invite_tokens =
  client.disposable_tokens(username: "eviltrout", quantity: 5, group_names: "security,support")

# write to CSV file
CSV.open(File.expand_path("../invite_tokens.csv", __FILE__), "w") do |csv|
  invite_tokens.each { |value| csv << [value] }
end


================================================
FILE: examples/example.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# get latest topics
puts client.latest_topics


================================================
FILE: examples/group_set_user_notification_level.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

@target_username = "YOUR_TARGET_USERNAME"
@target_group_id = # YOUR NUMERIC TARGET GROUP ID
  @user = client.user(@target_username)

# each user's group and the group's default notification level are stored under user['groups']
@user["groups"].each do |group|
  if group["id"] == @target_group_id
    @group_name = group["name"]
    @default_level = group["default_notification_level"]
  end
end

# and the user's notification setting for each group is stored under user['group_users]
@user["group_users"].each do |users_group|
  if users_group["group_id"] == @target_group_id
    @notification_level = users_group["notification_level"]
    puts "Group ID:#{@target_group_id} #{@group_name}    Current Notification Level: #{@notification_level}    Default: #{@default_level}"
    response = client.group_set_user_notification_level(@group_name, @user["id"], @default_level)
    puts response
    @users_group_users_after_update = client.user(@target_username)["group_users"]
    # this just pulls the user from the database again to make sure we updated the user's group notification level
    @users_group_users_after_update.each do |users_group_second_pass|
      if users_group_second_pass["group_id"] == @target_group_id
        puts "Updated ID:#{@target_group_id} #{@group_name}    Notification Level: #{users_group_second_pass["notification_level"]}    Default: #{@default_level}"
      end
    end
  end
end


================================================
FILE: examples/groups.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

response = client.create_group(name: "engineering_team")
group_id = response["basic_group"]["id"]

client.group_add(group_id, username: "sam")
client.group_add(group_id, username: "jeff")
client.group_add(group_id, usernames: %w[neil dan])
client.group_add(group_id, user_id: 123)
client.group_add(group_id, user_ids: [123, 456])

client.group_remove(group_id, username: "neil")
client.group_remove(group_id, user_id: 123)

client.delete_group(group_id)

## List users of a group

members = client.group_members("trust_level_0")
puts members


================================================
FILE: examples/invite_users.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# invite user
invite = client.invite_user(email: "name@example.com", group_ids: "41,42")

#update invite
client.update_invite(invite["id"], email: "namee@example.com")

# resend invite
client.resend_invite("namee@example.com")

# invite to a topic
client.invite_to_topic(1, email: "foo@bar.com")

# if the user is an admin you may invite to a group as well
client.invite_to_topic(1, email: "foo@bar.com", group_ids: "1,2,3")

# retrieve invite
puts client.retrieve_invite(email: "foo@bar.com")

# resend all invites
client.resend_all_invites

# destroy invite
client.destroy_invite(invite["id"])

# destroy all expired invites
client.destroy_all_expired_invites


================================================
FILE: examples/manage_api_keys.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# generate user api key
response = client.create_api_key(key: { description: "Key to The Batmobile", username: "batman" })

api_key_id = response["key"]["id"]

puts response
# sample output: {"key"=>{"id"=>13, "key"=>"abc", "description"=>"Key to the Batmobile"}}

response = client.revoke_api_key(api_key_id)

puts response
# sample output: {"key"=>{"id"=>13, "key"=>"abc", "description"=>"Key to the Batmobile", "revoked_at"=>"2021-01-01T00:00:00.000Z"}}

response = client.undo_revoke_api_key(api_key_id)

puts response
# sample output: {"key"=>{"id"=>13, "key"=>"abc", "description"=>"Key to the Batmobile", "revoked_at"=>nil}}

response = client.list_api_keys

puts response
# sample output: {"keys"=>[{"id"=>13, "key"=>"abc", "description"=>"Key to the Batmobile"}]}

response = client.delete_api_key(api_key_id)

puts response
# sample output: {"success"=>"OK"}


================================================
FILE: examples/notifications.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# watch an entire category
client.category_set_user_notification_level(1, notification_level: 3)

# mute a topic
client.topic_set_user_notification_level(1, notification_level: 0)

# get user notifications
client.notifications(username: "discourse")


================================================
FILE: examples/polls.rb
================================================
# frozen_string_literal: true

$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

options = ["8b4736b1ae3dfb5a28088530f036f9e5"]
poll_option_votes = client.poll_vote post_id: 5, poll_name: "poll", options: options
puts poll_option_votes.body["vote"]

poll_option_votes = client.toggle_poll_status(post_id: 5, poll_name: "poll", status: "closed")
puts poll_option_votes[:body]["poll"]["status"]

poll_option_votes = client.poll_voters(post_id: 5, poll_name: "poll")
puts poll_option_votes["voters"]


================================================
FILE: examples/post_action.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# Post Action Type IDs
# 1 - Bookmark
# 2 - Like
# 3 - Flag: Off-topic
# 4 - Flag: Inappropriate
# 5 - Vote
# 6 - Notify User
# 7 - Flag - Notify Moderators
# 8 - Flag - Spam

# Like a post
client.create_post_action(id: 2, post_action_type_id: 2)

# Flag a topic as spam
client.create_topic_action(id: 1, post_action_type_id: 8)

# Unlike a post
client.destroy_post_action(id: 3, post_action_type_id: 2)


================================================
FILE: examples/search.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# search for term
puts client.search("discourse")


================================================
FILE: examples/sent_private_messages.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

client.sent_private_messages("test_user")


================================================
FILE: examples/sso.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

client.sync_sso(
  sso_secret: "discourse_sso_rocks",
  name: "Test Name",
  username: "test_name",
  email: "name@example.com",
  external_id: "2",
)


================================================
FILE: examples/topic_lists.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# get latest topics
puts client.latest_topics({})

# get top topics
puts client.top_topics

# recategorize topic
puts client.recategorize_topic(topic_id: 108, category_id: 5)

# get all categories
puts client.categories({})


================================================
FILE: examples/update_user.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# update username from "robin" to "batman"
puts client.update_username("robin", "batman")
# update name of user whose username is "batman"
puts client.update_user("batman", name: "Bruce Wayne")
# update email of user whose username is "batman"
puts client.update_email("batman", "batman@gotham.com")
# update avatar of user whose username is "batman"
puts client.update_avatar(
       username: "batman",
       url: "http://meta-discourse.r.worldssl.net/uploads/default/2497/724a6ef2e79d2bc7.png",
     )
# update trust level of user whose id is "102"
puts client.update_trust_level(user_id: 102, level: 2)
# update user bio, location or website
puts client.update_user(
       "batman",
       bio_raw: "I am Batman.",
       location: "Gotham",
       website: "https://en.wikipedia.org/wiki/Batman",
     )

# log out everywhere and refresh browser of user whose id is "2"
puts client.log_out(2)


================================================
FILE: examples/upload_file.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require File.expand_path("../../lib/discourse_api", __FILE__)

config = DiscourseApi::ExampleHelper.load_yml

client = DiscourseApi::Client.new(config["host"] || "http://localhost:3000")
client.api_key = config["api_key"] || "YOUR_API_KEY"
client.api_username = config["api_username"] || "YOUR_USERNAME"

# Upload a file
file = Faraday::UploadIO.new("grumpy_cat.pdf", "application/pdf")
client.upload_file(file: file)

# Upload a file via URL
client.upload_file(url: "https://giphy.com/grumpy_cat.gif")


================================================
FILE: lib/discourse_api/api/api_key.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module ApiKey
      def list_api_keys
        response = get("/admin/api/keys")
        response[:body]
      end

      def create_api_key(args)
        args = API.params(args).required(:key).to_h
        post("/admin/api/keys", args)
      end

      def revoke_api_key(id)
        post("/admin/api/keys/#{id}/revoke")
      end

      def undo_revoke_api_key(id)
        post("/admin/api/keys/#{id}/undo-revoke")
      end

      def delete_api_key(id)
        delete("/admin/api/keys/#{id}")
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/backups.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Backups
      def backups
        response = get("/admin/backups.json")
        response.body
      end

      def create_backup
        post("/admin/backups", with_uploads: true)
      end

      def restore_backup(file_name)
        post("/admin/backups/#{file_name}/restore")
      end

      def download_backup(file_name, destination)
        response = get("/admin/backups/#{file_name}")
        # write file
        File.open("#{destination}/#{file_name}", "wb") { |fp| fp.write(response.body) }
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/badges.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Badges
      def badges
        response = get("/admin/badges.json")
        response.body
      end

      def user_badges(username)
        response = get("/user-badges/#{username}.json")
        response.body["badges"]
      end

      def grant_user_badge(params = {})
        post("/user_badges", params)
      end

      def create_badge(params = {})
        args =
          API
            .params(params)
            .required(:name, :badge_type_id)
            .optional(
              :description,
              :allow_title,
              :multiple_grant,
              :icon,
              :listable,
              :target_posts,
              :query,
              :enabled,
              :auto_revoke,
              :badge_grouping_id,
              :trigger,
              :show_posts,
              :image,
              :long_description,
            )
        post("/admin/badges.json", args)
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/categories.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Categories
      # :color and :text_color are RGB hexadecimal strings
      # :permissions is a hash with the group name and permission_type which is
      # an integer 1 = Full 2 = Create Post 3 = Read Only
      def create_category(args = {})
        params = common_category_params(args)
        response = post("/categories", params.to_h)
        response["category"]
      end

      def update_category(args = {})
        category_id = args[:id]
        params = common_category_params(args, include_id: true)
        response = put("/categories/#{category_id}", params.to_h)
        response["body"]["category"] if response["body"]
      end

      def reorder_categories(args = {})
        params = API.params(args).required(:mapping)
        post("/categories/reorder", params)
      end

      def delete_category(id)
        response = delete("/categories/#{id}")
        response[:body]["success"]
      end

      def categories(params = {})
        categories_full(params)["category_list"]["categories"]
      end

      def categories_full(params = {})
        response = get("/categories.json", params)
        response[:body]
      end

      def category_latest_topics(args = {})
        response = category_latest_topics_full(args)
        if response["errors"]
          response["errors"]
        else
          response["topic_list"]["topics"]
        end
      end

      def category_latest_topics_full(args = {})
        params = API.params(args).required(:category_slug).optional(:page).to_h
        url = "/c/#{params[:category_slug]}/l/latest.json"
        url = "#{url}?page=#{params[:page]}" if params.include?(:page)
        response = get(url)
        response[:body]
      end

      def category_top_topics(category_slug)
        response = category_top_topics_full(category_slug)
        if response["errors"]
          response["errors"]
        else
          response["topic_list"]["topics"]
        end
      end

      def category_top_topics_full(category_slug)
        response = get("/c/#{category_slug}/l/top.json")
        response[:body]
      end

      def category_new_topics(category_slug)
        response = category_new_topics_full(category_slug)
        response["topic_list"]["topics"]
      end

      def category_new_topics_full(category_slug)
        response = get("/c/#{category_slug}/l/new.json")
        response[:body]
      end

      def category(id)
        response = get("/c/#{id}/show")
        response[:body]["category"]
      end

      # TODO: Deprecated. Remove after 20210727
      def category_set_user_notification(args = {})
        category_id = args[:id]
        args = API.params(args).required(:notification_level)
        post("/category/#{category_id}/notifications", args)
      end

      def category_set_user_notification_level(category_id, params)
        params = API.params(params).required(:notification_level)
        post("/category/#{category_id}/notifications", params)
      end

      private

      def common_category_params(args, include_id: false)
        params = API.params(args)
        params = params.required(:id) if include_id
        params
          .required(:name)
          .optional(
            :color,
            :text_color,
            :slug,
            :permissions,
            :auto_close_hours,
            :auto_close_based_on_last_post,
            :position,
            :email_in,
            :email_in_allow_strangers,
            :logo_url,
            :background_url,
            :allow_badges,
            :topic_template,
            :custom_fields,
            :description,
            :reviewable_by_group_name,
            :show_subcategory_list,
            :subcategory_list_style,
            :allowed_tags,
            :allowed_tag_groups,
            :required_tag_group_name,
            :topic_featured_links_allowed,
            :search_priority,
            :form_template_ids,
          )
          .default(parent_category_id: nil)
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/dashboard.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Dashboard
      def get_dashboard_stats
        response = get("admin/dashboard.json")
        response[:body]
      end

      def get_dashboard_stats_totals
        stats = get_dashboard_stats
        global_reports = stats["global_reports"]
        users = global_reports[1]
        topics = global_reports[3]
        posts = global_reports[4]

        { "users" => users["total"], "topics" => topics["total"], "posts" => posts["total"] }
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/email.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Email
      def email_settings
        response = get("/admin/email.json")
        response.body
      end

      def list_email(filter)
        response = get("/admin/email/#{filter}.json")
        response.body
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/groups.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Groups
      def create_group(args)
        args =
          API
            .params(args)
            .required(:name)
            .default(visibility_level: 0)
            .optional(
              :mentionable_level,
              :messageable_level,
              :automatic_membership_email_domains,
              :automatic_membership_retroactive,
              :title,
              :primary_group,
              :grant_trust_level,
              :incoming_email,
              :flair_url,
              :flair_bg_color,
              :flair_color,
              :bio_raw,
              :members_visibility_level,
              :public_admission,
              :public_exit,
              :allow_membership_requests,
              :full_name,
              :default_notification_level,
              :usernames,
              :owner_usernames,
              :membership_request_template,
            )
            .to_h
        post("/admin/groups", group: args)
      end

      def update_group(group_id, args)
        args =
          API
            .params(args)
            .default(visibility_level: 0)
            .optional(
              :mentionable_level,
              :messageable_level,
              :name,
              :automatic_membership_email_domains,
              :title,
              :primary_group,
              :grant_trust_level,
              :incoming_email,
              :flair_url,
              :flair_bg_color,
              :flair_color,
              :bio_raw,
              :visibility_level,
              :public_admission,
              :public_exit,
              :allow_membership_requests,
              :full_name,
              :default_notification_level,
              :membership_request_template,
            )
            .to_h
        put("/groups/#{group_id}", group: args)
      end

      def group_add_owners(group_id, args)
        args = API.params(args).required(:usernames).to_h
        put("/admin/groups/#{group_id}/owners.json", group: args)
      end

      def group_remove_owners(group_id, args)
        args = API.params(args).required(:usernames).to_h
        delete("/admin/groups/#{group_id}/owners.json", group: args)
      end

      def groups(args = {})
        params = API.params(args).optional(:page).to_h

        url = "/groups.json"
        url += "?page=#{params[:page]}" if params.include?(:page)
        response = get(url)
        response.body
      end

      def group(group_name)
        response = get("/groups/#{group_name}.json")
        response.body
      end

      def group_add(group_id, users)
        users.keys.each do |key|
          # Accept arrays and convert to comma-delimited string.
          users[key] = users[key].join(",") if users[key].respond_to? :join

          # Accept non-plural user_id or username, but send pluralized version in the request.
          if key.to_s[-1] != "s"
            users["#{key}s"] = users[key]
            users.delete(key)
          end
        end

        put("/admin/groups/#{group_id}/members.json", users)
      end

      def group_remove(group_id, users)
        users.keys.each do |key|
          # Accept arrays and convert to comma-delimited string.
          users[key] = users[key].join(",") if users[key].respond_to? :join

          # Accept non-plural user_id or username, but send pluralized version in the request.
          if key.to_s[-1] != "s"
            users["#{key}s"] = users[key]
            users.delete(key)
          end
        end

        delete("/admin/groups/#{group_id}/members.json", users)
      end

      def delete_group(group_id)
        delete("/admin/groups/#{group_id}.json")
      end

      def group_members(group_name, params = {})
        options = params
        params = API.params(params).optional(:offset, :limit).default(offset: 0, limit: 100).to_h
        response = get("/groups/#{group_name}/members.json", params)

        if options[:all] == true
          response.body
        else
          response.body["members"]
        end
      end

      def group_set_user_notification_level(group, user_id, notification_level)
        post(
          "/groups/#{group}/notifications?user_id=#{user_id}&notification_level=#{notification_level}",
        )
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/invite.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Invite
      def invite_user(params = {})
        args =
          API
            .params(params)
            .optional(
              :email,
              :skip_email,
              :custom_message,
              :max_redemptions_allowed,
              :topic_id,
              :group_ids,
              :expires_at,
            )
            .to_h

        post("/invites", args)
      end

      # TODO: Deprecated. Remove after 20220506
      def invite_user_to_topic(params = {})
        deprecated(__method__, "invite_to_topic")
        invite_to_topic(params[:topic_id], params)
      end

      def invite_to_topic(topic_id, params = {})
        args = API.params(params).optional(:email, :user, :group_ids, :custom_message).to_h

        post("/t/#{topic_id}/invite", args)
      end

      def retrieve_invite(params = {})
        args = API.params(params).required(:email).to_h

        response = get("invites/retrieve.json", args)

        response.body
      end

      # requires this plugin => https://github.com/discourse/discourse-invite-tokens
      def disposable_tokens(params = {})
        post("/invite-token/generate", params)
      end

      def update_invite(invite_id, params = {})
        args =
          API
            .params(params)
            .optional(
              :topic_id,
              :group_ids,
              :group_names,
              :email,
              :send_email,
              :custom_message,
              :max_redemptions_allowed,
              :expires_at,
            )
            .to_h

        put("invites/#{invite_id}", args)
      end

      def destroy_all_expired_invites
        post("invites/destroy-all-expired")
      end

      def resend_all_invites
        post("invites/reinvite-all")
      end

      def resend_invite(email)
        post("invites/reinvite", { email: email })
      end

      def destroy_invite(invite_id)
        delete("/invites", { id: invite_id })
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/notifications.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Notifications
      def notifications(params = {})
        params = API.params(params).optional(:username, :recent, :limit, :offset, :filter)

        response = get("/notifications.json", params)
        response[:body]
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/params.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    def self.params(args)
      Params.new(args)
    end

    class Params
      def initialize(args)
        raise ArgumentError.new("Required to be initialized with a Hash") unless args.is_a? Hash
        @args = args
        @required = []
        @optional = []
        @defaults = {}
      end

      def required(*keys)
        @required.concat(keys)
        @required.each do |k|
          raise ArgumentError.new("#{k} is required but not specified") unless @args.key?(k)
        end
        self
      end

      def optional(*keys)
        @optional.concat(keys)
        self
      end

      def default(args)
        args.each { |k, v| @defaults[k] = v }
        self
      end

      def to_h
        h = {}

        @required.each { |k| h[k] = @args[k] }

        @optional.each { |k| h[k] = @args[k] if @args.include?(k) }

        @defaults.each { |k, v| @args.key?(k) ? h[k] = @args[k] : h[k] = v }

        h
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/polls.rb
================================================
# frozen_string_literal: true

module DiscourseApi
  module API
    module Polls
      def poll_vote(args)
        args = API.params(args).required(:post_id, :poll_name, :options).optional(:created_at)
        put("/polls/vote", args)
      end

      def toggle_poll_status(args)
        args =
          API
            .params(args)
            .required(:post_id, :poll_name, :status)
            .optional(:api_username)
            .optional(:raise_errors)
        put("/polls/toggle_status", args)
      end

      def poll_voters(args)
        args = API.params(args).required(:post_id, :poll_name).optional(:opts)
        response = get("/polls/voters.json", args)
        response[:body]
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/posts.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Posts
      def create_post(args)
        args = API.params(args).required(:topic_id, :raw).optional(:created_at, :api_username)
        post("/posts", args)
      end

      def create_post_action(args)
        args = API.params(args).required(:id, :post_action_type_id)
        post("/post_actions", args.to_h.merge(flag_topic: false))
      end

      def get_post(id, args = {})
        args = API.params(args).optional(:version)
        response = get("/posts/#{id}.json", args)
        response[:body]
      end

      def posts(args = {})
        args = API.params(args).default(before: 0)
        response = get("/posts.json", args)
        response[:body]
      end

      def wikify_post(id)
        put("/posts/#{id}/wiki", wiki: true)
      end

      def edit_post(id, raw)
        put("/posts/#{id}", post: { raw: raw })
      end

      def delete_post(id)
        delete("/posts/#{id}.json")
      end

      def destroy_post_action(post_id, post_action_type_id)
        delete("/post_actions/#{post_id}.json", post_action_type_id: post_action_type_id)
      end

      def post_action_users(post_id, post_action_type_id)
        response =
          get("/post_action_users.json", id: post_id, post_action_type_id: post_action_type_id)
        response[:body]
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/private_messages.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module PrivateMessages
      # TODO: Deprecated. Remove after 20220628
      def create_private_message(args = {})
        deprecated(__method__, "create_pm")
        args[:target_recipients] = args.delete :target_usernames
        create_pm(args.to_h)
      end

      # :target_recipients REQUIRED comma separated list of usernames
      # :category OPTIONAL name of category, not ID
      # :created_at OPTIONAL seconds since epoch.
      def create_pm(args = {})
        args[:archetype] = "private_message"
        args =
          API
            .params(args)
            .required(:title, :raw, :target_recipients, :archetype)
            .optional(:category, :created_at, :api_username)
        post("/posts", args.to_h)
      end

      def private_messages(username, *args)
        response = get("topics/private-messages/#{username}.json", args)
        response[:body]["topic_list"]["topics"]
      end

      def sent_private_messages(username, *args)
        response = get("topics/private-messages-sent/#{username}.json", args)
        response[:body]["topic_list"]["topics"]
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/search.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Search
      # Returns search results that match the specified term.
      #
      # @param term [String] a search term
      # @param options [Hash] A customizable set of options
      # @option options [String] :type_filter Returns results of the specified type.
      # @return [Array] Return results as an array of Hashes.
      def search(term, options = {})
        raise ArgumentError.new("#{term} is required but not specified") unless term
        raise ArgumentError.new("#{term} is required but not specified") if term.empty?

        response = get("/search", options.merge(q: term))
        response[:body]
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/site_settings.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module SiteSettings
      def site_setting_update(args = {})
        params = API.params(args).required(:name, :value).to_h
        new_site_setting = { params[:name] => params[:value] }

        put("/admin/site_settings/#{params[:name]}", new_site_setting)
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/sso.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module SSO
      def sync_sso(params = {})
        sso = DiscourseApi::SingleSignOn.parse_hash(params)

        post("/admin/users/sync_sso", sso.payload)
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/tags.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Tags
      def show_tag(tag)
        response = get("/tag/#{tag}")
        response[:body]
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/topics.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Topics
      # :category OPTIONAL name of category, not ID
      # :skip_validations OPTIONAL boolean
      # :auto_track OPTIONAL boolean
      # :created_at OPTIONAL seconds since epoch.
      def create_topic(args = {})
        args =
          API
            .params(args)
            .required(:title, :raw)
            .optional(
              :skip_validations,
              :category,
              :auto_track,
              :created_at,
              :api_username,
              :tags,
              :external_id,
              :visible,
            )
        post("/posts", args.to_h)
      end

      def create_topic_action(args)
        args = API.params(args).required(:id, :post_action_type_id)
        post("/post_actions", args.to_h.merge(flag_topic: true))
      end

      # timestamp is seconds past the epoch.
      def edit_topic_timestamp(topic_id, timestamp)
        put("/t/#{topic_id}/change-timestamp", timestamp: timestamp)
      end

      def latest_topics(params = {})
        response = get("/latest.json", params)
        response[:body]["topic_list"]["topics"]
      end

      def top_topics(params = {})
        response = get("/top.json", params)
        response[:body]["topic_list"]["topics"]
      end

      def new_topics(params = {})
        response = get("/new.json", params)
        response[:body]["topic_list"]["topics"]
      end

      def rename_topic(topic_id, title)
        put("/t/#{topic_id}.json", topic_id: topic_id, title: title)
      end

      def recategorize_topic(topic_id, category_id)
        put("/t/#{topic_id}.json", topic_id: topic_id, category_id: category_id)
      end

      # TODO: Deprecated. Remove after 20201231
      def change_topic_status(topic_slug, topic_id, params = {})
        deprecated(__method__, "update_topic_status")
        update_topic_status(topic_id, params)
      end

      def update_topic_status(topic_id, params = {})
        params = API.params(params).required(:status, :enabled).optional(:api_username)
        put("/t/#{topic_id}/status", params)
      end

      def topic(id, params = {})
        response = get("/t/#{id}.json", params)
        response[:body]
      end

      def topics_by(username, params = {})
        response = get("/topics/created-by/#{username}.json", params)
        response[:body]["topic_list"]["topics"]
      end

      def delete_topic(id)
        delete("/t/#{id}.json")
      end

      def topic_posts(topic_id, post_ids = [], params = {})
        params =
          API.params(params).optional(
            :asc,
            :filter,
            :include_raw,
            :include_suggested,
            :post_number,
            :username_filters,
          )

        url = ["/t/#{topic_id}/posts.json"]
        if post_ids.count > 0
          url.push("?")
          url.push(post_ids.map { |id| "post_ids[]=#{id}" }.join("&"))
        end
        response = get(url.join, params)
        response[:body]
      end

      def change_owner(topic_id, params = {})
        params = API.params(params).required(:username, :post_ids)

        post("/t/#{topic_id}/change-owner.json", params)
      end

      def topic_set_user_notification_level(topic_id, params)
        params = API.params(params).required(:notification_level)
        post("/t/#{topic_id}/notifications", params)
      end

      def bookmark_topic(topic_id)
        put("/t/#{topic_id}/bookmark.json")
      end

      def remove_topic_bookmark(topic_id)
        put("/t/#{topic_id}/remove_bookmarks.json")
      end

      def get_topic_url_by_external_id(external_id)
        get("/t/external_id/#{external_id}")
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/uploads.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Uploads
      def upload_file(args)
        args =
          API
            .params(args)
            .optional(:file, :url, :user_id)
            .default(type: "composer", synchronous: true)
            .to_h
        post("/uploads", args)
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/user_actions.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module UserActions
      def user_replies(username)
        params = { username: username, filter: "5" }
        response = get("/user_actions.json", params)
        response.body["user_actions"]
      end

      def user_topics_and_replies(username)
        params = { username: username, filter: "4,5" }
        response = get("/user_actions.json", params)
        response.body["user_actions"]
      end
    end
  end
end


================================================
FILE: lib/discourse_api/api/users.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  module API
    module Users
      def activate(id)
        put("/admin/users/#{id}/activate")
      end

      def user(username, params = {})
        response = get("/users/#{username}.json", params)
        response[:body]["user"]
      end

      def user_sso(user_id)
        response = get("/admin/users/#{user_id}.json")
        response[:body]["single_sign_on_record"]
      end

      def update_avatar(username, args)
        args =
          API.params(args).optional(:file, :url).default(type: "avatar", synchronous: true).to_h
        upload_response = post("/uploads", args)
        put("/u/#{username}/preferences/avatar/pick", upload_id: upload_response["id"])
      end

      def update_email(username, email)
        put("/u/#{username}/preferences/email", email: email)
      end

      def update_user(username, args)
        args =
          API
            .params(args)
            .optional(
              :name,
              :title,
              :bio_raw,
              :location,
              :website,
              :profile_background,
              :card_background,
              :email_messages_level,
              :mailing_list_mode,
              :homepage_id,
              :theme_ids,
              :user_fields,
            )
            .to_h
        put("/u/#{username}", args)
      end

      def update_username(username, new_username)
        put("/u/#{username}/preferences/username", new_username: new_username)
      end

      def update_trust_level(user_id, args)
        args = API.params(args).required(:level).to_h
        response = put("/admin/users/#{user_id}/trust_level", args)
        response[:body]
      end

      def create_user(args)
        args =
          API
            .params(args)
            .required(:name, :email, :password, :username)
            .optional(:active, :approved, :staged, :user_fields)
            .to_h
        post("/users", args)
      end

      def log_out(id)
        post("/admin/users/#{id}/log_out")
      end

      def list_users(type, params = {})
        response = get("admin/users/list/#{type}.json", params)
        response[:body]
      end

      def grant_admin(user_id)
        response = put("admin/users/#{user_id}/grant_admin")
        response[:body]
      end

      def revoke_admin(user_id)
        response = put("admin/users/#{user_id}/revoke_admin")
        response[:body]
      end

      def grant_moderation(user_id)
        response = put("admin/users/#{user_id}/grant_moderation")
        response[:body]
      end

      def revoke_moderation(user_id)
        put("admin/users/#{user_id}/revoke_moderation")
      end

      def by_external_id(external_id)
        response = get("/users/by-external/#{external_id}")
        response[:body]["user"]
      end

      def suspend(user_id, suspend_until, reason)
        put("/admin/users/#{user_id}/suspend", suspend_until: suspend_until, reason: reason)
      end

      def unsuspend(user_id)
        put("/admin/users/#{user_id}/unsuspend")
      end

      def anonymize(user_id)
        put("/admin/users/#{user_id}/anonymize")
      end

      def delete_user(user_id, delete_posts = false)
        delete("/admin/users/#{user_id}.json?delete_posts=#{delete_posts}")
      end

      def check_username(username)
        response = get("/users/check_username.json?username=#{CGI.escape(username)}")
        response[:body]
      end

      def deactivate(user_id)
        put("/admin/users/#{user_id}/deactivate")
      end
    end
  end
end


================================================
FILE: lib/discourse_api/client.rb
================================================
# frozen_string_literal: true
require "faraday"
require "faraday/follow_redirects"
require "faraday/multipart"
require "json"
require "uri"
require "discourse_api/version"
require "discourse_api/api/categories"
require "discourse_api/api/search"
require "discourse_api/api/sso"
require "discourse_api/api/tags"
require "discourse_api/api/topics"
require "discourse_api/api/polls"
require "discourse_api/api/posts"
require "discourse_api/api/users"
require "discourse_api/api/groups"
require "discourse_api/api/invite"
require "discourse_api/api/private_messages"
require "discourse_api/api/notifications"
require "discourse_api/api/badges"
require "discourse_api/api/email"
require "discourse_api/api/api_key"
require "discourse_api/api/backups"
require "discourse_api/api/dashboard"
require "discourse_api/api/uploads"
require "discourse_api/api/user_actions"
require "discourse_api/api/site_settings"

module DiscourseApi
  class Client
    attr_accessor :api_key
    attr_accessor :basic_auth
    attr_reader :host, :api_username, :timeout

    DEFAULT_TIMEOUT = 30

    include DiscourseApi::API::Categories
    include DiscourseApi::API::Search
    include DiscourseApi::API::SSO
    include DiscourseApi::API::Tags
    include DiscourseApi::API::Topics
    include DiscourseApi::API::Polls
    include DiscourseApi::API::Posts
    include DiscourseApi::API::Users
    include DiscourseApi::API::Groups
    include DiscourseApi::API::Invite
    include DiscourseApi::API::PrivateMessages
    include DiscourseApi::API::Notifications
    include DiscourseApi::API::Badges
    include DiscourseApi::API::Email
    include DiscourseApi::API::ApiKey
    include DiscourseApi::API::Backups
    include DiscourseApi::API::Dashboard
    include DiscourseApi::API::Uploads
    include DiscourseApi::API::UserActions
    include DiscourseApi::API::SiteSettings

    def initialize(host, api_key = nil, api_username = nil)
      raise ArgumentError, "host needs to be defined" if host.nil? || host.empty?
      @host = host
      @api_key = api_key
      @api_username = api_username
      @use_relative = check_subdirectory(host)
    end

    def timeout=(timeout)
      @timeout = timeout
      @connection.options.timeout = timeout if @connection
    end

    def api_username=(api_username)
      @api_username = api_username
      @connection.headers["Api-Username"] = api_username unless @connection.nil?
    end

    def connection_options
      @connection_options ||= {
        url: @host,
        request: {
          timeout: @timeout || DEFAULT_TIMEOUT,
        },
        headers: {
          accept: "application/json",
          user_agent: user_agent,
        },
      }
    end

    def ssl(options)
      connection_options[:ssl] = options
    end

    def delete(path, params = {})
      request(:delete, path, params)
    end

    def get(path, params = {})
      request(:get, path, params)
    end

    def post(path, params = {})
      response = request(:post, path, params)
      case response.status
      when 200, 201, 204
        response.body
      else
        raise DiscourseApi::Error, response.body
      end
    end

    def put(path, params = {})
      request(:put, path, params)
    end

    def patch(path, params = {})
      request(:patch, path, params)
    end

    def user_agent
      @user_agent ||= "DiscourseAPI Ruby Gem #{DiscourseApi::VERSION}"
    end

    def deprecated(old, new)
      warn "[DEPRECATED]: `#{old}` is deprecated. Please use `#{new}` instead."
    end

    private

    def connection
      @connection ||=
        Faraday.new connection_options do |conn|
          # Allow uploading of files
          conn.request :multipart

          # Convert request params to "www-form-encoded"
          conn.request :url_encoded

          # Allow to interact with forums behind basic HTTP authentication
          if basic_auth
            conn.request :authorization, :basic, basic_auth[:user], basic_auth[:password]
          end

          # Follow redirects
          conn.response :follow_redirects, limit: 5

          # Parse responses as JSON
          conn.response :json, content_type: "application/json"

          # For HTTP debugging, uncomment
          # conn.response :logger

          # Use Faraday's default HTTP adapter
          conn.adapter Faraday.default_adapter

          # Pass api_key and api_username on every request
          unless api_username.nil?
            conn.headers["Api-Key"] = api_key
            conn.headers["Api-Username"] = api_username
          end
        end
    end

    def request(method, path, params = {})
      unless Hash === params
        params = params.to_h if params.respond_to? :to_h
      end
      path = @use_relative ? path.sub(%r{^/}, "") : path
      response = connection.send(method.to_sym, path, params)
      handle_error(response)
      response.env
    rescue Faraday::ClientError, JSON::ParserError
      raise DiscourseApi::Error
    rescue Faraday::ConnectionFailed
      raise DiscourseApi::Timeout
    end

    def handle_error(response)
      case response.status
      when 403
        raise DiscourseApi::UnauthenticatedError.new(response.env[:body], response.env)
      when 404, 410
        raise DiscourseApi::NotFoundError.new(response.env[:body], response.env)
      when 422
        raise DiscourseApi::UnprocessableEntity.new(response.env[:body], response.env)
      when 429
        raise DiscourseApi::TooManyRequests.new(response.env[:body], response.env)
      when 500...600
        raise DiscourseApi::Error.new(response.env[:body])
      end
    end

    def check_subdirectory(host)
      URI(host).request_uri != "/"
    end
  end
end


================================================
FILE: lib/discourse_api/error.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  class DiscourseError < StandardError
    attr_reader :response

    def initialize(message, response = nil)
      super(message)
      @response = response
    end
  end

  class Error < DiscourseError
    attr_reader :wrapped_exception

    # Initializes a new Error object
    #
    # @param exception [Exception, String]
    # @return [DiscourseApi::Error]
    def initialize(exception = $!)
      @wrapped_exception = exception
      exception.respond_to?(:message) ? super(exception.message) : super(exception.to_s)
    end
  end

  class UnauthenticatedError < DiscourseError
  end

  class NotFoundError < DiscourseError
  end

  class UnprocessableEntity < DiscourseError
  end

  class TooManyRequests < DiscourseError
  end

  class Timeout < Error
  end
end


================================================
FILE: lib/discourse_api/example_helper.rb
================================================
# frozen_string_literal: true
require "yaml"

module DiscourseApi
  class ExampleHelper
    def self.load_yml
      config_yml = File.expand_path("../../../config.yml", __FILE__)
      puts config_yml
      begin
        config = YAML.load_file config_yml
      rescue Errno::ENOENT
        config = {}
      end
      config
    end
  end
end


================================================
FILE: lib/discourse_api/single_sign_on.rb
================================================
# frozen_string_literal: true
require "base64"
require "rack"
require "openssl"

module DiscourseApi
  class SingleSignOn
    class ParseError < RuntimeError
    end
    class MissingConfigError < RuntimeError
    end

    ACCESSORS = %i[
      add_groups
      admin
      avatar_force_update
      avatar_url
      bio
      card_background_url
      confirmed_2fa
      email
      external_id
      groups
      locale
      locale_force_update
      moderator
      name
      no_2fa_methods
      nonce
      profile_background_url
      remove_groups
      require_2fa
      require_activation
      return_sso_url
      suppress_welcome_message
      title
      username
    ]

    FIXNUMS = []

    BOOLS = %i[
      admin
      avatar_force_update
      confirmed_2fa
      locale_force_update
      moderator
      no_2fa_methods
      require_2fa
      require_activation
      suppress_welcome_message
    ]
    ARRAYS = [:groups]
    #NONCE_EXPIRY_TIME = 10.minutes # minutes is a rails method and is causing an error. Is this needed in the api?

    attr_accessor(*ACCESSORS)
    attr_writer :custom_fields, :sso_secret, :sso_url

    def self.sso_secret
      raise MissingConfigError, "sso_secret not implemented on class, be sure to set it on instance"
    end

    def self.sso_url
      raise MissingConfigError, "sso_url not implemented on class, be sure to set it on instance"
    end

    def self.parse_hash(payload)
      sso = new

      sso.sso_secret = payload.delete(:sso_secret)
      sso.sso_url = payload.delete(:sso_url)

      ACCESSORS.each do |k|
        val = payload[k]

        val = val.to_i if FIXNUMS.include? k
        val = %w[true false].include?(val) ? val == "true" : nil if BOOLS.include? k
        val = val.split(",") if ARRAYS.include?(k) && !val.nil?
        sso.send("#{k}=", val)
      end

      # Set custom_fields
      sso.custom_fields = payload[:custom_fields]

      # Add custom_fields (old format)
      payload.each do |k, v|
        if field = k[/^custom\.(.+)$/, 1]
          # Maintain adding of .custom bug
          sso.custom_fields["custom.#{field}"] = v
        end
      end

      sso
    end

    def self.parse(payload, sso_secret = nil)
      sso = new
      sso.sso_secret = sso_secret if sso_secret

      parsed = Rack::Utils.parse_query(payload)
      if parsed["sso"].nil? || sso.sign(parsed["sso"]) != parsed["sig"]
        diags =
          "\n\nsso: #{parsed["sso"].inspect}\n\nsig: #{parsed["sig"].inspect}\n\nexpected sig: #{sso.sign(parsed.fetch("sso", ""))}"
        if parsed["sso"].nil? || parsed["sso"] =~ %r{[^a-zA-Z0-9=\r\n/+]}m
          raise ParseError,
                "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}"
        else
          raise ParseError, "Bad signature for payload #{diags}"
        end
      end

      decoded = Base64.decode64(parsed["sso"])
      decoded_hash = Rack::Utils.parse_query(decoded)

      ACCESSORS.each do |k|
        val = decoded_hash[k.to_s]
        val = val.to_i if FIXNUMS.include? k
        val = %w[true false].include?(val) ? val == "true" : nil if BOOLS.include? k
        val = val.split(",") if ARRAYS.include?(k) && !val.nil?
        sso.send("#{k}=", val)
      end

      decoded_hash.each do |k, v|
        if field = k[/^custom\.(.+)$/, 1]
          sso.custom_fields[field] = v
        end
      end

      sso
    end

    def diagnostics
      DiscourseApi::SingleSignOn::ACCESSORS.map { |a| "#{a}: #{send(a)}" }.join("\n")
    end

    def sso_secret
      @sso_secret || self.class.sso_secret
    end

    def sso_url
      @sso_url || self.class.sso_url
    end

    def custom_fields
      @custom_fields ||= {}
    end

    def sign(payload)
      OpenSSL::HMAC.hexdigest("sha256", sso_secret, payload)
    end

    def to_url(base_url = nil)
      base = "#{base_url || sso_url}"
      "#{base}#{base.include?("?") ? "&" : "?"}#{payload}"
    end

    def payload
      payload = Base64.strict_encode64(unsigned_payload)
      "sso=#{CGI.escape(payload)}&sig=#{sign(payload)}"
    end

    def unsigned_payload
      payload = {}

      ACCESSORS.each do |k|
        next if (val = send k) == nil
        payload[k] = val
      end

      @custom_fields.each { |k, v| payload["custom.#{k}"] = v.to_s } if @custom_fields

      Rack::Utils.build_query(payload)
    end
  end
end


================================================
FILE: lib/discourse_api/version.rb
================================================
# frozen_string_literal: true
module DiscourseApi
  VERSION = "2.1.0"
end


================================================
FILE: lib/discourse_api.rb
================================================
# frozen_string_literal: true
require "discourse_api/api/params"
require "discourse_api/client"
require "discourse_api/error"
require "discourse_api/version"
require "discourse_api/example_helper"
require "discourse_api/single_sign_on"


================================================
FILE: spec/discourse_api/api/api_key_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::ApiKey do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#list_api_keys" do
    before do
      url = "#{host}/admin/api/keys"
      stub_get(url).to_return(
        body: fixture("list_api_keys.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.list_api_keys
      url = "#{host}/admin/api/keys"
      expect(a_get(url)).to have_been_made
    end

    it "returns the requested api keys" do
      keys = client.list_api_keys
      expect(keys["keys"]).to be_an Array
      expect(keys["keys"].first).to be_a Hash
      expect(keys["keys"].first).to have_key("key")
    end
  end

  describe "#create_api_key" do
    before do
      url = "#{host}/admin/api/keys"
      stub_post(url).to_return(
        body: fixture("api_key.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.create_api_key(key: { username: "robin" })
      url = "#{host}/admin/api/keys"
      expect(a_post(url)).to have_been_made
    end

    it "returns the generated api key" do
      api_key = client.create_api_key(key: { username: "robin" })
      expect(api_key).to be_a Hash
      expect(api_key["key"]).to have_key("key")
    end
  end

  describe "#revoke_api_key" do
    before do
      url = "#{host}/admin/api/keys/10/revoke"
      stub_post(url).to_return(
        body: fixture("api_key.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.revoke_api_key(10)
      url = "#{host}/admin/api/keys/10/revoke"
      expect(a_post(url)).to have_been_made
    end

    it "returns the api key" do
      api_key = client.revoke_api_key(10)
      expect(api_key["key"]).to have_key("key")
    end
  end

  describe "#undo_revoke_api_key" do
    before do
      url = "#{host}/admin/api/keys/10/undo-revoke"
      stub_post(url).to_return(
        body: fixture("api_key.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.undo_revoke_api_key(10)
      url = "#{host}/admin/api/keys/10/undo-revoke"
      expect(a_post(url)).to have_been_made
    end

    it "returns the api key" do
      api_key = client.undo_revoke_api_key(10)
      expect(api_key["key"]).to have_key("key")
    end
  end

  describe "#delete_api_key" do
    before do
      url = "#{host}/admin/api/keys/10"
      stub_delete(url).to_return(
        body: '{"success": "OK"}',
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.delete_api_key(10)
      url = "#{host}/admin/api/keys/10"
      expect(a_delete(url)).to have_been_made
    end

    it "returns 200" do
      response = client.delete_api_key(10)
      expect(response["status"]).to eq(200)
    end
  end
end


================================================
FILE: spec/discourse_api/api/backups_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Backups do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#backups" do
    before do
      stub_get("#{host}/admin/backups.json").to_return(
        body: fixture("backups.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.backups
      expect(a_get("#{host}/admin/backups.json")).to have_been_made
    end

    it "returns the requested backups" do
      backups = client.backups
      expect(backups).to be_an Array
      expect(backups.first).to be_a Hash
      expect(backups.first).to have_key("filename")
    end
  end
end


================================================
FILE: spec/discourse_api/api/badges_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Badges do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#badges" do
    before do
      stub_get("#{host}/admin/badges.json").to_return(
        body: fixture("badges.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.badges
      expect(a_get("#{host}/admin/badges.json")).to have_been_made
    end

    it "returns the requested badges" do
      badges = client.badges
      expect(badges).to be_a Hash
      expect(badges["badges"]).to be_an Array
    end
  end

  describe "#user-badges" do
    before do
      stub_get("#{host}/user-badges/test_user.json").to_return(
        body: fixture("user_badges.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.user_badges("test_user")
      expect(a_get("#{host}/user-badges/test_user.json")).to have_been_made
    end

    it "returns the requested user badges" do
      badges = client.user_badges("test_user")
      expect(badges).to be_an Array
      expect(badges.first).to be_a Hash
      expect(badges.first).to have_key("badge_type_id")
    end
  end
end


================================================
FILE: spec/discourse_api/api/categories_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Categories do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#categories" do
    before do
      stub_get("#{host}/categories.json").to_return(
        body: fixture("categories.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.categories
      expect(a_get("#{host}/categories.json")).to have_been_made
    end

    it "returns the requested categories" do
      categories = client.categories
      expect(categories).to be_an Array
      expect(categories.first).to be_a Hash
    end

    it "returns the requested categories with hash arg" do
      categories = client.categories({})
      expect(categories).to be_an Array
      expect(categories.first).to be_a Hash
    end
  end

  describe "#categories_full" do
    before do
      stub_get("#{host}/categories.json").to_return(
        body: fixture("categories.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.categories
      expect(a_get("#{host}/categories.json")).to have_been_made
    end

    it "returns the entire categories response" do
      categories = client.categories_full
      expect(categories).to be_a Hash
      expect(categories).to have_key "category_list"
      expect(categories).to have_key "featured_users"
    end
  end

  describe "#category_latest_topics" do
    before do
      stub_get("#{host}/c/category-slug/l/latest.json").to_return(
        body: fixture("category_latest_topics.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "returns the latest topics in a category" do
      latest_topics = client.category_latest_topics(category_slug: "category-slug")
      expect(latest_topics).to be_an Array
    end
  end

  describe "#category_latest_topics_full" do
    before do
      stub_get("#{host}/c/category-slug/l/latest.json").to_return(
        body: fixture("category_latest_topics.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "returns the entire latest topics in a category response" do
      latest_topics = client.category_latest_topics_full(category_slug: "category-slug")
      expect(latest_topics).to be_a Hash
      expect(latest_topics).to have_key "topic_list"
      expect(latest_topics).to have_key "users"
    end
  end

  describe "#category_top_topics" do
    before do
      stub_get("#{host}/c/category-slug/l/top.json").to_return(
        body: fixture("category_topics.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "returns the top topics in a category" do
      topics = client.category_top_topics("category-slug")
      expect(topics).to be_an Array
    end
  end

  describe "#category_top_topics_full" do
    before do
      stub_get("#{host}/c/category-slug/l/top.json").to_return(
        body: fixture("category_topics.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "returns the entire top topics in a category response" do
      topics = client.category_top_topics_full("category-slug")
      expect(topics).to be_a Hash
      expect(topics).to have_key "topic_list"
      expect(topics).to have_key "users"
    end
  end

  describe "#category_new_topics" do
    before do
      stub_get("#{host}/c/category-slug/l/new.json").to_return(
        body: fixture("category_topics.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "returns the new topics in a category" do
      topics = client.category_new_topics("category-slug")
      expect(topics).to be_an Array
    end
  end

  describe "#category_new_topics_full" do
    before do
      stub_get("#{host}/c/category-slug/l/new.json").to_return(
        body: fixture("category_topics.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "returns the new topics in a category" do
      topics = client.category_new_topics_full("category-slug")
      expect(topics).to be_a Hash
      expect(topics).to have_key "topic_list"
      expect(topics).to have_key "users"
    end
  end

  describe "#category_new_category" do
    before do
      stub_post("#{host}/categories")
      client.create_category(
        name: "test_category",
        color: "283890",
        text_color: "FFFFFF",
        description: "This is a description",
        permissions: {
          "group_1" => 1,
          "admins" => 1,
        },
      )
    end

    it "makes a create category request" do
      expect(
        a_post("#{host}/categories").with(
          body:
            "color=283890&description=This+is+a+description&name=test_category&parent_category_id&permissions%5Badmins%5D=1&permissions%5Bgroup_1%5D=1&text_color=FFFFFF",
        ),
      ).to have_been_made
    end
  end

  describe "#reorder_categories" do
    before do
      stub_post("#{host}/categories/reorder").to_return(
        body: fixture("notification_success.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes a categories reordering request" do
      payload = { "1": 2, "2": 3, "3": 4, "4": 1 }
      response = client.reorder_categories(mapping: payload.to_json)
      expect(
        a_post("#{host}/categories/reorder").with(
          body: "mapping=#{CGI.escape(payload.to_json.to_s)}",
        ),
      ).to have_been_made
      expect(response["success"]).to eq("OK")
    end
  end

  describe "#category_set_user_notification" do
    before do
      stub_post("#{host}/category/1/notifications").to_return(
        body: fixture("notification_success.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the post request" do
      response = client.category_set_user_notification(id: 1, notification_level: 3)
      expect(a_post("#{host}/category/1/notifications")).to have_been_made
      expect(response["success"]).to eq("OK")
    end
  end

  describe "#category_set_user_notification_level" do
    before do
      stub_post("#{host}/category/1/notifications").to_return(
        body: fixture("notification_success.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the post request" do
      response = client.category_set_user_notification_level(1, notification_level: 3)
      expect(
        a_post("#{host}/category/1/notifications").with(body: "notification_level=3"),
      ).to have_been_made
      expect(response["success"]).to eq("OK")
    end
  end
end


================================================
FILE: spec/discourse_api/api/email_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Email do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#email_settings" do
    before do
      stub_get("#{host}/admin/email.json").to_return(
        body: fixture("email_settings.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.email_settings
      expect(a_get("#{host}/admin/email.json")).to have_been_made
    end

    it "returns the requested settings" do
      settings = client.email_settings
      expect(settings).to be_a Hash
      expect(settings).to have_key("delivery_method")
      expect(settings).to have_key("settings")
    end
  end

  describe "#list_email_all" do
    before do
      stub_get("#{host}/admin/email/all.json").to_return(
        body: fixture("email_list_all.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.list_email("all")
      expect(a_get("#{host}/admin/email/all.json")).to have_been_made
    end

    it "returns all email" do
      all_email = client.list_email("all")
      expect(all_email).to be_an Array
    end
  end
end


================================================
FILE: spec/discourse_api/api/groups_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Groups do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#groups" do
    before do
      stub_get("#{host}/groups.json").to_return(
        body: fixture("groups.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.groups
      expect(a_get("#{host}/groups.json")).to have_been_made
    end

    it "returns the requested groups" do
      groups = client.groups
      expect(groups).to be_an Array
      groups.each { |g| expect(g).to be_a Hash }
    end

    it "returns a single group" do
      stub_get("#{host}/groups/some-group.json").to_return(
        body: fixture("group.json"),
        headers: {
          content_type: "application/json",
        },
      )
      group = client.group("some-group")
      expect(group["basic_group"]).to be_a Hash
    end

    it "create new groups" do
      stub_post("#{host}/admin/groups")
      client.create_group(name: "test_group")
      params = escape_params("group[name]" => "test_group", "group[visibility_level]" => 0)
      expect(a_post("#{host}/admin/groups").with(body: params)).to have_been_made
    end

    it "update an existing group" do
      stub_put("#{host}/groups/42")
      client.update_group(42, name: "test_group")
      params = escape_params("group[name]" => "test_group", "group[visibility_level]" => 0)
      expect(a_put("#{host}/groups/42").with(body: params)).to have_been_made
    end

    describe "add members" do
      before { stub_request(:put, "#{host}/admin/groups/123/members.json") }

      it "adds a single member by username" do
        client.group_add(123, username: "sam")
        expect(
          a_request(:put, "#{host}/admin/groups/123/members.json").with(body: { usernames: "sam" }),
        ).to have_been_made
      end

      it "adds an array of members by username" do
        client.group_add(123, usernames: %w[sam jeff])
        expect(
          a_request(:put, "#{host}/admin/groups/123/members.json").with(
            body: {
              usernames: "sam,jeff",
            },
          ),
        ).to have_been_made
      end

      it "adds a single member by user_id" do
        client.group_add(123, user_id: 456)
        expect(
          a_request(:put, "#{host}/admin/groups/123/members.json").with(body: { user_ids: "456" }),
        ).to have_been_made
      end

      it "adds an array of members by user_id" do
        client.group_add(123, user_id: [123, 456])
        expect(
          a_request(:put, "#{host}/admin/groups/123/members.json").with(
            body: {
              user_ids: "123,456",
            },
          ),
        ).to have_been_made
      end
    end

    describe "remove members" do
      let(:url) { "#{host}/admin/groups/123/members.json?usernames=sam" }

      before { stub_delete(url) }

      it "removes member" do
        client.group_remove(123, username: "sam")
        expect(a_delete(url)).to have_been_made
      end
    end

    describe "add owners" do
      let(:url) { "#{host}/admin/groups/123/owners.json" }

      before { stub_put(url) }

      it "makes the member an owner" do
        client.group_add_owners(123, usernames: "sam")
        params = escape_params("group[usernames]" => "sam")
        expect(
          a_request(:put, "#{host}/admin/groups/123/owners.json").with(body: params),
        ).to have_been_made
      end
    end

    describe "remove owners" do
      let(:url) { "#{host}/admin/groups/123/owners.json?group%5Busernames%5D=sam" }

      before { stub_delete(url) }

      it "removes the owner role from the group member" do
        client.group_remove_owners(123, usernames: "sam")
        expect(a_delete(url)).to have_been_made
      end
    end

    describe "group members" do
      it "list members" do
        stub_get("#{host}/groups/mygroup/members.json?limit=100&offset=0").to_return(
          body: fixture("members_0.json"),
          headers: {
            content_type: "application/json",
          },
        )
        stub_get("#{host}/groups/mygroup/members.json?limit=100&offset=100").to_return(
          body: fixture("members_1.json"),
          headers: {
            content_type: "application/json",
          },
        )
        members = client.group_members("mygroup")
        expect(a_get("#{host}/groups/mygroup/members.json?limit=100&offset=0")).to have_been_made
        expect(members.length).to eq(100)
        members = client.group_members("mygroup", offset: 100)
        expect(a_get("#{host}/groups/mygroup/members.json?limit=100&offset=100")).to have_been_made
        expect(members.length).to eq(90)
      end

      context "with :all params" do
        it "lists members and owners" do
          stub_get("#{host}/groups/mygroup/members.json?limit=100&offset=0").to_return(
            body: fixture("members_2.json"),
            headers: {
              content_type: "application/json",
            },
          )
          member_data = client.group_members("mygroup", all: true)
          expect(a_get("#{host}/groups/mygroup/members.json?limit=100&offset=0")).to have_been_made
          expect(member_data["members"].length).to eq(100)
          expect(member_data["owners"].length).to eq(7)
          expect(member_data.keys.sort).to eq(%w[members meta owners])
        end
      end
    end

    describe "group user notification level" do
      before { stub_post("#{host}/groups/mygroup/notifications?user_id=77&notification_level=3") }

      it "updates user's notification level for group" do
        client.group_set_user_notification_level("mygroup", 77, 3)
        expect(
          a_post("#{host}/groups/mygroup/notifications?user_id=77&notification_level=3"),
        ).to have_been_made
      end
    end
  end
end


================================================
FILE: spec/discourse_api/api/invite_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Invite do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#invite_user" do
    before do
      stub_post("#{host}/invites").to_return(
        body: fixture("topic_invite_user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.invite_user(email: "fake_user@example.com", group_ids: "41,42")
      expect(a_post("#{host}/invites")).to have_been_made
    end

    it "returns success" do
      response = client.invite_user(email: "fake_user@example.com", group_ids: "41,42")
      expect(response).to be_a Hash
      expect(response["success"]).to be_truthy
    end
  end

  describe "#update_invite" do
    before do
      stub_put("#{host}/invites/27").to_return(
        body: fixture("topic_invite_user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "updates invite" do
      client.update_invite(27, email: "namee@example.com")
      expect(a_put("#{host}/invites/27")).to have_been_made
    end
  end

  describe "#retrieve_invite" do
    before do
      stub_get("#{host}/invites/retrieve.json?email=foo@bar.com").to_return(
        body: fixture("retrieve_invite.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.retrieve_invite(email: "foo@bar.com")
      expect(a_get("#{host}/invites/retrieve.json?email=foo@bar.com")).to have_been_made
    end

    it "returns the requested topics" do
      invites = client.retrieve_invite(email: "foo@bar.com")
      expect(invites).to be_an Hash
    end

    it "returns the requested invite" do
      invites = client.retrieve_invite(email: "foo@bar.com")
      expect(invites["email"]).to eq("foo@bar.com")
      expect(invites).to have_key("invite_key")
    end
  end

  describe "#destroy_all_expired_invites" do
    let(:url) { "#{host}/invites/destroy-all-expired" }

    before do
      stub_post(url).to_return(
        body: '{"success": "OK"}',
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "destroys all expired invites" do
      client.destroy_all_expired_invites
      expect(a_post(url)).to have_been_made
    end
  end

  describe "#resend_all_invites" do
    let(:url) { "#{host}/invites/reinvite-all" }

    before do
      stub_post(url).to_return(
        body: '{"success": "OK"}',
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "resends all invites" do
      client.resend_all_invites
      expect(a_post(url)).to have_been_made
    end
  end

  describe "#resend_invite" do
    let(:url) { "#{host}/invites/reinvite" }

    before do
      stub_post(url).to_return(
        body: '{"success": "OK"}',
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "resends invite" do
      client.resend_invite("foo@bar.com")
      expect(a_post(url)).to have_been_made
    end
  end

  describe "#destroy_invite" do
    let(:url) { "#{host}/invites?id=27" }

    before do
      stub_delete(url).to_return(
        body: '{"success": "OK"}',
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "destroys the invite" do
      client.destroy_invite(27)
      expect(a_delete(url)).to have_been_made
    end
  end
end


================================================
FILE: spec/discourse_api/api/notifications_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Notifications do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#notifications" do
    before do
      stub_get("#{host}/notifications.json").to_return(
        body: fixture("notifications.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.notifications
      expect(a_get("#{host}/notifications.json")).to have_been_made
    end

    it "returns the requested notifications" do
      notifications = client.notifications
      expect(notifications).to be_an Array
      expect(notifications.first).to be_an Hash
      expect(notifications[0]["notification_type"]).to eq(9)
    end
  end
end


================================================
FILE: spec/discourse_api/api/params_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Params do
  def params_for(h)
    DiscourseApi::API::Params.new(h).required(:r1).optional(:o1, :o2).default(d1: "default")
  end

  it "should raise on missing required params" do
    expect { params_for({ o1: "test" }).to_h }.to raise_error(ArgumentError)
  end

  it "should not raise when a required param is false" do
    expect { params_for({ r1: false }).to_h }.not_to raise_error
  end

  it "should not include optional params when not provided" do
    expect(params_for({ r1: "test" }).to_h).not_to include(:o1)
  end

  it "should include optional params if provided but blank" do
    expect(params_for({ r1: "test", o2: nil }).to_h).to include(:o2)
  end

  it "should include default params when defined but not provided" do
    expect(params_for({ r1: "test" }).to_h).to include(d1: "default")
  end

  it "should include default params when defined and provided" do
    expect(params_for({ r1: "test", d1: "override" }).to_h).to include(d1: "override")
  end

  it "should include optional and default params when defined and provided" do
    expect(params_for({ r1: "test", o1: "optional", d1: "override" }).to_h).to include(
      o1: "optional",
      d1: "override",
    )
  end
end


================================================
FILE: spec/discourse_api/api/polls_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Polls do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#poll vote" do
    before do
      path = "#{host}/polls/vote"
      stub_put(path).to_return(
        body: fixture("polls_vote.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      options = ["8b4736b1ae3dfb5a28088530f036f9e5"]
      client.poll_vote post_id: 5, poll_name: "poll", options: options
      expect(a_put("#{host}/polls/vote")).to have_been_made
    end

    it "returns the expected votes" do
      options = ["8b4736b1ae3dfb5a28088530f036f9e5"]
      vote = client.poll_vote post_id: 5, poll_name: "poll", options: options
      expect(vote.body).to be_a Hash
      expect(vote.body["poll"]["options"]).to be_an Array
      expect(vote.body["vote"]).to eq(["8b4736b1ae3dfb5a28088530f036f9e5"])
    end

    describe "#poll toggle_status" do
      before do
        path = "#{host}/polls/toggle_status"
        stub_put(path).to_return(
          body: fixture("polls_toggle_status.json"),
          headers: {
            content_type: "application/json",
          },
        )
      end

      it "toggles the poll status to closed" do
        client.toggle_poll_status post_id: 5, poll_name: "poll", status: "closed"
        expect(a_put("#{host}/polls/toggle_status")).to have_been_made
      end

      it "returns the expected results of closed poll" do
        returned_poll_status =
          client.toggle_poll_status post_id: 5, poll_name: "poll", status: "closed"
        expect(returned_poll_status.body).to be_a Hash
        returned_poll_status.body["poll"]["options"].each { |g| expect(g).to be_a Hash }
      end
    end

    describe "#poll voters" do
      before do
        stub_get("#{host}/polls/voters.json?post_id=5&poll_name=poll").to_return(
          body: fixture("polls_voters.json"),
          headers: {
            content_type: "application/json",
          },
        )
      end

      it "requests the correct resource" do
        client.poll_voters post_id: 5, poll_name: "poll"
        expect(a_get("#{host}/polls/voters.json?post_id=5&poll_name=poll")).to have_been_made
      end

      it "returns the expected votes" do
        voters = client.poll_voters post_id: 5, poll_name: "poll"
        expect(voters).to be_a Hash
        voters.each { |g| expect(g).to be_an Array }
        expect(voters["voters"]["e539a9df8700d0d05c69356a07b768cf"]).to be_an Array
        expect(voters["voters"]["e539a9df8700d0d05c69356a07b768cf"][0]["id"]).to eq(356)
      end
    end
  end
end


================================================
FILE: spec/discourse_api/api/posts_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Posts do
  let(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#get_post" do
    before do
      stub_get("#{host}/posts/11.json").to_return(
        body: fixture("post.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "fetches a post" do
      the_post = client.get_post(11)
      expect(the_post).to be_a Hash
      expect(the_post["id"]).to eq(11)
    end
  end

  describe "#posts" do
    before do
      stub_get("#{host}/posts.json?before=0").to_return(
        body: fixture("posts_latest.json"),
        headers: {
          content_type: "application/json",
        },
      )
      stub_get("#{host}/posts.json?before=14").to_return(
        body: fixture("posts_before.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "fetches latest posts" do
      the_posts = client.posts()
      expect(the_posts).to be_a Hash
      expect(the_posts["latest_posts"][0]["id"]).to eq(15)
    end

    it "fetches posts before 14" do
      the_posts = client.posts(before: 14)
      expect(the_posts).to be_a Hash
      expect(the_posts["latest_posts"][0]["id"]).to eq(14)
    end
  end

  describe "#wikify_post" do
    before { stub_put("#{host}/posts/9999/wiki") }

    it "fails on unknown post" do
      client.wikify_post(9999)
      expect(a_put("#{host}/posts/9999/wiki")).to have_been_made
    end
  end

  describe "#delete_post" do
    before { stub_delete("#{host}/posts/9999.json") }

    it "deletes a post" do
      client.delete_post(9999)
      expect(a_delete("#{host}/posts/9999.json")).to have_been_made
    end
  end

  describe "#post_action_users" do
    before do
      stub_get("#{host}/post_action_users.json?id=11&post_action_type_id=2").to_return(
        body: fixture("post_action_users.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "fetches post_action_users" do
      post_action_users = client.post_action_users(11, 2)
      expect(post_action_users).to be_a Hash
      expect(post_action_users["post_action_users"][0]["id"]).to eq(1286)
    end
  end
end


================================================
FILE: spec/discourse_api/api/private_messages_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::PrivateMessages do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#private_messages" do
    before do
      stub_get("#{host}/topics/private-messages/test_user.json").to_return(
        body: fixture("private_messages.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.private_messages("test_user")
      expect(a_get("#{host}/topics/private-messages/test_user.json")).to have_been_made
    end

    it "returns the requested private messages" do
      private_messages = client.private_messages("test_user")
      expect(private_messages).to be_an Array
    end
  end

  describe "#sent_private_messages" do
    before do
      stub_get("#{host}/topics/private-messages-sent/test_user.json").to_return(
        body: fixture("private_messages.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.sent_private_messages("test_user")
      expect(a_get("#{host}/topics/private-messages-sent/test_user.json")).to have_been_made
    end

    it "returns the requested sent private messages" do
      private_messages = client.sent_private_messages("test_user")
      expect(private_messages).to be_an Array
    end
  end

  describe "#create_pm" do
    before do
      stub_post("#{host}/posts")
      client.create_pm(
        title: "Confidential: Hello World!",
        raw: "This is the raw markdown for my private message",
        target_recipients: "user1,user2",
      )
    end

    it "makes a create private message request" do
      expect(
        a_post("#{host}/posts").with(
          body:
            "archetype=private_message&raw=This+is+the+raw+markdown+for+my+private+message&target_recipients=user1%2Cuser2&title=Confidential%3A+Hello+World%21",
        ),
      ).to have_been_made
    end
  end
end


================================================
FILE: spec/discourse_api/api/search_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Search do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#search" do
    before do
      stub_get("#{host}/search").with(query: { q: "test" }).to_return(
        body: fixture("search.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.search("test")
      expect(a_get("#{host}/search").with(query: { q: "test" })).to have_been_made
    end

    it "returns the requested search" do
      results = client.search("test")
      expect(results).to be_an Array
      expect(results.first).to be_a Hash
    end

    it "raises an ArgumentError for nil" do
      expect { client.search(nil) }.to raise_error(ArgumentError)
    end

    it "raises an ArgumentError for empty string" do
      expect { client.search("") }.to raise_error(ArgumentError)
    end
  end
end


================================================
FILE: spec/discourse_api/api/site_settings_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::SiteSettings do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#site_setting_update" do
    before do
      stub_put("#{host}/admin/site_settings/foo")
      client.site_setting_update(name: "foo", value: "bar")
    end

    it "makes a site_settings_update request" do
      expect(a_put("#{host}/admin/site_settings/foo").with(body: "foo=bar")).to have_been_made
    end
  end
end


================================================
FILE: spec/discourse_api/api/sso_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::SSO do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  let(:params) do
    {
      :sso_secret => "abc",
      :sso_url => "www.google.com",
      :name => "Some User",
      :username => "some_user",
      :email => "some@email.com",
      :external_id => "abc",
      :suppress_welcome_message => false,
      :avatar_url => "https://www.website.com",
      :title => "ruby",
      :avatar_force_update => false,
      :add_groups => %w[a b],
      :remove_groups => %w[c d],
      # old format (which results in custom.custom.field_1 in unsigned_payload)
      "custom.field_1" => "tomato",
      # new format
      :custom_fields => {
        field_2: "potato",
      },
    }
  end
  let(:expected_unsigned_payload) do
    "add_groups=a&add_groups=b&avatar_url=https%3A%2F%2Fwww.website.com" \
      "&email=some%40email.com&external_id=abc&name=Some+User&remove_groups=c" \
      "&remove_groups=d&title=ruby&username=some_user&custom.field_2=potato" \
      "&custom.custom.field_1=tomato"
  end
  let(:sso_double) { DiscourseApi::SingleSignOn.parse_hash(params) }

  describe "#sync_sso" do
    before do
      stub_post(/.*sync_sso.*/).to_return(
        body: fixture("user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "assigns params to sso instance" do
      allow(DiscourseApi::SingleSignOn).to(receive(:parse_hash).with(params).and_return(sso_double))

      client.sync_sso(params)

      expect(sso_double.custom_fields).to eql(
        { "custom.field_1" => "tomato", :field_2 => "potato" },
      )
      expect(sso_double.unsigned_payload).to eql(expected_unsigned_payload)
    end

    it "requests the correct resource" do
      client.sync_sso({ :sso_secret => "test_d7fd0429940", "custom.riffle_url" => "test" })
      expect(a_post(/.*sync_sso.*/)).to have_been_made
    end
  end
end


================================================
FILE: spec/discourse_api/api/topics_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Topics do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#change_topic_status" do
    before do
      stub_put("#{host}/t/57/status").to_return(
        body: fixture("topic.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "changes the topic status" do
      client.update_topic_status(57, { status: "visible", enabled: false })
      expect(a_put("#{host}/t/57/status")).to have_been_made
    end
  end

  describe "#invite_to_topic" do
    before do
      stub_post("#{host}/t/12/invite").to_return(
        body: fixture("topic_invite_user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.invite_to_topic(12, email: "fake_user@example.com")
      expect(a_post("#{host}/t/12/invite")).to have_been_made
    end

    it "returns success" do
      response = client.invite_to_topic(12, email: "fake_user@example.com")
      expect(response).to be_a Hash
      expect(response["success"]).to be_truthy
    end
  end

  describe "#latest_topics" do
    before do
      stub_get("#{host}/latest.json").to_return(
        body: fixture("latest.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.latest_topics
      expect(a_get("#{host}/latest.json")).to have_been_made
    end

    it "returns the requested topics" do
      topics = client.latest_topics
      expect(topics).to be_an Array
      expect(topics.first).to be_a Hash
    end

    it "can take a hash param" do
      topics = client.latest_topics({})
      expect(topics).to be_an Array
      expect(topics.first).to be_a Hash
    end
  end

  describe "#top_topics" do
    before do
      stub_get("#{host}/top.json").to_return(
        body: fixture("top.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.top_topics
      expect(a_get("#{host}/top.json")).to have_been_made
    end

    it "returns the requested topics" do
      topics = client.top_topics
      expect(topics).to be_an Array
      expect(topics.first).to be_a Hash
    end

    it "can take a hash param" do
      topics = client.top_topics({})
      expect(topics).to be_an Array
      expect(topics.first).to be_a Hash
    end
  end

  describe "#new_topics" do
    before do
      stub_get("#{host}/new.json").to_return(
        body: fixture("new.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.new_topics
      expect(a_get("#{host}/new.json")).to have_been_made
    end

    it "returns the requested topics" do
      client.api_username = "test_user"
      topics = client.new_topics
      expect(topics).to be_an Array
      expect(topics.first).to be_a Hash
    end
  end

  describe "#topic" do
    before do
      stub_get("#{host}/t/57.json").to_return(
        body: fixture("topic.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.topic(57)
      expect(a_get("#{host}/t/57.json")).to have_been_made
    end

    it "returns the requested topic" do
      topic = client.topic(57)
      expect(topic).to be_a Hash
      expect(topic["id"]).to eq(57)
    end
  end

  describe "#update_topic" do
    before do
      stub_put("#{host}/t/57.json").to_return(
        body: fixture("topic.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "renames the topic" do
      client.rename_topic(57, "A new title!")
      expect(a_put("#{host}/t/57.json")).to have_been_made
    end

    it "assigns the topic a new category" do
      client.recategorize_topic(57, 3)
      expect(a_put("#{host}/t/57.json")).to have_been_made
    end
  end

  describe "#topics_by" do
    before do
      stub_get("#{host}/topics/created-by/test.json").to_return(
        body: fixture("topics_created_by.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.topics_by("test")
      expect(a_get("#{host}/topics/created-by/test.json")).to have_been_made
    end

    it "returns the requested topics" do
      topics = client.topics_by("test")
      expect(topics).to be_an Array
      expect(topics.first).to be_a Hash
    end
  end

  describe "#topic_posts" do
    before do
      stub_get(%r{#{host}/t/57/posts\.json}).to_return(
        body: fixture("topic_posts.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.topic_posts(57)
      expect(a_get("#{host}/t/57/posts.json")).to have_been_made
    end

    it "allows scoping to specific post ids" do
      client.topic_posts(57, [123, 456])
      expect(a_get("#{host}/t/57/posts.json?post_ids[]=123&post_ids[]=456")).to have_been_made
    end

    it "returns the requested topic posts" do
      body = client.topic_posts(57, [123])
      expect(body).to be_a Hash
      expect(body["post_stream"]["posts"]).to be_an Array
      expect(body["post_stream"]["posts"].first).to be_a Hash
    end

    it "can retrieve a topic posts' raw attribute" do
      body = client.topic_posts(57, [123], { include_raw: true })
      expect(body).to be_a Hash
      expect(body["post_stream"]["posts"]).to be_an Array
      expect(body["post_stream"]["posts"].first).to be_a Hash
      expect(body["post_stream"]["posts"].first["raw"]).to be_an Array
    end
  end

  describe "#create_topic_with_tags" do
    before do
      stub_post("#{host}/posts").to_return(
        body: fixture("create_topic_with_tags.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the post request" do
      client.create_topic title: "Sample Topic Title",
                          raw: "Sample topic content body",
                          tags: %w[asdf fdsa]
      expect(a_post("#{host}/posts")).to have_been_made
    end

    it "returns success" do
      response =
        client.create_topic title: "Sample Topic Title",
                            raw: "Sample topic content body",
                            tags: %w[asdf fdsa]
      expect(response).to be_a Hash
      expect(response["topic_id"]).to eq 21
    end
  end

  describe "#topic_set_user_notification_level" do
    before do
      stub_post("#{host}/t/1/notifications").to_return(
        body: fixture("notification_success.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the post request" do
      response = client.topic_set_user_notification_level(1, notification_level: 3)
      expect(
        a_post("#{host}/t/1/notifications").with(body: "notification_level=3"),
      ).to have_been_made
      expect(response["success"]).to eq("OK")
    end
  end

  describe "#bookmark_topic" do
    before do
      stub_put("#{host}/t/1/bookmark.json").to_return(
        body: "",
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the put request" do
      response = client.bookmark_topic(1)
      expect(a_put("#{host}/t/1/bookmark.json")).to have_been_made
      expect(response.body).to eq(nil)
    end
  end

  describe "#remove_topic_bookmark" do
    before do
      stub_put("#{host}/t/1/remove_bookmarks.json").to_return(
        body: "",
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the put request" do
      response = client.remove_topic_bookmark(1)
      expect(a_put("#{host}/t/1/remove_bookmarks.json")).to have_been_made
      expect(response.body).to eq(nil)
    end
  end
end


================================================
FILE: spec/discourse_api/api/uploads_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Uploads do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#upload_file" do
    before do
      stub_post("#{host}/uploads").to_return(
        body: fixture("upload_file.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "uploads an image via URL" do
      image =
        "https://meta-discourse.global.ssl.fastly.net/user_avatar/meta.discourse.org/sam/120/5243.png"
      args = { url: image }
      response = client.upload_file(args)
      expect(response["url"]).to eq(
        "/uploads/default/original/1X/417e624d2453925e6c68748b9aa67637c284b5aa.jpg",
      )
    end

    it "uploads a file" do
      file = Faraday::UploadIO.new("spec/fixtures/upload_file.json", "application/json")
      args = { file: file }
      response = client.upload_file(args)
      expect(response["url"]).to eq(
        "/uploads/default/original/1X/417e624d2453925e6c68748b9aa67637c284b5aa.jpg",
      )
    end
  end
end


================================================
FILE: spec/discourse_api/api/user_actions_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::UserActions do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#user_replies" do
    before do
      stub_get("#{host}/user_actions.json?username=testuser&filter=5").to_return(
        body: fixture("replies.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.user_replies("testuser")
      expect(a_get("#{host}/user_actions.json?username=testuser&filter=5")).to have_been_made
    end

    it "returns the requested user" do
      replies = client.user_replies("testuser")
      expect(replies).to be_an Array
    end
  end

  describe "#user_topics_and_replies" do
    before do
      stub_get("#{host}/user_actions.json?username=testuser&filter=4,5").to_return(
        body: fixture("replies_and_topics.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.user_topics_and_replies("testuser")
      expect(a_get("#{host}/user_actions.json?username=testuser&filter=4,5")).to have_been_made
    end

    it "returns the requested user" do
      replies = client.user_topics_and_replies("testuser")
      expect(replies).to be_an Array
    end
  end
end


================================================
FILE: spec/discourse_api/api/users_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::API::Users do
  subject(:client) { DiscourseApi::Client.new("#{host}", "test_d7fd0429940", "test_user") }

  describe "#user" do
    before do
      stub_get("#{host}/users/test.json").to_return(
        body: fixture("user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.user("test")
      expect(a_get("#{host}/users/test.json")).to have_been_made
    end

    it "returns the requested user" do
      user = client.user("test")
      expect(user).to be_a Hash
    end

    it "works with optional params" do
      user = client.user("test", {})
      expect(user).to be_a Hash
    end
  end

  describe "#user_sso" do
    before do
      stub_get("#{host}/admin/users/15.json").to_return(
        body: fixture("admin_user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.user_sso(15)
      expect(a_get("#{host}/admin/users/15.json")).to have_been_made
    end

    it "has single_sign_on_record" do
      user_sso = client.user_sso(15)
      expect(user_sso).to be_a Hash
      expect(user_sso).to have_key("external_id")
    end
  end

  describe "#update_avatar" do
    before do
      stub_post("#{host}/uploads").to_return(
        body: fixture("upload_avatar.json"),
        headers: {
          content_type: "application/json",
        },
      )
      stub_put("#{host}/u/test_user/preferences/avatar/pick").to_return(
        body: fixture("user_update_avatar_success.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "uploads an image" do
      sam =
        "https://meta-discourse.global.ssl.fastly.net/user_avatar/meta.discourse.org/sam/120/5243.png"
      args = { url: sam }
      response = client.update_avatar("test_user", args)
      expect(response[:body]["success"]).to eq("OK")
    end
  end

  describe "#update_email" do
    before do
      stub_put("#{host}/u/fake_user/preferences/email").to_return(
        body: fixture("user_update_user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the put request" do
      client.update_email("fake_user", "fake_user_2@example.com")
      expect(a_put("#{host}/u/fake_user/preferences/email")).to have_been_made
    end

    it "returns success" do
      response = client.update_email("fake_user", "fake_user_2@example.com")
      expect(response[:body]["success"]).to be_truthy
    end
  end

  describe "#update_user" do
    before do
      stub_put("#{host}/u/fake_user").to_return(
        body: fixture("user_update_user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the put request" do
      client.api_key = "test_d7fd0429940"
      client.api_username = "test_user"
      client.update_user("fake_user", name: "Fake User 2")
      expect(a_put("#{host}/u/fake_user")).to have_been_made
    end

    it "returns success" do
      client.api_key = "test_d7fd0429940"
      client.api_username = "test_user"
      response = client.update_user("fake_user", name: "Fake User 2")
      expect(response[:body]["success"]).to be_truthy
    end
  end

  describe "#update_username" do
    before do
      stub_put("#{host}/u/fake_user/preferences/username").to_return(
        body: fixture("user_update_username.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the put request" do
      client.update_username("fake_user", "fake_user_2")
      expect(a_put("#{host}/u/fake_user/preferences/username")).to have_been_made
    end

    it "returns the updated username" do
      response = client.update_username("fake_user", "fake_user_2")
      expect(response[:body]["username"]).to eq("fake_user_2")
    end
  end

  describe "#create_user" do
    before do
      stub_post("#{host}/users").to_return(
        body: fixture("user_create_success.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the post request" do
      client.create_user name: "Test User",
                         email: "test2@example.com",
                         password: "P@ssword",
                         username: "test2"
      expect(a_post("#{host}/users")).to have_been_made
    end

    it "returns success" do
      response =
        client.create_user name: "Test User",
                           email: "test2@example.com",
                           password: "P@ssword",
                           username: "test2"
      expect(response).to be_a Hash
      expect(response["success"]).to be_truthy
    end
  end

  describe "#activate_user" do
    before do
      stub_put("#{host}/admin/users/15/activate").to_return(
        body: fixture("user_activate_success.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the put request" do
      client.activate(15)
      expect(a_put("#{host}/admin/users/15/activate")).to have_been_made
    end

    it "returns success" do
      response = client.activate(15)
      expect(response[:body]["success"]).to eq("OK")
    end
  end

  describe "#log_out_success" do
    before do
      stub_post("#{host}/admin/users/4/log_out").to_return(
        body: fixture("user_log_out_success.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes a post request" do
      client.log_out(4)
      expect(a_post("#{host}/admin/users/4/log_out")).to have_been_made
    end

    it "returns success" do
      response = client.log_out(4)
      expect(response).to be_a Hash
      expect(response["success"]).to eq("OK")
    end
  end

  describe "#log_out_unsuccessful" do
    before do
      stub_post("#{host}/admin/users/90/log_out").to_return(
        status: 404,
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "Raises API Error" do
      expect { client.log_out(90) }.to raise_error DiscourseApi::NotFoundError
    end
  end

  describe "#list_users" do
    before do
      stub_get("#{host}/admin/users/list/active.json").to_return(
        body: fixture("user_list.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.list_users("active")
      expect(a_get("#{host}/admin/users/list/active.json")).to have_been_made
    end

    it "returns the requested users" do
      users = client.list_users("active")
      expect(users).to be_an Array
      expect(users.first).to be_a Hash
    end
  end

  describe "#update_trust_level" do
    before do
      url = "#{host}/admin/users/2/trust_level"
      stub_put(url).to_return(
        body: fixture("update_trust_level.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the correct put request" do
      params = { level: 2 }
      client.update_trust_level(2, params)
      url = "#{host}/admin/users/2/trust_level"
      expect(a_put(url)).to have_been_made
    end

    it "updates the trust_level" do
      params = { level: 2 }
      admin_user = client.update_trust_level(2, params)
      expect(admin_user).to be_a Hash
      expect(admin_user["admin_user"]).to have_key("trust_level")
    end
  end

  describe "#grant admin" do
    before do
      url = "#{host}/admin/users/11/grant_admin"
      stub_put(url).to_return(
        body: fixture("user_grant_admin.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the correct put request" do
      client.grant_admin(11)
      url = "#{host}/admin/users/11/grant_admin"
      expect(a_put(url)).to have_been_made
    end

    it "makes the user an admin" do
      result = client.grant_admin(11)
      expect(result).to be_a Hash
      expect(result["admin_user"]["admin"]).to eq(true)
    end
  end

  describe "#grant moderation" do
    before do
      url = "#{host}/admin/users/11/grant_moderation"
      stub_put(url).to_return(
        body: fixture("user_grant_moderator.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "makes the correct put request" do
      client.grant_moderation(11)
      url = "#{host}/admin/users/11/grant_moderation"
      expect(a_put(url)).to have_been_made
    end

    it "makes the user a moderator" do
      result = client.grant_moderation(11)
      expect(result).to be_a Hash
      expect(result["admin_user"]["moderator"]).to eq(true)
    end
  end

  describe "#revoke moderation" do
    before do
      url = "#{host}/admin/users/11/revoke_moderation"
      stub_put(url).to_return(body: "", status: 200)
    end

    it "makes the correct put request" do
      result = client.revoke_moderation(11)
      url = "#{host}/admin/users/11/revoke_moderation"
      expect(a_put(url)).to have_been_made
      expect(result.status).to eq(200)
    end
  end

  describe "#by_external_id" do
    before do
      stub_get("#{host}/users/by-external/1").to_return(
        body: fixture("user.json"),
        headers: {
          content_type: "application/json",
        },
      )
    end

    it "requests the correct resource" do
      client.by_external_id(1)
      expect(a_get("#{host}/users/by-external/1")).to have_been_made
    end

    it "returns the requested user" do
      user = client.by_external_id(1)
      expect(user["id"]).to eq 1
    end
  end

  describe "#suspend" do
    before do
      url = "#{host}/admin/users/11/suspend"
      stub_put(url).to_return(body: "", status: 200)
    end

    it "makes the correct put request" do
      result = client.suspend(11, "2030-01-01", "no reason")
      url = "#{host}/admin/users/11/suspend"
      expect(a_put(url)).to have_been_made
      expect(result.status).to eq(200)
    end
  end

  describe "#unsuspend" do
    before do
      url = "#{host}/admin/users/11/unsuspend"
      stub_put(url).to_return(body: "", status: 200)
    end

    it "makes the correct put request" do
      result = client.unsuspend(11)
      url = "#{host}/admin/users/11/unsuspend"
      expect(a_put(url)).to have_been_made
      expect(result.status).to eq(200)
    end
  end

  describe "#anonymize" do
    before do
      url = "#{host}/admin/users/11/anonymize"
      stub_put(url).to_return(body: "", status: 200)
    end

    it "makes the correct put request" do
      result = client.anonymize(11)
      url = "#{host}/admin/users/11/anonymize"
      expect(a_put(url)).to have_been_made
      expect(result.status).to eq(200)
    end
  end

  describe "#delete_user" do
    before do
      url = "#{host}/admin/users/11.json?delete_posts=true"
      stub_delete(url).to_return(body: '{"deleted": true}', status: 200)
    end

    it "makes the correct delete request" do
      result = client.delete_user(11, true)
      url = "#{host}/admin/users/11.json?delete_posts=true"
      expect(a_delete(url)).to have_been_made
      expect(result.body).to eq('{"deleted": true}')
      expect(result.status).to eq(200)
    end
  end

  describe "#check_username" do
    let(:url) { "#{host}/users/check_username.json?username=sparrow" }
    let(:body) { '{"available":false,"suggestion":"sparrow1"}' }

    before { stub_get(url).to_return(body: body, headers: { content_type: "application/json" }) }

    it "requests the correct resource" do
      client.check_username("sparrow")
      expect(a_get(url)).to have_been_made
    end

    it "returns the result" do
      result = client.check_username("sparrow")
      expect(result["available"]).to eq false
    end

    context "when non-URI characters are used" do
      let(:url) { "#{host}/users/check_username.json?username=1_%5B4%5D%21+%40the%24%23%3F" }
      let(:body) { '{"errors":["must only include numbers, letters, dashes, and underscores"]}' }

      it "escapes them" do
        client.check_username("1_[4]! @the$#?")
        expect(a_get(url)).to have_been_made
      end

      it "returns the result" do
        result = client.check_username("1_[4]! @the$#?")
        expect(
          result["errors"].first,
        ).to eq "must only include numbers, letters, dashes, and underscores"
      end
    end
  end

  describe "#deactivate" do
    before { stub_put("#{host}/admin/users/15/deactivate").to_return(body: nil) }

    it "makes the put request" do
      client.deactivate(15)
      expect(a_put("#{host}/admin/users/15/deactivate")).to have_been_made
    end

    it "returns success" do
      response = client.deactivate(15)
      expect(response.status).to eq(200)
    end
  end
end


================================================
FILE: spec/discourse_api/client_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"

describe DiscourseApi::Client do
  subject(:client) { DiscourseApi::Client.new(host) }

  describe ".new" do
    it "requires a host argument" do
      expect { DiscourseApi::Client.new }.to raise_error ArgumentError
    end

    it "defaults api key to nil" do
      expect(client.api_key).to be_nil
    end

    it "defaults api username to nil" do
      expect(client.api_username).to be_nil
    end

    it "accepts an api key argument" do
      client = DiscourseApi::Client.new(host, "test")
      expect(client.api_key).to eq("test")
    end

    it "accepts an api username argument" do
      client = DiscourseApi::Client.new(host, "test", "test_user")
      expect(client.api_username).to eq("test_user")
    end
  end

  describe "#timeout" do
    context "with a custom timeout" do
      it "is set to Faraday connection" do
        expect(client.send(:connection).options.timeout).to eq(30)
      end
    end

    context "with the default timeout" do
      it "is set to Faraday connection" do
        client.timeout = 25
        expect(client.send(:connection).options.timeout).to eq(25)
      end
    end

    it "raises DiscourseApi::Timeout" do
      stub_get("#{host}/t/1.json").to_timeout

      expect { client.topic(1) }.to raise_error(DiscourseApi::Timeout)
    end
  end

  describe "#api_key" do
    it "is publicly accessible" do
      client.api_key = "test_d7fd0429940"
      expect(client.api_key).to eq("test_d7fd0429940")
    end
  end

  describe "#api_username" do
    it "is publicly accessible" do
      client.api_username = "test_user"
      expect(client.api_username).to eq("test_user")
    end
  end

  describe "#host" do
    it "is publicly readable" do
      expect(client.host).to eq("#{host}")
    end

    it "is not publicly writeable" do
      expect(client).not_to respond_to(:host=)
    end
  end

  describe "#connection" do
    it "looks like a Faraday connection" do
      expect(client.send(:connection)).to respond_to :run_request
    end

    it "memorizes the connection" do
      c1, c2 = client.send(:connection), client.send(:connection)
      expect(c1.object_id).to eq(c2.object_id)
    end
  end

  describe "#delete" do
    before do
      stub_delete("#{host}/test/delete").with(query: { deleted: "object" })
      client.api_key = "test_d7fd0429940"
      client.api_username = "test_user"
    end

    it "allows custom delete requests" do
      client.delete("/test/delete", { deleted: "object" })
      expect(a_delete("#{host}/test/delete").with(query: { deleted: "object" })).to have_been_made
    end

    context "when using a host with a subdirectory" do
      subject(:client) { DiscourseApi::Client.new("#{host}/forum") }

      before { stub_delete("#{host}/forum/test/delete").with(query: { deleted: "object" }) }

      it "allows custom delete requests" do
        client.delete("/test/delete", { deleted: "object" })
        expect(
          a_delete("#{host}/forum/test/delete").with(query: { deleted: "object" }),
        ).to have_been_made
      end
    end
  end

  describe "#post" do
    before do
      stub_post("#{host}/test/post").with(body: { created: "object" })
      client.api_key = "test_d7fd0429940"
      client.api_username = "test_user"
    end

    it "allows custom post requests" do
      client.post("/test/post", { created: "object" })
      expect(a_post("#{host}/test/post").with(body: { created: "object" })).to have_been_made
    end

    context "when using a host with a subdirectory" do
      subject(:client) { DiscourseApi::Client.new("#{host}/forum") }

      before { stub_post("#{host}/forum/test/post").with(body: { created: "object" }) }

      it "allows custom post requests" do
        client.post("/test/post", { created: "object" })
        expect(
          a_post("#{host}/forum/test/post").with(body: { created: "object" }),
        ).to have_been_made
      end
    end
  end

  describe "#put" do
    before do
      stub_put("#{host}/test/put").with(body: { updated: "object" })
      client.api_key = "test_d7fd0429940"
      client.api_username = "test_user"
    end

    it "allows custom put requests" do
      client.put("/test/put", { updated: "object" })
      expect(a_put("#{host}/test/put").with(body: { updated: "object" })).to have_been_made
    end

    context "when using a host with a subdirectory" do
      subject(:client) { DiscourseApi::Client.new("#{host}/forum") }

      before { stub_put("#{host}/forum/test/put").with(body: { updated: "object" }) }

      it "allows custom post requests" do
        client.put("/test/put", { updated: "object" })
        expect(a_put("#{host}/forum/test/put").with(body: { updated: "object" })).to have_been_made
      end
    end
  end

  describe "#request" do
    it "catches 500 errors" do
      connection = instance_double(Faraday::Connection)
      allow(connection).to receive(:get).and_return(
        OpenStruct.new(env: { body: "error page html" }, status: 500),
      )
      allow(Faraday).to receive(:new).and_return(connection)
      expect { client.send(:request, :get, "/test") }.to raise_error DiscourseApi::Error
    end

    it "catches Faraday errors" do
      allow(Faraday).to receive(:new).and_raise(Faraday::ClientError.new("BOOM!"))
      expect { client.send(:request, :get, "/test") }.to raise_error DiscourseApi::Error
    end

    it "catches JSON::ParserError errors" do
      allow(Faraday).to receive(:new).and_raise(JSON::ParserError.new("unexpected token"))
      expect { client.send(:request, :get, "/test") }.to raise_error DiscourseApi::Error
    end
  end
end


================================================
FILE: spec/discourse_api/single_sign_on_spec.rb
================================================
# frozen_string_literal: true

require "spec_helper"

describe DiscourseApi::SingleSignOn do
  describe DiscourseApi::SingleSignOn::MissingConfigError do
    it "inherits from RuntimeError for backward compatibility" do
      expect(DiscourseApi::SingleSignOn::MissingConfigError).to be < RuntimeError
    end
  end

  describe DiscourseApi::SingleSignOn::ParseError do
    it "inherits from RuntimeError for backward compatibility" do
      expect(DiscourseApi::SingleSignOn::ParseError).to be < RuntimeError
    end
  end

  describe ".sso_secret" do
    it "raises MissingConfigError when sso_secret is not present" do
      expect { described_class.sso_secret }.to raise_error(
        DiscourseApi::SingleSignOn::MissingConfigError,
      )
    end
  end

  describe ".sso_url" do
    it "raises MissingConfigError when sso_url is not present" do
      expect { described_class.sso_url }.to raise_error(
        DiscourseApi::SingleSignOn::MissingConfigError,
      )
    end
  end

  describe ".parse" do
    context "when sso is present" do
      it "raises ParseError when there's a signature mismatch" do
        sso = described_class.new
        sso.sso_secret = "abcd"
        expect { described_class.parse(sso.payload, "dcba") }.to raise_error(
          DiscourseApi::SingleSignOn::ParseError,
        )
      end
    end

    context "when sso is missing" do
      it "raises ParseError when there's a signature mismatch" do
        sso = described_class.new
        sso.sso_secret = "abcd"
        missing_sso = Rack::Utils.parse_query(sso.payload)
        missing_sso.delete("sso")
        malformed_query = Rack::Utils.build_query(missing_sso)

        expect { described_class.parse(malformed_query, "dcba") }.to raise_error(
          DiscourseApi::SingleSignOn::ParseError,
          /The SSO field should/i,
        )
      end
    end
  end
end


================================================
FILE: spec/fixtures/admin_user.json
================================================
{
  "id": 4,
  "username": "47f6974294f6e3eb9686",
  "avatar_template": "/letter_avatar_proxy/v2/letter/4/8e8cbc/{size}.png",
  "active": false,
  "admin": false,
  "moderator": false,
  "last_seen_at": null,
  "last_emailed_at": null,
  "created_at": "2017-06-23T16:30:22.539Z",
  "last_seen_age": null,
  "last_emailed_age": null,
  "created_at_age": "17m",
  "username_lower": "47f6974294f6e3eb9686",
  "trust_level": 0,
  "trust_level_locked": false,
  "flag_level": 0,
  "title": null,
  "suspended_at": null,
  "suspended_till": null,
  "suspended": null,
  "blocked": false,
  "time_read": "< 1m",
  "staged": false,
  "days_visited": 0,
  "posts_read_count": 0,
  "topics_entered": 0,
  "post_count": 0,
  "name": "47f6974294f6e3eb9686f898f98a67d7",
  "can_send_activation_email": true,
  "can_activate": true,
  "can_deactivate": true,
  "ip_address": "192.168.56.1",
  "registration_ip_address": null,
  "can_grant_admin": true,
  "can_revoke_admin": false,
  "can_grant_moderation": true,
  "can_revoke_moderation": false,
  "can_impersonate": true,
  "like_count": 0,
  "like_given_count": 0,
  "topic_count": 0,
  "flags_given_count": 0,
  "flags_received_count": 0,
  "private_topics_count": 0,
  "can_delete_all_posts": true,
  "can_be_deleted": true,
  "can_be_anonymized": true,
  "suspend_reason": null,
  "primary_group_id": null,
  "badge_count": 0,
  "warnings_received_count": 0,
  "bounce_score": 0,
  "reset_bounce_score_after": null,
  "can_view_action_logs": true,
  "single_sign_on_record": {
    "user_id": 4,
    "external_id": "847",
    "last_payload": "nonce=bcbe1f1b1202fe597feaa2e3f23d566c&name=47f6974294f6e3eb9686f898f98a67d7&username=47f6974294f6e3eb9686f898f98a67d7&email=47f6974294f6e3eb9686f898f98a67d7%40test.com&require_activation=true&external_id=847",
    "created_at": "2017-06-23T16:30:22.658Z",
    "updated_at": "2017-06-23T16:30:22.658Z",
    "external_username": "47f6974294f6e3eb9686f898f98a67d7",
    "external_email": "47f6974294f6e3eb9686f898f98a67d7@test.com",
    "external_name": "47f6974294f6e3eb9686f898f98a67d7",
    "external_avatar_url": null
  },
  "approved_by": null,
  "suspended_by": null,
  "groups": [
    {
      "id": 10,
      "automatic": true,
      "name": "trust_level_0",
      "display_name": "trust_level_0",
      "user_count": 6,
      "alias_level": 0,
      "visible": true,
      "automatic_membership_email_domains": null,
      "automatic_membership_retroactive": false,
      "primary_group": false,
      "title": null,
      "grant_trust_level": null,
      "incoming_email": null,
      "has_messages": false,
      "flair_url": null,
      "flair_bg_color": null,
      "flair_color": null,
      "bio_raw": null,
      "bio_cooked": null,
      "public": false,
      "allow_membership_requests": false,
      "full_name": null,
      "default_notification_level": 3
    }
  ]
}


================================================
FILE: spec/fixtures/api_key.json
================================================
{
  "key": {
    "id": 5,
    "key": "e722e04df8cf6d097d565ca04eea1ff8e9e8f09beb405bae6f0c79852916f334",
    "user": {
      "id": 2,
      "username": "robin",
      "uploaded_avatar_id": 3,
      "avatar_template": "/user_avatar/localhost/robin/{size}/3.png"
    }
  }
}


================================================
FILE: spec/fixtures/backups.json
================================================
[
  {
    "filename": "discourse-2015-01-10-065015.tar.gz",
    "size": 557075,
    "link": "//localhost:3000/admin/backups/discourse-2015-01-10-065015.tar.gz"
  },
  {
    "filename": "2014-02-10-065935.tar.gz",
    "size": 5,
    "link": "//localhost:3000/admin/backups/2014-02-10-065935.tar.gz"
  }
]


================================================
FILE: spec/fixtures/badges.json
================================================
{
  "badges": [
    {
      "id": 9,
      "name": "Autobiographer",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 1,
      "system": true,
      "query": "    SELECT u.id user_id, current_timestamp granted_at\n    FROM users u\n    JOIN user_profiles up on u.id = up.user_id\n    WHERE bio_raw IS NOT NULL AND LENGTH(TRIM(bio_raw)) > 10 AND\n          uploaded_avatar_id IS NOT NULL AND\n          (:backfill OR u.id IN (:user_ids) )\n",
      "trigger": 8,
      "target_posts": false,
      "auto_revoke": true,
      "show_posts": false,
      "badge_type_id": 3
    },
    {
      "id": 11,
      "name": "First Like",
      "description": null,
      "grant_count": 1,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 1,
      "system": true,
      "query": "    SELECT pa1.user_id, pa1.created_at granted_at, pa1.post_id\n    FROM (\n      SELECT pa.user_id, min(pa.id) id\n      FROM post_actions pa\n      JOIN badge_posts p on p.id = pa.post_id\n      WHERE post_action_type_id = 2 AND\n        (:backfill OR pa.post_id IN (:post_ids) )\n      GROUP BY pa.user_id\n    ) x\n    JOIN post_actions pa1 on pa1.id = x.id\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 3
    },
    {
      "id": 14,
      "name": "First Link",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 1,
      "system": true,
      "query": "    SELECT l.user_id, l.post_id, l.created_at granted_at\n    FROM\n    (\n      SELECT MIN(l1.id) id\n      FROM topic_links l1\n      JOIN badge_posts p1 ON p1.id = l1.post_id\n      JOIN badge_posts p2 ON p2.id = l1.link_post_id\n      WHERE NOT reflection AND p1.topic_id <> p2.topic_id AND not quote AND\n        (:backfill OR ( p1.id in (:post_ids) ))\n      GROUP BY l1.user_id\n    ) ids\n    JOIN topic_links l ON l.id = ids.id\n",
      "trigger": 2,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 3
    },
    {
      "id": 15,
      "name": "First Quote",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 1,
      "system": true,
      "query": "    SELECT ids.user_id, q.post_id, q.created_at granted_at\n    FROM\n    (\n      SELECT p1.user_id, MIN(q1.id) id\n      FROM quoted_posts q1\n      JOIN badge_posts p1 ON p1.id = q1.post_id\n      JOIN badge_posts p2 ON p2.id = q1.quoted_post_id\n      WHERE (:backfill OR ( p1.id IN (:post_ids) ))\n      GROUP BY p1.user_id\n    ) ids\n    JOIN quoted_posts q ON q.id = ids.id\n",
      "trigger": 2,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 3
    },
    {
      "id": 12,
      "name": "First Share",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 1,
      "system": true,
      "query": "    SELECT views.user_id, i2.post_id, i2.created_at granted_at\n    FROM\n    (\n      SELECT i.user_id, MIN(i.id) i_id\n      FROM incoming_links i\n      JOIN badge_posts p on p.id = i.post_id\n      WHERE i.user_id IS NOT NULL\n      GROUP BY i.user_id\n    ) as views\n    JOIN incoming_links i2 ON i2.id = views.i_id\n",
      "trigger": 0,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 3
    },
    {
      "id": 16,
      "name": "Read Guidelines",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 1,
      "system": true,
      "query": "    SELECT user_id, read_faq granted_at\n    FROM user_stats\n    WHERE read_faq IS NOT NULL AND (user_id IN (:user_ids) OR :backfill)\n",
      "trigger": 8,
      "target_posts": false,
      "auto_revoke": true,
      "show_posts": false,
      "badge_type_id": 3
    },
    {
      "id": 17,
      "name": "Reader",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 1,
      "system": true,
      "query": "    SELECT id user_id, current_timestamp granted_at\n    FROM users\n    WHERE id IN\n    (\n      SELECT pt.user_id\n      FROM post_timings pt\n      JOIN badge_posts b ON b.post_number = pt.post_number AND\n                            b.topic_id = pt.topic_id\n      JOIN topics t ON t.id = pt.topic_id\n      LEFT JOIN user_badges ub ON ub.badge_id = 17 AND ub.user_id = pt.user_id\n      WHERE ub.id IS NULL AND t.posts_count > 100\n      GROUP BY pt.user_id, pt.topic_id, t.posts_count\n      HAVING count(*) >= t.posts_count\n    )\n",
      "trigger": null,
      "target_posts": false,
      "auto_revoke": false,
      "show_posts": false,
      "badge_type_id": 3
    },
    {
      "id": 23,
      "name": "Great Share",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 2,
      "system": true,
      "query": "    SELECT views.user_id, i2.post_id, i2.created_at granted_at\n    FROM\n    (\n      SELECT i.user_id, MIN(i.id) i_id\n      FROM incoming_links i\n      JOIN badge_posts p on p.id = i.post_id\n      WHERE i.user_id IS NOT NULL\n      GROUP BY i.user_id,i.post_id\n      HAVING COUNT(*) > 1000\n    ) as views\n    JOIN incoming_links i2 ON i2.id = views.i_id\n",
      "trigger": 0,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 1
    },
    {
      "id": 22,
      "name": "Good Share",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 2,
      "system": true,
      "query": "    SELECT views.user_id, i2.post_id, i2.created_at granted_at\n    FROM\n    (\n      SELECT i.user_id, MIN(i.id) i_id\n      FROM incoming_links i\n      JOIN badge_posts p on p.id = i.post_id\n      WHERE i.user_id IS NOT NULL\n      GROUP BY i.user_id,i.post_id\n      HAVING COUNT(*) > 300\n    ) as views\n    JOIN incoming_links i2 ON i2.id = views.i_id\n",
      "trigger": 0,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 2
    },
    {
      "id": 10,
      "name": "Editor",
      "description": null,
      "grant_count": 1,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 2,
      "system": true,
      "query": "    SELECT p.user_id, min(p.id) post_id, min(p.created_at) granted_at\n    FROM badge_posts p\n    WHERE p.self_edits > 0 AND\n        (:backfill OR p.id IN (:post_ids) )\n    GROUP BY p.user_id\n",
      "trigger": 2,
      "target_posts": false,
      "auto_revoke": true,
      "show_posts": false,
      "badge_type_id": 3
    },
    {
      "id": 13,
      "name": "First Flag",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 2,
      "system": true,
      "query": "    SELECT pa1.user_id, pa1.created_at granted_at, pa1.post_id\n    FROM (\n      SELECT pa.user_id, min(pa.id) id\n      FROM post_actions pa\n      JOIN badge_posts p on p.id = pa.post_id\n      WHERE post_action_type_id IN (3,4,7,8) AND\n        (:backfill OR pa.post_id IN (:post_ids) )\n      GROUP BY pa.user_id\n    ) x\n    JOIN post_actions pa1 on pa1.id = x.id\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": false,
      "show_posts": false,
      "badge_type_id": 3
    },
    {
      "id": 21,
      "name": "Nice Share",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 2,
      "system": true,
      "query": "    SELECT views.user_id, i2.post_id, i2.created_at granted_at\n    FROM\n    (\n      SELECT i.user_id, MIN(i.id) i_id\n      FROM incoming_links i\n      JOIN badge_posts p on p.id = i.post_id\n      WHERE i.user_id IS NOT NULL\n      GROUP BY i.user_id,i.post_id\n      HAVING COUNT(*) > 25\n    ) as views\n    JOIN incoming_links i2 ON i2.id = views.i_id\n",
      "trigger": 0,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 3
    },
    {
      "id": 5,
      "name": "Welcome",
      "description": null,
      "grant_count": 1,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 2,
      "system": true,
      "query": "    SELECT p.user_id, min(post_id) post_id, min(pa.created_at) granted_at\n    FROM post_actions pa\n    JOIN badge_posts p on p.id = pa.post_id\n    WHERE post_action_type_id = 2 AND\n        (:backfill OR pa.post_id IN (:post_ids) )\n    GROUP BY p.user_id\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 3
    },
    {
      "id": 8,
      "name": "Great Post",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 3,
      "system": true,
      "query": "\n    SELECT p.user_id, p.id post_id, p.updated_at granted_at\n    FROM badge_posts p\n    WHERE p.post_number > 1 AND p.like_count >= 50 AND\n      (:backfill OR p.id IN (:post_ids) )\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 1
    },
    {
      "id": 20,
      "name": "Great Topic",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 3,
      "system": true,
      "query": "\n    SELECT p.user_id, p.id post_id, p.updated_at granted_at\n    FROM badge_posts p\n    WHERE p.post_number = 1 AND p.like_count >= 50 AND\n      (:backfill OR p.id IN (:post_ids) )\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 1
    },
    {
      "id": 7,
      "name": "Good Post",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 3,
      "system": true,
      "query": "\n    SELECT p.user_id, p.id post_id, p.updated_at granted_at\n    FROM badge_posts p\n    WHERE p.post_number > 1 AND p.like_count >= 25 AND\n      (:backfill OR p.id IN (:post_ids) )\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 2
    },
    {
      "id": 19,
      "name": "Good Topic",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 3,
      "system": true,
      "query": "\n    SELECT p.user_id, p.id post_id, p.updated_at granted_at\n    FROM badge_posts p\n    WHERE p.post_number = 1 AND p.like_count >= 25 AND\n      (:backfill OR p.id IN (:post_ids) )\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 2
    },
    {
      "id": 6,
      "name": "Nice Post",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 3,
      "system": true,
      "query": "\n    SELECT p.user_id, p.id post_id, p.updated_at granted_at\n    FROM badge_posts p\n    WHERE p.post_number > 1 AND p.like_count >= 10 AND\n      (:backfill OR p.id IN (:post_ids) )\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 3
    },
    {
      "id": 18,
      "name": "Nice Topic",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": true,
      "icon": "fa-certificate",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 3,
      "system": true,
      "query": "\n    SELECT p.user_id, p.id post_id, p.updated_at granted_at\n    FROM badge_posts p\n    WHERE p.post_number = 1 AND p.like_count >= 10 AND\n      (:backfill OR p.id IN (:post_ids) )\n",
      "trigger": 1,
      "target_posts": true,
      "auto_revoke": true,
      "show_posts": true,
      "badge_type_id": 3
    },
    {
      "id": 4,
      "name": "Leader",
      "description": null,
      "grant_count": 0,
      "allow_title": true,
      "multiple_grant": false,
      "icon": "fa-user",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 4,
      "system": true,
      "query": "\n    SELECT u.id user_id, current_timestamp granted_at FROM users u\n    WHERE trust_level >= 4 AND (\n      :backfill OR u.id IN (:user_ids)\n    )\n",
      "trigger": 4,
      "target_posts": false,
      "auto_revoke": true,
      "show_posts": false,
      "badge_type_id": 1
    },
    {
      "id": 3,
      "name": "Regular",
      "description": null,
      "grant_count": 0,
      "allow_title": true,
      "multiple_grant": false,
      "icon": "fa-user",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 4,
      "system": true,
      "query": "\n    SELECT u.id user_id, current_timestamp granted_at FROM users u\n    WHERE trust_level >= 3 AND (\n      :backfill OR u.id IN (:user_ids)\n    )\n",
      "trigger": 4,
      "target_posts": false,
      "auto_revoke": true,
      "show_posts": false,
      "badge_type_id": 2
    },
    {
      "id": 1,
      "name": "Basic User",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-user",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 4,
      "system": true,
      "query": "\n    SELECT u.id user_id, current_timestamp granted_at FROM users u\n    WHERE trust_level >= 1 AND (\n      :backfill OR u.id IN (:user_ids)\n    )\n",
      "trigger": 4,
      "target_posts": false,
      "auto_revoke": true,
      "show_posts": false,
      "badge_type_id": 3
    },
    {
      "id": 2,
      "name": "Member",
      "description": null,
      "grant_count": 0,
      "allow_title": false,
      "multiple_grant": false,
      "icon": "fa-user",
      "image": null,
      "listable": true,
      "enabled": true,
      "badge_grouping_id": 4,
      "system": true,
      "query": "\n    SELECT u.id user_id, current_timestamp granted_at FROM users u\n    WHERE trust_level >= 2 AND (\n      :backfill OR u.id IN (:user_ids)\n    )\n",
      "trigger": 4,
      "target_posts": false,
      "auto_revoke": true,
      "show_posts": false,
      "badge_type_id": 3
    }
  ],
  "badge_types": [
    {
      "id": 3,
      "name": "Bronze",
      "sort_order": 7
    },
    {
      "id": 1,
      "name": "Gold",
      "sort_order": 9
    },
    {
      "id": 2,
      "name": "Silver",
      "sort_order": 8
    }
  ],
  "badge_groupings": [
    {
      "id": 1,
      "name": "Getting Started",
      "description": null,
      "position": 10
    },
    {
      "id": 2,
      "name": "Community",
      "description": null,
      "position": 11
    },
    {
      "id": 3,
      "name": "Posting",
      "description": null,
      "position": 12
    },
    {
      "id": 4,
      "name": "Trust Level",
      "description": null,
      "position": 13
    },
    {
      "id": 5,
      "name": "Other",
      "description": null,
      "position": 14
    }
  ],
  "admin_badges": {
    "protected_system_fields": [
      "badge_type_id",
      "multiple_grant",
      "target_posts",
      "show_posts",
      "query",
      "trigger",
      "auto_revoke",
      "listable"
    ],
    "triggers": {
      "none": 0,
      "post_action": 1,
      "post_revision": 2,
      "trust_level_change": 4,
      "user_change": 8
    },
    "badge_ids": [
      9,
      11,
      14,
      15,
      12,
      16,
      17,
      23,
      22,
      10,
      13,
      21,
      5,
      8,
      20,
      7,
      19,
      6,
      18,
      4,
      3,
      1,
      2
    ],
    "badge_grouping_ids": [
      1,
      2,
      3,
      4,
      5
    ],
    "badge_type_ids": [
      1,
      2,
      3
    ]
  }
}


================================================
FILE: spec/fixtures/categories.json
================================================
{
  "featured_users": [],
  "category_list": {
    "can_create_category": false,
    "can_create_topic": false,
    "draft": null,
    "draft_key": "new_topic",
    "draft_sequence": null,
    "categories": [
      {
        "id": 1,
        "name": "test1",
        "color": "0000FF",
        "text_color": "FFFFFF",
        "slug": "test1",
        "topic_count": 0,
        "description": "Test category #1.",
        "topic_url": "/t/category-definition-for-test1/1",
        "hotness": 5,
        "read_restricted": false,
        "permission": null,
        "post_count": 0,
        "topics_week": 0,
        "topics_month": 0,
        "topics_year": 0,
        "description_excerpt": "Test category #1.",
        "featured_user_id    s": [],
        "topics": []
      },
      {
        "id": 2,
        "name": "test2",
        "color": "00FF00",
        "text_color": "FFFFFF",
        "slug": "test2",
        "topic_count": 0,
        "description": "Test category #2.",
        "topic_url": "/t/category-definition-for-test2/2",
        "hotness": 5,
        "read_restricted": false,
        "permission": null,
        "post_count": 0,
        "topics_week": 0,
        "topics_month": 0,
        "topics_year": 0,
        "description_excerpt": "Test category #2.",
        "featured_user_ids": [],
        "topics": []
      },
      {
        "id": 3,
        "name": "test3",
        "color": "FF0000",
        "text_color": "FFFFFF",
        "slug": "test3",
        "topic_count": 0,
        "description": "Test category #3.",
        "topic_url": "/t/category-definition-for-test3/3",
        "hotness": 5,
        "read_restricted": false,
        "permission": null,
        "post_count": 0,
        "topics_week": 0,
        "topics_month": 0,
        "topics_year": 0,
        "description_excerpt": "Test category #3.",
        "featured_user_ids": [],
        "topics": []
      }
    ]
  }
}


================================================
FILE: spec/fixtures/category_latest_topics.json
================================================
{
  "users": [
    {
      "id": 3,
      "username": "fuzzy",
      "avatar_template": "//www.gravatar.com/avatar/c0c59575e86a794d733db4ee29b2baf4.png?s={size}&r=pg&d=identicon"
    }
  ],
  "topic_list": {
    "can_create_topic": true,
    "draft": null,
    "draft_key": "new_topic",
    "draft_sequence": 2,
    "topics": [
      {
        "id": 5,
        "title": "About the category",
        "fancy_title": "About the category",
        "slug": "about-the-category",
        "posts_count": 2,
        "reply_count": 0,
        "highest_post_number": 2,
        "image_url": null,
        "created_at": "2014-04-16T14:10:50.950-04:00",
        "last_posted_at": "2014-04-16T14:31:10.406-04:00",
        "bumped": true,
        "bumped_at": "2014-04-17T13:35:01.102-04:00",
        "unseen": false,
        "last_read_post_number": 2,
        "unread": 0,
        "new_posts": 0,
        "pinned": true,
        "unpinned": null,
        "excerpt": null,
        "visible    ": true,
        "closed": false,
        "archived": false,
        "views": 0,
        "like_count": 0,
        "starred": false,
        "has_summary": false,
        "archetype": "regular",
        "last_poster_username": "fuzzy",
        "category_id": 2,
        "posters": [
          {
            "extras": "latest",
            "description": "Original Poster, Most Recent Poster",
            "user_id": 3
          }
        ]
      },
      {
        "id": 4,
        "title": "This is a markdown post",
        "fancy_title": "This is a markdown post",
        "slug": "this-is-a-markdown-post",
        "posts_count": 2,
        "reply_count": 0,
        "highest_post_number": 2,
        "image_url": null,
        "created_at": "2014-04-16T13:52:58.777-04:00",
        "last_posted_at": "2014-04-16T14:31:37.371-04:00",
        "bumped": true,
        "bumped_at": "2014-04-16T14:31:37.371-04:00",
        "unseen": false,
        "last_read_post_number": 2,
        "unread": 0,
        "new_posts": 0,
        "pinned": false,
        "unpinned    ": null,
        "visible": true,
        "closed": false,
        "archived": false,
        "views": 0,
        "like_count": 0,
        "starred": false,
        "has_summary": false,
        "archetype": "regular",
        "last_poster_username": "fuzzy",
        "category_id": 2,
        "posters": [
          {
            "extras": "latest",
            "description": "Original Poster, Most Recent Poster",
            "user_id": 3
          }
        ]
      }
    ]
  }
}


================================================
FILE: spec/fixtures/category_topics.json
================================================
{
  "users": [
    {
      "id": 3,
      "username": "fuzzy",
      "avatar_template": "//www.gravatar.com/avatar/c0c59575e86a794d733db4ee29b2baf4.png?s={size}&r=pg&d=identicon"
    }
  ],
  "topic_list": {
    "can_create_topic": true,
    "draft": null,
    "draft_key": "new_topic",
    "draft_sequence": 2,
    "topics": [
      {
        "id": 5,
        "title": "About the category",
        "fancy_title": "About the category",
        "slug": "about-the-category",
        "posts_count": 2,
        "reply_count": 0,
        "highest_post_number": 2,
        "image_url": null,
        "created_at": "2014-04-16T14:10:50.950-04:00",
        "last_posted_at": "2014-04-16T14:31:10.406-04:00",
        "bumped": true,
        "bumped_at": "2014-04-17T13:35:01.102-04:00",
        "unseen": false,
        "last_read_post_number": 2,
        "unread": 0,
        "new_posts": 0,
        "pinned": true,
        "unpinned": null,
        "excerpt": null,
        "visible    ": true,
        "closed": false,
        "archived": false,
        "views": 0,
        "like_count": 0,
        "starred": false,
        "has_summary": false,
        "archetype": "regular",
        "last_poster_username": "fuzzy",
        "category_id": 2,
        "posters": [
          {
            "extras": "latest",
            "description": "Original Poster, Most Recent Poster",
            "user_id": 3
          }
        ]
      },
      {
        "id": 4,
        "title": "This is a markdown post",
        "fancy_title": "This is a markdown post",
        "slug": "this-is-a-markdown-post",
        "posts_count": 2,
        "reply_count": 0,
        "highest_post_number": 2,
        "image_url": null,
        "created_at": "2014-04-16T13:52:58.777-04:00",
        "last_posted_at": "2014-04-16T14:31:37.371-04:00",
        "bumped": true,
        "bumped_at": "2014-04-16T14:31:37.371-04:00",
        "unseen": false,
        "last_read_post_number": 2,
        "unread": 0,
        "new_posts": 0,
        "pinned": false,
        "unpinned    ": null,
        "visible": true,
        "closed": false,
        "archived": false,
        "views": 0,
        "like_count": 0,
        "starred": false,
        "has_summary": false,
        "archetype": "regular",
        "last_poster_username": "fuzzy",
        "category_id": 2,
        "posters": [
          {
            "extras": "latest",
            "description": "Original Poster, Most Recent Poster",
            "user_id": 3
          }
        ]
      }
    ]
  }
}

================================================
FILE: spec/fixtures/create_topic_with_tags.json
================================================
{
  "id": 29,
  "name": null,
  "username": "blake",
  "avatar_template": "/user_avatar/localhost/blake/{size}/3_2.png",
  "created_at": "2020-07-09T15:52:43.802Z",
  "cooked": "<p>cb8ee24eb3c5a81139958cf2b6bee9bd 2a7f7ea218e24c9ee6528cd383527f99 5a3006b92918946230385393a83b6005</p>",
  "post_number": 1,
  "post_type": 1,
  "updated_at": "2020-07-09T15:52:43.802Z",
  "reply_count": 0,
  "reply_to_post_number": null,
  "quote_count": 0,
  "incoming_link_count": 0,
  "reads": 0,
  "readers_count": 0,
  "score": 0,
  "yours": true,
  "topic_id": 21,
  "topic_slug": "1412a036bfe-6480718525e-9f71f98ef46",
  "display_username": null,
  "primary_group_name": null,
  "primary_group_flair_url": null,
  "primary_group_flair_bg_color": null,
  "primary_group_flair_color": null,
  "version": 1,
  "can_edit": true,
  "can_delete": false,
  "can_recover": false,
  "can_wiki": true,
  "user_title": null,
  "actions_summary": [
    {
      "id": 3,
      "can_act": true
    },
    {
      "id": 4,
      "can_act": true
    },
    {
      "id": 8,
      "can_act": true
    },
    {
      "id": 7,
      "can_act": true
    }
  ],
  "moderator": false,
  "admin": true,
  "staff": true,
  "user_id": 1,
  "draft_sequence": 0,
  "hidden": false,
  "trust_level": 1,
  "deleted_at": null,
  "user_deleted": false,
  "edit_reason": null,
  "can_view_edit_history": true,
  "wiki": false,
  "reviewable_id": null,
  "reviewable_score_count": 0,
  "reviewable_score_pending_count": 0
}


================================================
FILE: spec/fixtures/email_list_all.json
================================================
[
  {
    "id": 64,
    "reply_key": null,
    "to_address": "steve7@example.com",
    "email_type": "signup",
    "user_id": 22,
    "created_at": "2015-01-03T13:33:46.730Z",
    "skipped": false,
    "user": {
      "id": 22,
      "username": "steve7",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve7/{size}/2.png"
    }
  },
  {
    "id": 63,
    "reply_key": null,
    "to_address": "steve6@example.com",
    "email_type": "user_private_message",
    "user_id": 21,
    "created_at": "2015-01-02T17:18:47.098Z",
    "skipped": false,
    "user": {
      "id": 21,
      "username": "steve6",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve6/{size}/2.png"
    }
  },
  {
    "id": 62,
    "reply_key": null,
    "to_address": "steve5@example.com",
    "email_type": "user_private_message",
    "user_id": 20,
    "created_at": "2015-01-02T16:05:38.192Z",
    "skipped": false,
    "user": {
      "id": 20,
      "username": "steve5",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve5/{size}/2.png"
    }
  },
  {
    "id": 61,
    "reply_key": null,
    "to_address": "steve4@example.com",
    "email_type": "user_private_message",
    "user_id": 19,
    "created_at": "2015-01-02T15:59:27.137Z",
    "skipped": false,
    "user": {
      "id": 19,
      "username": "steve4",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve4/{size}/2.png"
    }
  },
  {
    "id": 60,
    "reply_key": null,
    "to_address": "steve5@example.com",
    "email_type": "signup",
    "user_id": 20,
    "created_at": "2015-01-02T15:57:57.061Z",
    "skipped": false,
    "user": {
      "id": 20,
      "username": "steve5",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve5/{size}/2.png"
    }
  },
  {
    "id": 59,
    "reply_key": null,
    "to_address": "steve4@example.com",
    "email_type": "signup",
    "user_id": 19,
    "created_at": "2015-01-02T15:52:36.210Z",
    "skipped": false,
    "user": {
      "id": 19,
      "username": "steve4",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve4/{size}/2.png"
    }
  },
  {
    "id": 58,
    "reply_key": null,
    "to_address": "steve3@example.com",
    "email_type": "signup",
    "user_id": 18,
    "created_at": "2015-01-02T15:50:29.330Z",
    "skipped": false,
    "user": {
      "id": 18,
      "username": "steve3",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve3/{size}/2.png"
    }
  },
  {
    "id": 57,
    "reply_key": null,
    "to_address": "steve3@example.com",
    "email_type": "user_private_message",
    "user_id": 18,
    "created_at": "2015-01-02T14:07:40.357Z",
    "skipped": true,
    "skipped_reason": "The notification this email is about has already been read",
    "user": {
      "id": 18,
      "username": "steve3",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve3/{size}/2.png"
    }
  },
  {
    "id": 56,
    "reply_key": null,
    "to_address": "steve3@example.com",
    "email_type": "signup",
    "user_id": 18,
    "created_at": "2015-01-02T13:53:04.352Z",
    "skipped": false,
    "user": {
      "id": 18,
      "username": "steve3",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve3/{size}/2.png"
    }
  },
  {
    "id": 55,
    "reply_key": null,
    "to_address": "steve2@example.com",
    "email_type": "signup",
    "user_id": 17,
    "created_at": "2015-01-02T13:39:40.455Z",
    "skipped": false,
    "user": {
      "id": 17,
      "username": "steve2",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve2/{size}/2.png"
    }
  },
  {
    "id": 54,
    "reply_key": null,
    "to_address": "batman@gotham.com",
    "email_type": "signup",
    "user_id": 1,
    "created_at": "2015-01-02T13:38:11.418Z",
    "skipped": false,
    "user": {
      "id": 1,
      "username": "test_user",
      "uploaded_avatar_id": 7,
      "avatar_template": "/user_avatar/localhost/test_user/{size}/7.png"
    }
  },
  {
    "id": 53,
    "reply_key": null,
    "to_address": "dave3@gotham.com",
    "email_type": "signup",
    "user_id": 10,
    "created_at": "2015-01-01T13:08:57.914Z",
    "skipped": false,
    "user": {
      "id": 10,
      "username": "dave3",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/dave3/{size}/2.png"
    }
  },
  {
    "id": 52,
    "reply_key": null,
    "to_address": "dave2@gotham.com",
    "email_type": "authorize_email",
    "user_id": 9,
    "created_at": "2015-01-01T01:22:45.479Z",
    "skipped": false,
    "user": {
      "id": 9,
      "username": "dave2",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/dave2/{size}/2.png"
    }
  },
  {
    "id": 51,
    "reply_key": null,
    "to_address": "phil@example.com",
    "email_type": "user_private_message",
    "user_id": 7,
    "created_at": "2015-01-01T01:01:33.316Z",
    "skipped": false,
    "user": {
      "id": 7,
      "username": "phil12",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/phil12/{size}/2.png"
    }
  },
  {
    "id": 50,
    "reply_key": null,
    "to_address": "dave@example.com",
    "email_type": "user_private_message",
    "user_id": 8,
    "created_at": "2015-01-01T01:01:33.222Z",
    "skipped": false,
    "user": {
      "id": 8,
      "username": "dave",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/dave/{size}/2.png"
    }
  },
  {
    "id": 49,
    "reply_key": null,
    "to_address": "steve@example.com",
    "email_type": "user_private_message",
    "user_id": 4,
    "created_at": "2015-01-01T01:01:33.200Z",
    "skipped": false,
    "user": {
      "id": 4,
      "username": "steve",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve/{size}/2.png"
    }
  },
  {
    "id": 48,
    "reply_key": null,
    "to_address": "steve@example.com",
    "email_type": "signup",
    "user_id": 4,
    "created_at": "2015-01-01T01:01:33.141Z",
    "skipped": false,
    "user": {
      "id": 4,
      "username": "steve",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve/{size}/2.png"
    }
  },
  {
    "id": 47,
    "reply_key": null,
    "to_address": "steve999@example.com",
    "email_type": "authorize_email",
    "user_id": 4,
    "created_at": "2015-01-01T01:01:32.846Z",
    "skipped": false,
    "user": {
      "id": 4,
      "username": "steve",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve/{size}/2.png"
    }
  },
  {
    "id": 46,
    "reply_key": null,
    "to_address": "steve345@example.com",
    "email_type": "authorize_email",
    "user_id": 4,
    "created_at": "2015-01-01T01:01:32.844Z",
    "skipped": false,
    "user": {
      "id": 4,
      "username": "steve",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/steve/{size}/2.png"
    }
  },
  {
    "id": 45,
    "reply_key": null,
    "to_address": "robin3@gotham.com",
    "email_type": "authorize_email",
    "user_id": 2,
    "created_at": "2015-01-01T01:01:24.446Z",
    "skipped": false,
    "user": {
      "id": 2,
      "username": "robin",
      "uploaded_avatar_id": 3,
      "avatar_template": "/user_avatar/localhost/robin/{size}/3.png"
    }
  },
  {
    "id": 44,
    "reply_key": null,
    "to_address": "robin3@gotham.com",
    "email_type": "authorize_email",
    "user_id": 2,
    "created_at": "2015-01-01T01:01:24.445Z",
    "skipped": false,
    "user": {
      "id": 2,
      "username": "robin",
      "uploaded_avatar_id": 3,
      "avatar_template": "/user_avatar/localhost/robin/{size}/3.png"
    }
  },
  {
    "id": 43,
    "reply_key": null,
    "to_address": "robin3@gotham.com",
    "email_type": "authorize_email",
    "user_id": 2,
    "created_at": "2015-01-01T01:01:24.444Z",
    "skipped": false,
    "user": {
      "id": 2,
      "username": "robin",
      "uploaded_avatar_id": 3,
      "avatar_template": "/user_avatar/localhost/robin/{size}/3.png"
    }
  },
  {
    "id": 42,
    "reply_key": null,
    "to_address": "robin3@gotham.com",
    "email_type": "authorize_email",
    "user_id": 2,
    "created_at": "2015-01-01T01:01:24.441Z",
    "skipped": false,
    "user": {
      "id": 2,
      "username": "robin",
      "uploaded_avatar_id": 3,
      "avatar_template": "/user_avatar/localhost/robin/{size}/3.png"
    }
  },
  {
    "id": 41,
    "reply_key": null,
    "to_address": "no_email_found",
    "email_type": "signup",
    "user_id": null,
    "created_at": "2014-12-31T18:12:05.707Z",
    "skipped": true,
    "skipped_reason": "Can't find user with id 6",
    "user": null
  },
  {
    "id": 40,
    "reply_key": null,
    "to_address": "foo@bar.com",
    "email_type": "authorize_email",
    "user_id": 8,
    "created_at": "2014-12-31T17:56:39.302Z",
    "skipped": false,
    "user": {
      "id": 8,
      "username": "dave",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/dave/{size}/2.png"
    }
  },
  {
    "id": 39,
    "reply_key": null,
    "to_address": "dave2@example.com",
    "email_type": "user_private_message",
    "user_id": 9,
    "created_at": "2014-12-31T17:56:21.004Z",
    "skipped": false,
    "user": {
      "id": 9,
      "username": "dave2",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/dave2/{size}/2.png"
    }
  },
  {
    "id": 38,
    "reply_key": null,
    "to_address": "fake@example.com",
    "email_type": "signup",
    "user_id": 16,
    "created_at": "2014-12-31T17:54:32.454Z",
    "skipped": false,
    "user": {
      "id": 16,
      "username": "carl",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/carl/{size}/2.png"
    }
  },
  {
    "id": 37,
    "reply_key": null,
    "to_address": "test_user2@example.com",
    "email_type": "authorize_email",
    "user_id": 1,
    "created_at": "2014-12-31T17:53:48.162Z",
    "skipped": false,
    "user": {
      "id": 1,
      "username": "test_user",
      "uploaded_avatar_id": 7,
      "avatar_template": "/user_avatar/localhost/test_user/{size}/7.png"
    }
  },
  {
    "id": 36,
    "reply_key": null,
    "to_address": "foo@bar.com",
    "email_type": "authorize_email",
    "user_id": 7,
    "created_at": "2014-12-31T17:13:15.797Z",
    "skipped": false,
    "user": {
      "id": 7,
      "username": "phil12",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/phil12/{size}/2.png"
    }
  },
  {
    "id": 35,
    "reply_key": null,
    "to_address": "batman@gotham.com",
    "email_type": "authorize_email",
    "user_id": 7,
    "created_at": "2014-12-31T17:13:06.259Z",
    "skipped": false,
    "user": {
      "id": 7,
      "username": "phil12",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/phil12/{size}/2.png"
    }
  },
  {
    "id": 34,
    "reply_key": null,
    "to_address": "dave7@example.com",
    "email_type": "signup",
    "user_id": 14,
    "created_at": "2014-12-31T17:12:43.031Z",
    "skipped": false,
    "user": {
      "id": 14,
      "username": "dave7",
      "uploaded_avatar_id": null,
      "avatar_template": "/letter_avatar/dave7/{size}/2.png"
    }
  },
  {
    "id": 33,
    "reply_key": null,
    "to_address": "batman@gotham.com",
    "email_type": "authorize_email",
    "user_id": 2,
    "created_at": "2014-
Download .txt
gitextract_ygzwukcj/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .rubocop.yml
├── .streerc
├── CHANGELOG.md
├── Gemfile
├── Guardfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── config_example.yml
├── discourse_api.gemspec
├── examples/
│   ├── backups.rb
│   ├── badges.rb
│   ├── bookmark_topic.rb
│   ├── category.rb
│   ├── change_topic_status.rb
│   ├── create_private_message.rb
│   ├── create_topic.rb
│   ├── create_update_category.rb
│   ├── create_user.rb
│   ├── dashboard.rb
│   ├── disposable_invite_tokens.rb
│   ├── example.rb
│   ├── group_set_user_notification_level.rb
│   ├── groups.rb
│   ├── invite_users.rb
│   ├── manage_api_keys.rb
│   ├── notifications.rb
│   ├── polls.rb
│   ├── post_action.rb
│   ├── search.rb
│   ├── sent_private_messages.rb
│   ├── sso.rb
│   ├── topic_lists.rb
│   ├── update_user.rb
│   └── upload_file.rb
├── lib/
│   ├── discourse_api/
│   │   ├── api/
│   │   │   ├── api_key.rb
│   │   │   ├── backups.rb
│   │   │   ├── badges.rb
│   │   │   ├── categories.rb
│   │   │   ├── dashboard.rb
│   │   │   ├── email.rb
│   │   │   ├── groups.rb
│   │   │   ├── invite.rb
│   │   │   ├── notifications.rb
│   │   │   ├── params.rb
│   │   │   ├── polls.rb
│   │   │   ├── posts.rb
│   │   │   ├── private_messages.rb
│   │   │   ├── search.rb
│   │   │   ├── site_settings.rb
│   │   │   ├── sso.rb
│   │   │   ├── tags.rb
│   │   │   ├── topics.rb
│   │   │   ├── uploads.rb
│   │   │   ├── user_actions.rb
│   │   │   └── users.rb
│   │   ├── client.rb
│   │   ├── error.rb
│   │   ├── example_helper.rb
│   │   ├── single_sign_on.rb
│   │   └── version.rb
│   └── discourse_api.rb
└── spec/
    ├── discourse_api/
    │   ├── api/
    │   │   ├── api_key_spec.rb
    │   │   ├── backups_spec.rb
    │   │   ├── badges_spec.rb
    │   │   ├── categories_spec.rb
    │   │   ├── email_spec.rb
    │   │   ├── groups_spec.rb
    │   │   ├── invite_spec.rb
    │   │   ├── notifications_spec.rb
    │   │   ├── params_spec.rb
    │   │   ├── polls_spec.rb
    │   │   ├── posts_spec.rb
    │   │   ├── private_messages_spec.rb
    │   │   ├── search_spec.rb
    │   │   ├── site_settings_spec.rb
    │   │   ├── sso_spec.rb
    │   │   ├── topics_spec.rb
    │   │   ├── uploads_spec.rb
    │   │   ├── user_actions_spec.rb
    │   │   └── users_spec.rb
    │   ├── client_spec.rb
    │   └── single_sign_on_spec.rb
    ├── fixtures/
    │   ├── admin_user.json
    │   ├── api_key.json
    │   ├── backups.json
    │   ├── badges.json
    │   ├── categories.json
    │   ├── category_latest_topics.json
    │   ├── category_topics.json
    │   ├── create_topic_with_tags.json
    │   ├── email_list_all.json
    │   ├── email_settings.json
    │   ├── group.json
    │   ├── groups.json
    │   ├── hot.json
    │   ├── latest.json
    │   ├── list_api_keys.json
    │   ├── members_0.json
    │   ├── members_1.json
    │   ├── members_2.json
    │   ├── new.json
    │   ├── notification_success.json
    │   ├── notifications.json
    │   ├── polls_toggle_status.json
    │   ├── polls_vote.json
    │   ├── polls_voters.json
    │   ├── post.json
    │   ├── post_action_users.json
    │   ├── posts_before.json
    │   ├── posts_latest.json
    │   ├── private_messages.json
    │   ├── regenerate_api_key.json
    │   ├── replies.json
    │   ├── replies_and_topics.json
    │   ├── retrieve_invite.json
    │   ├── search.json
    │   ├── top.json
    │   ├── topic.json
    │   ├── topic_invite_user.json
    │   ├── topic_posts.json
    │   ├── topics_created_by.json
    │   ├── update_trust_level.json
    │   ├── upload_avatar.json
    │   ├── upload_file.json
    │   ├── user.json
    │   ├── user_activate_success.json
    │   ├── user_badges.json
    │   ├── user_create_success.json
    │   ├── user_grant_admin.json
    │   ├── user_grant_moderator.json
    │   ├── user_list.json
    │   ├── user_log_out_success.json
    │   ├── user_update_avatar_success.json
    │   ├── user_update_user.json
    │   └── user_update_username.json
    └── spec_helper.rb
Download .txt
SYMBOL INDEX (249 symbols across 28 files)

FILE: lib/discourse_api/api/api_key.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type ApiKey (line 4) | module ApiKey
        function list_api_keys (line 5) | def list_api_keys
        function create_api_key (line 10) | def create_api_key(args)
        function revoke_api_key (line 15) | def revoke_api_key(id)
        function undo_revoke_api_key (line 19) | def undo_revoke_api_key(id)
        function delete_api_key (line 23) | def delete_api_key(id)

FILE: lib/discourse_api/api/backups.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Backups (line 4) | module Backups
        function backups (line 5) | def backups
        function create_backup (line 10) | def create_backup
        function restore_backup (line 14) | def restore_backup(file_name)
        function download_backup (line 18) | def download_backup(file_name, destination)

FILE: lib/discourse_api/api/badges.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Badges (line 4) | module Badges
        function badges (line 5) | def badges
        function user_badges (line 10) | def user_badges(username)
        function grant_user_badge (line 15) | def grant_user_badge(params = {})
        function create_badge (line 19) | def create_badge(params = {})

FILE: lib/discourse_api/api/categories.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Categories (line 4) | module Categories
        function create_category (line 8) | def create_category(args = {})
        function update_category (line 14) | def update_category(args = {})
        function reorder_categories (line 21) | def reorder_categories(args = {})
        function delete_category (line 26) | def delete_category(id)
        function categories (line 31) | def categories(params = {})
        function categories_full (line 35) | def categories_full(params = {})
        function category_latest_topics (line 40) | def category_latest_topics(args = {})
        function category_latest_topics_full (line 49) | def category_latest_topics_full(args = {})
        function category_top_topics (line 57) | def category_top_topics(category_slug)
        function category_top_topics_full (line 66) | def category_top_topics_full(category_slug)
        function category_new_topics (line 71) | def category_new_topics(category_slug)
        function category_new_topics_full (line 76) | def category_new_topics_full(category_slug)
        function category (line 81) | def category(id)
        function category_set_user_notification (line 87) | def category_set_user_notification(args = {})
        function category_set_user_notification_level (line 93) | def category_set_user_notification_level(category_id, params)
        function common_category_params (line 100) | def common_category_params(args, include_id: false)

FILE: lib/discourse_api/api/dashboard.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Dashboard (line 4) | module Dashboard
        function get_dashboard_stats (line 5) | def get_dashboard_stats
        function get_dashboard_stats_totals (line 10) | def get_dashboard_stats_totals

FILE: lib/discourse_api/api/email.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Email (line 4) | module Email
        function email_settings (line 5) | def email_settings
        function list_email (line 10) | def list_email(filter)

FILE: lib/discourse_api/api/groups.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Groups (line 4) | module Groups
        function create_group (line 5) | def create_group(args)
        function update_group (line 38) | def update_group(group_id, args)
        function group_add_owners (line 68) | def group_add_owners(group_id, args)
        function group_remove_owners (line 73) | def group_remove_owners(group_id, args)
        function groups (line 78) | def groups(args = {})
        function group (line 87) | def group(group_name)
        function group_add (line 92) | def group_add(group_id, users)
        function group_remove (line 107) | def group_remove(group_id, users)
        function delete_group (line 122) | def delete_group(group_id)
        function group_members (line 126) | def group_members(group_name, params = {})
        function group_set_user_notification_level (line 138) | def group_set_user_notification_level(group, user_id, notification...

FILE: lib/discourse_api/api/invite.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Invite (line 4) | module Invite
        function invite_user (line 5) | def invite_user(params = {})
        function invite_user_to_topic (line 24) | def invite_user_to_topic(params = {})
        function invite_to_topic (line 29) | def invite_to_topic(topic_id, params = {})
        function retrieve_invite (line 35) | def retrieve_invite(params = {})
        function disposable_tokens (line 44) | def disposable_tokens(params = {})
        function update_invite (line 48) | def update_invite(invite_id, params = {})
        function destroy_all_expired_invites (line 67) | def destroy_all_expired_invites
        function resend_all_invites (line 71) | def resend_all_invites
        function resend_invite (line 75) | def resend_invite(email)
        function destroy_invite (line 79) | def destroy_invite(invite_id)

FILE: lib/discourse_api/api/notifications.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Notifications (line 4) | module Notifications
        function notifications (line 5) | def notifications(params = {})

FILE: lib/discourse_api/api/params.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      function params (line 4) | def self.params(args)
      class Params (line 8) | class Params
        method initialize (line 9) | def initialize(args)
        method required (line 17) | def required(*keys)
        method optional (line 25) | def optional(*keys)
        method default (line 30) | def default(args)
        method to_h (line 35) | def to_h

FILE: lib/discourse_api/api/polls.rb
  type DiscourseApi (line 3) | module DiscourseApi
    type API (line 4) | module API
      type Polls (line 5) | module Polls
        function poll_vote (line 6) | def poll_vote(args)
        function toggle_poll_status (line 11) | def toggle_poll_status(args)
        function poll_voters (line 21) | def poll_voters(args)

FILE: lib/discourse_api/api/posts.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Posts (line 4) | module Posts
        function create_post (line 5) | def create_post(args)
        function create_post_action (line 10) | def create_post_action(args)
        function get_post (line 15) | def get_post(id, args = {})
        function posts (line 21) | def posts(args = {})
        function wikify_post (line 27) | def wikify_post(id)
        function edit_post (line 31) | def edit_post(id, raw)
        function delete_post (line 35) | def delete_post(id)
        function destroy_post_action (line 39) | def destroy_post_action(post_id, post_action_type_id)
        function post_action_users (line 43) | def post_action_users(post_id, post_action_type_id)

FILE: lib/discourse_api/api/private_messages.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type PrivateMessages (line 4) | module PrivateMessages
        function create_private_message (line 6) | def create_private_message(args = {})
        function create_pm (line 15) | def create_pm(args = {})
        function private_messages (line 25) | def private_messages(username, *args)
        function sent_private_messages (line 30) | def sent_private_messages(username, *args)

FILE: lib/discourse_api/api/search.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Search (line 4) | module Search
        function search (line 11) | def search(term, options = {})

FILE: lib/discourse_api/api/site_settings.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type SiteSettings (line 4) | module SiteSettings
        function site_setting_update (line 5) | def site_setting_update(args = {})

FILE: lib/discourse_api/api/sso.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type SSO (line 4) | module SSO
        function sync_sso (line 5) | def sync_sso(params = {})

FILE: lib/discourse_api/api/tags.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Tags (line 4) | module Tags
        function show_tag (line 5) | def show_tag(tag)

FILE: lib/discourse_api/api/topics.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Topics (line 4) | module Topics
        function create_topic (line 9) | def create_topic(args = {})
        function create_topic_action (line 27) | def create_topic_action(args)
        function edit_topic_timestamp (line 33) | def edit_topic_timestamp(topic_id, timestamp)
        function latest_topics (line 37) | def latest_topics(params = {})
        function top_topics (line 42) | def top_topics(params = {})
        function new_topics (line 47) | def new_topics(params = {})
        function rename_topic (line 52) | def rename_topic(topic_id, title)
        function recategorize_topic (line 56) | def recategorize_topic(topic_id, category_id)
        function change_topic_status (line 61) | def change_topic_status(topic_slug, topic_id, params = {})
        function update_topic_status (line 66) | def update_topic_status(topic_id, params = {})
        function topic (line 71) | def topic(id, params = {})
        function topics_by (line 76) | def topics_by(username, params = {})
        function delete_topic (line 81) | def delete_topic(id)
        function topic_posts (line 85) | def topic_posts(topic_id, post_ids = [], params = {})
        function change_owner (line 105) | def change_owner(topic_id, params = {})
        function topic_set_user_notification_level (line 111) | def topic_set_user_notification_level(topic_id, params)
        function bookmark_topic (line 116) | def bookmark_topic(topic_id)
        function remove_topic_bookmark (line 120) | def remove_topic_bookmark(topic_id)
        function get_topic_url_by_external_id (line 124) | def get_topic_url_by_external_id(external_id)

FILE: lib/discourse_api/api/uploads.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Uploads (line 4) | module Uploads
        function upload_file (line 5) | def upload_file(args)

FILE: lib/discourse_api/api/user_actions.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type UserActions (line 4) | module UserActions
        function user_replies (line 5) | def user_replies(username)
        function user_topics_and_replies (line 11) | def user_topics_and_replies(username)

FILE: lib/discourse_api/api/users.rb
  type DiscourseApi (line 2) | module DiscourseApi
    type API (line 3) | module API
      type Users (line 4) | module Users
        function activate (line 5) | def activate(id)
        function user (line 9) | def user(username, params = {})
        function user_sso (line 14) | def user_sso(user_id)
        function update_avatar (line 19) | def update_avatar(username, args)
        function update_email (line 26) | def update_email(username, email)
        function update_user (line 30) | def update_user(username, args)
        function update_username (line 52) | def update_username(username, new_username)
        function update_trust_level (line 56) | def update_trust_level(user_id, args)
        function create_user (line 62) | def create_user(args)
        function log_out (line 72) | def log_out(id)
        function list_users (line 76) | def list_users(type, params = {})
        function grant_admin (line 81) | def grant_admin(user_id)
        function revoke_admin (line 86) | def revoke_admin(user_id)
        function grant_moderation (line 91) | def grant_moderation(user_id)
        function revoke_moderation (line 96) | def revoke_moderation(user_id)
        function by_external_id (line 100) | def by_external_id(external_id)
        function suspend (line 105) | def suspend(user_id, suspend_until, reason)
        function unsuspend (line 109) | def unsuspend(user_id)
        function anonymize (line 113) | def anonymize(user_id)
        function delete_user (line 117) | def delete_user(user_id, delete_posts = false)
        function check_username (line 121) | def check_username(username)
        function deactivate (line 126) | def deactivate(user_id)

FILE: lib/discourse_api/client.rb
  type DiscourseApi (line 29) | module DiscourseApi
    class Client (line 30) | class Client
      method initialize (line 58) | def initialize(host, api_key = nil, api_username = nil)
      method timeout= (line 66) | def timeout=(timeout)
      method api_username= (line 71) | def api_username=(api_username)
      method connection_options (line 76) | def connection_options
      method ssl (line 89) | def ssl(options)
      method delete (line 93) | def delete(path, params = {})
      method get (line 97) | def get(path, params = {})
      method post (line 101) | def post(path, params = {})
      method put (line 111) | def put(path, params = {})
      method patch (line 115) | def patch(path, params = {})
      method user_agent (line 119) | def user_agent
      method deprecated (line 123) | def deprecated(old, new)
      method connection (line 129) | def connection
      method request (line 163) | def request(method, path, params = {})
      method handle_error (line 177) | def handle_error(response)
      method check_subdirectory (line 192) | def check_subdirectory(host)

FILE: lib/discourse_api/error.rb
  type DiscourseApi (line 2) | module DiscourseApi
    class DiscourseError (line 3) | class DiscourseError < StandardError
      method initialize (line 6) | def initialize(message, response = nil)
    class Error (line 12) | class Error < DiscourseError
      method initialize (line 19) | def initialize(exception = $!)
    class UnauthenticatedError (line 25) | class UnauthenticatedError < DiscourseError
    class NotFoundError (line 28) | class NotFoundError < DiscourseError
    class UnprocessableEntity (line 31) | class UnprocessableEntity < DiscourseError
    class TooManyRequests (line 34) | class TooManyRequests < DiscourseError
    class Timeout (line 37) | class Timeout < Error

FILE: lib/discourse_api/example_helper.rb
  type DiscourseApi (line 4) | module DiscourseApi
    class ExampleHelper (line 5) | class ExampleHelper
      method load_yml (line 6) | def self.load_yml

FILE: lib/discourse_api/single_sign_on.rb
  type DiscourseApi (line 6) | module DiscourseApi
    class SingleSignOn (line 7) | class SingleSignOn
      class ParseError (line 8) | class ParseError < RuntimeError
      class MissingConfigError (line 10) | class MissingConfigError < RuntimeError
      method sso_secret (line 59) | def self.sso_secret
      method sso_url (line 63) | def self.sso_url
      method parse_hash (line 67) | def self.parse_hash(payload)
      method parse (line 96) | def self.parse(payload, sso_secret = nil)
      method diagnostics (line 132) | def diagnostics
      method sso_secret (line 136) | def sso_secret
      method sso_url (line 140) | def sso_url
      method custom_fields (line 144) | def custom_fields
      method sign (line 148) | def sign(payload)
      method to_url (line 152) | def to_url(base_url = nil)
      method payload (line 157) | def payload
      method unsigned_payload (line 162) | def unsigned_payload

FILE: lib/discourse_api/version.rb
  type DiscourseApi (line 2) | module DiscourseApi

FILE: spec/discourse_api/api/params_spec.rb
  function params_for (line 5) | def params_for(h)

FILE: spec/spec_helper.rb
  function host (line 22) | def host
  function a_delete (line 26) | def a_delete(path)
  function a_get (line 30) | def a_get(path)
  function a_post (line 34) | def a_post(path)
  function a_put (line 38) | def a_put(path)
  function stub_delete (line 42) | def stub_delete(path)
  function stub_get (line 46) | def stub_get(path)
  function stub_post (line 50) | def stub_post(path)
  function stub_put (line 54) | def stub_put(path)
  function fixture_path (line 58) | def fixture_path
  function fixture (line 62) | def fixture(file)
  function escape_params (line 66) | def escape_params(params)
Condensed preview — 139 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (350K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1289,
    "preview": "name: CI\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 34,
    "preview": "Gemfile.lock\ncoverage\n/config.yml\n"
  },
  {
    "path": ".rubocop.yml",
    "chars": 88,
    "preview": "inherit_gem:\n  rubocop-discourse: stree-compat.yml\n\nDiscourse/Plugins:\n  Enabled: false\n"
  },
  {
    "path": ".streerc",
    "chars": 78,
    "preview": "--print-width=100\n--plugins=plugin/trailing_comma,plugin/disable_auto_ternary\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 9606,
    "preview": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changel"
  },
  {
    "path": "Gemfile",
    "chars": 128,
    "preview": "# frozen_string_literal: true\nsource \"https://rubygems.org\"\n\n# Specify your gem's dependencies in discourse_api.gemspec\n"
  },
  {
    "path": "Guardfile",
    "chars": 192,
    "preview": "# frozen_string_literal: true\nguard :rspec do\n  watch(%r{^spec/.+_spec\\.rb$})\n  watch(%r{^lib/(.+)\\.rb$})     { |m| \"spe"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1099,
    "preview": "Copyright (c) 2014 Civilized Discourse Construction Kit, Inc.\n\nMIT License\n\nPermission is hereby granted, free of charge"
  },
  {
    "path": "README.md",
    "chars": 3370,
    "preview": "# DiscourseApi\n\nThe Discourse API gem allows you to consume the Discourse API\n\n## Installation\n\nAdd this line to your ap"
  },
  {
    "path": "Rakefile",
    "chars": 251,
    "preview": "# frozen_string_literal: true\nrequire 'bundler/gem_tasks'\n\nrequire 'rspec/core/rake_task'\nRSpec::Core::RakeTask.new(:spe"
  },
  {
    "path": "config_example.yml",
    "chars": 517,
    "preview": "# Specify your environment by making a copy of this file to create a file in your root directory called config.yml with "
  },
  {
    "path": "discourse_api.gemspec",
    "chars": 1710,
    "preview": "# frozen_string_literal: true\n\nlib = File.expand_path(\"lib\", __dir__)\n$LOAD_PATH.unshift(lib) if !$LOAD_PATH.include?(li"
  },
  {
    "path": "examples/backups.rb",
    "chars": 643,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/badges.rb",
    "chars": 1000,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/bookmark_topic.rb",
    "chars": 514,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/category.rb",
    "chars": 1081,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/change_topic_status.rb",
    "chars": 1266,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/create_private_message.rb",
    "chars": 559,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/create_topic.rb",
    "chars": 1004,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/create_update_category.rb",
    "chars": 1091,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/create_user.rb",
    "chars": 600,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/dashboard.rb",
    "chars": 601,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/disposable_invite_tokens.rb",
    "chars": 782,
    "preview": "# frozen_string_literal: true\n# requires this plugin => https://github.com/discourse/discourse-invite-tokens\n\nrequire \"c"
  },
  {
    "path": "examples/example.rb",
    "chars": 440,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/group_set_user_notification_level.rb",
    "chars": 1810,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/groups.rb",
    "chars": 936,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/invite_users.rb",
    "chars": 1056,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/manage_api_keys.rb",
    "chars": 1263,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/notifications.rb",
    "chars": 644,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/polls.rb",
    "chars": 811,
    "preview": "# frozen_string_literal: true\n\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../."
  },
  {
    "path": "examples/post_action.rb",
    "chars": 798,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/search.rb",
    "chars": 444,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/sent_private_messages.rb",
    "chars": 436,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/sso.rb",
    "chars": 545,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/topic_lists.rb",
    "chars": 618,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/update_user.rb",
    "chars": 1294,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "examples/upload_file.rb",
    "chars": 592,
    "preview": "# frozen_string_literal: true\n$LOAD_PATH.unshift File.expand_path(\"../../lib\", __FILE__)\nrequire File.expand_path(\"../.."
  },
  {
    "path": "lib/discourse_api/api/api_key.rb",
    "chars": 591,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module ApiKey\n      def list_api_keys\n        respons"
  },
  {
    "path": "lib/discourse_api/api/backups.rb",
    "chars": 605,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Backups\n      def backups\n        response = g"
  },
  {
    "path": "lib/discourse_api/api/badges.rb",
    "chars": 1015,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Badges\n      def badges\n        response = get"
  },
  {
    "path": "lib/discourse_api/api/categories.rb",
    "chars": 4084,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Categories\n      # :color and :text_color are "
  },
  {
    "path": "lib/discourse_api/api/dashboard.rb",
    "chars": 544,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Dashboard\n      def get_dashboard_stats\n      "
  },
  {
    "path": "lib/discourse_api/api/email.rb",
    "chars": 315,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Email\n      def email_settings\n        respons"
  },
  {
    "path": "lib/discourse_api/api/groups.rb",
    "chars": 4362,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Groups\n      def create_group(args)\n        ar"
  },
  {
    "path": "lib/discourse_api/api/invite.rb",
    "chars": 2051,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Invite\n      def invite_user(params = {})\n    "
  },
  {
    "path": "lib/discourse_api/api/notifications.rb",
    "chars": 323,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Notifications\n      def notifications(params ="
  },
  {
    "path": "lib/discourse_api/api/params.rb",
    "chars": 1018,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    def self.params(args)\n      Params.new(args)\n    end\n"
  },
  {
    "path": "lib/discourse_api/api/polls.rb",
    "chars": 726,
    "preview": "# frozen_string_literal: true\n\nmodule DiscourseApi\n  module API\n    module Polls\n      def poll_vote(args)\n        args "
  },
  {
    "path": "lib/discourse_api/api/posts.rb",
    "chars": 1379,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Posts\n      def create_post(args)\n        args"
  },
  {
    "path": "lib/discourse_api/api/private_messages.rb",
    "chars": 1187,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module PrivateMessages\n      # TODO: Deprecated. Remo"
  },
  {
    "path": "lib/discourse_api/api/search.rb",
    "chars": 722,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Search\n      # Returns search results that mat"
  },
  {
    "path": "lib/discourse_api/api/site_settings.rb",
    "chars": 354,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module SiteSettings\n      def site_setting_update(arg"
  },
  {
    "path": "lib/discourse_api/api/sso.rb",
    "chars": 250,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module SSO\n      def sync_sso(params = {})\n        ss"
  },
  {
    "path": "lib/discourse_api/api/tags.rb",
    "chars": 193,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Tags\n      def show_tag(tag)\n        response "
  },
  {
    "path": "lib/discourse_api/api/topics.rb",
    "chars": 3735,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Topics\n      # :category OPTIONAL name of cate"
  },
  {
    "path": "lib/discourse_api/api/uploads.rb",
    "chars": 345,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Uploads\n      def upload_file(args)\n        ar"
  },
  {
    "path": "lib/discourse_api/api/user_actions.rb",
    "chars": 492,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module UserActions\n      def user_replies(username)\n "
  },
  {
    "path": "lib/discourse_api/api/users.rb",
    "chars": 3578,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  module API\n    module Users\n      def activate(id)\n        put(\"/adm"
  },
  {
    "path": "lib/discourse_api/client.rb",
    "chars": 5691,
    "preview": "# frozen_string_literal: true\nrequire \"faraday\"\nrequire \"faraday/follow_redirects\"\nrequire \"faraday/multipart\"\nrequire \""
  },
  {
    "path": "lib/discourse_api/error.rb",
    "chars": 821,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  class DiscourseError < StandardError\n    attr_reader :response\n\n    "
  },
  {
    "path": "lib/discourse_api/example_helper.rb",
    "chars": 344,
    "preview": "# frozen_string_literal: true\nrequire \"yaml\"\n\nmodule DiscourseApi\n  class ExampleHelper\n    def self.load_yml\n      conf"
  },
  {
    "path": "lib/discourse_api/single_sign_on.rb",
    "chars": 4496,
    "preview": "# frozen_string_literal: true\nrequire \"base64\"\nrequire \"rack\"\nrequire \"openssl\"\n\nmodule DiscourseApi\n  class SingleSignO"
  },
  {
    "path": "lib/discourse_api/version.rb",
    "chars": 74,
    "preview": "# frozen_string_literal: true\nmodule DiscourseApi\n  VERSION = \"2.1.0\"\nend\n"
  },
  {
    "path": "lib/discourse_api.rb",
    "chars": 236,
    "preview": "# frozen_string_literal: true\nrequire \"discourse_api/api/params\"\nrequire \"discourse_api/client\"\nrequire \"discourse_api/e"
  },
  {
    "path": "spec/discourse_api/api/api_key_spec.rb",
    "chars": 3163,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::ApiKey do\n  subject(:client) { Discours"
  },
  {
    "path": "spec/discourse_api/api/backups_spec.rb",
    "chars": 771,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Backups do\n  subject(:client) { Discour"
  },
  {
    "path": "spec/discourse_api/api/badges_spec.rb",
    "chars": 1358,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Badges do\n  subject(:client) { Discours"
  },
  {
    "path": "spec/discourse_api/api/categories_spec.rb",
    "chars": 6907,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Categories do\n  subject(:client) { Disc"
  },
  {
    "path": "spec/discourse_api/api/email_spec.rb",
    "chars": 1322,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Email do\n  subject(:client) { Discourse"
  },
  {
    "path": "spec/discourse_api/api/groups_spec.rb",
    "chars": 5932,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Groups do\n  subject(:client) { Discours"
  },
  {
    "path": "spec/discourse_api/api/invite_spec.rb",
    "chars": 3574,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Invite do\n  subject(:client) { Discours"
  },
  {
    "path": "spec/discourse_api/api/notifications_spec.rb",
    "chars": 835,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Notifications do\n  subject(:client) { D"
  },
  {
    "path": "spec/discourse_api/api/params_spec.rb",
    "chars": 1282,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Params do\n  def params_for(h)\n    Disco"
  },
  {
    "path": "spec/discourse_api/api/polls_spec.rb",
    "chars": 2722,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Polls do\n  subject(:client) { Discourse"
  },
  {
    "path": "spec/discourse_api/api/posts_spec.rb",
    "chars": 2286,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Posts do\n  let(:client) { DiscourseApi:"
  },
  {
    "path": "spec/discourse_api/api/private_messages_spec.rb",
    "chars": 2063,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::PrivateMessages do\n  subject(:client) {"
  },
  {
    "path": "spec/discourse_api/api/search_spec.rb",
    "chars": 1005,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Search do\n  subject(:client) { Discours"
  },
  {
    "path": "spec/discourse_api/api/site_settings_spec.rb",
    "chars": 522,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::SiteSettings do\n  subject(:client) { Di"
  },
  {
    "path": "spec/discourse_api/api/sso_spec.rb",
    "chars": 1998,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::SSO do\n  subject(:client) { DiscourseAp"
  },
  {
    "path": "spec/discourse_api/api/topics_spec.rb",
    "chars": 8126,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Topics do\n  subject(:client) { Discours"
  },
  {
    "path": "spec/discourse_api/api/uploads_spec.rb",
    "chars": 1105,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Uploads do\n  subject(:client) { Discour"
  },
  {
    "path": "spec/discourse_api/api/user_actions_spec.rb",
    "chars": 1396,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::UserActions do\n  subject(:client) { Dis"
  },
  {
    "path": "spec/discourse_api/api/users_spec.rb",
    "chars": 12930,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::API::Users do\n  subject(:client) { Discourse"
  },
  {
    "path": "spec/discourse_api/client_spec.rb",
    "chars": 5645,
    "preview": "# frozen_string_literal: true\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::Client do\n  subject(:client) { DiscourseApi:"
  },
  {
    "path": "spec/discourse_api/single_sign_on_spec.rb",
    "chars": 1868,
    "preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe DiscourseApi::SingleSignOn do\n  describe DiscourseApi::Si"
  },
  {
    "path": "spec/fixtures/admin_user.json",
    "chars": 2871,
    "preview": "{\n  \"id\": 4,\n  \"username\": \"47f6974294f6e3eb9686\",\n  \"avatar_template\": \"/letter_avatar_proxy/v2/letter/4/8e8cbc/{size}."
  },
  {
    "path": "spec/fixtures/api_key.json",
    "chars": 273,
    "preview": "{\n  \"key\": {\n    \"id\": 5,\n    \"key\": \"e722e04df8cf6d097d565ca04eea1ff8e9e8f09beb405bae6f0c79852916f334\",\n    \"user\": {\n "
  },
  {
    "path": "spec/fixtures/backups.json",
    "chars": 304,
    "preview": "[\n  {\n    \"filename\": \"discourse-2015-01-10-065015.tar.gz\",\n    \"size\": 557075,\n    \"link\": \"//localhost:3000/admin/back"
  },
  {
    "path": "spec/fixtures/badges.json",
    "chars": 18291,
    "preview": "{\n  \"badges\": [\n    {\n      \"id\": 9,\n      \"name\": \"Autobiographer\",\n      \"description\": null,\n      \"grant_count\": 0,\n"
  },
  {
    "path": "spec/fixtures/categories.json",
    "chars": 1922,
    "preview": "{\n  \"featured_users\": [],\n  \"category_list\": {\n    \"can_create_category\": false,\n    \"can_create_topic\": false,\n    \"dra"
  },
  {
    "path": "spec/fixtures/category_latest_topics.json",
    "chars": 2532,
    "preview": "{\n  \"users\": [\n    {\n      \"id\": 3,\n      \"username\": \"fuzzy\",\n      \"avatar_template\": \"//www.gravatar.com/avatar/c0c59"
  },
  {
    "path": "spec/fixtures/category_topics.json",
    "chars": 2531,
    "preview": "{\n  \"users\": [\n    {\n      \"id\": 3,\n      \"username\": \"fuzzy\",\n      \"avatar_template\": \"//www.gravatar.com/avatar/c0c59"
  },
  {
    "path": "spec/fixtures/create_topic_with_tags.json",
    "chars": 1480,
    "preview": "{\n  \"id\": 29,\n  \"name\": null,\n  \"username\": \"blake\",\n  \"avatar_template\": \"/user_avatar/localhost/blake/{size}/3_2.png\","
  },
  {
    "path": "spec/fixtures/email_list_all.json",
    "chars": 18289,
    "preview": "[\n  {\n    \"id\": 64,\n    \"reply_key\": null,\n    \"to_address\": \"steve7@example.com\",\n    \"email_type\": \"signup\",\n    \"user"
  },
  {
    "path": "spec/fixtures/email_settings.json",
    "chars": 172,
    "preview": "{\n  \"delivery_method\": \"smtp\",\n  \"settings\": [\n    {\n      \"name\": \"address\",\n      \"value\": \"localhost\"\n    },\n    {\n  "
  },
  {
    "path": "spec/fixtures/group.json",
    "chars": 176,
    "preview": "{\"basic_group\": {\n        \"id\": 101,\n        \"name\": \"group_1\",\n        \"user_count\": 17,\n        \"automatic\": false,\n  "
  },
  {
    "path": "spec/fixtures/groups.json",
    "chars": 328,
    "preview": "[\n    {\n        \"id\": 101,\n        \"name\": \"group_1\",\n        \"user_count\": 17,\n        \"automatic\": false,\n        \"ali"
  },
  {
    "path": "spec/fixtures/hot.json",
    "chars": 3104,
    "preview": "{\n  \"users\": [\n    {\n      \"id\": 1,\n      \"username\": \"test_users\",\n      \"avatar_template\": \"//www.gravatar.com/avatar/"
  },
  {
    "path": "spec/fixtures/latest.json",
    "chars": 3399,
    "preview": "{\n  \"users\": [\n    {\n      \"id\": 1,\n      \"username\": \"test_users\",\n      \"avatar_template\": \"//www.gravatar.com/avatar/"
  },
  {
    "path": "spec/fixtures/list_api_keys.json",
    "chars": 262,
    "preview": "{\n  \"keys\": [\n    {\n      \"id\": 1,\n      \"key\": \"test_d7fd0429940\",\n      \"user\": {\n        \"id\": 1,\n        \"username\":"
  },
  {
    "path": "spec/fixtures/members_0.json",
    "chars": 8392,
    "preview": "{\n    \"members\": [{\n        \"id\": 1,\n        \"name\": \"Test 0\",\n        \"username\": \"Test_0\"\n    }, {\n        \"id\": 2,\n  "
  },
  {
    "path": "spec/fixtures/members_1.json",
    "chars": 7850,
    "preview": "{\n    \"members\": [{\n        \"id\": 101,\n        \"name\": \"Test 100\",\n        \"username\": \"Test_100\"\n    }, {\n        \"id\":"
  },
  {
    "path": "spec/fixtures/members_2.json",
    "chars": 9086,
    "preview": "{\n    \"members\": [{\n        \"id\": 1,\n        \"name\": \"Test 0\",\n        \"username\": \"Test_0\"\n    }, {\n        \"id\": 2,\n  "
  },
  {
    "path": "spec/fixtures/new.json",
    "chars": 3087,
    "preview": "{\n  \"users\": [\n    {\n      \"id\": 1,\n      \"username\": \"test_user\",\n      \"avatar_template\": \"//www.gravatar.com/avatar/t"
  },
  {
    "path": "spec/fixtures/notification_success.json",
    "chars": 22,
    "preview": "{\n  \"success\": \"OK\"\n}\n"
  },
  {
    "path": "spec/fixtures/notifications.json",
    "chars": 376,
    "preview": "[\n  {\n    \"notification_type\": 9,\n    \"read\": false,\n    \"created_at\": \"2014-04-02T11:58:00.670-04:00\",\n    \"post_number"
  },
  {
    "path": "spec/fixtures/polls_toggle_status.json",
    "chars": 1452,
    "preview": "{\n  \"poll\": {\n    \"name\": \"poll\",\n    \"type\": \"regular\",\n    \"status\": \"closed\",\n    \"public\": true,\n    \"results\": \"alw"
  },
  {
    "path": "spec/fixtures/polls_vote.json",
    "chars": 1744,
    "preview": "{\n  \"poll\": {\n    \"name\": \"poll\",\n    \"type\": \"regular\",\n    \"status\": \"open\",\n    \"public\": true,\n    \"results\": \"alway"
  },
  {
    "path": "spec/fixtures/polls_voters.json",
    "chars": 521,
    "preview": "{\n  \"voters\": {\n    \"e539a9df8700d0d05c69356a07b768cf\": [\n      {\n        \"id\": 356,\n        \"username\": \"David_Ashby\",\n"
  },
  {
    "path": "spec/fixtures/post.json",
    "chars": 2867,
    "preview": "{\n  \"id\": 11,\n  \"name\": \"system\",\n  \"username\": \"system\",\n  \"avatar_template\": \"/user_avatar/localhost/system/{size}/1.p"
  },
  {
    "path": "spec/fixtures/post_action_users.json",
    "chars": 426,
    "preview": "{\n  \"post_action_users\":  [\n    {\n      \"id\": 1286,\n      \"username\": \"john_smith\",\n      \"avatar_template\": \"/user_avat"
  },
  {
    "path": "spec/fixtures/posts_before.json",
    "chars": 5747,
    "preview": "{\n  \"latest_posts\": [\n    {\n      \"id\": 14,\n      \"name\": null,\n      \"username\": \"johndoe\",\n      \"avatar_template\": \"/"
  },
  {
    "path": "spec/fixtures/posts_latest.json",
    "chars": 7181,
    "preview": "{\n  \"latest_posts\": [\n    {\n      \"id\": 15,\n      \"name\": null,\n      \"username\": \"johndoe\",\n      \"avatar_template\": \"/"
  },
  {
    "path": "spec/fixtures/private_messages.json",
    "chars": 1810,
    "preview": "{\n  \"users\": [\n    {\n      \"id\": 1,\n      \"username\": \"test_user\",\n      \"uploaded_avatar_id\": null,\n      \"avatar_templ"
  },
  {
    "path": "spec/fixtures/regenerate_api_key.json",
    "chars": 149,
    "preview": "{\n    \"api_key\": {\n        \"id\": 10,\n        \"key\": \"d8a80230fd14531c9b4809c7800b1fe78073a3771d3d2a56a5671bace55bc393\",\n"
  },
  {
    "path": "spec/fixtures/replies.json",
    "chars": 1085,
    "preview": "{\n    \"user_actions\": [\n        {\n            \"action_type\": 5,\n            \"created_at\": \"2016-06-29T20:27:55.767Z\",\n  "
  },
  {
    "path": "spec/fixtures/replies_and_topics.json",
    "chars": 1743,
    "preview": "{\n  \"user_actions\": [\n    {\n      \"action_type\": 5,\n      \"created_at\": \"2016-08-10T14:52:49.660Z\",\n      \"excerpt\": \"te"
  },
  {
    "path": "spec/fixtures/retrieve_invite.json",
    "chars": 3165,
    "preview": "{\n  \"id\": 26,\n  \"invite_key\": \"CGUHjNC4Na\",\n  \"link\": \"http://localhost:3000/invites/CGUHjNC4Na\",\n  \"email\": \"foo@bar.co"
  },
  {
    "path": "spec/fixtures/search.json",
    "chars": 404,
    "preview": "[\n  {\n    \"type\": \"topic\",\n    \"name\": \"Topics\",\n    \"more\": true,\n    \"results\": [\n      {\n        \"id\": 1,\n        \"ti"
  },
  {
    "path": "spec/fixtures/top.json",
    "chars": 3234,
    "preview": "{\n  \"topic_list\": {\n    \"can_create_topic\": false,\n    \"more_topics_url\": \"/latest.json?page=1\",\n    \"draft\": null,\n    "
  },
  {
    "path": "spec/fixtures/topic.json",
    "chars": 21729,
    "preview": "{\n  \"post_stream\": {\n    \"posts\": [\n      {\n        \"id\": 83,\n        \"name\": \"Sam Saffron\",\n        \"username\": \"samsaf"
  },
  {
    "path": "spec/fixtures/topic_invite_user.json",
    "chars": 22,
    "preview": "{\n  \"success\": true\n}\n"
  },
  {
    "path": "spec/fixtures/topic_posts.json",
    "chars": 1872,
    "preview": "{\n  \"post_stream\":{\n    \"posts\":[\n      {\n        \"id\":123456,\n        \"name\":\"Someones Name\",\n        \"username\":\"Some-"
  },
  {
    "path": "spec/fixtures/topics_created_by.json",
    "chars": 1240,
    "preview": "{\n  \"users\": [\n    {\n      \"id\": 1,\n      \"username\": \"test_user\",\n      \"avatar_template\": \"//www.gravatar.com/avatar/t"
  },
  {
    "path": "spec/fixtures/update_trust_level.json",
    "chars": 1009,
    "preview": "{\n  \"admin_user\": {\n    \"id\": 2,\n    \"username\": \"robin\",\n    \"uploaded_avatar_id\": 3,\n    \"avatar_template\": \"/user_ava"
  },
  {
    "path": "spec/fixtures/upload_avatar.json",
    "chars": 391,
    "preview": "{\n  \"id\": 11,\n  \"user_id\": 1,\n  \"original_filename\": \"avatar.jpg\",\n  \"filesize\": 86900,\n  \"width\": 360,\n  \"height\": 360,"
  },
  {
    "path": "spec/fixtures/upload_file.json",
    "chars": 396,
    "preview": "{\n  \"id\": 11,\n  \"user_id\": 1,\n  \"original_filename\": \"grumpy_cat.gif\",\n  \"filesize\": 86900,\n  \"width\": 360,\n  \"height\": "
  },
  {
    "path": "spec/fixtures/user.json",
    "chars": 1830,
    "preview": "{\n  \"user_badges\": [],\n  \"user\": {\n    \"id\": 1,\n    \"username\": \"test_user\",\n    \"uploaded_avatar_id\": null,\n    \"avatar"
  },
  {
    "path": "spec/fixtures/user_activate_success.json",
    "chars": 22,
    "preview": "{\n  \"success\": \"OK\"\n}\n"
  },
  {
    "path": "spec/fixtures/user_badges.json",
    "chars": 4019,
    "preview": "{\n  \"user_badges\": [\n    {\n      \"id\": 3,\n      \"granted_at\": \"2014-11-30T14:27:19.404Z\",\n      \"badge_id\": 10,\n      \"u"
  },
  {
    "path": "spec/fixtures/user_create_success.json",
    "chars": 204,
    "preview": "{\n  \"success\": true,\n  \"active\": false,\n  \"message\": \"You're almost done! We sent an activation email to <b>test2@exampl"
  },
  {
    "path": "spec/fixtures/user_grant_admin.json",
    "chars": 1116,
    "preview": "{\n    \"admin_user\": {\n        \"id\": 11,\n        \"username\": \"dave4\",\n        \"uploaded_avatar_id\": null,\n        \"avatar"
  },
  {
    "path": "spec/fixtures/user_grant_moderator.json",
    "chars": 1116,
    "preview": "{\n    \"admin_user\": {\n        \"id\": 11,\n        \"username\": \"dave4\",\n        \"uploaded_avatar_id\": null,\n        \"avatar"
  },
  {
    "path": "spec/fixtures/user_list.json",
    "chars": 15361,
    "preview": "[\n  {\n    \"id\": 1,\n    \"username\": \"test_user\",\n    \"uploaded_avatar_id\": 7,\n    \"avatar_template\": \"/user_avatar/localh"
  },
  {
    "path": "spec/fixtures/user_log_out_success.json",
    "chars": 22,
    "preview": "{\n  \"success\": \"OK\"\n}\n"
  },
  {
    "path": "spec/fixtures/user_update_avatar_success.json",
    "chars": 22,
    "preview": "{\n  \"success\": \"OK\"\n}\n"
  },
  {
    "path": "spec/fixtures/user_update_user.json",
    "chars": 22,
    "preview": "{\n  \"success\": true\n}\n"
  },
  {
    "path": "spec/fixtures/user_update_username.json",
    "chars": 43,
    "preview": "{\n  \"id\": 7,\n  \"username\": \"fake_user_2\"\n}\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "chars": 1102,
    "preview": "# frozen_string_literal: true\nrequire \"simplecov\"\n\nSimpleCov.formatter =\n  SimpleCov::Formatter::MultiFormatter.new([Sim"
  }
]

About this extraction

This page contains the full source code of the discourse/discourse_api GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 139 files (306.0 KB), approximately 93.2k tokens, and a symbol index with 249 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!