# frozen_string_literal: true require_relative 'model' require_relative 'messageable' require_relative 'searchable' module Redd module Models # A subreddit. class Subreddit < Model include Messageable include Searchable # A mapping from keys returned by #settings to keys required by #modify_settings SETTINGS_MAP = { subreddit_type: :type, language: :lang, content_options: :link_type, default_set: :allow_top, header_hover_text: :'header-title' } # @!group Listings # Get the appropriate listing. # @param sort [:hot, :new, :top, :controversial, :comments, :rising, :gilded] the type of # listing # @param options [Hash] a list of options to send with the request # @option options [String] :after return results after the given fullname # @option options [String] :before return results before the given fullname # @option options [Integer, nil] :limit maximum number of items to return (nil for no limit) # @option options [:hour, :day, :week, :month, :year, :all] :time the time period to consider # when sorting # # @note The option :time only applies to the top and controversial sorts. # @return [Listing] def listing(sort, **options) options[:t] = options.delete(:time) if options.key?(:time) PaginatedListing.new(client, options) do |**req_options| client.model( :get, "/r/#{read_attribute(:display_name)}/#{sort}", options.merge(req_options) ) end end # @!method hot(**options) # @!method new(**options) # @!method top(**options) # @!method controversial(**options) # @!method comments(**options) # @!method rising(**options) # @!method gilded(**options) # # @see #listing %i[hot new top controversial comments rising gilded].each do |sort| define_method(sort) { |**options| listing(sort, **options) } end # @!endgroup # @!group Moderator Listings # Get the appropriate moderator listing. # @param type [:reports, :spam, :modqueue, :unmoderated, :edited] the type of listing # @param params [Hash] a list of params to send with the request # @option params [String] :after return results after the given fullname # @option params [String] :before return results before the given fullname # @option params [Integer] :count the number of items already seen in the listing # @option params [1..100] :limit the maximum number of things to return # @option params [:links, :comments] :only the type of objects required # # @return [Listing] def moderator_listing(type, **params) client.model(:get, "/r/#{read_attribute(:display_name)}/about/#{type}", params) end # @!method reports(**params) # @!method spam(**params) # @!method modqueue(**params) # @!method unmoderated(**params) # @!method edited(**params) # # @see #moderator_listing %i[reports spam modqueue unmoderated edited].each do |type| define_method(type) { |**params| moderator_listing(type, **params) } end # @!endgroup # @!group Relationship Listings # Get the appropriate relationship listing. # @param type [:banned, :muted, :wikibanned, :contributors, :wikicontributors, :moderators] # the type of listing # @param params [Hash] a list of params to send with the request # @option params [String] :after return results after the given fullname # @option params [String] :before return results before the given fullname # @option params [Integer] :count the number of items already seen in the listing # @option params [1..100] :limit the maximum number of things to return # @option params [String] :user find a specific user # # @return [Array] def relationship_listing(type, **params) # TODO: add methods to determine if a certain user was banned/muted/etc # TODO: return User types? user_list = client.get("/r/#{read_attribute(:display_name)}/about/#{type}", params).body user_list[:data][:children] end # @!method banned(**params) # @!method muted(**params) # @!method wikibanned(**params) # @!method contributors(**params) # @!method wikicontributors(**params) # @!method moderators(**params) # # @see #relationship_listing %i[banned muted wikibanned contributors wikicontributors moderators].each do |type| define_method(type) { |**params| relationship_listing(type, **params) } end # @!endgroup # @return [Array] the subreddit's wiki pages def wiki_pages client.get("/r/#{read_attribute(:display_name)}/wiki/pages").body[:data] end # Get a wiki page by its title. # @param title [String] the page's title # @return [WikiPage] def wiki_page(title) WikiPage.new(client, title: title, subreddit: self) end # Search a subreddit. # @param query [String] the search query # @param params [Hash] refer to {Searchable} to see search parameters # @see Searchable#search def search(query, **params) restricted_params = { restrict_to: read_attribute(:display_name) }.merge(params) super(query, restricted_params) end # Submit a link or a text post to the subreddit. # @note If both text and url are provided, url takes precedence. # # @param title [String] the title of the submission # @param text [String] the text of the self-post # @param url [String] the URL of the link # @param resubmit [Boolean] whether to post a link to the subreddit despite it having been # posted there before (you monster) # @param sendreplies [Boolean] whether to send the replies to your inbox # @return [Submission] The returned object (url, id and name) def submit(title, text: nil, url: nil, resubmit: false, sendreplies: true) params = { title: title, sr: read_attribute(:display_name), resubmit: resubmit, sendreplies: sendreplies } params[:kind] = url ? 'link' : 'self' params[:url] = url if url params[:text] = text if text Submission.new(client, client.post('/api/submit', params).body[:json][:data]) end # Compose a message to the moderators of a subreddit. # # @param subject [String] the subject of the message # @param text [String] the message text # @param from [Subreddit, nil] the subreddit to send the message on behalf of def send_message(subject:, text:, from: nil) super(to: "/r/#{read_attribute(:display_name)}", subject: subject, text: text, from: from) end # Set the flair for a link or a user for this subreddit. # @param thing [User, Submission] the object whose flair to edit # @param text [String] a string no longer than 64 characters # @param css_class [String] the css class to assign to the flair def set_flair(thing, text, css_class: nil) key = thing.is_a?(User) ? :name : :link params = { :text => text, key => thing.name } params[:css_class] = css_class if css_class client.post("/r/#{read_attribute(:display_name)}/api/flair", params) end # Get a listing of all user flairs. # @param params [Hash] a list of params to send with the request # @option params [String] :after return results after the given fullname # @option params [String] :before return results before the given fullname # @option params [Integer] :count the number of items already seen in the listing # @option params [String] :name prefer {#get_flair} # @option params [:links, :comments] :only the type of objects required # # @return [Listing>] def flair_listing(**params) res = client.get("/r/#{read_attribute(:display_name)}/api/flairlist", params).body Listing.new(client, children: res[:users], before: res[:prev], after: res[:next]) end # Get the user's flair data. # @param user [User] the user whose flair to fetch # @return [Hash, nil] def get_flair(user) # We have to do this because reddit returns all flairs if given a nonexistent user flair = flair_listing(name: user.name).first return flair if flair && flair[:user].casecmp(user.name).zero? nil end # Remove the flair from a user # @param thing [User, String] a User from which to remove flair def delete_flair(user) name = user.is_a?(User) ? user.name : user client.post("/r/#{read_attribute(:display_name)}/api/deleteflair", name: name) end # Set a Submission's or User's flair based on a flair template id. # @param thing [User, Submission] an object to assign a template to # @param template_id [String] the UUID of the flair template to assign # @param text [String] optional text for the flair def set_flair_template(thing, template_id, text: nil) key = thing.is_a?(User) ? :name : :link params = { key => thing.name, flair_template_id: template_id, text: text } client.post("/r/#{read_attribute(:display_name)}/api/selectflair", params) end # Add the subreddit to the user's subscribed subreddits. def subscribe(action: :sub, skip_initial_defaults: false) client.post( '/api/subscribe', sr_name: read_attribute(:display_name), action: action, skip_initial_defaults: skip_initial_defaults ) end # Remove the subreddit from the user's subscribed subreddits. def unsubscribe subscribe(action: :unsub) end # Get the subreddit's CSS. # @return [String, nil] the stylesheet or nil if no stylesheet exists def stylesheet url = client.get("/r/#{read_attribute(:display_name)}/stylesheet").headers['location'] HTTP.get(url).body.to_s rescue Errors::NotFound nil end # Edit the subreddit's stylesheet. # @param text [String] the updated CSS # @param reason [String] the reason for modifying the stylesheet def update_stylesheet(text, reason: nil) params = { op: 'save', stylesheet_contents: text } params[:reason] = reason if reason client.post("/r/#{read_attribute(:display_name)}/api/subreddit_stylesheet", params) end # @return [Hash] the subreddit's settings def settings client.get("/r/#{read_attribute(:display_name)}/about/edit").body[:data] end # Modify the subreddit's settings. # @param params [Hash] the settings to change # @see https://www.reddit.com/dev/api#POST_api_site_admin def modify_settings(**params) full_params = settings.merge(params) full_params[:sr] = read_attribute(:name) SETTINGS_MAP.each { |src, dest| full_params[dest] = full_params.delete(src) } client.post('/api/site_admin', full_params) end # Get the moderation log. # @param params [Hash] a list of params to send with the request # @option params [String] :after return results after the given fullname # @option params [String] :before return results before the given fullname # @option params [Integer] :count the number of items already seen in the listing # @option params [1..100] :limit the maximum number of things to return # @option params [String] :type filter events to a specific type # # @return [Listing] def mod_log(**params) client.model(:get, "/r/#{read_attribute(:display_name)}/about/log", params) end # Invite a user to moderate this subreddit. # @param user [User] the user to invite # @param permissions [String] the permission string to invite the user with def invite_moderator(user, permissions: '+all') add_relationship(type: 'moderator_invite', name: user.name, permissions: permissions) end # Take back a moderator request. # @param user [User] the requested user def uninvite_moderator(user) remove_relationship(type: 'moderator_invite', name: user.name) end # Accept an invite to become a moderator of this subreddit. def accept_moderator_invite client.post("/r/#{read_attribute(:display_name)}/api/accept_moderator_invite") end # Dethrone a moderator. # @param user [User] the user to remove def remove_moderator(user) remove_relationship(type: 'moderator', name: user.name) end # Leave from being a moderator on a subreddit. def leave_moderator client.post('/api/leavemoderator', id: read_attribute(:name)) end # Add a contributor to the subreddit. # @param user [User] the user to add def add_contributor(user) add_relationship(type: 'contributor', name: user.name) end # Remove a contributor from the subreddit. # @param user [User] the user to remove def remove_contributor(user) remove_relationship(type: 'contributor', name: user.name) end # Leave from being a contributor on a subreddit. def leave_contributor client.post('/api/leavecontributor', id: read_attribute(:name)) end # Ban a user from a subreddit. # @param user [User] the user to ban # @param params [Hash] additional options to supply with the request # @option params [String] :ban_reason the reason for the ban # @option params [String] :ban_message a message sent to the banned user # @option params [String] :note a note that only moderators can see # @option params [Integer] :duration the number of days to ban the user (if temporary) def ban(user, **params) add_relationship(type: 'banned', name: user.name, **params) end # Remove a ban on a user. # @param user [User] the user to unban def unban(user) remove_relationship(type: 'banned', name: user.name) end # Allow a user to contribute to the wiki. # @param user [User] the user to add def add_wiki_contributor(user) add_relationship(type: 'wikicontributor', name: user.name) end # No longer allow a user to contribute to the wiki. # @param user [User] the user to remove def remove_wiki_contributor(user) remove_relationship(type: 'wikicontributor', name: user.name) end # Ban a user from contributing to the wiki. # @param user [User] the user to ban # @param params [Hash] additional options to supply with the request # @option params [String] :ban_reason the reason for the ban (not sure this matters) # @option params [String] :note a note that only moderators can see # @option params [Integer] :duration the number of days to ban the user (if temporary) def ban_wiki_contributor(user, **params) add_relationship(type: 'wikibanned', name: user.name, **params) end # No longer ban a user from contributing to the wiki. # @param user [User] the user to unban def unban_wiki_contributor(user) remove_relationship(type: 'wikibanned', name: user.name) end # Upload a subreddit-specific image. # @param file [String, IO] the image file to upload # @param image_type ['jpg', 'png'] the image type # @param upload_type ['img', 'header', 'icon', 'banner'] where to upload the image # @param image_name [String] the name of the image (if upload_type is 'img') # @return [String] the url of the uploaded file def upload_image(file:, image_type:, upload_type:, image_name: nil) file_data = HTTP::FormData::File.new(file) params = { img_type: image_type, upload_type: upload_type, file: file_data } params[:name] = image_name if upload_type.to_s == 'img' client.post("/r/#{read_attribute(:display_name)}/api/upload_sr_img", params).body[:img_src] end # Delete a subreddit-specific image. # @param upload_type ['img', 'header', 'icon', 'banner'] the image to delete # @param image_name [String] the image name (if upload_type is 'img') def delete_image(upload_type:, image_name: nil) unless %w[img header icon banner].include?(upload_type) raise ArgumentError, 'unknown upload_type' end params = {} params[:name] = image_name if upload_type.to_s == 'img' client.post("/r/#{read_attribute(:display_name)}/api/delete_sr_#{upload_type}", params) end # @!attribute [r] display_name # @return [String] the subreddit's name property :display_name, :required # @!attribute [r] id # @return [String] the subreddit's t5_ id. property :id # @!attribute [r] title # @return [String] the subreddit's page title text. property :title # @!attribute [r] user_is_contributor? # @return [Boolean] whether the logged-in user is the subreddit's contributor property :user_is_contributor?, from: :user_is_contributor # @!attribute [r] banner_image # @return [String] the url to the subreddit's banner image property :banner_image, from: :banner_img # @!attribute [r] banner_size # @return [Array] the banner dimensions property :banner_size # @!attribute [r] user_flair_text # @return [String] the logged-in user's flair text property :user_flair_text # @!attribute [r] user_flair_css_class # @return [String] the css class for the user's flair property :user_flair_css_class # @!attribute [r] user_is_banned # @return [Boolean] whether the logged-in user is banned from this subreddit property :user_is_banned?, from: :user_is_banned # @!attribute [r] user_is_moderator? # @return [Boolean] whether the logged-in user is a moderator of the subreddit property :user_is_moderator?, from: :user_is_moderator # @!attribute [r] user_is_muted? # @return [Boolean] whether the logged-in user is muted from the subreddit property :user_is_muted?, from: :user_is_muted # @!attribute [r] user_is_subscriber # @return [Boolean] whether the logged-in user is a subscriber to the subreddit property :user_is_subscriber?, from: :user_is_subscriber # @!attribute [r] wiki_enabled? # @return [Boolean] whether the wiki is enabled for this subreddit property :wiki_enabled?, from: :wiki_enabled # @!attribute [r] show_media? # @return [Boolean] whether media is shown property :show_media?, from: :show_media # @!attribute [r] description # @return [String] the subreddit description property :description # @!attribute [r] description_html # @return [String] the html-rendered version of the subreddit description property :description_html # @!attribute [r] submit_text # @return [String] the submit text property :submit_text # @!attribute [r] submit_text_html # @return [String] the submit text html property :submit_text_html # @!attribute [r] can_set_flair? # @return [Boolean] whether the user can set the flair in the subreddit property :can_set_flair?, from: :user_can_flair_in_sr # @!attribute [r] header_img # @return [String] the url to the header image property :header_image, from: :header_img # @!attribute [r] header_size # @return [Array] the dimensions of the header image property :header_size # @!attribute [r] collapse_deleted_comments? # @return [Boolean] whether deleted comments are collapsed property :collapse_deleted_comments?, from: :collapse_deleted_comments # @!attribute [r] user_has_favorited? # @return [Boolean] whether the user has favourited the subreddit property :user_has_favorited?, from: :user_has_favorited # @!attribute [r] public_description # @return [String] the public description property :public_description # @!attribute [r] public_description_html # @return [String] the html-rendered version of the public description property :public_description_html # @!attribute [r] over_18? # @return [Boolean] whether the user is marked as over 18 property :over_18?, from: :over18 # @!attribute [r] spoilers_enabled? # @return [Boolean] whether the subreddit has spoilers enabled property :spoilers_enabled?, from: :spoilers_enabled # @!attribute [r] icon_size # @return [Array] the subreddit icon size property :icon_size # @!attribute [r] audience_target # @return [String] no clue what this means property :audience_target # @!attribute [r] suggested_comment_sort # @return [String] the suggested comment sort property :suggested_comment_sort # @!attribute [r] active_user_count # @return [Integer] the number of active users property :active_user_count # @!attribute [r] accounts_active # @return [Integer] the number of active accounts property :accounts_active # @!attribute [r] subscribers # @return [Integer] the subreddit's subscriber count property :subscribers # @!attribute [r] icon_image # @return [String] the url to the icon image property :icon_image, from: :icon_img # @!attribute [r] header_title # @return [String] the header's "title" attribute (i.e. mouseover text) property :header_title # @!attribute [r] display_name_prefixed # @return [String] the display name, prefixed with a "r/". # @deprecated not really deprecated, but prefer just using the display_name directly property :display_name_prefixed, default: ->() { "r/#{read_attribute(:display_name)}" } # @!attribute [r] submit_link_label # @return [String] the label text on the submit link button property :submit_link_label # @!attribute [r] submit_text_label # @return [String] the label text on the submit text button property :submit_text_label # @!attribute [r] public_traffic # @return [Boolean] whether the traffic page is public property :public_traffic?, from: :public_traffic # @!attribute [r] key_color # @return [String] a hex color code, not sure what this does property :key_color # @!attribute [r] user_flair_visible? # @return [Boolean] whether the user's flair is shown to others property :user_flair_visible?, from: :user_sr_flair_enabled # @!attribute [r] user_flair_enabled? # @return [Boolean] whether the subreddit allows setting user flairs property :user_flair_enabled?, from: :user_flair_enabled_in_sr # @!attribute [r] language # @return [String] the subreddit's language code property :language, from: :lang # @!attribute [r] enrolled_in_new_modmail? # @return [Boolean] whether the subreddit is enrolled in the new modmail property :enrolled_in_new_modmail?, from: :is_enrolled_in_new_modmail # @!attribute [r] whitelist_status # @return [String] not sure what this does, something to do with ads? property :whitelist_status # @!attribute [r] url # @return [String] the subreddit's **relative** url (e.g. /r/Redd/) property :url, default: ->() { "/r/#{read_attribute(:display_name)}/" } # @!attribute [r] quarantined? # @return [Boolean] whether the subreddit is quarantined property :quarantined?, from: :quarantine # @!attribute [r] hide_ads? # @return [Boolean] whether ads are hidden? property :hide_ads?, from: :hide_ads # @!attribute [r] created_at # @return [Time] the time the subreddit was created property :created_at, from: :created_utc, with: ->(t) { Time.at(t) } # @!attribute [r] accounts_active_is_fuzzed # @return [Boolean] whether active accounts is fuzzed property :accounts_active_is_fuzzed?, from: :accounts_active_is_fuzzed # @!attribute [r] advertiser_category # @return [String] the advertiser category property :advertiser_category # @!attribute [r] subreddit_theme_enabled? # @return [Boolean] whether the subreddit theme is enabled property :subreddit_theme_enabled?, from: :user_sr_theme_enabled # @!attribute [r] link_flair_enabled # @return [Boolean] whether link flairs are enabled property :link_flair_enabled?, from: :link_flair_enabled # @!attribute [r] allow_images? # @return [Boolean] whether images are allowed property :allow_images?, from: :allow_images # @!attribute [r] show_media_preview # @return [Boolean] whether media previews are shown property :show_media_preview?, from: :show_media_preview # @!attribute [r] comment_score_hide_mins # @return [Integer] the number of minutes the comment score is hidden property :comment_score_hide_mins # @!attribute [r] subreddit_type # @return [String] whether it's a public, private, or gold-restricted subreddit property :subreddit_type # @!attribute [r] submission_type # @return [String] the allowed submission type (?) property :submission_type private def lazer_reload fully_loaded! self[:name] ? load_from_fullname : load_from_display_name end # Return the attributes using the display_name (best option). def load_from_display_name client.get("/r/#{read_attribute(:display_name)}/about").body[:data] end # Load the attributes using the subreddit fullname (not so best option). def load_from_fullname response = client.get('/api/info', id: read_attribute(:name)) raise Errors::NotFound.new(response) if response.body[:data][:children].empty? response.body[:data][:children][0][:data] end def add_relationship(**params) client.post("/r/#{read_attribute(:display_name)}/api/friend", params) end def remove_relationship(**params) client.post("/r/#{read_attribute(:display_name)}/api/unfriend", params) end end end end