···1818 new(timestamp)
1919 end
20202121+ @spec at_time(DateTime.t()) :: String.t()
2222+ def at_time(%DateTime{} = datetime) do
2323+ clock_id = :rand.uniform(1024) - 1
2424+ timestamp_μs = DateTime.to_unix(datetime, :microsecond) * 1024
2525+ new(timestamp_μs + clock_id)
2626+ end
2727+2128 @spec new(non_neg_integer()) :: String.t()
2229 def new(int) do
2330 int
+7-3
lib/annot_at/atproto/xrpc.ex
···8686 end
87878888 defp needs_nonce?(401, headers) do
8989- headers
9090- |> Map.get("www-authenticate")
9191- |> Enum.any?(&String.contains?(&1, "use_dpop_nonce"))
8989+ case Map.fetch(headers, "www-authenticate") do
9090+ {:ok, headers} ->
9191+ Enum.any?(headers, &String.contains?(&1, "use_dpop_nonce"))
9292+9393+ :error ->
9494+ false
9595+ end
9296 end
93979498 defp needs_nonce?(_status, _headers), do: false
+7
lib/annot_at/feeds/entry.ex
···1616 summary: String.t() | nil,
1717 content: String.t() | nil
1818 }
1919+2020+ def hash(%__MODULE__{} = entry) do
2121+ # Join collapses the potential nils that :crypto.hash doesn't accept.
2222+ iodata = Enum.join([entry.title, entry.summary, entry.content])
2323+ hash = :crypto.hash(:sha256, iodata)
2424+ Base.encode16(hash, case: :lower)
2525+ end
1926end
+17
lib/annot_at/publishing.ex
···1010 alias AnnotAt.Accounts.Scope
1111 alias AnnotAt.Accounts.User
1212 alias AnnotAt.Atproto.TID
1313+ alias AnnotAt.Publishing.Post
1314 alias AnnotAt.Publishing.Site
1415 alias AnnotAt.Repo
1516···78797980 site
8081 |> Site.changeset(attrs)
8282+ |> Repo.update()
8383+ end
8484+8585+ def list_posts(%Site{id: site_id}) do
8686+ Repo.all(from p in Post, where: p.site_id == ^site_id, order_by: [desc: p.inserted_at])
8787+ end
8888+8989+ def create_post(%Site{id: site_id}, attrs) do
9090+ %Post{site_id: site_id}
9191+ |> Post.changeset(attrs)
9292+ |> Repo.insert()
9393+ end
9494+9595+ def update_post(%Post{} = post, attrs) do
9696+ post
9797+ |> Post.changeset(attrs)
8198 |> Repo.update()
8299 end
83100
+35
lib/annot_at/publishing/post.ex
···11+defmodule AnnotAt.Publishing.Post do
22+ use Ecto.Schema
33+44+ import Ecto.Changeset
55+66+ @type t :: %__MODULE__{
77+ site_id: integer(),
88+ guid: String.t(),
99+ rkey: String.t(),
1010+ content_hash: String.t()
1111+ }
1212+1313+ schema "posts" do
1414+ # The stable ID of the blog post, or the URL if not available
1515+ field :guid, :string
1616+ field :rkey, :string
1717+ # We store the hash of the content to detect changes in the future
1818+ field :content_hash, :string
1919+2020+ belongs_to :site, AnnotAt.Publishing.Site
2121+2222+ timestamps(type: :utc_datetime)
2323+ end
2424+2525+ def changeset(post, attrs) do
2626+ post
2727+ |> cast(attrs, [:guid, :rkey, :content_hash])
2828+ |> validate_required([:guid, :rkey, :content_hash])
2929+ |> validate_length(:guid, max: 2048)
3030+ |> validate_length(:rkey, max: 512)
3131+ |> validate_length(:content_hash, max: 64)
3232+ |> unique_constraint(:guid, name: :posts_site_id_guid_index)
3333+ |> foreign_key_constraint(:site_id)
3434+ end
3535+end
···3838 live "/sites", SitesLive
3939 live "/sites/new", SiteNewLive
4040 live "/sites/:id", SiteLive
4141+ live "/sites/:id/posts", PostsLive
4142 end
4243 end
4344