Class: Tempest::Post

Inherits:
Data
  • Object
show all
Defined in:
lib/tempest/post.rb

Constant Summary collapse

EMBED_KINDS =

AT Protocol embed ‘$type` values mapped to short symbols used by the REPL. `record` (quote) and `external` (link card) are intentionally absent: they’re surfaced through other UI (URL annotation), so they don’t get a media-marker emoji.

{
  "app.bsky.embed.images" => :images,
  "app.bsky.embed.video"  => :video,
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(uri:, cid:, handle:, display_name:, text:, created_at:, facets: [], reply_parent_uri: nil, embed_kind: nil) ⇒ Post

Returns a new instance of Post.



15
16
17
18
# File 'lib/tempest/post.rb', line 15

def initialize(uri:, cid:, handle:, display_name:, text:, created_at:,
               facets: [], reply_parent_uri: nil, embed_kind: nil)
  super
end

Instance Attribute Details

#cidObject (readonly)

Returns the value of attribute cid

Returns:

  • (Object)

    the current value of cid



5
6
7
# File 'lib/tempest/post.rb', line 5

def cid
  @cid
end

#created_atObject (readonly)

Returns the value of attribute created_at

Returns:

  • (Object)

    the current value of created_at



5
6
7
# File 'lib/tempest/post.rb', line 5

def created_at
  @created_at
end

#display_nameObject (readonly)

Returns the value of attribute display_name

Returns:

  • (Object)

    the current value of display_name



5
6
7
# File 'lib/tempest/post.rb', line 5

def display_name
  @display_name
end

#embed_kindObject (readonly)

Returns the value of attribute embed_kind

Returns:

  • (Object)

    the current value of embed_kind



5
6
7
# File 'lib/tempest/post.rb', line 5

def embed_kind
  @embed_kind
end

#facetsObject (readonly)

Returns the value of attribute facets

Returns:

  • (Object)

    the current value of facets



5
6
7
# File 'lib/tempest/post.rb', line 5

def facets
  @facets
end

#handleObject (readonly)

Returns the value of attribute handle

Returns:

  • (Object)

    the current value of handle



5
6
7
# File 'lib/tempest/post.rb', line 5

def handle
  @handle
end

#reply_parent_uriObject (readonly)

Returns the value of attribute reply_parent_uri

Returns:

  • (Object)

    the current value of reply_parent_uri



5
6
7
# File 'lib/tempest/post.rb', line 5

def reply_parent_uri
  @reply_parent_uri
end

#textObject (readonly)

Returns the value of attribute text

Returns:

  • (Object)

    the current value of text



5
6
7
# File 'lib/tempest/post.rb', line 5

def text
  @text
end

#uriObject (readonly)

Returns the value of attribute uri

Returns:

  • (Object)

    the current value of uri



5
6
7
# File 'lib/tempest/post.rb', line 5

def uri
  @uri
end

Class Method Details

.create(client, did:, text:, reply: nil, langs: nil, created_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ")) ⇒ Object

Compose a record for com.atproto.repo.createRecord (app.bsky.feed.post). When ‘reply` is provided, both root and parent are set to the same target. This is correct for top-level replies and a known v1 trade-off for replies deeper in a thread (AppView will nest the reply under `parent` instead of the original conversation root).



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
# File 'lib/tempest/post.rb', line 54

def self.create(client, did:, text:, reply: nil, langs: nil,
                created_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ"))
  record = {
    "$type" => "app.bsky.feed.post",
    "text" => text,
    "createdAt" => created_at,
  }
  if reply
    ref = { "uri" => reply[:uri], "cid" => reply[:cid] }
    record["reply"] = { "root" => ref, "parent" => ref }
  end

  record["langs"] = langs if langs && !langs.empty?

  link_facets = detect_link_facets(text)
  record["facets"] = link_facets unless link_facets.empty?

  client.post(
    "com.atproto.repo.createRecord",
    body: {
      repo: did,
      collection: "app.bsky.feed.post",
      record: record,
    },
  )
end

Scans ‘text` for bare URLs and builds AT Protocol link facets pointing at each match. Without this, the AppView treats URLs as plain text and does not render them as clickable links.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/tempest/post.rb', line 104

def self.detect_link_facets(text)
  return [] if text.nil? || text.empty?

  bytes = text.b
  facets = []
  pos = 0
  while (match = /https?:\/\/\S+/n.match(bytes, pos))
    byte_start = match.begin(0)
    byte_end = match.end(0)
    uri = match[0].dup.force_encoding(Encoding::UTF_8)
    facets << {
      "index" => { "byteStart" => byte_start, "byteEnd" => byte_end },
      "features" => [
        { "$type" => "app.bsky.richtext.facet#link", "uri" => uri },
      ],
    }
    pos = byte_end
  end
  facets
end

.embed_kind_from(embed) ⇒ Object

The view-side ‘$type` carries a `#view` suffix (e.g. `app.bsky.embed.images#view`); the raw record uses the bare form. Strip the suffix before looking up so both feed and Jetstream payloads classify identically.



43
44
45
46
47
# File 'lib/tempest/post.rb', line 43

def self.embed_kind_from(embed)
  return nil unless embed.is_a?(Hash)
  type = embed["$type"].to_s.sub(/#view\z/, "")
  EMBED_KINDS[type]
end

.from_feed_view(post) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/tempest/post.rb', line 20

def self.from_feed_view(post)
  post = post || {}
  author = post["author"] || {}
  record = post["record"] || {}
  reply = record["reply"]
  reply_parent = reply.is_a?(Hash) ? reply["parent"] : nil
  new(
    uri: post["uri"],
    cid: post["cid"],
    handle: author["handle"],
    display_name: author["displayName"],
    text: record["text"],
    created_at: record["createdAt"],
    facets: Facet.parse(record["facets"]),
    reply_parent_uri: reply_parent.is_a?(Hash) ? reply_parent["uri"] : nil,
    embed_kind: embed_kind_from(post["embed"] || record["embed"]),
  )
end

.like(client, did:, subject_uri:, subject_cid:, created_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ")) ⇒ Object

Compose an app.bsky.feed.like record referencing the subject post and send it via com.atproto.repo.createRecord. The AppView surfaces this in like counts and notifications for the target post.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/tempest/post.rb', line 84

def self.like(client, did:, subject_uri:, subject_cid:,
              created_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ"))
  record = {
    "$type" => "app.bsky.feed.like",
    "subject" => { "uri" => subject_uri, "cid" => subject_cid },
    "createdAt" => created_at,
  }
  client.post(
    "com.atproto.repo.createRecord",
    body: {
      repo: did,
      collection: "app.bsky.feed.like",
      record: record,
    },
  )
end