summaryrefslogtreecommitdiff
path: root/helpers.rb
blob: 556f187b9a760f3d29cd54a8ed02151d2db44be5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# frozen_string_literal: true

require 'English'

helpers do
  def curl(ext, url)
    p url
    response = `/run/current-system/sw/bin/curl --fail-with-body -sSL #{ext} #{url}`
    $CHILD_STATUS.success? ? response : nil
  end

  def fetch(url, accept = 'application/activity+json')
    response = curl("-H 'Accept: #{accept}'", url)
    response ? JSON.parse(response) : nil
  end

  # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb
  # , url
  def outbox(type, object, recipients, add_recipients = false)
    activity = {
      '@context' => 'https://www.w3.org/ns/activitystreams',
      'type' => type,
      'actor' => ACTOR,
      'object' => object
    }

    now = Time.now
    date = now.strftime('%Y-%m-%dT%H:%M:%S.%N')
    httpdate = now.utc.httpdate
    basename = "#{date}.json"
    activity['id'] = File.join(OUTBOX_URL, basename)
    activity['published'] = httpdate
    if activity['object'] and activity['object']['type'] and !activity['object']['id']
      rel_path = File.join activity['object']['type'].downcase, basename
      activity['object']['published'] = httpdate
      activity['object']['id'] = File.join(OUTBOX_URL, rel_path)
      File.open(File.join(OUTBOX_DIR, rel_path), 'w+') { |f| f.puts activity.to_json }
    end
    File.open(File.join(OUTBOX_DIR, basename), 'w+') { |f| f.puts activity.to_json }

    keypair = OpenSSL::PKey::RSA.new(File.read('private.pem'))
    body = activity.to_json
    sha256 = OpenSSL::Digest.new('SHA256')
    digest = "SHA-256=#{sha256.base64digest(body)}"

    # assumes that recipient collections have been expanded by sender
    # put all recipients into 'to', avoid 'cc' 'bto' 'bcc' 'audience' !!
    activity['to'] = recipients if add_recipients
    inboxes = if recipients.include? 'https://www.w3.org/ns/activitystreams#Public'
                people.collect { |p| p[2] }.uniq # cached sharedInboxes
              else
                []
              end
    recipients.uniq.each do |url|
      next if [ACTOR, 'https://www.w3.org/ns/activitystreams#Public'].include? url

      actor = fetch url
      next unless actor and actor['inbox']

      inbox = actor['endpoints']['sharedInbox']
      inboxes << (inbox || actor['inbox'])
    end
    inboxes.compact.uniq.each do |inbox|
      uri = URI(inbox)
      string = "(request-target): post #{uri.request_uri}\nhost: #{uri.host}\ndate: #{httpdate}\ndigest: #{digest}\ncontent-type: application/activity+json"
      signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), string))
      signed_header = "keyId=\"#{ACTOR}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"#{signature}\""

      curl(
        "-X POST -H 'Content-Type: application/activity+json' -H 'Host: #{uri.host}' -H 'Date: #{httpdate}' -H 'Digest: #{digest}' -H 'Signature: #{signed_header}' -d '#{body}'", inbox
      )
    end
  end

  def mention(actor)
    person = people.select { |p| p[1] == actor }
    if person.empty?
      a = fetch(actor)
      return nil unless a

      mention = "#{a['preferredUsername']}@#{URI(actor).host}"
      cache mention, actor, a
      mention
    else
      person[0][0]
    end
  end

  def actor(mention)
    mention = mention.sub(/^@/, '').chomp
    actors = people.select { |p| p[0] == mention }
    if actors.empty?
      _, server = mention.split('@')
      a = fetch("https://#{server}/.well-known/webfinger?resource=acct:#{mention}", 'application/jrd+json')
      return nil unless a

      actor = a['links'].select do |l|
        l['rel'] == 'self'
      end[0]['href']
      cache mention, actor, a
      actor
    else
      actors[0][1]
    end
  end

  def people
    File.read('cache/people.tsv').split("\n").collect { |l| l.chomp.split("\t") }
  end

  def cache(mention, actor, a)
    sharedInbox = a['endpoints']['sharedInbox'] if a['endpoints'] and a['endpoints']['sharedInbox']
    File.open('cache/people.tsv', 'a') { |f| f.puts "#{mention}\t#{actor}\t#{sharedInbox}" }
  end
end