hpanot

hpanot: get your annotations in Markdown from Hypothes.is (https://hypothes.is)
git clone git://git.alex.balgavy.eu/hpanot.git
Log | Files | Refs | README | LICENSE

hpanot (3645B)


      1 #!/usr/bin/env ruby
      2 # frozen_string_literal: true
      3 
      4 require 'open-uri'
      5 require 'json'
      6 
      7 # hypothes.is interface
      8 class Hypothesis
      9   # a hypothes.is annotation
     10   class Annotation
     11     attr_reader :uri, :title
     12     def initialize(text:, comment:, title:, uri:)
     13       @text = text
     14       @comment = comment.empty? ? nil : comment
     15       @title = title
     16       @uri = uri
     17     end
     18 
     19     def to_markdown
     20       str = @text.split("\n").reduce('') { |acc, line| acc + "> #{line.gsub(/[‘’]/, %q{'}).gsub(/[“”]/, %q{"})}\n" }
     21       str += "\n"
     22       str += "#{@comment}\n" if @comment
     23       str
     24     end
     25 
     26     def to_markdown_title_url
     27       "[#{@title}](#{@uri})\n"
     28     end
     29   end
     30 
     31   def initialize
     32     %w[HYPOTHESIS_API_KEY HYPOTHESIS_USERNAME].each do |var|
     33       die "Please set the #{var} environment variable" if ENV[var].nil?
     34     end
     35 
     36     @apikey = ENV['HYPOTHESIS_API_KEY']
     37     @username = ENV['HYPOTHESIS_USERNAME']
     38     @headers = { 'Host' => 'hypothes.is', 'Accept' => 'application/json', 'Authorization' => "Bearer #{@apikey}" }
     39     @baseurl = 'https://hypothes.is/api'
     40 
     41     ping || die("Could not access the API #{@baseurl}")
     42   end
     43 
     44   def search_host(host)
     45     annotations "wildcard_uri=http://#{host}/*&limit=200&order=asc"
     46   end
     47 
     48   def search_uri(uri)
     49     annotations "uri=#{uri}&limit=200&order=asc"
     50   end
     51 
     52   def recent_urls(num_urls = 2)
     53     # FIXME: this is a very roundabout, inefficient method. Waiting for an API for this from hypothes.is
     54     grouped_annotations = annotations('sort=created&order=desc&limit=200').group_by(&:uri)
     55     n_recent = grouped_annotations.keys[0, num_urls]
     56     n_recent.inject([]) do |arr, uri|
     57       arr << { uri: uri, title: grouped_annotations[uri].first.title }
     58     end
     59   end
     60 
     61   private
     62 
     63   def ping
     64     URI.open("#{@baseurl}/", @headers)
     65     true
     66   rescue OpenURI::HTTPError
     67     false
     68   end
     69 
     70   def die(msg)
     71     warn msg
     72     exit 1
     73   end
     74 
     75   def request(endpoint)
     76     URI.open(@baseurl + endpoint, @headers) do |response|
     77       JSON.parse(response.read)['rows']
     78     end
     79   rescue OpenURI::HTTPError
     80     warn "Error getting data from #{endpoint}"
     81     []
     82   end
     83 
     84   def annotations(uri_search_str)
     85     data = request "/search?user=acct:#{@username}@hypothes.is&#{uri_search_str}"
     86     data.reduce([]) do |arr, annot|
     87       arr << Annotation.new(text: annot['target'].first['selector']
     88                                   .select { |f| f['type'] == 'TextQuoteSelector' }.first['exact'],
     89                             comment: annot['text'],
     90                             title: annot['document']['title'].first,
     91                             uri: annot['uri'])
     92     end
     93   end
     94 end
     95 
     96 def print_usage
     97   puts <<~HEREDOC
     98     Usage: hpanot [command] [arg1, [arg2...]]
     99 
    100     Commands:
    101     site URL      get annotations for a website
    102     recent [n]    get n (default 2) most recently annotated websites
    103 
    104   HEREDOC
    105 end
    106 
    107 ARGV.each do |opt|
    108   case opt
    109   when '-h' || '--help'
    110     print_usage
    111     exit 0
    112   end
    113 end
    114 
    115 params = ARGV.reject { |arg| arg.start_with? '-' }
    116 if params.empty?
    117   print_usage
    118   exit 1
    119 end
    120 
    121 case params[0]
    122 when 'site'
    123   die 'No website specified, use pass -h to print usage.' unless params[1]
    124   h = Hypothesis.new
    125   site = params[1]
    126   annotations = site.start_with?('http') ? h.search_uri(site) : h.search_host(site)
    127   puts annotations.first.to_markdown_title_url + "\n" + annotations.reduce('') { |str, annot| str + annot.to_markdown }
    128 when 'recent'
    129   h = Hypothesis.new
    130   begin
    131     recent_urls = params[1] ? h.recent_urls(Integer(params[1])) : h.recent_urls
    132     recent_urls.each { |u| puts "#{u[:uri]}\t#{u[:title]}" }
    133   rescue ArgumentError
    134     die 'Argument 2 must be an integer.'
    135   end
    136 else
    137   print_usage
    138   exit 1
    139 end