discourse/config/routes.rb
David Taylor 31db83527b DEV: Introduce PresenceChannel API for core and plugin use
PresenceChannel aims to be a generic system for allow the server, and end-users, to track the number and identity of users performing a specific task on the site. For example, it might be used to track who is currently 'replying' to a specific topic, editing a specific wiki post, etc.

A few key pieces of information about the system:
- PresenceChannels are identified by a name of the format `/prefix/blah`, where `prefix` has been configured by some core/plugin implementation, and `blah` can be any string the implementation wants to use.
- Presence is a boolean thing - each user is either present, or not present. If a user has multiple clients 'present' in a channel, they will be deduplicated so that the user is only counted once
- Developers can configure the existence and configuration of channels 'just in time' using a callback. The result of this is cached for 2 minutes.
- Configuration of a channel can specify permissions in a similar way to MessageBus (public boolean, a list of allowed_user_ids, and a list of allowed_group_ids). A channel can also be placed in 'count_only' mode, where the identity of present users is not revealed to end-users.
- The backend implementation uses redis lua scripts, and is designed to scale well. In the future, hard limits may be introduced on the maximum number of users that can be present in a channel.
- Clients can enter/leave at will. If a client has not marked itself 'present' in the last 60 seconds, they will automatically 'leave' the channel. The JS implementation takes care of this regular check-in.
- On the client-side, PresenceChannel instances can be fetched from the `presence` ember service. Each PresenceChannel can be used entered/left/subscribed/unsubscribed, and the service will automatically deduplicate information before interacting with the server.
- When a client joins a PresenceChannel, the JS implementation will automatically make a GET request for the current channel state. To avoid this, the channel state can be serialized into one of your existing endpoints, and then passed to the `subscribe` method on the channel.
- The PresenceChannel JS object is an ember object. The `users` and `count` property can be used directly in ember templates, and in computed properties.
- It is important to make sure that you `unsubscribe()` and `leave()` any PresenceChannel objects after use

An example implementation may look something like this. On the server:

```ruby
register_presence_channel_prefix("site") do |channel|
  next nil unless channel == "/site/online"
  PresenceChannel::Config.new(public: true)
end
```

And on the client, a component could be implemented like this:

```javascript
import Component from "@ember/component";
import { inject as service } from "@ember/service";

export default Component.extend({
  presence: service(),
  init() {
    this._super(...arguments);
    this.set("presenceChannel", this.presence.getChannel("/site/online"));
  },
  didInsertElement() {
    this.presenceChannel.enter();
    this.presenceChannel.subscribe();
  },
  willDestroyElement() {
    this.presenceChannel.leave();
    this.presenceChannel.unsubscribe();
  },
});
```

With this template:

```handlebars
Online: {{presenceChannel.count}}
<ul>
  {{#each presenceChannel.users as |user|}} 
    <li>{{avatar user imageSize="tiny"}} {{user.username}}</li>
  {{/each}}
</ul>
```
2021-08-27 16:26:06 +01:00

1015 lines
53 KiB
Ruby

# frozen_string_literal: true
require "sidekiq/web"
require "mini_scheduler/web"
# The following constants have been replaced with `RouteFormat` and are deprecated.
USERNAME_ROUTE_FORMAT = /[%\w.\-]+?/ unless defined? USERNAME_ROUTE_FORMAT
BACKUP_ROUTE_FORMAT = /.+\.(sql\.gz|tar\.gz|tgz)/i unless defined? BACKUP_ROUTE_FORMAT
Discourse::Application.routes.draw do
def patch(*) end # Disable PATCH requests
scope path: nil, constraints: { format: /(json|html|\*\/\*)/ } do
relative_url_root = (defined?(Rails.configuration.relative_url_root) && Rails.configuration.relative_url_root) ? Rails.configuration.relative_url_root + '/' : '/'
match "/404", to: "exceptions#not_found", via: [:get, :post]
get "/404-body" => "exceptions#not_found_body"
get "/bootstrap" => "bootstrap#index"
post "webhooks/aws" => "webhooks#aws"
post "webhooks/mailgun" => "webhooks#mailgun"
post "webhooks/mailjet" => "webhooks#mailjet"
post "webhooks/mandrill" => "webhooks#mandrill"
post "webhooks/postmark" => "webhooks#postmark"
post "webhooks/sendgrid" => "webhooks#sendgrid"
post "webhooks/sparkpost" => "webhooks#sparkpost"
scope path: nil, constraints: { format: /.*/ } do
if Rails.env.development?
mount Sidekiq::Web => "/sidekiq"
mount Logster::Web => "/logs"
else
# only allow sidekiq in master site
mount Sidekiq::Web => "/sidekiq", constraints: AdminConstraint.new(require_master: true)
mount Logster::Web => "/logs", constraints: AdminConstraint.new
end
end
resources :about do
collection do
get "live_post_counts"
end
end
get "finish-installation" => "finish_installation#index"
get "finish-installation/register" => "finish_installation#register"
post "finish-installation/register" => "finish_installation#register"
get "finish-installation/confirm-email" => "finish_installation#confirm_email"
put "finish-installation/resend-email" => "finish_installation#resend_email"
get "pub/check-slug" => "published_pages#check_slug"
get "pub/by-topic/:topic_id" => "published_pages#details"
put "pub/by-topic/:topic_id" => "published_pages#upsert"
delete "pub/by-topic/:topic_id" => "published_pages#destroy"
get "pub/:slug" => "published_pages#show"
resources :directory_items
get "site" => "site#site"
namespace :site do
get "settings"
get "custom_html"
get "banner"
get "emoji"
end
get "site/basic-info" => 'site#basic_info'
get "site/statistics" => 'site#statistics'
get "srv/status" => "forums#status"
get "wizard" => "wizard#index"
get 'wizard/steps' => 'steps#index'
get 'wizard/steps/:id' => "wizard#index"
put 'wizard/steps/:id' => "steps#update"
namespace :admin, constraints: StaffConstraint.new do
get "" => "admin#index"
get 'plugins' => 'plugins#index'
resources :site_settings, constraints: AdminConstraint.new do
collection do
get "category/:id" => "site_settings#index"
end
put "user_count" => "site_settings#user_count"
end
get "reports" => "reports#index"
get "reports/bulk" => "reports#bulk"
get "reports/:type" => "reports#show"
resources :groups, only: [:create] do
member do
put "owners" => "groups#add_owners"
delete "owners" => "groups#remove_owner"
put "primary" => "groups#set_primary"
end
end
resources :groups, except: [:create], constraints: AdminConstraint.new do
collection do
put "automatic_membership_count" => "groups#automatic_membership_count"
end
end
get "groups/:type" => "groups#show", constraints: AdminConstraint.new
get "groups/:type/:id" => "groups#show", constraints: AdminConstraint.new
resources :users, id: RouteFormat.username, except: [:show] do
collection do
get "list" => "users#index"
get "list/:query" => "users#index"
get "ip-info" => "users#ip_info"
delete "delete-others-with-same-ip" => "users#delete_other_accounts_with_same_ip"
get "total-others-with-same-ip" => "users#total_other_accounts_with_same_ip"
put "approve-bulk" => "users#approve_bulk"
end
delete "penalty_history", constraints: AdminConstraint.new
put "suspend"
put "delete_posts_batch"
put "unsuspend"
put "revoke_admin", constraints: AdminConstraint.new
put "grant_admin", constraints: AdminConstraint.new
put "revoke_moderation", constraints: AdminConstraint.new
put "grant_moderation", constraints: AdminConstraint.new
put "approve"
post "log_out", constraints: AdminConstraint.new
put "activate"
put "deactivate"
put "silence"
put "unsilence"
put "trust_level"
put "trust_level_lock"
put "primary_group"
post "groups" => "users#add_group", constraints: AdminConstraint.new
delete "groups/:group_id" => "users#remove_group", constraints: AdminConstraint.new
get "badges"
get "leader_requirements" => "users#tl3_requirements"
get "tl3_requirements"
put "anonymize"
post "merge"
post "reset_bounce_score"
put "disable_second_factor"
delete "sso_record"
end
get "users/:id.json" => 'users#show', defaults: { format: 'json' }
get 'users/:id/:username' => 'users#show', constraints: { username: RouteFormat.username }
get 'users/:id/:username/badges' => 'users#show'
get 'users/:id/:username/tl3_requirements' => 'users#show'
post "users/sync_sso" => "users#sync_sso", constraints: AdminConstraint.new
resources :impersonate, constraints: AdminConstraint.new
resources :email, constraints: AdminConstraint.new do
collection do
post "test"
get "sent"
get "skipped"
get "bounced"
get "received"
get "rejected"
get "/incoming/:id/raw" => "email#raw_email"
get "/incoming/:id" => "email#incoming"
get "/incoming_from_bounced/:id" => "email#incoming_from_bounced"
get "preview-digest" => "email#preview_digest"
get "send-digest" => "email#send_digest"
get "smtp_should_reject"
post "handle_mail"
get "advanced-test"
post "advanced-test" => "email#advanced_test"
end
end
scope "/logs" do
resources :staff_action_logs, only: [:index]
get 'staff_action_logs/:id/diff' => 'staff_action_logs#diff'
resources :screened_emails, only: [:index, :destroy]
resources :screened_ip_addresses, only: [:index, :create, :update, :destroy] do
collection do
post "roll_up"
end
end
resources :screened_urls, only: [:index]
resources :search_logs, only: [:index]
get 'search_logs/term/' => 'search_logs#term'
end
get "/logs" => "staff_action_logs#index"
# alias
get '/logs/watched_words', to: redirect(relative_url_root + 'admin/customize/watched_words')
get '/logs/watched_words/*path', to: redirect(relative_url_root + 'admin/customize/watched_words/%{path}')
get "customize" => "color_schemes#index", constraints: AdminConstraint.new
get "customize/themes" => "themes#index", constraints: AdminConstraint.new
get "customize/colors" => "color_schemes#index", constraints: AdminConstraint.new
get "customize/colors/:id" => "color_schemes#index", constraints: AdminConstraint.new
get "customize/permalinks" => "permalinks#index", constraints: AdminConstraint.new
get "customize/embedding" => "embedding#show", constraints: AdminConstraint.new
put "customize/embedding" => "embedding#update", constraints: AdminConstraint.new
resources :themes, constraints: AdminConstraint.new
post "themes/import" => "themes#import"
post "themes/upload_asset" => "themes#upload_asset"
post "themes/generate_key_pair" => "themes#generate_key_pair"
get "themes/:id/preview" => "themes#preview"
put "themes/:id/setting" => "themes#update_single_setting"
scope "/customize", constraints: AdminConstraint.new do
resources :user_fields, constraints: AdminConstraint.new
resources :emojis, constraints: AdminConstraint.new
get 'themes/:id/:target/:field_name/edit' => 'themes#index'
get 'themes/:id' => 'themes#index'
get "themes/:id/export" => "themes#export"
# They have periods in their URLs often:
get 'site_texts' => 'site_texts#index'
get 'site_texts/:id.json' => 'site_texts#show', constraints: { id: /[\w.\-\+\%\&]+/i }
get 'site_texts/:id' => 'site_texts#show', constraints: { id: /[\w.\-\+\%\&]+/i }
put 'site_texts/:id.json' => 'site_texts#update', constraints: { id: /[\w.\-\+\%\&]+/i }
put 'site_texts/:id' => 'site_texts#update', constraints: { id: /[\w.\-\+\%\&]+/i }
delete 'site_texts/:id.json' => 'site_texts#revert', constraints: { id: /[\w.\-\+\%\&]+/i }
delete 'site_texts/:id' => 'site_texts#revert', constraints: { id: /[\w.\-\+\%\&]+/i }
get 'reseed' => 'site_texts#get_reseed_options'
post 'reseed' => 'site_texts#reseed'
get 'email_templates' => 'email_templates#index'
get 'email_templates/(:id)' => 'email_templates#show', constraints: { id: /[0-9a-z_.]+/ }
put 'email_templates/(:id)' => 'email_templates#update', constraints: { id: /[0-9a-z_.]+/ }
delete 'email_templates/(:id)' => 'email_templates#revert', constraints: { id: /[0-9a-z_.]+/ }
get 'robots' => 'robots_txt#show'
put 'robots.json' => 'robots_txt#update'
delete 'robots.json' => 'robots_txt#reset'
resource :email_style, only: [:show, :update]
get 'email_style/:field' => 'email_styles#show', constraints: { field: /html|css/ }
end
resources :embeddable_hosts, constraints: AdminConstraint.new
resources :color_schemes, constraints: AdminConstraint.new
resources :permalinks, constraints: AdminConstraint.new
scope "/customize" do
resources :watched_words, only: [:index, :create, :update, :destroy] do
collection do
get "action/:id" => "watched_words#index"
get "action/:id/download" => "watched_words#download"
delete "action/:id" => "watched_words#clear_all"
end
end
post "watched_words/upload" => "watched_words#upload"
end
get "version_check" => "versions#show"
get "dashboard" => "dashboard#index"
get "dashboard/general" => "dashboard#general"
get "dashboard/moderation" => "dashboard#moderation"
get "dashboard/security" => "dashboard#security"
get "dashboard/reports" => "dashboard#reports"
get "dashboard/new-features" => "dashboard#new_features"
put "dashboard/mark-new-features-as-seen" => "dashboard#mark_new_features_as_seen"
resources :dashboard, only: [:index] do
collection do
get "problems"
end
end
resources :api, only: [:index], constraints: AdminConstraint.new do
collection do
resources :keys, controller: 'api', only: [:index, :show, :update, :create, :destroy] do
collection do
get 'scopes' => 'api#scopes'
end
member do
post "revoke" => "api#revoke_key"
post "undo-revoke" => "api#undo_revoke_key"
end
end
resources :web_hooks
get 'web_hook_events/:id' => 'web_hooks#list_events', as: :web_hook_events
get 'web_hooks/:id/events' => 'web_hooks#list_events'
get 'web_hooks/:id/events/bulk' => 'web_hooks#bulk_events'
post 'web_hooks/:web_hook_id/events/:event_id/redeliver' => 'web_hooks#redeliver_event'
post 'web_hooks/:id/ping' => 'web_hooks#ping'
end
end
resources :backups, only: [:index, :create], constraints: AdminConstraint.new do
member do
get "" => "backups#show", constraints: { id: RouteFormat.backup }
put "" => "backups#email", constraints: { id: RouteFormat.backup }
delete "" => "backups#destroy", constraints: { id: RouteFormat.backup }
post "restore" => "backups#restore", constraints: { id: RouteFormat.backup }
end
collection do
get "logs" => "backups#logs"
get "status" => "backups#status"
delete "cancel" => "backups#cancel"
post "rollback" => "backups#rollback"
put "readonly" => "backups#readonly"
get "upload" => "backups#check_backup_chunk"
post "upload" => "backups#upload_backup_chunk"
get "upload_url" => "backups#create_upload_url"
end
end
resources :badges, constraints: AdminConstraint.new do
collection do
get "/award/:badge_id" => "badges#award"
post "/award/:badge_id" => "badges#mass_award"
get "types" => "badges#badge_types"
post "badge_groupings" => "badges#save_badge_groupings"
post "preview" => "badges#preview"
end
end
end # admin namespace
get "email/unsubscribe/:key" => "email#unsubscribe", as: "email_unsubscribe"
get "email/unsubscribed" => "email#unsubscribed", as: "email_unsubscribed"
post "email/unsubscribe/:key" => "email#perform_unsubscribe", as: "email_perform_unsubscribe"
get "extra-locales/:bundle" => "extra_locales#show"
resources :session, id: RouteFormat.username, only: [:create, :destroy, :become] do
if !Rails.env.production?
get 'become'
end
collection do
post "forgot_password"
end
end
get "review" => "reviewables#index" # For ember app
get "review/:reviewable_id" => "reviewables#show", constraints: { reviewable_id: /\d+/ }
get "review/:reviewable_id/explain" => "reviewables#explain", constraints: { reviewable_id: /\d+/ }
get "review/count" => "reviewables#count"
get "review/topics" => "reviewables#topics"
get "review/settings" => "reviewables#settings"
put "review/settings" => "reviewables#settings"
put "review/:reviewable_id/perform/:action_id" => "reviewables#perform", constraints: {
reviewable_id: /\d+/,
action_id: /[a-z\_]+/
}
put "review/:reviewable_id" => "reviewables#update", constraints: { reviewable_id: /\d+/ }
delete "review/:reviewable_id" => "reviewables#destroy", constraints: { reviewable_id: /\d+/ }
resources :reviewable_claimed_topics
get "session/sso" => "session#sso"
get "session/sso_login" => "session#sso_login"
get "session/sso_provider" => "session#sso_provider"
get "session/current" => "session#current"
get "session/csrf" => "session#csrf"
get "session/hp" => "session#get_honeypot_value"
get "session/email-login/:token" => "session#email_login_info"
post "session/email-login/:token" => "session#email_login"
get "session/otp/:token" => "session#one_time_password", constraints: { token: /[0-9a-f]+/ }
post "session/otp/:token" => "session#one_time_password", constraints: { token: /[0-9a-f]+/ }
get "composer_messages" => "composer_messages#index"
resources :static
post "login" => "static#enter"
get "login" => "static#show", id: "login"
get "password-reset" => "static#show", id: "password_reset"
get "faq" => "static#show", id: "faq"
get "tos" => "static#show", id: "tos", as: 'tos'
get "privacy" => "static#show", id: "privacy", as: 'privacy'
get "signup" => "static#show", id: "signup"
get "login-preferences" => "static#show", id: "login"
%w{guidelines rules conduct}.each do |faq_alias|
get faq_alias => "static#show", id: "guidelines", as: faq_alias
end
get "my/*path", to: 'users#my_redirect'
get ".well-known/change-password", to: redirect(relative_url_root + 'my/preferences/account', status: 302)
get "user-cards" => "users#cards", format: :json
get "directory-columns" => "directory_columns#index", format: :json
get "edit-directory-columns" => "edit_directory_columns#index", format: :json
put "edit-directory-columns" => "edit_directory_columns#update", format: :json
%w{users u}.each_with_index do |root_path, index|
get "#{root_path}" => "users#index", constraints: { format: 'html' }
resources :users, except: [:index, :new, :show, :update, :destroy], path: root_path do
collection do
get "check_username"
get "check_email"
get "is_local_username"
end
end
post "#{root_path}/second_factors" => "users#list_second_factors"
put "#{root_path}/second_factor" => "users#update_second_factor"
post "#{root_path}/create_second_factor_security_key" => "users#create_second_factor_security_key"
post "#{root_path}/register_second_factor_security_key" => "users#register_second_factor_security_key"
put "#{root_path}/security_key" => "users#update_security_key"
post "#{root_path}/create_second_factor_totp" => "users#create_second_factor_totp"
post "#{root_path}/enable_second_factor_totp" => "users#enable_second_factor_totp"
put "#{root_path}/disable_second_factor" => "users#disable_second_factor"
put "#{root_path}/second_factors_backup" => "users#create_second_factor_backup"
put "#{root_path}/update-activation-email" => "users#update_activation_email"
post "#{root_path}/email-login" => "users#email_login"
get "#{root_path}/admin-login" => "users#admin_login"
put "#{root_path}/admin-login" => "users#admin_login"
post "#{root_path}/toggle-anon" => "users#toggle_anon"
post "#{root_path}/read-faq" => "users#read_faq"
get "#{root_path}/search/users" => "users#search_users"
get({ "#{root_path}/account-created/" => "users#account_created" }.merge(index == 1 ? { as: :users_account_created } : { as: :old_account_created }))
get "#{root_path}/account-created/resent" => "users#account_created"
get "#{root_path}/account-created/edit-email" => "users#account_created"
get({ "#{root_path}/password-reset/:token" => "users#password_reset_show" }.merge(index == 1 ? { as: :password_reset_token } : {}))
get "#{root_path}/confirm-email-token/:token" => "users#confirm_email_token", constraints: { format: 'json' }
put "#{root_path}/password-reset/:token" => "users#password_reset_update"
get "#{root_path}/activate-account/:token" => "users#activate_account"
put({ "#{root_path}/activate-account/:token" => "users#perform_account_activation" }.merge(index == 1 ? { as: 'perform_activate_account' } : {}))
get "#{root_path}/confirm-old-email/:token" => "users_email#show_confirm_old_email"
put "#{root_path}/confirm-old-email" => "users_email#confirm_old_email"
get "#{root_path}/confirm-new-email/:token" => "users_email#show_confirm_new_email"
put "#{root_path}/confirm-new-email" => "users_email#confirm_new_email"
get({
"#{root_path}/confirm-admin/:token" => "users#confirm_admin",
constraints: { token: /[0-9a-f]+/ }
}.merge(index == 1 ? { as: 'confirm_admin' } : {}))
post "#{root_path}/confirm-admin/:token" => "users#confirm_admin", constraints: { token: /[0-9a-f]+/ }
get "#{root_path}/:username/private-messages" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/personal" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/personal/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username }
get "#{root_path}/:username/messages/group/:group_name/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username }
get "#{root_path}/:username/messages/tags/:tag_id" => "user_actions#private_messages", constraints: StaffConstraint.new
get "#{root_path}/:username.json" => "users#show", constraints: { username: RouteFormat.username }, defaults: { format: :json }
get({ "#{root_path}/:username" => "users#show", constraints: { username: RouteFormat.username } }.merge(index == 1 ? { as: 'user' } : {}))
put "#{root_path}/:username" => "users#update", constraints: { username: RouteFormat.username }, defaults: { format: :json }
get "#{root_path}/:username/emails" => "users#check_emails", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/sso-email" => "users#check_sso_email", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/sso-payload" => "users#check_sso_payload", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/security" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/preferences/primary-email" => "users#update_primary_email", format: :json, constraints: { username: RouteFormat.username }
delete "#{root_path}/:username/preferences/email" => "users#destroy_email", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/users" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: { username: RouteFormat.username }
post "#{root_path}/:username/preferences/email" => "users_email#create", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/preferences/username" => "users#username", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/second-factor" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/second-factor-backup" => "users#preferences", constraints: { username: RouteFormat.username }
delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/preferences/avatar/select" => "users#select_avatar", constraints: { username: RouteFormat.username }
post "#{root_path}/:username/preferences/revoke-account" => "users#revoke_account", constraints: { username: RouteFormat.username }
post "#{root_path}/:username/preferences/revoke-auth-token" => "users#revoke_auth_token", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/notification_level" => "users#notification_level", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: RouteFormat.username }
post "#{root_path}/action/send_activation_email" => "users#send_activation_email"
get "#{root_path}/:username/summary" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: { username: RouteFormat.username }
get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", format: :rss, constraints: { username: RouteFormat.username }
get "#{root_path}/:username/activity.json" => "posts#user_posts_feed", format: :json, constraints: { username: RouteFormat.username }
get "#{root_path}/:username/activity" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/activity/:filter" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/badges" => "users#badges", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/bookmarks" => "users#bookmarks", constraints: { username: RouteFormat.username, format: /(json|ics)/ }
get "#{root_path}/:username/notifications" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: { username: RouteFormat.username }
delete "#{root_path}/:username" => "users#destroy", constraints: { username: RouteFormat.username }
get "#{root_path}/by-external/:external_id" => "users#show", constraints: { external_id: /[^\/]+/ }
get "#{root_path}/by-external/:external_provider/:external_id" => "users#show", constraints: { external_id: /[^\/]+/ }
get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/private-message-topic-tracking-state" => "users#private_message_topic_tracking_state", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/profile-hidden" => "users#profile_hidden"
put "#{root_path}/:username/feature-topic" => "users#feature_topic", constraints: { username: RouteFormat.username }
put "#{root_path}/:username/clear-featured-topic" => "users#clear_featured_topic", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/card.json" => "users#show_card", format: :json, constraints: { username: RouteFormat.username }
end
get "user-badges/:username.json" => "user_badges#username", constraints: { username: RouteFormat.username }, defaults: { format: :json }
get "user-badges/:username" => "user_badges#username", constraints: { username: RouteFormat.username }
post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: { username: RouteFormat.username }
get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormat.username, format: :png }
get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormat.username, format: :png }
get "letter_avatar_proxy/:version/letter/:letter/:color/:size.png" => "user_avatars#show_proxy_letter", constraints: { format: :png }
get "svg-sprite/:hostname/svg-:theme_id-:version.js" => "svg_sprite#show", constraints: { hostname: /[\w\.-]+/, version: /\h{40}/, theme_id: /([0-9]+)?/, format: :js }
get "svg-sprite/search/:keyword" => "svg_sprite#search", format: false, constraints: { keyword: /[-a-z0-9\s\%]+/ }
get "svg-sprite/picker-search" => "svg_sprite#icon_picker_search", defaults: { format: :json }
get "svg-sprite/:hostname/icon(/:color)/:name.svg" => "svg_sprite#svg_icon", constraints: { hostname: /[\w\.-]+/, name: /[-a-z0-9\s\%]+/, color: /(\h{3}{1,2})/, format: :svg }
get "highlight-js/:hostname/:version.js" => "highlight_js#show", constraints: { hostname: /[\w\.-]+/, format: :js }
get "stylesheets/:name.css.map" => "stylesheets#show_source_map", constraints: { name: /[-a-z0-9_]+/ }
get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ }
get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme", constraints: { format: :json }
get "theme-javascripts/:digest.js" => "theme_javascripts#show", constraints: { digest: /\h{40}/ }
get "theme-javascripts/tests/:theme_id-:digest.js" => "theme_javascripts#show_tests"
post "uploads/lookup-metadata" => "uploads#metadata"
post "uploads" => "uploads#create"
post "uploads/lookup-urls" => "uploads#lookup_urls"
# direct to s3 uploads
post "uploads/generate-presigned-put" => "uploads#generate_presigned_put", format: :json
post "uploads/complete-external-upload" => "uploads#complete_external_upload", format: :json
# multipart uploads
post "uploads/create-multipart" => "uploads#create_multipart", format: :json
post "uploads/complete-multipart" => "uploads#complete_multipart", format: :json
post "uploads/abort-multipart" => "uploads#abort_multipart", format: :json
post "uploads/batch-presign-multipart-parts" => "uploads#batch_presign_multipart_parts", format: :json
# used to download original images
get "uploads/:site/:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, sha: /\h{40}/, extension: /[a-z0-9\._]+/i }
get "uploads/short-url/:base62(.:extension)" => "uploads#show_short", constraints: { site: /\w+/, base62: /[a-zA-Z0-9]+/, extension: /[a-zA-Z0-9\._-]+/i }, as: :upload_short
# used to download attachments
get "uploads/:site/original/:tree:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, tree: /([a-z0-9]+\/)+/i, sha: /\h{40}/, extension: /[a-z0-9\._]+/i }
if Rails.env.test?
get "uploads/:site/test_:index/original/:tree:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, index: /\d+/, tree: /([a-z0-9]+\/)+/i, sha: /\h{40}/, extension: /[a-z0-9\._]+/i }
end
# used to download attachments (old route)
get "uploads/:site/:id/:sha" => "uploads#show", constraints: { site: /\w+/, id: /\d+/, sha: /\h{16}/, format: /.*/ }
get "secure-media-uploads/*path(.:extension)" => "uploads#show_secure", constraints: { extension: /[a-z0-9\._]+/i }
get "posts" => "posts#latest", id: "latest_posts", constraints: { format: /(json|rss)/ }
get "private-posts" => "posts#latest", id: "private_posts", constraints: { format: /(json|rss)/ }
get "posts/by_number/:topic_id/:post_number" => "posts#by_number"
get "posts/by-date/:topic_id/:date" => "posts#by_date"
get "posts/:id/reply-history" => "posts#reply_history"
get "posts/:id/reply-ids" => "posts#reply_ids"
get "posts/:id/reply-ids/all" => "posts#all_reply_ids"
get "posts/:username/deleted" => "posts#deleted_posts", constraints: { username: RouteFormat.username }
get "posts/:username/flagged" => "posts#flagged_posts", constraints: { username: RouteFormat.username }
%w{groups g}.each do |root_path|
resources :groups, id: RouteFormat.username, path: root_path do
get "posts.rss" => "groups#posts_feed", format: :rss
get "mentions.rss" => "groups#mentions_feed", format: :rss
get 'members'
get 'posts'
get 'mentions'
get 'counts'
get 'mentionable'
get 'messageable'
get 'logs' => 'groups#histories'
post 'test_email_settings'
collection do
get "check-name" => 'groups#check_name'
get 'custom/new' => 'groups#new', constraints: StaffConstraint.new
get "search" => "groups#search"
end
member do
%w{
activity
activity/:filter
requests
messages
messages/inbox
messages/archive
manage
manage/profile
manage/members
manage/membership
manage/interaction
manage/email
manage/categories
manage/tags
manage/logs
}.each do |path|
get path => 'groups#show'
end
get "permissions" => "groups#permissions"
put "members" => "groups#add_members"
put "join" => "groups#join"
delete "members" => "groups#remove_member"
delete "leave" => "groups#leave"
post "request_membership" => "groups#request_membership"
put "handle_membership_request" => "groups#handle_membership_request"
post "notifications" => "groups#set_notifications"
end
end
end
# aliases so old API code works
delete "admin/groups/:id/members" => "groups#remove_member", constraints: AdminConstraint.new
put "admin/groups/:id/members" => "groups#add_members", constraints: AdminConstraint.new
resources :posts do
delete "bookmark", to: "posts#destroy_bookmark"
put "wiki"
put "post_type"
put "rebake"
put "unhide"
put "locked"
put "notice"
get "replies"
get "revisions/latest" => "posts#latest_revision"
get "revisions/:revision" => "posts#revisions", constraints: { revision: /\d+/ }
put "revisions/:revision/hide" => "posts#hide_revision", constraints: { revision: /\d+/ }
put "revisions/:revision/show" => "posts#show_revision", constraints: { revision: /\d+/ }
put "revisions/:revision/revert" => "posts#revert", constraints: { revision: /\d+/ }
put "recover"
collection do
delete "destroy_many"
put "merge_posts"
end
end
resources :bookmarks, only: %i[create destroy update] do
put "toggle_pin"
end
resources :notifications, except: :show do
collection do
put 'mark-read' => 'notifications#mark_read'
# creating an alias cause the api was extended to mark a single notification
# this allows us to cleanly target it
put 'read' => 'notifications#mark_read'
end
end
match "/auth/failure", to: "users/omniauth_callbacks#failure", via: [:get, :post]
get "/auth/:provider", to: "users/omniauth_callbacks#confirm_request"
match "/auth/:provider/callback", to: "users/omniauth_callbacks#complete", via: [:get, :post]
get "/associate/:token", to: "users/associate_accounts#connect_info", constraints: { token: /\h{32}/ }
post "/associate/:token", to: "users/associate_accounts#connect", constraints: { token: /\h{32}/ }
resources :clicks do
collection do
post "track"
end
end
get "excerpt" => "excerpt#show"
resources :post_action_users
resources :post_readers, only: %i[index]
resources :post_actions do
collection do
get "users"
post "defer_flags"
end
end
resources :user_actions
resources :badges, only: [:index]
get "/badges/:id(/:slug)" => "badges#show", constraints: { format: /(json|html|rss)/ }
resources :user_badges, only: [:index, :create, :destroy] do
put "toggle_favorite" => "user_badges#toggle_favorite", constraints: { format: :json }
end
get '/c', to: redirect(relative_url_root + 'categories')
resources :categories, except: [:show, :new, :edit]
post "categories/reorder" => "categories#reorder"
scope path: 'category/:category_id' do
post "/move" => "categories#move"
post "/notifications" => "categories#set_notifications"
put "/slug" => "categories#update_slug"
end
get "category/*path" => "categories#redirect"
get "categories_and_latest" => "categories#categories_and_latest"
get "categories_and_top" => "categories#categories_and_top"
get "c/:id/show" => "categories#show"
get "c/*category_slug/find_by_slug" => "categories#find_by_slug"
get "c/*category_slug/edit(/:tab)" => "categories#find_by_slug", constraints: { format: 'html' }
get "/new-category" => "categories#show", constraints: { format: 'html' }
get "c/*category_slug_path_with_id.rss" => "list#category_feed", format: :rss
scope path: 'c/*category_slug_path_with_id' do
get "/none" => "list#category_none_latest"
TopTopic.periods.each do |period|
get "/none/l/top/#{period}", to: redirect("/none/l/top?period=#{period}", status: 301)
get "/l/top/#{period}", to: redirect("/l/top?period=#{period}", status: 301)
end
Discourse.filters.each do |filter|
get "/none/l/#{filter}" => "list#category_none_#{filter}", as: "category_none_#{filter}"
get "/l/#{filter}" => "list#category_#{filter}", as: "category_#{filter}"
end
get "/all" => "list#category_default", as: "category_all", constraints: { format: 'html' }
get "/" => "list#category_default", as: "category_default"
end
get "hashtags" => "hashtags#show"
TopTopic.periods.each do |period|
get "top/#{period}.rss", to: redirect("top.rss?period=#{period}", status: 301)
get "top/#{period}.json", to: redirect("top.json?period=#{period}", status: 301)
get "top/#{period}", to: redirect("top?period=#{period}", status: 301)
end
Discourse.anonymous_filters.each do |filter|
get "#{filter}.rss" => "list##{filter}_feed", format: :rss
end
Discourse.filters.each do |filter|
get "#{filter}" => "list##{filter}"
end
get "search/query" => "search#query"
get "search" => "search#show"
post "search/click" => "search#click"
# Topics resource
get "t/:id" => "topics#show"
put "t/:id" => "topics#update"
delete "t/:id" => "topics#destroy"
put "t/:id/archive-message" => "topics#archive_message"
put "t/:id/move-to-inbox" => "topics#move_to_inbox"
put "t/:id/convert-topic/:type" => "topics#convert_topic"
put "t/:id/publish" => "topics#publish"
put "t/:id/shared-draft" => "topics#update_shared_draft"
put "t/:id/reset-bump-date" => "topics#reset_bump_date"
put "topics/bulk"
put "topics/reset-new" => 'topics#reset_new'
put "topics/pm-reset-new" => 'topics#private_message_reset_new'
post "topics/timings"
get 'topics/similar_to' => 'similar_topics#index'
resources :similar_topics
get "topics/feature_stats"
scope "/topics", username: RouteFormat.username do
get "created-by/:username" => "list#topics_by", as: "topics_by", defaults: { format: :json }
get "private-messages-all/:username" => "list#private_messages_all", as: "topics_private_messages_all", defaults: { format: :json }
get "private-messages-all-sent/:username" => "list#private_messages_all_sent", as: "topics_private_messages_all_sent", defaults: { format: :json }
get "private-messages-all-new/:username" => "list#private_messages_all_new", as: "topics_private_messages_all_new", defaults: { format: :json }
get "private-messages-all-unread/:username" => "list#private_messages_all_unread", as: "topics_private_messages_all_unread", defaults: { format: :json }
get "private-messages-all-archive/:username" => "list#private_messages_all_archive", as: "topics_private_messages_all_archive", defaults: { format: :json }
get "private-messages/:username" => "list#private_messages", as: "topics_private_messages", defaults: { format: :json }
get "private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", defaults: { format: :json }
get "private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", defaults: { format: :json }
get "private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", defaults: { format: :json }
get "private-messages-tags/:username/:tag_id.json" => "list#private_messages_tag", as: "topics_private_messages_tag", defaults: { format: :json }
get "private-messages-new/:username" => "list#private_messages_new", as: "topics_private_messages_new", defaults: { format: :json }
get "private-messages-warnings/:username" => "list#private_messages_warnings", as: "topics_private_messages_warnings", defaults: { format: :json }
get "groups/:group_name" => "list#group_topics", as: "group_topics", group_name: RouteFormat.username
scope "/private-messages-group/:username", group_name: RouteFormat.username do
get ":group_name.json" => "list#private_messages_group", as: "topics_private_messages_group"
get ":group_name/archive.json" => "list#private_messages_group_archive", as: "topics_private_messages_group_archive"
get ":group_name/new.json" => "list#private_messages_group_new", as: "topics_private_messages_group_new"
get ":group_name/unread.json" => "list#private_messages_group_unread", as: "topics_private_messages_group_unread"
end
end
get 'embed/topics' => 'embed#topics'
get 'embed/comments' => 'embed#comments'
get 'embed/count' => 'embed#count'
get 'embed/info' => 'embed#info'
get "new-topic" => "list#latest"
get "new-message" => "list#latest"
# Topic routes
get "t/id_for/:slug" => "topics#id_for_slug"
get "t/:slug/:topic_id/print" => "topics#show", format: :html, print: true, constraints: { topic_id: /\d+/ }
get "t/:slug/:topic_id/wordpress" => "topics#wordpress", constraints: { topic_id: /\d+/ }
get "t/:topic_id/wordpress" => "topics#wordpress", constraints: { topic_id: /\d+/ }
get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", constraints: { topic_id: /\d+/ }
get "t/:slug/:topic_id/summary" => "topics#show", defaults: { summary: true }, constraints: { topic_id: /\d+/ }
get "t/:topic_id/summary" => "topics#show", constraints: { topic_id: /\d+/ }
put "t/:slug/:topic_id" => "topics#update", constraints: { topic_id: /\d+/ }
put "t/:slug/:topic_id/star" => "topics#star", constraints: { topic_id: /\d+/ }
put "t/:topic_id/star" => "topics#star", constraints: { topic_id: /\d+/ }
put "t/:slug/:topic_id/status" => "topics#status", constraints: { topic_id: /\d+/ }
put "t/:topic_id/status" => "topics#status", constraints: { topic_id: /\d+/ }
put "t/:topic_id/clear-pin" => "topics#clear_pin", constraints: { topic_id: /\d+/ }
put "t/:topic_id/re-pin" => "topics#re_pin", constraints: { topic_id: /\d+/ }
put "t/:topic_id/mute" => "topics#mute", constraints: { topic_id: /\d+/ }
put "t/:topic_id/unmute" => "topics#unmute", constraints: { topic_id: /\d+/ }
post "t/:topic_id/timer" => "topics#timer", constraints: { topic_id: /\d+/ }
put "t/:topic_id/make-banner" => "topics#make_banner", constraints: { topic_id: /\d+/ }
put "t/:topic_id/remove-banner" => "topics#remove_banner", constraints: { topic_id: /\d+/ }
put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user", constraints: { topic_id: /\d+/ }
put "t/:topic_id/remove-allowed-group" => "topics#remove_allowed_group", constraints: { topic_id: /\d+/ }
put "t/:topic_id/recover" => "topics#recover", constraints: { topic_id: /\d+/ }
get "t/:topic_id/:post_number" => "topics#show", constraints: { topic_id: /\d+/, post_number: /\d+/ }
get "t/:topic_id/last" => "topics#show", post_number: 99999999, constraints: { topic_id: /\d+/ }
get "t/:slug/:topic_id.rss" => "topics#feed", format: :rss, constraints: { topic_id: /\d+/ }
get "t/:slug/:topic_id" => "topics#show", constraints: { topic_id: /\d+/ }
get "t/:slug/:topic_id/:post_number" => "topics#show", constraints: { topic_id: /\d+/, post_number: /\d+/ }
get "t/:slug/:topic_id/last" => "topics#show", post_number: 99999999, constraints: { topic_id: /\d+/ }
get "t/:topic_id/posts" => "topics#posts", constraints: { topic_id: /\d+/ }, format: :json
get "t/:topic_id/post_ids" => "topics#post_ids", constraints: { topic_id: /\d+/ }, format: :json
get "t/:topic_id/excerpts" => "topics#excerpts", constraints: { topic_id: /\d+/ }, format: :json
post "t/:topic_id/timings" => "topics#timings", constraints: { topic_id: /\d+/ }
post "t/:topic_id/invite" => "topics#invite", constraints: { topic_id: /\d+/ }
post "t/:topic_id/invite-group" => "topics#invite_group", constraints: { topic_id: /\d+/ }
post "t/:topic_id/invite-notify" => "topics#invite_notify", constraints: { topic_id: /\d+/ }
post "t/:topic_id/move-posts" => "topics#move_posts", constraints: { topic_id: /\d+/ }
post "t/:topic_id/merge-topic" => "topics#merge_topic", constraints: { topic_id: /\d+/ }
post "t/:topic_id/change-owner" => "topics#change_post_owners", constraints: { topic_id: /\d+/ }
put "t/:topic_id/change-timestamp" => "topics#change_timestamps", constraints: { topic_id: /\d+/ }
delete "t/:topic_id/timings" => "topics#destroy_timings", constraints: { topic_id: /\d+/ }
put "t/:topic_id/bookmark" => "topics#bookmark", constraints: { topic_id: /\d+/ }
put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: { topic_id: /\d+/ }
put "t/:topic_id/tags" => "topics#update_tags", constraints: { topic_id: /\d+/ }
put "t/:topic_id/slow_mode" => "topics#set_slow_mode", constraints: { topic_id: /\d+/ }
post "t/:topic_id/notifications" => "topics#set_notifications" , constraints: { topic_id: /\d+/ }
get "p/:post_id(/:user_id)" => "posts#short_link"
get "/posts/:id/cooked" => "posts#cooked"
get "/posts/:id/expand-embed" => "posts#expand_embed"
get "/posts/:id/raw" => "posts#markdown_id"
get "/posts/:id/raw-email" => "posts#raw_email"
get "raw/:topic_id(/:post_number)" => "posts#markdown_num"
resources :invites, except: [:show]
get "/invites/:id" => "invites#show", constraints: { format: :html }
put "/invites/:id" => "invites#update"
post "invites/upload_csv" => "invites#upload_csv"
post "invites/destroy-all-expired" => "invites#destroy_all_expired"
post "invites/reinvite" => "invites#resend_invite"
post "invites/reinvite-all" => "invites#resend_all_invites"
delete "invites" => "invites#destroy"
put "invites/show/:id" => "invites#perform_accept_invitation", as: 'perform_accept_invite'
get "invites/retrieve" => "invites#retrieve"
resources :export_csv do
collection do
post "export_entity" => "export_csv#export_entity"
end
end
get "onebox" => "onebox#show"
get "inline-onebox" => "inline_onebox#show"
get "exception" => "list#latest"
get "message-bus/poll" => "message_bus#poll"
resources :drafts, only: [:index]
get "draft" => "draft#show"
post "draft" => "draft#update"
delete "draft" => "draft#destroy"
if service_worker_asset = Rails.application.assets_manifest.assets['service-worker.js']
# https://developers.google.com/web/fundamentals/codelabs/debugging-service-workers/
# Normally the browser will wait until a user closes all tabs that contain the
# current site before updating to a new Service Worker.
# Support the old Service Worker path to avoid routing error filling up the
# logs.
get "/service-worker.js" => "static#service_worker_asset", format: :js
get service_worker_asset => "static#service_worker_asset", format: :js
elsif Rails.env.development?
get "/service-worker.js" => "static#service_worker_asset", format: :js
end
get "cdn_asset/:site/*path" => "static#cdn_asset", format: false, constraints: { format: /.*/ }
get "brotli_asset/*path" => "static#brotli_asset", format: false, constraints: { format: /.*/ }
get "favicon/proxied" => "static#favicon", format: false
get "robots.txt" => "robots_txt#index"
get "robots-builder.json" => "robots_txt#builder"
get "offline.html" => "offline#index"
get "manifest.webmanifest" => "metadata#manifest", as: :manifest
get "manifest.json" => "metadata#manifest"
get ".well-known/assetlinks.json" => "metadata#app_association_android"
get "apple-app-site-association" => "metadata#app_association_ios", format: false
get "opensearch" => "metadata#opensearch", constraints: { format: :xml }
scope '/tag/:tag_id' do
constraints format: :json do
get '/' => 'tags#show', as: 'tag_show'
get '/info' => 'tags#info'
get '/notifications' => 'tags#notifications'
put '/notifications' => 'tags#update_notifications'
put '/' => 'tags#update'
delete '/' => 'tags#destroy'
post '/synonyms' => 'tags#create_synonyms'
delete '/synonyms/:synonym_id' => 'tags#destroy_synonym'
Discourse.filters.each do |filter|
get "/l/#{filter}" => "tags#show_#{filter}", as: "tag_show_#{filter}"
end
end
constraints format: :rss do
get '/' => 'tags#tag_feed'
end
end
scope "/tags" do
get '/' => 'tags#index'
get '/filter/list' => 'tags#index'
get '/filter/search' => 'tags#search'
get '/personal_messages/:username' => 'tags#personal_messages', constraints: { username: RouteFormat.username }
post '/upload' => 'tags#upload'
get '/unused' => 'tags#list_unused'
delete '/unused' => 'tags#destroy_unused'
constraints(tag_id: /[^\/]+?/, format: /json|rss/) do
scope path: '/c/*category_slug_path_with_id' do
Discourse.filters.each do |filter|
get "/none/:tag_id/l/#{filter}" => "tags#show_#{filter}", as: "tag_category_none_show_#{filter}", defaults: { no_subcategories: true }
end
get '/none/:tag_id' => 'tags#show', as: 'tag_category_none_show', defaults: { no_subcategories: true }
Discourse.filters.each do |filter|
get "/:tag_id/l/#{filter}" => "tags#show_#{filter}", as: "tag_category_show_#{filter}"
end
get '/:tag_id' => 'tags#show', as: 'tag_category_show'
end
get '/intersection/:tag_id/*additional_tag_ids' => 'tags#show', as: 'tag_intersection'
end
get '*tag_id', to: redirect(relative_url_root + 'tag/%{tag_id}')
end
resources :tag_groups, constraints: StaffConstraint.new, except: [:edit]
get '/tag_groups/filter/search' => 'tag_groups#search', format: :json
Discourse.filters.each do |filter|
root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), as: "list_#{filter}"
end
# special case for categories
root to: "categories#index", constraints: HomePageConstraint.new("categories"), as: "categories_index"
root to: 'finish_installation#index', constraints: HomePageConstraint.new("finish_installation"), as: 'installation_redirect'
get "/user-api-key/new" => "user_api_keys#new"
post "/user-api-key" => "user_api_keys#create"
post "/user-api-key/revoke" => "user_api_keys#revoke"
post "/user-api-key/undo-revoke" => "user_api_keys#undo_revoke"
get "/user-api-key/otp" => "user_api_keys#otp"
post "/user-api-key/otp" => "user_api_keys#create_otp"
get "/safe-mode" => "safe_mode#index"
post "/safe-mode" => "safe_mode#enter", as: "safe_mode_enter"
unless Rails.env.production?
get "/qunit" => "qunit#index"
get "/wizard/qunit" => "wizard#qunit"
end
get "/theme-qunit" => "qunit#theme"
post "/push_notifications/subscribe" => "push_notification#subscribe"
post "/push_notifications/unsubscribe" => "push_notification#unsubscribe"
resources :csp_reports, only: [:create]
get "/permalink-check", to: 'permalinks#check'
post "/do-not-disturb" => "do_not_disturb#create"
delete "/do-not-disturb" => "do_not_disturb#destroy"
post "/presence/update" => "presence#update"
get "/presence/get" => "presence#get"
get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new
end
end