Class: Shep::Session
- Inherits:
-
Object
- Object
- Shep::Session
- Defined in:
- lib/shep/session.rb
Overview
Represents a connection to a Mastodon (or equivalent) server.
## Conventions
‘fetch_*` methods retrieve a single Mastodon object, an `Entity` subinstance.
‘each_*` methods retrieve multiple objects, also `Entity` subinstances. If called with a block, the block is evaluated on each item in turn and the block’s result is ignored. Otherwise, it returns an ‘Enumerator` which can be used in the usual ways.
Some examples:
# Evaluate a block on each status
session.each_status(account) { |status| do_thing(status) }
# Retrieve the last 100 statuses in an array
statuses = session.each_status(account, limit: 100).to_a
# Retrieve the last 200 statuses via an enumerator and do
# extra transformation on the result before collecting
# them in an array.
statuses = session.each_status(account, limit: 200)
.select{|status| BESTIES.include? status.account.username }
.map{|status| status.id}
.to_a
The actual web API “paginates” the output. That is, it returns the first 40 (or so) items and then provides a link to the next chunk. Shep’s ‘each_*` methods handle this for you automatically. This means that unless you use `limit:`, the `each_*` methods will retrieve all available items, at least until you reach the rate limit (see below).
Note that it is safe to leave an ‘each_*` methods block with `break`, `return`, an exception, or any other such mechanism.
The remaining Mastodon API methods will in some way modify the state of the server and return an Entity subinstance on success.
All API calls throw an exception on failure.
## Rate Limits
Mastodon servers restrict the number of times you can use a specific endpoint within a time period as a way to prevent abuse. Shep provides several tools for handling these limits gracefully.
-
The method #rate_limit will return a Struct that tells you how many requests you have left and when the count is reset.
-
If a rate limit is exceeded, the method will throw an Error::RateLimit exception instead of an ordinary Error::Http exception.
-
If the Session is created with argument ‘rate_limit_retry:` set to true, the Session will instead wait out the reset time and try again.
If you enable the wait-and-retry mechanism, you can also provide a hook function (i.e. a thing that responds to ‘call`) via constructor argument `retry_hook:`. This is called with one argument, the result of #rate_limit for the limited API endpoint, immediately before Shep starts waiting for the limit to reset.
The built-in wait time takes the callback’s execution time into account so it’s possible to use the callback to do your own waiting and use that time more productively.
Alternately, all of the ‘each_*` methods have a `limit:` parameter so it’s easy to avoid making too many API calls and many have a ‘max_id:` parameter that allows you to continue where you left off.
Instance Attribute Summary collapse
-
#host ⇒ String
readonly
The Server’s hostname.
-
#logger ⇒ Logger
readonly
The logger object.
-
#user_agent ⇒ String
readonly
User-Agent string; frozen.
Instance Method Summary collapse
-
#delete_status(id) ⇒ Entity::Status
Delete the status at ID.
-
#dismiss_notification(id) ⇒ Object
Dismiss the notification with the given ID.
-
#each_boost_acct(status_id, limit: nil) {|item| ... } ⇒ Enumerator
Retrieve each Entity::Account that boosted the given status.
-
#each_fave_acct(status_id, limit: nil) {|item| ... } ⇒ Enumerator
Retrieve each account that favourited the given status.
-
#each_follower(account_id, limit: nil) {|item| ... } ⇒ Enumerator
Retrieve the follower list of an account.
-
#each_following(account_id, limit: nil) {|item| ... } ⇒ Enumerator
Retrieve the list of accounts this account follows.
-
#each_home_status(limit: nil, local: false, max_id: "", remote: false, only_media: false) {|item| ... } ⇒ Enumerator
Retrieve each Entity::Status in the home timeline.
-
#each_notification(types: [], exclude_types: [], limit: nil, account_id: nil) {|item| ... } ⇒ Enumerator
Retrieve each notification.
-
#each_public_status(limit: nil, max_id: "", local: false, remote: false, only_media: false) {|item| ... } ⇒ Enumerator
Retrieve the instance’s public timeline(s).
-
#each_status(account_id, limit: nil, max_id: "", only_media: false, exclude_replies: false, exclude_reblogs: false, pinned: false, tagged: nil) {|item| ... } ⇒ Enumerator
Retrieve the account’s statuses.
-
#each_tag_status(hashtag_s, limit: nil, max_id: "", local: false, remote: false, only_media: false, all: [], none: []) {|item| ... } ⇒ Enumerator
Retrieve a tag’s timeline.
-
#edit_status(id, status, media_ids: [], spoiler_text: "", language: "en") ⇒ Entity::Status
Update the status with the given Id.
-
#fetch_account(id) ⇒ Entity::Account
Fetch user details by ID.
-
#fetch_account_by_username(handle) ⇒ Entity::Account?
Fetch user details by username.
-
#fetch_context(id) ⇒ Entity::Context
Fetch the context (parent and child status) of status at ‘id’.
-
#fetch_notification(ntfn_id) ⇒ Entity::Notification
Fetch an individual notification by ID.
-
#fetch_status(id) ⇒ Entity::Status
Fetch a single status.
-
#fetch_status_src(id) ⇒ Entity::StatusSource
Fetch the editable source of status at id.
-
#fetch_status_with_media(id, media_dir = '.', refetch: true) ⇒ Entity::Status, Hash
Fetch the given status and also any attached media.
-
#initialize(host:, token: nil, user_agent: "ShepRubyGem/#{Shep::Version}", ua_comment: nil, rate_limit_retry: false, retry_hook: nil, logger: nil, debug_http: false) ⇒ Session
constructor
Initialize a new Session.
-
#post_status(text, visibility: :private, media_ids: [], spoiler_text: "", language: "") ⇒ Entity::Status
Post a status containing the given text at the specified visibility with zero or more media attachments.
-
#rate_limit ⇒ Struct.new(:limit, :remaining, :reset)
Return the rate limit information from the last REST request.
-
#rate_limit_desc ⇒ String
Return a human-readable summary of the rate limit.
-
#upload_media(path, content_type: nil, description: nil, focus_x: nil, focus_y: nil) ⇒ Entity::MediaAttachment
Upload the media contained in the file at ‘path’.
-
#verify_credentials ⇒ Entity::Account
Return the Entity::Account object for the token we’re using.
Constructor Details
#initialize(host:, token: nil, user_agent: "ShepRubyGem/#{Shep::Version}", ua_comment: nil, rate_limit_retry: false, retry_hook: nil, logger: nil, debug_http: false) ⇒ Session
Initialize a new Shep::Session.
By default, the User-Agent header is set to the gem’s identifier, but may be overridden with the ‘user_agent` parameter. It is your responsibility to make sure it is formatted correctly. You can also append comment text to the given User-Agent string with `ua_comment`; this lets you add a comment to the default text.
Parameter ‘logger` may be a `Logger` object, `nil`, or a `Symbol` whose value is the name of one of the supported log levels. In the latter case, a new Logger is created and set to that level. If `nil` is given, a dummy `Logger` is created and used.
If ‘debug_http` is true, compression is disabled and the transactions are sent to `STDERR` via `Net::HTTP.set_debug_output`.
WARNING: this opens a serious security hole and should not be used in production.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/shep/session.rb', line 132 def initialize(host:, token: nil, user_agent: "ShepRubyGem/#{Shep::Version}", ua_comment: nil, rate_limit_retry: false, retry_hook: nil, logger: nil, debug_http: false) @host = host @token = token @logger = init_logger(logger) @user_agent = user_agent @user_agent += " #{ua_comment}" if ua_comment @user_agent.freeze @rate_limit_retry = rate_limit_retry @retry_hook = retry_hook @debug_http = debug_http @rate_limit = Struct.new(:limit, :remaining, :reset).new raise Error::Caller.new("retry_hook: must a callable or nil") unless @retry_hook == nil || @retry_hook.respond_to?(:call) end |
Instance Attribute Details
#host ⇒ String (readonly)
The Server’s hostname
90 91 92 |
# File 'lib/shep/session.rb', line 90 def host @host end |
#logger ⇒ Logger (readonly)
The logger object
90 91 92 |
# File 'lib/shep/session.rb', line 90 def logger @logger end |
#user_agent ⇒ String (readonly)
User-Agent string; frozen
90 91 92 |
# File 'lib/shep/session.rb', line 90 def user_agent @user_agent end |
Instance Method Details
#delete_status(id) ⇒ Entity::Status
Delete the status at ID.
823 |
# File 'lib/shep/session.rb', line 823 def delete_status(id) = rest_delete("statuses/#{id}", Entity::Status) |
#dismiss_notification(id) ⇒ Object
Dismiss the notification with the given ID.
Warning: due to the complexity involved in repeatably sending a notification to an account, there is limited test coverage for this method.
832 833 834 835 836 |
# File 'lib/shep/session.rb', line 832 def dismiss_notification(id) url = rest_uri("notifications/#{id}/dismiss", {}) basic_rest_post_or_put(url, {}) return nil end |
#each_boost_acct(status_id, limit: nil) {|item| ... } ⇒ Enumerator
Retrieve each Entity::Account that boosted the given status.
574 575 576 577 578 579 580 |
# File 'lib/shep/session.rb', line 574 def each_boost_acct(status_id, limit: nil, &block) query = magically_get_caller_kwargs(binding, method(__method__)) rest_get_seq("statuses/#{status_id}/reblogged_by", Entity::Account, query, block) end |
#each_fave_acct(status_id, limit: nil) {|item| ... } ⇒ Enumerator
Retrieve each account that favourited the given status.
594 595 596 597 598 599 600 |
# File 'lib/shep/session.rb', line 594 def each_fave_acct(status_id, limit: nil, &block) query = magically_get_caller_kwargs(binding, method(__method__)) rest_get_seq("statuses/#{status_id}/favourited_by", Entity::Account, query, block) end |
#each_follower(account_id, limit: nil) {|item| ... } ⇒ Enumerator
Retrieve the follower list of an account.
As of Mastodon 4.0, no longer requires a token.
366 367 368 369 370 371 372 |
# File 'lib/shep/session.rb', line 366 def each_follower(account_id, limit: nil, &block) query = magically_get_caller_kwargs(binding, method(__method__)) return rest_get_seq("accounts/#{account_id}/followers", Entity::Account, query, block) end |
#each_following(account_id, limit: nil) {|item| ... } ⇒ Enumerator
Retrieve the list of accounts this account follows
386 387 388 389 390 391 392 |
# File 'lib/shep/session.rb', line 386 def each_following(account_id, limit: nil, &block) query = magically_get_caller_kwargs(binding, method(__method__)) return rest_get_seq("accounts/#{account_id}/following", Entity::Account, query, block) end |
#each_home_status(limit: nil, local: false, max_id: "", remote: false, only_media: false) {|item| ... } ⇒ Enumerator
Retrieve each Entity::Status in the home timeline.
Requires token.
551 552 553 554 555 556 557 558 559 |
# File 'lib/shep/session.rb', line 551 def each_home_status(limit: nil, local: false, max_id: "", remote: false, only_media: false, &block) query = magically_get_caller_kwargs(binding, method(__method__)) rest_get_seq("timelines/home", Entity::Status, query, block) end |
#each_notification(types: [], exclude_types: [], limit: nil, account_id: nil) {|item| ... } ⇒ Enumerator
Retrieve each notification.
Requires a bearer token.
Notification types are indicated by of the following symbols:
‘:mention`, `:status`, `:reblog`, `:follow`, `:follow_request` `:favourite`, `:poll`, `:update`, `:admin.sign_up`, or `:admin.report`
This method will throw an ‘Error::Caller` exception if an unknown value is used.
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 |
# File 'lib/shep/session.rb', line 634 def each_notification(types: [], exclude_types: [], limit: nil, account_id: nil, &block) allowed_notifications = %i{mention status reblog follow follow_request favourite poll update admin.sign_up admin.report} # Remove duplicates and convert strings to symbols [types, exclude_types].each{|param| param.map!{|item| item.intern} param.uniq! } # Now, ensure there are no incorrect notification types. (types + exclude_types).each{|filter| assert("Unknown notification type: #{filter}") { allowed_notifications.include?(filter.intern) } } query = magically_get_caller_kwargs(binding, method(__method__)) rest_get_seq("notifications", Entity::Notification, query, block) end |
#each_public_status(limit: nil, max_id: "", local: false, remote: false, only_media: false) {|item| ... } ⇒ Enumerator
Retrieve the instance’s public timeline(s)
May require a token depending on the instance’s settings.
456 457 458 459 460 461 462 463 464 |
# File 'lib/shep/session.rb', line 456 def each_public_status(limit: nil, max_id: "", local: false, remote: false, only_media: false, &block) query = magically_get_caller_kwargs(binding, method(__method__)) rest_get_seq("timelines/public", Entity::Status, query, block) end |
#each_status(account_id, limit: nil, max_id: "", only_media: false, exclude_replies: false, exclude_reblogs: false, pinned: false, tagged: nil) {|item| ... } ⇒ Enumerator
Retrieve the account’s statuses
422 423 424 425 426 427 428 429 430 431 432 433 |
# File 'lib/shep/session.rb', line 422 def each_status(account_id, limit: nil, max_id: "", only_media: false, exclude_replies: false, exclude_reblogs: false, pinned: false, tagged: nil, &block) query = magically_get_caller_kwargs(binding, method(__method__)) rest_get_seq("accounts/#{account_id}/statuses", Entity::Status, query, block) end |
#each_tag_status(hashtag_s, limit: nil, max_id: "", local: false, remote: false, only_media: false, all: [], none: []) {|item| ... } ⇒ Enumerator
Retrieve a tag’s timeline.
The tag may either be a String (containing one hashtag) or an Array containing one or more. If more than one hashtag is given, all statuses containing any of the given hashtags are retrieved. (This uses the ‘any[]` parameter in the API.)
There is currently no check for contradictory tag lists.
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 |
# File 'lib/shep/session.rb', line 499 def each_tag_status(hashtag_s, limit: nil, max_id: "", local: false, remote: false, only_media: false, all: [], none: [], &block) query = magically_get_caller_kwargs(binding, method(__method__)) any = [] if hashtag_s.is_a?(Array) hashtag_s = hashtag_s.dup hashtag = hashtag_s.shift any = hashtag_s else hashtag = hashtag_s end assert("Empty hashtag!") { hashtag && !hashtag.empty? } query[:any] = any unless any.empty? rest_get_seq("timelines/tag/#{hashtag}", Entity::Status, query, block) end |
#edit_status(id, status, media_ids: [], spoiler_text: "", language: "en") ⇒ Entity::Status
Update the status with the given Id.
Requires token with sufficient permission for the account that owns the status.
Notionally, this method will change all of the affected status parts each time it’s invoked, passing the default parameter if none is given. This is because it is unclear how the API handles omitted fields so we just don’t do that. (You can force it to omit an argument by setting it to nil; this may or may not work for you.)
739 740 741 742 743 744 745 746 747 748 749 750 |
# File 'lib/shep/session.rb', line 739 def edit_status(id, status, media_ids: [], spoiler_text: "", language: "en") formhash = magically_get_caller_kwargs(binding, method(__method__), strip_ignorables: false) formhash[:status] = status formhash[:sensitive] = !!spoiler_text && !spoiler_text.empty? formdata = formhash2array(formhash) return rest_put("statuses/#{id}", Entity::Status, formdata) end |
#fetch_account(id) ⇒ Entity::Account
Fetch user details by ID
238 239 240 |
# File 'lib/shep/session.rb', line 238 def fetch_account(id) return rest_get("accounts/#{id}", Entity::Account, {}) end |
#fetch_account_by_username(handle) ⇒ Entity::Account?
Fetch user details by username.
The username must belong to a user on the current server.
253 254 255 256 257 258 259 |
# File 'lib/shep/session.rb', line 253 def fetch_account_by_username(handle) return rest_get("accounts/lookup", Entity::Account, {acct: handle}) rescue Error::Http => oopsie # As a special case, return nil if the lookup fails return nil if oopsie.response.is_a?(Net::HTTPNotFound) raise oopsie end |
#fetch_context(id) ⇒ Entity::Context
Fetch the context (parent and child status) of status at ‘id’
285 |
# File 'lib/shep/session.rb', line 285 def fetch_context(id) = rest_get("statuses/#{id}/context", Entity::Context, {}) |
#fetch_notification(ntfn_id) ⇒ Entity::Notification
Fetch an individual notification by ID.
Requires a token with sufficient permissions.
270 271 |
# File 'lib/shep/session.rb', line 270 def fetch_notification(ntfn_id) = rest_get("notifications/#{ntfn_id}", Entity::Notification, {}) |
#fetch_status(id) ⇒ Entity::Status
Fetch a single status
278 |
# File 'lib/shep/session.rb', line 278 def fetch_status(id) = rest_get("statuses/#{id}", Entity::Status, {}) |
#fetch_status_src(id) ⇒ Entity::StatusSource
Fetch the editable source of status at id.
Requires token.
294 295 |
# File 'lib/shep/session.rb', line 294 def fetch_status_src(id) = rest_get("statuses/#{id}/source", Entity::StatusSource, {}) |
#fetch_status_with_media(id, media_dir = '.', refetch: true) ⇒ Entity::Status, Hash
Fetch the given status and also any attached media.
Media is downloaded into the given directory unless a file with the expected name is already there (and ‘refetch` is not `true`).
Filenames are chosen by the function; the second return value (a ‘Hash`) can be used to find them. Value order also corresponds to the order of the returned `Status`’s ‘media_attachments` field.
Note that intermediate files unique temporary names while downloading is in progress. This means it is safe to set ‘refetch` to false even if a previous download attempt failed. However, it is would be necessary to delete the intermediate file, which has the suffic “.tmp”.
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/shep/session.rb', line 325 def fetch_status_with_media(id, media_dir = '.', refetch: true) status = fetch_status(id) media = {} status..each { |ma| outfile = File.join(media_dir, File.basename(ma.url.path)) if !refetch && File.exist?(outfile) @logger.info "Found '#{outfile}'; skipping." else tmp = File.join(media_dir, SecureRandom.uuid + '.tmp') begin basic_get_binary(ma.url, tmp) FileUtils.mv(tmp, outfile) rescue Error::Http => e FileUtils.rm(tmp, force: true) raise e end end media[ma.url.to_s] = outfile } return [status, media] end |
#post_status(text, visibility: :private, media_ids: [], spoiler_text: "", language: "") ⇒ Entity::Status
Post a status containing the given text at the specified visibility with zero or more media attachments.
visibility can be one of ‘public’, ‘private’, ‘unlisted’ or ‘direct’; these can be strings of symbols)
media_ids is an array containing the ID strings of any media that may need to be attached.
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 |
# File 'lib/shep/session.rb', line 686 def post_status(text, visibility: :private, media_ids: [], spoiler_text: "", language: "") raise Error::Caller.new("Invalid visibility: #{visibility}") unless %i{public unlisted private direct}.include? visibility.intern query = magically_get_caller_kwargs(binding, method(__method__)) query[:status] = text query[:sensitive] = true if spoiler_text && !spoiler_text.empty? # We need to convert to an array of keys and values rather than # a hash because passing an array argument requires duplicate # keys. This causes Net::HTTP to submit it as a multipart form, # but we can cope. formdata = formhash2array(query) return rest_post("statuses", Entity::Status, formdata) end |
#rate_limit ⇒ Struct.new(:limit, :remaining, :reset)
Return the rate limit information from the last REST request.
The result is a Struct with the following fields:
-
limit Integer - Number of allowed requests per time period
-
remaining Integer - Number of requests you have left
-
reset Time - Future time when the limit resets
Note that different types of operations have different rate limits. For example, most endpoints can be called up to 300 times within 5 minutes but no more than 30 media uploads are allowed within a 30 minute time period.
Note also that some Shep methods will perform multiple API requests; this is only ever the rate limit information from the latest of these.
200 |
# File 'lib/shep/session.rb', line 200 def rate_limit = @rate_limit.dup.freeze |
#rate_limit_desc ⇒ String
Return a human-readable summary of the rate limit.
text
207 208 209 210 211 212 213 |
# File 'lib/shep/session.rb', line 207 def rate_limit_desc rem = (@rate_limit.remaining || '?').to_s lim = (@rate_limit.limit || '?').to_s reset = @rate_limit.reset ? (@rate_limit.reset - Time.now).round : '?' return "#{rem}/#{lim}, #{reset}s" end |
#upload_media(path, content_type: nil, description: nil, focus_x: nil, focus_y: nil) ⇒ Entity::MediaAttachment
Upload the media contained in the file at ‘path’.
Requires token with sufficient permission for the account that owns the status.
Note that Mastodon processes attachments asynchronously, so the attachment may not be available for display when this method returns. Posting an unprocessed status as an attachment works as expected but it’s unclear what happens between posting and when the processing task completes. Usually, this shouldn’t matter to you.
If a rate limit is reached during a call to this method and ‘rate_limit_retry:` was set, the media file to upload should not be touched in any way until the method returns.
790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 |
# File 'lib/shep/session.rb', line 790 def upload_media(path, content_type: nil, description: nil, focus_x: nil, focus_y: nil) formdata = [ ['filename', File.basename(path)], ['file', File.open(path, "rb"), {content_type: content_type}], ] formdata.push ['description', description] if description # Focus args are more exacting so we do some checks here. !!focus_x == !!focus_y or raise Error::Caller.new("Args 'focus_x/y' must *both* be set or unset.") if focus_x raise Error::Caller.new("focus_x/y not a float between -1 and 1") unless (focus_x.is_a?(Float) && focus_y.is_a?(Float) && focus_x >= -1.0 && focus_x <= 1.0 && focus_y >= -1.0 && focus_y <= 1.0) formdata.push ['focus', "#{focus_x},#{focus_y}"] end return rest_post("media", Entity::MediaAttachment, formdata, v2: true) end |
#verify_credentials ⇒ Entity::Account
Return the Entity::Account object for the token we’re using.
Requires a token (obviously).
227 228 229 |
# File 'lib/shep/session.rb', line 227 def verify_credentials return rest_get('accounts/verify_credentials', Entity::Account, {}) end |