Music streaming on ATProto!

feat: backend scaffold & basic codegen for lexicons

ovyerus.com bf621c2b de8d20e2

verified
Changed files
+1764 -1
.vscode
apps
backend
config
lib
atproto
sh
comet
v0
actor
getProfile
getProfiles
profile
feed
comment
defs
getActorPlaylists
getActorTracks
like
play
playlist
playlistTrack
repost
track
richtext
facet
comet
comet_web
priv
test
+1
.gitignore
··· 7 7 .wrangler 8 8 .svelte-kit 9 9 build 10 + .elixir_ls 10 11 11 12 # OS 12 13 .DS_Store
+1
.vscode/settings.json
··· 5 5 "url": "https://gist.githubusercontent.com/mary-ext/6e428031c18799d1587048b456d118cb/raw/4322c492384ac5da33986dee9588938a88d922f1/schema.json" 6 6 } 7 7 ], 8 + "elixirLS.projectDir": "./apps/backend", 8 9 "search.exclude": { 9 10 "**/node_modules": true, 10 11 "**/bower_components": true,
+5
apps/backend/.formatter.exs
··· 1 + [ 2 + import_deps: [:ecto, :ecto_sql, :phoenix], 3 + subdirectories: ["priv/*/migrations"], 4 + inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}", "priv/*/seeds.exs"] 5 + ]
+27
apps/backend/.gitignore
··· 1 + # The directory Mix will write compiled artifacts to. 2 + /_build/ 3 + 4 + # If you run "mix test --cover", coverage assets end up here. 5 + /cover/ 6 + 7 + # The directory Mix downloads your dependencies sources to. 8 + /deps/ 9 + 10 + # Where 3rd-party dependencies like ExDoc output generated docs. 11 + /doc/ 12 + 13 + # Ignore .fetch files in case you like to edit your project deps locally. 14 + /.fetch 15 + 16 + # If the VM crashes, it generates a dump, let's ignore it too. 17 + erl_crash.dump 18 + 19 + # Also ignore archive artifacts (built via "mix archive.build"). 20 + *.ez 21 + 22 + # Temporary files, for example, from tests. 23 + /tmp/ 24 + 25 + # Ignore package tarball (built via "mix hex.build"). 26 + comet-*.tar 27 +
+24
apps/backend/README.md
··· 1 + # Comet AppView 2 + 3 + [Phoenix](https://www.phoenixframework.org)-powered AppView for Comet. 4 + 5 + --- 6 + 7 + To start your Phoenix server: 8 + 9 + - Run `mix setup` to install and setup dependencies 10 + - Start Phoenix endpoint with `mix phx.server` or inside IEx with 11 + `iex -S mix phx.server` 12 + 13 + Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 14 + 15 + Ready to run in production? Please 16 + [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 17 + 18 + ## Learn more 19 + 20 + - Official website: https://www.phoenixframework.org/ 21 + - Guides: https://hexdocs.pm/phoenix/overview.html 22 + - Docs: https://hexdocs.pm/phoenix 23 + - Forum: https://elixirforum.com/c/phoenix-forum 24 + - Source: https://github.com/phoenixframework/phoenix
+35
apps/backend/config/config.exs
··· 1 + # This file is responsible for configuring your application 2 + # and its dependencies with the aid of the Config module. 3 + # 4 + # This configuration file is loaded before any dependency and 5 + # is restricted to this project. 6 + 7 + # General application configuration 8 + import Config 9 + 10 + config :comet, 11 + ecto_repos: [Comet.Repo], 12 + generators: [timestamp_type: :utc_datetime, binary_id: true] 13 + 14 + # Configures the endpoint 15 + config :comet, CometWeb.Endpoint, 16 + url: [host: "localhost"], 17 + adapter: Bandit.PhoenixAdapter, 18 + render_errors: [ 19 + formats: [json: CometWeb.ErrorJSON], 20 + layout: false 21 + ], 22 + pubsub_server: Comet.PubSub, 23 + live_view: [signing_salt: "oq2xYeBj"] 24 + 25 + # Configures Elixir's Logger 26 + config :logger, :console, 27 + format: "$time $metadata[$level] $message\n", 28 + metadata: [:request_id] 29 + 30 + # Use Jason for JSON parsing in Phoenix 31 + config :phoenix, :json_library, Jason 32 + 33 + # Import environment specific config. This must remain at the bottom 34 + # of this file so it overrides the configuration defined above. 35 + import_config "#{config_env()}.exs"
+63
apps/backend/config/dev.exs
··· 1 + import Config 2 + 3 + # Configure your database 4 + config :comet, Comet.Repo, 5 + username: "postgres", 6 + password: "postgres", 7 + hostname: "localhost", 8 + database: "comet_dev", 9 + stacktrace: true, 10 + show_sensitive_data_on_connection_error: true, 11 + pool_size: 10 12 + 13 + # For development, we disable any cache and enable 14 + # debugging and code reloading. 15 + # 16 + # The watchers configuration can be used to run external 17 + # watchers to your application. For example, we can use it 18 + # to bundle .js and .css sources. 19 + config :comet, CometWeb.Endpoint, 20 + # Binding to loopback ipv4 address prevents access from other machines. 21 + # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 22 + http: [ip: {127, 0, 0, 1}, port: 4000], 23 + check_origin: false, 24 + code_reloader: true, 25 + debug_errors: true, 26 + secret_key_base: "Vw9UaVO8YBKiooaOlZ2Rhx7xJHydL9s2YIviOwiiQz8Cy24+mLBB3Fj+9jvOIdQE", 27 + watchers: [] 28 + 29 + # ## SSL Support 30 + # 31 + # In order to use HTTPS in development, a self-signed 32 + # certificate can be generated by running the following 33 + # Mix task: 34 + # 35 + # mix phx.gen.cert 36 + # 37 + # Run `mix help phx.gen.cert` for more information. 38 + # 39 + # The `http:` config above can be replaced with: 40 + # 41 + # https: [ 42 + # port: 4001, 43 + # cipher_suite: :strong, 44 + # keyfile: "priv/cert/selfsigned_key.pem", 45 + # certfile: "priv/cert/selfsigned.pem" 46 + # ], 47 + # 48 + # If desired, both `http:` and `https:` keys can be 49 + # configured to run both http and https servers on 50 + # different ports. 51 + 52 + # Enable dev routes for dashboard and mailbox 53 + config :comet, dev_routes: true 54 + 55 + # Do not include metadata nor timestamps in development logs 56 + config :logger, :console, format: "[$level] $message\n" 57 + 58 + # Set a higher stacktrace during development. Avoid configuring such 59 + # in production as building large stacktraces may be expensive. 60 + config :phoenix, :stacktrace_depth, 20 61 + 62 + # Initialize plugs at runtime for faster development compilation 63 + config :phoenix, :plug_init_mode, :runtime
+7
apps/backend/config/prod.exs
··· 1 + import Config 2 + 3 + # Do not print debug messages in production 4 + config :logger, level: :info 5 + 6 + # Runtime production configuration, including reading 7 + # of environment variables, is done on config/runtime.exs.
+99
apps/backend/config/runtime.exs
··· 1 + import Config 2 + 3 + # config/runtime.exs is executed for all environments, including 4 + # during releases. It is executed after compilation and before the 5 + # system starts, so it is typically used to load production configuration 6 + # and secrets from environment variables or elsewhere. Do not define 7 + # any compile-time configuration in here, as it won't be applied. 8 + # The block below contains prod specific runtime configuration. 9 + 10 + # ## Using releases 11 + # 12 + # If you use `mix release`, you need to explicitly enable the server 13 + # by passing the PHX_SERVER=true when you start it: 14 + # 15 + # PHX_SERVER=true bin/comet start 16 + # 17 + # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 + # script that automatically sets the env var above. 19 + if System.get_env("PHX_SERVER") do 20 + config :comet, CometWeb.Endpoint, server: true 21 + end 22 + 23 + if config_env() == :prod do 24 + database_url = 25 + System.get_env("DATABASE_URL") || 26 + raise """ 27 + environment variable DATABASE_URL is missing. 28 + For example: ecto://USER:PASS@HOST/DATABASE 29 + """ 30 + 31 + maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] 32 + 33 + config :comet, Comet.Repo, 34 + # ssl: true, 35 + url: database_url, 36 + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), 37 + socket_options: maybe_ipv6 38 + 39 + # The secret key base is used to sign/encrypt cookies and other secrets. 40 + # A default value is used in config/dev.exs and config/test.exs but you 41 + # want to use a different value for prod and you most likely don't want 42 + # to check this value into version control, so we use an environment 43 + # variable instead. 44 + secret_key_base = 45 + System.get_env("SECRET_KEY_BASE") || 46 + raise """ 47 + environment variable SECRET_KEY_BASE is missing. 48 + You can generate one by calling: mix phx.gen.secret 49 + """ 50 + 51 + host = System.get_env("PHX_HOST") || "example.com" 52 + port = String.to_integer(System.get_env("PORT") || "4000") 53 + 54 + config :comet, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 55 + 56 + config :comet, CometWeb.Endpoint, 57 + url: [host: host, port: 443, scheme: "https"], 58 + http: [ 59 + # Enable IPv6 and bind on all interfaces. 60 + # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 61 + # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 62 + # for details about using IPv6 vs IPv4 and loopback vs public addresses. 63 + ip: {0, 0, 0, 0, 0, 0, 0, 0}, 64 + port: port 65 + ], 66 + secret_key_base: secret_key_base 67 + 68 + # ## SSL Support 69 + # 70 + # To get SSL working, you will need to add the `https` key 71 + # to your endpoint configuration: 72 + # 73 + # config :comet, CometWeb.Endpoint, 74 + # https: [ 75 + # ..., 76 + # port: 443, 77 + # cipher_suite: :strong, 78 + # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 79 + # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 80 + # ] 81 + # 82 + # The `cipher_suite` is set to `:strong` to support only the 83 + # latest and more secure SSL ciphers. This means old browsers 84 + # and clients may not be supported. You can set it to 85 + # `:compatible` for wider support. 86 + # 87 + # `:keyfile` and `:certfile` expect an absolute path to the key 88 + # and cert in disk or a relative path inside priv, for example 89 + # "priv/ssl/server.key". For all supported SSL configuration 90 + # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 91 + # 92 + # We also recommend setting `force_ssl` in your config/prod.exs, 93 + # ensuring no data is ever sent via http, always redirecting to https: 94 + # 95 + # config :comet, CometWeb.Endpoint, 96 + # force_ssl: [hsts: true] 97 + # 98 + # Check `Plug.SSL` for all available options in `force_ssl`. 99 + end
+27
apps/backend/config/test.exs
··· 1 + import Config 2 + 3 + # Configure your database 4 + # 5 + # The MIX_TEST_PARTITION environment variable can be used 6 + # to provide built-in test partitioning in CI environment. 7 + # Run `mix help test` for more information. 8 + config :comet, Comet.Repo, 9 + username: "postgres", 10 + password: "postgres", 11 + hostname: "localhost", 12 + database: "comet_test#{System.get_env("MIX_TEST_PARTITION")}", 13 + pool: Ecto.Adapters.SQL.Sandbox, 14 + pool_size: System.schedulers_online() * 2 15 + 16 + # We don't run a server during test. If one is required, 17 + # you can enable the server option below. 18 + config :comet, CometWeb.Endpoint, 19 + http: [ip: {127, 0, 0, 1}, port: 4002], 20 + secret_key_base: "eaG5CrPmVserxnUlu8DyG8I6i3m3TBDOi8fsKn2niwYUMhjps0YkWWMGRnoSXvGf", 21 + server: false 22 + 23 + # Print only warnings and errors during test 24 + config :logger, level: :warning 25 + 26 + # Initialize plugs at runtime for faster test compilation 27 + config :phoenix, :plug_init_mode, :runtime
+169
apps/backend/lib/atproto/atproto.ex
··· 1 + # AUTOGENERATED: This file was generated using the mix task `lexgen`. 2 + defmodule Atproto do 3 + @default_pds_hostname Application.compile_env!(:comet, :default_pds_hostname) 4 + 5 + @typedoc """ 6 + A type representing the names of the options that can be passed to `query/3` and `procedure/3`. 7 + """ 8 + @type xrpc_opt :: :pds_hostname | :authorization 9 + 10 + @typedoc """ 11 + A keyword list of options that can be passed to `query/3` and `procedure/3`. 12 + """ 13 + @type xrpc_opts :: [{xrpc_opt(), any()}] 14 + 15 + @doc """ 16 + Converts a JSON string, or decoded JSON map, into a struct based on the given module. 17 + 18 + This function uses `String.to_existing_atom/1` to convert the keys of the map to atoms, meaning this will throw an error if the input JSON contains keys which are not already defined as atoms in the existing structs or codebase. 19 + """ 20 + @spec decode_to_struct(module(), binary() | map()) :: map() 21 + def decode_to_struct(module, json) when is_binary(json) do 22 + decode_to_struct(module, Jason.decode!(json, keys: :atoms!)) 23 + end 24 + 25 + def decode_to_struct(module, map) when is_map(map) do 26 + Map.merge(module.new(), map) 27 + end 28 + 29 + @doc """ 30 + Raises an error if any required parameters are missing from the given map. 31 + """ 32 + @spec ensure_required(map(), [String.t()]) :: map() 33 + def ensure_required(params, required) do 34 + if Enum.all?(required, fn key -> Map.has_key?(params, key) end) do 35 + params 36 + else 37 + raise ArgumentError, "Missing one or more required parameters: #{Enum.join(required, ", ")}" 38 + end 39 + end 40 + 41 + @doc """ 42 + Executes a "GET" HTTP request and returns the response body as a map. 43 + 44 + If the `:pds_hostname` option is not provided, the default PDS hostname as provided in the compile-time configuration will be used. 45 + """ 46 + @spec query(map(), String.t(), xrpc_opts()) :: Req.Request.t() 47 + def query(params, target, opts \\ []) do 48 + target 49 + |> endpoint(opts) 50 + |> URI.new!() 51 + |> URI.append_query(URI.encode_query(params)) 52 + |> Req.get(build_req_auth(opts)) 53 + |> handle_response(opts) 54 + end 55 + 56 + @doc """ 57 + Executes a "POST" HTTP request and returns the response body as a map. 58 + 59 + If the `:pds_hostname` option is not provided, the default PDS hostname as provided in the compile-time configuration will be used. 60 + """ 61 + @spec procedure(map(), String.t(), xrpc_opts()) :: {:ok | :refresh | :error, map()} 62 + def procedure(params, target, opts \\ []) do 63 + req_opts = 64 + opts 65 + |> build_req_auth() 66 + |> build_req_headers(opts, target) 67 + |> build_req_body(params, target) 68 + 69 + target 70 + |> endpoint(opts) 71 + |> URI.new!() 72 + |> Req.post(req_opts) 73 + |> handle_response(opts) 74 + end 75 + 76 + defp build_req_auth(opts) do 77 + case Keyword.get(opts, :access_token) do 78 + nil -> 79 + case Keyword.get(opts, :admin_token) do 80 + nil -> 81 + [] 82 + 83 + token -> 84 + [auth: {:basic, "admin:#{token}"}] 85 + end 86 + 87 + token -> 88 + [auth: {:bearer, token}] 89 + end 90 + end 91 + 92 + defp build_req_headers(req_opts, opts, "com.atproto.repo.uploadBlob") do 93 + [ 94 + {:headers, 95 + [ 96 + {"Content-Type", Keyword.fetch!(opts, :content_type)}, 97 + {"Content-Length", Keyword.fetch!(opts, :content_length)} 98 + ]} 99 + | req_opts 100 + ] 101 + end 102 + 103 + defp build_req_headers(req_opts, _opts, _target), do: req_opts 104 + 105 + defp build_req_body(opts, blob, "com.atproto.repo.uploadBlob") do 106 + [{:body, blob} | opts] 107 + end 108 + 109 + defp build_req_body(opts, %{} = params, _target) when map_size(params) > 0 do 110 + [{:json, params} | opts] 111 + end 112 + 113 + defp build_req_body(opts, _params, _target), do: opts 114 + 115 + defp endpoint(target, opts) do 116 + (Keyword.get(opts, :pds_hostname) || @default_pds_hostname) <> "/xrpc/" <> target 117 + end 118 + 119 + defp handle_response({:ok, %Req.Response{} = response}, opts) do 120 + case response.status do 121 + x when x in 200..299 -> 122 + {:ok, response.body} 123 + 124 + _ -> 125 + if response.body["error"] == "ExpiredToken" do 126 + {:ok, user} = 127 + Com.Atproto.Server.RefreshSession.main(%{}, 128 + access_token: Keyword.get(opts, :refresh_token) 129 + ) 130 + 131 + {:refresh, user} 132 + else 133 + {:error, response.body} 134 + end 135 + end 136 + end 137 + 138 + defp handle_response(error, _opts), do: error 139 + 140 + @doc """ 141 + Converts a "map-like" entity into a standard map. This will also omit any entries that have a `nil` value. 142 + 143 + This is useful for converting structs or schemas into regular maps before sending them over XRPC requests. 144 + 145 + You may optionally pass in an keyword list of options: 146 + 147 + - `:stringify` - `boolean` - If `true`, converts the keys to strings. Otherwise, converts keys to atoms. Default is `false`. 148 + - *Note*: When `false`, this feature uses the `to_existing_atom/1` function to avoid reckless conversion of string keys. 149 + """ 150 + @spec to_map(map() | struct()) :: map() 151 + def to_map(%{__struct__: _} = m, opts \\ []) do 152 + string_keys = Keyword.get(opts, :stringify, false) 153 + 154 + m 155 + |> Map.drop([:__struct__, :__meta__]) 156 + |> Enum.map(fn 157 + {_, nil} -> 158 + nil 159 + 160 + {k, v} when is_atom(k) -> 161 + if string_keys, do: {to_string(k), v}, else: {k, v} 162 + 163 + {k, v} when is_binary(k) -> 164 + if string_keys, do: {k, v}, else: {String.to_existing_atom(k), v} 165 + end) 166 + |> Enum.reject(&is_nil/1) 167 + |> Enum.into(%{}) 168 + end 169 + end
+15
apps/backend/lib/atproto/sh/comet/v0/actor/getProfile/xrpc.ex
··· 1 + defmodule Sh.Comet.V0.Actor.GetProfile do 2 + 3 + @doc """ 4 + Get the profile view of an actor. 5 + """ 6 + @spec main(%{ 7 + actor: String.t() 8 + }, Atproto.xrpc_opts()) :: {:ok, Sh.Comet.V0.Actor.Profile.View.t()} | {:error, any} 9 + def main(params \\ %{}, opts \\ []) do 10 + params 11 + |> Map.take([:actor]) 12 + |> Atproto.ensure_required([:actor]) 13 + |> Atproto.query("sh.comet.v0.actor.getProfile", opts) 14 + end 15 + end
+15
apps/backend/lib/atproto/sh/comet/v0/actor/getProfiles/xrpc.ex
··· 1 + defmodule Sh.Comet.V0.Actor.GetProfiles do 2 + 3 + @doc """ 4 + Get the profile views of multiple actors. 5 + """ 6 + @spec main(%{ 7 + actors: list(String.t()) 8 + }, Atproto.xrpc_opts()) :: {:ok, %{profiles: list(Sh.Comet.V0.Actor.Profile.View.t())}} | {:error, any} 9 + def main(params \\ %{}, opts \\ []) do 10 + params 11 + |> Map.take([:actors]) 12 + |> Atproto.ensure_required([:actors]) 13 + |> Atproto.query("sh.comet.v0.actor.getProfiles", opts) 14 + end 15 + end
+30
apps/backend/lib/atproto/sh/comet/v0/actor/profile/schema.ex
··· 1 + defmodule Sh.Comet.V0.Actor.Profile do 2 + use Ecto.Schema 3 + import Ecto.Changeset 4 + 5 + @doc """ 6 + A user's Comet profile. 7 + """ 8 + @primary_key {:id, :binary_id, autogenerate: false} 9 + schema "sh.comet.v0.actor.profile" do 10 + field :avatar, :map 11 + field :banner, :map 12 + field :createdAt, :utc_datetime 13 + field :description, :string 14 + field :descriptionFacets, :map 15 + field :displayName, :string 16 + field :featuredItems, {:array, :string} 17 + 18 + # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. 19 + # Ensure that you do not change this field via manual manipulation or changeset operations. 20 + field :"$type", :string, default: "sh.comet.v0.actor.profile" 21 + end 22 + 23 + def new(params \\ %{}), do: changeset(%__MODULE__{}, params) 24 + 25 + def changeset(struct, params \\ %{}) do 26 + struct 27 + |> cast(params, [:avatar, :banner, :createdAt, :description, :descriptionFacets, :displayName, :featuredItems]) 28 + |> validate_length(:featuredItems, max: 5) 29 + end 30 + end
+104
apps/backend/lib/atproto/sh/comet/v0/actor/profile/structs.ex
··· 1 + 2 + defmodule Sh.Comet.V0.Actor.Profile.ViewerState do 3 + @moduledoc """ 4 + Metadata about the requesting account's relationship with the user. TODO: determine if we create our own graph or inherit bsky's. 5 + """ 6 + 7 + @derive Jason.Encoder 8 + defstruct [ 9 + 10 + ] 11 + 12 + @type t() :: %__MODULE__{ 13 + 14 + } 15 + 16 + @spec new() :: t() 17 + def new(), do: %__MODULE__{} 18 + 19 + @spec from(binary() | map()) :: t() 20 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 21 + end 22 + 23 + defmodule Sh.Comet.V0.Actor.Profile.ViewFull do 24 + @moduledoc """ 25 + 26 + """ 27 + 28 + @derive Jason.Encoder 29 + defstruct [ 30 + avatar: nil, 31 + banner: nil, 32 + createdAt: nil, 33 + description: nil, 34 + descriptionFacets: nil, 35 + did: nil, 36 + displayName: nil, 37 + featuredItems: [], 38 + followersCount: 0, 39 + followsCount: 0, 40 + handle: nil, 41 + indexedAt: nil, 42 + playlistsCount: 0, 43 + tracksCount: 0, 44 + viewer: nil 45 + ] 46 + 47 + @type t() :: %__MODULE__{ 48 + avatar: String.t(), 49 + banner: String.t(), 50 + createdAt: DateTime.t(), 51 + description: String.t(), 52 + descriptionFacets: Sh.Comet.V0.Richtext.Facet.Main.t(), 53 + did: String.t(), 54 + displayName: String.t(), 55 + featuredItems: list(String.t()), 56 + followersCount: integer, 57 + followsCount: integer, 58 + handle: String.t(), 59 + indexedAt: DateTime.t(), 60 + playlistsCount: integer, 61 + tracksCount: integer, 62 + viewer: Sh.Comet.V0.Actor.Profile.ViewerState.t() 63 + } 64 + 65 + @spec new() :: t() 66 + def new(), do: %__MODULE__{} 67 + 68 + @spec from(binary() | map()) :: t() 69 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 70 + end 71 + 72 + defmodule Sh.Comet.V0.Actor.Profile.View do 73 + @moduledoc """ 74 + 75 + """ 76 + 77 + @derive Jason.Encoder 78 + defstruct [ 79 + avatar: nil, 80 + createdAt: nil, 81 + did: nil, 82 + displayName: nil, 83 + handle: nil, 84 + indexedAt: nil, 85 + viewer: nil 86 + ] 87 + 88 + @type t() :: %__MODULE__{ 89 + avatar: String.t(), 90 + createdAt: DateTime.t(), 91 + did: String.t(), 92 + displayName: String.t(), 93 + handle: String.t(), 94 + indexedAt: DateTime.t(), 95 + viewer: Sh.Comet.V0.Actor.Profile.ViewerState.t() 96 + } 97 + 98 + @spec new() :: t() 99 + def new(), do: %__MODULE__{} 100 + 101 + @spec from(binary() | map()) :: t() 102 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 103 + end 104 +
+30
apps/backend/lib/atproto/sh/comet/v0/feed/comment/schema.ex
··· 1 + defmodule Sh.Comet.V0.Feed.Comment do 2 + use Ecto.Schema 3 + import Ecto.Changeset 4 + 5 + @doc """ 6 + A comment on a piece of Comet media. 7 + """ 8 + @primary_key {:id, :id, autogenerate: false} 9 + schema "sh.comet.v0.feed.comment" do 10 + field :createdAt, :utc_datetime 11 + field :facets, {:array, :map} 12 + field :langs, {:array, :string} 13 + field :reply, :string 14 + field :subject, :string 15 + field :text, :string 16 + 17 + # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. 18 + # Ensure that you do not change this field via manual manipulation or changeset operations. 19 + field :"$type", :string, default: "sh.comet.v0.feed.comment" 20 + end 21 + 22 + def new(params \\ %{}), do: changeset(%__MODULE__{}, params) 23 + 24 + def changeset(struct, params \\ %{}) do 25 + struct 26 + |> cast(params, [:createdAt, :facets, :langs, :reply, :subject, :text]) 27 + |> validate_required([:createdAt, :subject, :text]) 28 + |> validate_length(:langs, max: 3) 29 + end 30 + end
+49
apps/backend/lib/atproto/sh/comet/v0/feed/defs/structs.ex
··· 1 + 2 + defmodule Sh.Comet.V0.Feed.Defs.ViewerState do 3 + @moduledoc """ 4 + Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests. 5 + """ 6 + 7 + @derive Jason.Encoder 8 + defstruct [ 9 + featured: false, 10 + like: nil, 11 + repost: nil 12 + ] 13 + 14 + @type t() :: %__MODULE__{ 15 + featured: boolean, 16 + like: String.t(), 17 + repost: String.t() 18 + } 19 + 20 + @spec new() :: t() 21 + def new(), do: %__MODULE__{} 22 + 23 + @spec from(binary() | map()) :: t() 24 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 25 + end 26 + 27 + defmodule Sh.Comet.V0.Feed.Defs.Link do 28 + @moduledoc """ 29 + Link for the track. Usually to acquire it in some way, e.g. via free download or purchase. | TODO: multiple links? 30 + """ 31 + 32 + @derive Jason.Encoder 33 + defstruct [ 34 + type: nil, 35 + value: nil 36 + ] 37 + 38 + @type t() :: %__MODULE__{ 39 + type: String.t(), 40 + value: String.t() 41 + } 42 + 43 + @spec new() :: t() 44 + def new(), do: %__MODULE__{} 45 + 46 + @spec from(binary() | map()) :: t() 47 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 48 + end 49 +
+17
apps/backend/lib/atproto/sh/comet/v0/feed/getActorPlaylists/xrpc.ex
··· 1 + defmodule Sh.Comet.V0.Feed.GetActorPlaylists do 2 + 3 + @doc """ 4 + Get a list of an actor's playlists. 5 + """ 6 + @spec main(%{ 7 + actor: String.t(), 8 + cursor: String.t(), 9 + limit: integer 10 + }, Atproto.xrpc_opts()) :: {:ok, %{cursor: String.t(), playlists: list(Sh.Comet.V0.Feed.Playlist.View.t())}} | {:error, any} 11 + def main(params \\ %{}, opts \\ []) do 12 + params 13 + |> Map.take([:actor, :cursor, :limit]) 14 + |> Atproto.ensure_required([:actor]) 15 + |> Atproto.query("sh.comet.v0.feed.getActorPlaylists", opts) 16 + end 17 + end
+17
apps/backend/lib/atproto/sh/comet/v0/feed/getActorTracks/xrpc.ex
··· 1 + defmodule Sh.Comet.V0.Feed.GetActorTracks do 2 + 3 + @doc """ 4 + Get a list of an actor's tracks. 5 + """ 6 + @spec main(%{ 7 + actor: String.t(), 8 + cursor: String.t(), 9 + limit: integer 10 + }, Atproto.xrpc_opts()) :: {:ok, %{cursor: String.t(), tracks: list(Sh.Comet.V0.Feed.Track.View.t())}} | {:error, any} 11 + def main(params \\ %{}, opts \\ []) do 12 + params 13 + |> Map.take([:actor, :cursor, :limit]) 14 + |> Atproto.ensure_required([:actor]) 15 + |> Atproto.query("sh.comet.v0.feed.getActorTracks", opts) 16 + end 17 + end
+25
apps/backend/lib/atproto/sh/comet/v0/feed/like/schema.ex
··· 1 + defmodule Sh.Comet.V0.Feed.Like do 2 + use Ecto.Schema 3 + import Ecto.Changeset 4 + 5 + @doc """ 6 + Record representing a 'like' of some media. Weakly linked with just an at-uri. 7 + """ 8 + @primary_key {:id, :id, autogenerate: false} 9 + schema "sh.comet.v0.feed.like" do 10 + field :createdAt, :utc_datetime 11 + field :subject, :string 12 + 13 + # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. 14 + # Ensure that you do not change this field via manual manipulation or changeset operations. 15 + field :"$type", :string, default: "sh.comet.v0.feed.like" 16 + end 17 + 18 + def new(params \\ %{}), do: changeset(%__MODULE__{}, params) 19 + 20 + def changeset(struct, params \\ %{}) do 21 + struct 22 + |> cast(params, [:createdAt, :subject]) 23 + |> validate_required([:createdAt, :subject]) 24 + end 25 + end
+25
apps/backend/lib/atproto/sh/comet/v0/feed/play/schema.ex
··· 1 + defmodule Sh.Comet.V0.Feed.Play do 2 + use Ecto.Schema 3 + import Ecto.Changeset 4 + 5 + @doc """ 6 + Record representing a 'play' of some media. 7 + """ 8 + @primary_key {:id, :id, autogenerate: false} 9 + schema "sh.comet.v0.feed.play" do 10 + field :createdAt, :utc_datetime 11 + field :subject, :string 12 + 13 + # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. 14 + # Ensure that you do not change this field via manual manipulation or changeset operations. 15 + field :"$type", :string, default: "sh.comet.v0.feed.play" 16 + end 17 + 18 + def new(params \\ %{}), do: changeset(%__MODULE__{}, params) 19 + 20 + def changeset(struct, params \\ %{}) do 21 + struct 22 + |> cast(params, [:createdAt, :subject]) 23 + |> validate_required([:createdAt, :subject]) 24 + end 25 + end
+32
apps/backend/lib/atproto/sh/comet/v0/feed/playlist/schema.ex
··· 1 + defmodule Sh.Comet.V0.Feed.Playlist do 2 + use Ecto.Schema 3 + import Ecto.Changeset 4 + 5 + @doc """ 6 + A Comet playlist, containing many audio tracks. 7 + """ 8 + @primary_key {:id, :id, autogenerate: false} 9 + schema "sh.comet.v0.feed.playlist" do 10 + field :createdAt, :utc_datetime 11 + field :description, :string 12 + field :descriptionFacets, :map 13 + field :image, :map 14 + field :link, :map 15 + field :tags, {:array, :string} 16 + field :title, :string 17 + field :type, :string 18 + 19 + # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. 20 + # Ensure that you do not change this field via manual manipulation or changeset operations. 21 + field :"$type", :string, default: "sh.comet.v0.feed.playlist" 22 + end 23 + 24 + def new(params \\ %{}), do: changeset(%__MODULE__{}, params) 25 + 26 + def changeset(struct, params \\ %{}) do 27 + struct 28 + |> cast(params, [:createdAt, :description, :descriptionFacets, :image, :link, :tags, :title, :type]) 29 + |> validate_required([:createdAt, :title, :type]) 30 + |> validate_length(:tags, max: 8) 31 + end 32 + end
+44
apps/backend/lib/atproto/sh/comet/v0/feed/playlist/structs.ex
··· 1 + 2 + defmodule Sh.Comet.V0.Feed.Playlist.View do 3 + @moduledoc """ 4 + 5 + """ 6 + 7 + @derive Jason.Encoder 8 + defstruct [ 9 + author: nil, 10 + cid: nil, 11 + commentCount: 0, 12 + image: nil, 13 + indexedAt: nil, 14 + likeCount: 0, 15 + record: nil, 16 + repostCount: 0, 17 + trackCount: 0, 18 + tracks: [], 19 + uri: nil, 20 + viewer: nil 21 + ] 22 + 23 + @type t() :: %__MODULE__{ 24 + author: Sh.Comet.V0.Actor.Profile.ViewFull.t(), 25 + cid: String.t(), 26 + commentCount: integer, 27 + image: String.t(), 28 + indexedAt: DateTime.t(), 29 + likeCount: integer, 30 + record: Sh.Comet.V0.Feed.Playlist.Main.t(), 31 + repostCount: integer, 32 + trackCount: integer, 33 + tracks: list(Sh.Comet.V0.Feed.Track.View.t()), 34 + uri: String.t(), 35 + viewer: Sh.Comet.V0.Feed.Defs.ViewerState.t() 36 + } 37 + 38 + @spec new() :: t() 39 + def new(), do: %__MODULE__{} 40 + 41 + @spec from(binary() | map()) :: t() 42 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 43 + end 44 +
+27
apps/backend/lib/atproto/sh/comet/v0/feed/playlistTrack/schema.ex
··· 1 + defmodule Sh.Comet.V0.Feed.PlaylistTrack do 2 + use Ecto.Schema 3 + import Ecto.Changeset 4 + 5 + @doc """ 6 + A link between a Comet track and a playlist. 7 + """ 8 + @primary_key {:id, :id, autogenerate: false} 9 + schema "sh.comet.v0.feed.playlistTrack" do 10 + field :playlist, :string 11 + field :position, :integer 12 + field :track, :string 13 + 14 + # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. 15 + # Ensure that you do not change this field via manual manipulation or changeset operations. 16 + field :"$type", :string, default: "sh.comet.v0.feed.playlistTrack" 17 + end 18 + 19 + def new(params \\ %{}), do: changeset(%__MODULE__{}, params) 20 + 21 + def changeset(struct, params \\ %{}) do 22 + struct 23 + |> cast(params, [:playlist, :position, :track]) 24 + |> validate_required([:playlist, :position, :track]) 25 + |> validate_length(:position, min: 0) 26 + end 27 + end
+25
apps/backend/lib/atproto/sh/comet/v0/feed/repost/schema.ex
··· 1 + defmodule Sh.Comet.V0.Feed.Repost do 2 + use Ecto.Schema 3 + import Ecto.Changeset 4 + 5 + @doc """ 6 + Record representing a 'repost' of some media. Weakly linked with just an at-uri. 7 + """ 8 + @primary_key {:id, :id, autogenerate: false} 9 + schema "sh.comet.v0.feed.repost" do 10 + field :createdAt, :utc_datetime 11 + field :subject, :string 12 + 13 + # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. 14 + # Ensure that you do not change this field via manual manipulation or changeset operations. 15 + field :"$type", :string, default: "sh.comet.v0.feed.repost" 16 + end 17 + 18 + def new(params \\ %{}), do: changeset(%__MODULE__{}, params) 19 + 20 + def changeset(struct, params \\ %{}) do 21 + struct 22 + |> cast(params, [:createdAt, :subject]) 23 + |> validate_required([:createdAt, :subject]) 24 + end 25 + end
+32
apps/backend/lib/atproto/sh/comet/v0/feed/track/schema.ex
··· 1 + defmodule Sh.Comet.V0.Feed.Track do 2 + use Ecto.Schema 3 + import Ecto.Changeset 4 + 5 + @doc """ 6 + A Comet audio track. TODO: should probably have some sort of pre-calculated waveform, or have a query to get one from a blob? 7 + """ 8 + @primary_key {:id, :id, autogenerate: false} 9 + schema "sh.comet.v0.feed.track" do 10 + field :audio, :map 11 + field :createdAt, :utc_datetime 12 + field :description, :string 13 + field :descriptionFacets, :map 14 + field :image, :map 15 + field :link, :map 16 + field :tags, {:array, :string} 17 + field :title, :string 18 + 19 + # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. 20 + # Ensure that you do not change this field via manual manipulation or changeset operations. 21 + field :"$type", :string, default: "sh.comet.v0.feed.track" 22 + end 23 + 24 + def new(params \\ %{}), do: changeset(%__MODULE__{}, params) 25 + 26 + def changeset(struct, params \\ %{}) do 27 + struct 28 + |> cast(params, [:audio, :createdAt, :description, :descriptionFacets, :image, :link, :tags, :title]) 29 + |> validate_required([:audio, :createdAt, :title]) 30 + |> validate_length(:tags, max: 8) 31 + end 32 + end
+44
apps/backend/lib/atproto/sh/comet/v0/feed/track/structs.ex
··· 1 + 2 + defmodule Sh.Comet.V0.Feed.Track.View do 3 + @moduledoc """ 4 + 5 + """ 6 + 7 + @derive Jason.Encoder 8 + defstruct [ 9 + audio: nil, 10 + author: nil, 11 + cid: nil, 12 + commentCount: 0, 13 + image: nil, 14 + indexedAt: nil, 15 + likeCount: 0, 16 + playCount: 0, 17 + record: nil, 18 + repostCount: 0, 19 + uri: nil, 20 + viewer: nil 21 + ] 22 + 23 + @type t() :: %__MODULE__{ 24 + audio: String.t(), 25 + author: Sh.Comet.V0.Actor.Profile.ViewFull.t(), 26 + cid: String.t(), 27 + commentCount: integer, 28 + image: String.t(), 29 + indexedAt: DateTime.t(), 30 + likeCount: integer, 31 + playCount: integer, 32 + record: Sh.Comet.V0.Feed.Track.Main.t(), 33 + repostCount: integer, 34 + uri: String.t(), 35 + viewer: Sh.Comet.V0.Feed.Defs.ViewerState.t() 36 + } 37 + 38 + @spec new() :: t() 39 + def new(), do: %__MODULE__{} 40 + 41 + @spec from(binary() | map()) :: t() 42 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 43 + end 44 +
+131
apps/backend/lib/atproto/sh/comet/v0/richtext/facet/structs.ex
··· 1 + 2 + defmodule Sh.Comet.V0.Richtext.Facet.Timestamp do 3 + @moduledoc """ 4 + Facet feature for a timestamp in a track. The text usually is in the format of 'hh:mm:ss' with the hour section being omitted if unnecessary. 5 + """ 6 + 7 + @derive Jason.Encoder 8 + defstruct [ 9 + timestamp: 0 10 + ] 11 + 12 + @type t() :: %__MODULE__{ 13 + timestamp: integer 14 + } 15 + 16 + @spec new() :: t() 17 + def new(), do: %__MODULE__{} 18 + 19 + @spec from(binary() | map()) :: t() 20 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 21 + end 22 + 23 + defmodule Sh.Comet.V0.Richtext.Facet.Tag do 24 + @moduledoc """ 25 + Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). 26 + """ 27 + 28 + @derive Jason.Encoder 29 + defstruct [ 30 + tag: nil 31 + ] 32 + 33 + @type t() :: %__MODULE__{ 34 + tag: String.t() 35 + } 36 + 37 + @spec new() :: t() 38 + def new(), do: %__MODULE__{} 39 + 40 + @spec from(binary() | map()) :: t() 41 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 42 + end 43 + 44 + defmodule Sh.Comet.V0.Richtext.Facet.Mention do 45 + @moduledoc """ 46 + Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. 47 + """ 48 + 49 + @derive Jason.Encoder 50 + defstruct [ 51 + did: nil 52 + ] 53 + 54 + @type t() :: %__MODULE__{ 55 + did: String.t() 56 + } 57 + 58 + @spec new() :: t() 59 + def new(), do: %__MODULE__{} 60 + 61 + @spec from(binary() | map()) :: t() 62 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 63 + end 64 + 65 + defmodule Sh.Comet.V0.Richtext.Facet.Main do 66 + @moduledoc """ 67 + Annotation of a sub-string within rich text. 68 + """ 69 + 70 + @derive Jason.Encoder 71 + defstruct [ 72 + features: [], 73 + index: nil 74 + ] 75 + 76 + @type t() :: %__MODULE__{ 77 + features: list(any), 78 + index: Sh.Comet.V0.Richtext.Facet.ByteSlice.t() 79 + } 80 + 81 + @spec new() :: t() 82 + def new(), do: %__MODULE__{} 83 + 84 + @spec from(binary() | map()) :: t() 85 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 86 + end 87 + 88 + defmodule Sh.Comet.V0.Richtext.Facet.Link do 89 + @moduledoc """ 90 + Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. 91 + """ 92 + 93 + @derive Jason.Encoder 94 + defstruct [ 95 + uri: nil 96 + ] 97 + 98 + @type t() :: %__MODULE__{ 99 + uri: String.t() 100 + } 101 + 102 + @spec new() :: t() 103 + def new(), do: %__MODULE__{} 104 + 105 + @spec from(binary() | map()) :: t() 106 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 107 + end 108 + 109 + defmodule Sh.Comet.V0.Richtext.Facet.ByteSlice do 110 + @moduledoc """ 111 + Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. 112 + """ 113 + 114 + @derive Jason.Encoder 115 + defstruct [ 116 + byteEnd: 0, 117 + byteStart: 0 118 + ] 119 + 120 + @type t() :: %__MODULE__{ 121 + byteEnd: integer, 122 + byteStart: integer 123 + } 124 + 125 + @spec new() :: t() 126 + def new(), do: %__MODULE__{} 127 + 128 + @spec from(binary() | map()) :: t() 129 + def from(json), do: Atproto.decode_to_struct(__MODULE__, json) 130 + end 131 +
+105
apps/backend/lib/atproto/tid.ex
··· 1 + defmodule Atproto.TID do 2 + @moduledoc """ 3 + A module for encoding and decoding TIDs. 4 + 5 + [TID](https://atproto.com/specs/tid) stands for "Timestamp Identifier". It is a 13-character string calculated from 53 bits representing a unix timestamp, in microsecond precision, plus 10 bits for an arbitrary "clock identifier", to help with uniqueness in distributed systems. 6 + 7 + The string is encoded as "base32-sortable", meaning that the characters for the base 32 encoding are set up in such a way that string comparisons yield the same result as integer comparisons, i.e. if the integer representation of the timestamp that creates TID "A" is greater than the integer representation of the timestamp that creates TID "B", then "A" > "B" is also true, and vice versa. 8 + """ 9 + 10 + import Bitwise 11 + 12 + @tid_char_set ~c(234567abcdefghijklmnopqrstuvwxyz) 13 + @tid_char_set_length 32 14 + 15 + defstruct [ 16 + :timestamp, 17 + :clock_id, 18 + :string 19 + ] 20 + 21 + @typedoc """ 22 + TIDs are composed of two parts: a timestamp and a clock identifier. They also have a human-readable string representation as a "base32-sortable" encoded string. 23 + """ 24 + @type t() :: %__MODULE__{ 25 + timestamp: integer(), 26 + clock_id: integer(), 27 + string: binary() 28 + } 29 + 30 + @doc """ 31 + Generates a random 10-bit clock identifier. 32 + """ 33 + @spec random_clock_id() :: integer() 34 + def random_clock_id(), do: :rand.uniform(1024) - 1 35 + 36 + @doc """ 37 + Generates a new TID for the current time. 38 + 39 + This is equivalent to calling `encode(nil)`. 40 + """ 41 + @spec new() :: t() 42 + def new(), do: encode(nil) 43 + 44 + @doc """ 45 + Encodes an integer or DateTime struct into a 13-character string that is "base32-sortable" encoded. 46 + 47 + If `timestamp` is nil, or not provided, the current time will be used as represented by `DateTime.utc_now()`. 48 + 49 + If `clock_id` is nil, or not provided, a random 10-bit integer will be used. 50 + 51 + If `timestamp` is an integer value, it *MUST* be a unix timestamp measured in microseconds. This function does not validate integer values. 52 + """ 53 + @spec encode(nil | integer() | DateTime.t(), nil | integer()) :: t() 54 + def encode(timestamp \\ nil, clock_id \\ nil) 55 + 56 + def encode(nil, clock_id), do: encode(DateTime.utc_now(), clock_id) 57 + 58 + def encode(timestamp, nil), do: encode(timestamp, random_clock_id()) 59 + 60 + def encode(%DateTime{} = datetime, clock_id) do 61 + datetime 62 + |> DateTime.to_unix(:microsecond) 63 + |> encode(clock_id) 64 + end 65 + 66 + def encode(timestamp, clock_id) when is_integer(timestamp) and is_integer(clock_id) do 67 + # Ensure we only use the lower 10 bit of clock_id 68 + clock_id = clock_id &&& 1023 69 + str = 70 + timestamp 71 + |> bsr(10) 72 + |> bsl(10) 73 + |> bxor(clock_id) 74 + |> do_encode("") 75 + %__MODULE__{timestamp: timestamp, clock_id: clock_id, string: str} 76 + end 77 + 78 + defp do_encode(0, acc), do: acc 79 + 80 + defp do_encode(number, acc) do 81 + c = rem(number, @tid_char_set_length) 82 + number = div(number, @tid_char_set_length) 83 + do_encode(number, <<Enum.at(@tid_char_set, c)>> <> acc) 84 + end 85 + 86 + @doc """ 87 + Decodes a binary string into a TID struct. 88 + """ 89 + @spec decode(binary()) :: t() 90 + def decode(tid) do 91 + num = do_decode(tid, 0) 92 + %__MODULE__{timestamp: bsr(num, 10), clock_id: num &&& 1023, string: tid} 93 + end 94 + 95 + defp do_decode(<<>>, acc), do: acc 96 + 97 + defp do_decode(<<char::utf8, rest::binary>>, acc) do 98 + idx = Enum.find_index(@tid_char_set, fn x -> x == char end) 99 + do_decode(rest, (acc * @tid_char_set_length) + idx) 100 + end 101 + end 102 + 103 + defimpl String.Chars, for: Atproto.TID do 104 + def to_string(tid), do: tid.string 105 + end
+9
apps/backend/lib/comet.ex
··· 1 + defmodule Comet do 2 + @moduledoc """ 3 + Comet keeps the contexts that define your domain 4 + and business logic. 5 + 6 + Contexts are also responsible for managing your data, regardless 7 + if it comes from the database, an external API or others. 8 + """ 9 + end
+34
apps/backend/lib/comet/application.ex
··· 1 + defmodule Comet.Application do 2 + # See https://hexdocs.pm/elixir/Application.html 3 + # for more information on OTP Applications 4 + @moduledoc false 5 + 6 + use Application 7 + 8 + @impl true 9 + def start(_type, _args) do 10 + children = [ 11 + CometWeb.Telemetry, 12 + Comet.Repo, 13 + {DNSCluster, query: Application.get_env(:comet, :dns_cluster_query) || :ignore}, 14 + {Phoenix.PubSub, name: Comet.PubSub}, 15 + # Start a worker by calling: Comet.Worker.start_link(arg) 16 + # {Comet.Worker, arg}, 17 + # Start to serve requests, typically the last entry 18 + CometWeb.Endpoint 19 + ] 20 + 21 + # See https://hexdocs.pm/elixir/Supervisor.html 22 + # for other strategies and supported options 23 + opts = [strategy: :one_for_one, name: Comet.Supervisor] 24 + Supervisor.start_link(children, opts) 25 + end 26 + 27 + # Tell Phoenix to update the endpoint configuration 28 + # whenever the application is updated. 29 + @impl true 30 + def config_change(changed, _new, removed) do 31 + CometWeb.Endpoint.config_change(changed, removed) 32 + :ok 33 + end 34 + end
+5
apps/backend/lib/comet/repo.ex
··· 1 + defmodule Comet.Repo do 2 + use Ecto.Repo, 3 + otp_app: :comet, 4 + adapter: Ecto.Adapters.Postgres 5 + end
+65
apps/backend/lib/comet_web.ex
··· 1 + defmodule CometWeb do 2 + @moduledoc """ 3 + The entrypoint for defining your web interface, such 4 + as controllers, components, channels, and so on. 5 + 6 + This can be used in your application as: 7 + 8 + use CometWeb, :controller 9 + use CometWeb, :html 10 + 11 + The definitions below will be executed for every controller, 12 + component, etc, so keep them short and clean, focused 13 + on imports, uses and aliases. 14 + 15 + Do NOT define functions inside the quoted expressions 16 + below. Instead, define additional modules and import 17 + those modules here. 18 + """ 19 + 20 + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 + 22 + def router do 23 + quote do 24 + use Phoenix.Router, helpers: false 25 + 26 + # Import common connection and controller functions to use in pipelines 27 + import Plug.Conn 28 + import Phoenix.Controller 29 + end 30 + end 31 + 32 + def channel do 33 + quote do 34 + use Phoenix.Channel 35 + end 36 + end 37 + 38 + def controller do 39 + quote do 40 + use Phoenix.Controller, 41 + formats: [:html, :json], 42 + layouts: [html: CometWeb.Layouts] 43 + 44 + import Plug.Conn 45 + 46 + unquote(verified_routes()) 47 + end 48 + end 49 + 50 + def verified_routes do 51 + quote do 52 + use Phoenix.VerifiedRoutes, 53 + endpoint: CometWeb.Endpoint, 54 + router: CometWeb.Router, 55 + statics: CometWeb.static_paths() 56 + end 57 + end 58 + 59 + @doc """ 60 + When used, dispatch to the appropriate controller/live_view/etc. 61 + """ 62 + defmacro __using__(which) when is_atom(which) do 63 + apply(__MODULE__, which, []) 64 + end 65 + end
+21
apps/backend/lib/comet_web/controllers/error_json.ex
··· 1 + defmodule CometWeb.ErrorJSON do 2 + @moduledoc """ 3 + This module is invoked by your endpoint in case of errors on JSON requests. 4 + 5 + See config/config.exs. 6 + """ 7 + 8 + # If you want to customize a particular status code, 9 + # you may add your own clauses, such as: 10 + # 11 + # def render("500.json", _assigns) do 12 + # %{errors: %{detail: "Internal Server Error"}} 13 + # end 14 + 15 + # By default, Phoenix returns the status message from 16 + # the template name. For example, "404.json" becomes 17 + # "Not Found". 18 + def render(template, _assigns) do 19 + %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 20 + end 21 + end
+51
apps/backend/lib/comet_web/endpoint.ex
··· 1 + defmodule CometWeb.Endpoint do 2 + use Phoenix.Endpoint, otp_app: :comet 3 + 4 + # The session will be stored in the cookie and signed, 5 + # this means its contents can be read but not tampered with. 6 + # Set :encryption_salt if you would also like to encrypt it. 7 + @session_options [ 8 + store: :cookie, 9 + key: "_comet_key", 10 + signing_salt: "zgKytneJ", 11 + same_site: "Lax" 12 + ] 13 + 14 + socket "/live", Phoenix.LiveView.Socket, 15 + websocket: [connect_info: [session: @session_options]], 16 + longpoll: [connect_info: [session: @session_options]] 17 + 18 + # Serve at "/" the static files from "priv/static" directory. 19 + # 20 + # You should set gzip to true if you are running phx.digest 21 + # when deploying your static files in production. 22 + plug Plug.Static, 23 + at: "/", 24 + from: :comet, 25 + gzip: false, 26 + only: CometWeb.static_paths() 27 + 28 + # Code reloading can be explicitly enabled under the 29 + # :code_reloader configuration of your endpoint. 30 + if code_reloading? do 31 + plug Phoenix.CodeReloader 32 + plug Phoenix.Ecto.CheckRepoStatus, otp_app: :comet 33 + end 34 + 35 + plug Phoenix.LiveDashboard.RequestLogger, 36 + param_key: "request_logger", 37 + cookie_key: "request_logger" 38 + 39 + plug Plug.RequestId 40 + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 41 + 42 + plug Plug.Parsers, 43 + parsers: [:urlencoded, :multipart, :json], 44 + pass: ["*/*"], 45 + json_decoder: Phoenix.json_library() 46 + 47 + plug Plug.MethodOverride 48 + plug Plug.Head 49 + plug Plug.Session, @session_options 50 + plug CometWeb.Router 51 + end
+27
apps/backend/lib/comet_web/router.ex
··· 1 + defmodule CometWeb.Router do 2 + use CometWeb, :router 3 + 4 + pipeline :api do 5 + plug :accepts, ["json"] 6 + end 7 + 8 + scope "/api", CometWeb do 9 + pipe_through :api 10 + end 11 + 12 + # Enable LiveDashboard in development 13 + if Application.compile_env(:comet, :dev_routes) do 14 + # If you want to use the LiveDashboard in production, you should put 15 + # it behind authentication and allow only admins to access it. 16 + # If your application does not have an admins-only section yet, 17 + # you can use Plug.BasicAuth to set up some basic authentication 18 + # as long as you are also using SSL (which you should anyway). 19 + import Phoenix.LiveDashboard.Router 20 + 21 + scope "/dev" do 22 + pipe_through [:fetch_session, :protect_from_forgery] 23 + 24 + live_dashboard "/dashboard", metrics: CometWeb.Telemetry 25 + end 26 + end 27 + end
+93
apps/backend/lib/comet_web/telemetry.ex
··· 1 + defmodule CometWeb.Telemetry do 2 + use Supervisor 3 + import Telemetry.Metrics 4 + 5 + def start_link(arg) do 6 + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 + end 8 + 9 + @impl true 10 + def init(_arg) do 11 + children = [ 12 + # Telemetry poller will execute the given period measurements 13 + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 + # Add reporters as children of your supervision tree. 16 + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 + ] 18 + 19 + Supervisor.init(children, strategy: :one_for_one) 20 + end 21 + 22 + def metrics do 23 + [ 24 + # Phoenix Metrics 25 + summary("phoenix.endpoint.start.system_time", 26 + unit: {:native, :millisecond} 27 + ), 28 + summary("phoenix.endpoint.stop.duration", 29 + unit: {:native, :millisecond} 30 + ), 31 + summary("phoenix.router_dispatch.start.system_time", 32 + tags: [:route], 33 + unit: {:native, :millisecond} 34 + ), 35 + summary("phoenix.router_dispatch.exception.duration", 36 + tags: [:route], 37 + unit: {:native, :millisecond} 38 + ), 39 + summary("phoenix.router_dispatch.stop.duration", 40 + tags: [:route], 41 + unit: {:native, :millisecond} 42 + ), 43 + summary("phoenix.socket_connected.duration", 44 + unit: {:native, :millisecond} 45 + ), 46 + sum("phoenix.socket_drain.count"), 47 + summary("phoenix.channel_joined.duration", 48 + unit: {:native, :millisecond} 49 + ), 50 + summary("phoenix.channel_handled_in.duration", 51 + tags: [:event], 52 + unit: {:native, :millisecond} 53 + ), 54 + 55 + # Database Metrics 56 + summary("comet.repo.query.total_time", 57 + unit: {:native, :millisecond}, 58 + description: "The sum of the other measurements" 59 + ), 60 + summary("comet.repo.query.decode_time", 61 + unit: {:native, :millisecond}, 62 + description: "The time spent decoding the data received from the database" 63 + ), 64 + summary("comet.repo.query.query_time", 65 + unit: {:native, :millisecond}, 66 + description: "The time spent executing the query" 67 + ), 68 + summary("comet.repo.query.queue_time", 69 + unit: {:native, :millisecond}, 70 + description: "The time spent waiting for a database connection" 71 + ), 72 + summary("comet.repo.query.idle_time", 73 + unit: {:native, :millisecond}, 74 + description: 75 + "The time the connection spent waiting before being checked out for the query" 76 + ), 77 + 78 + # VM Metrics 79 + summary("vm.memory.total", unit: {:byte, :kilobyte}), 80 + summary("vm.total_run_queue_lengths.total"), 81 + summary("vm.total_run_queue_lengths.cpu"), 82 + summary("vm.total_run_queue_lengths.io") 83 + ] 84 + end 85 + 86 + defp periodic_measurements do 87 + [ 88 + # A module, function and arguments to be invoked periodically. 89 + # This function must call :telemetry.execute/3 and a metric must be added above. 90 + # {CometWeb, :count_users, []} 91 + ] 92 + end 93 + end
+68
apps/backend/mix.exs
··· 1 + defmodule Comet.MixProject do 2 + use Mix.Project 3 + 4 + def project do 5 + [ 6 + app: :comet, 7 + version: "0.1.0", 8 + elixir: "~> 1.14", 9 + elixirc_paths: elixirc_paths(Mix.env()), 10 + start_permanent: Mix.env() == :prod, 11 + aliases: aliases(), 12 + deps: deps() 13 + ] 14 + end 15 + 16 + # Configuration for the OTP application. 17 + # 18 + # Type `mix help compile.app` for more information. 19 + def application do 20 + [ 21 + mod: {Comet.Application, []}, 22 + extra_applications: [:logger, :runtime_tools] 23 + ] 24 + end 25 + 26 + # Specifies which paths to compile per environment. 27 + defp elixirc_paths(:test), do: ["lib", "test/support"] 28 + defp elixirc_paths(_), do: ["lib"] 29 + 30 + # Specifies your project dependencies. 31 + # 32 + # Type `mix help deps` for examples and options. 33 + defp deps do 34 + [ 35 + {:phoenix, "~> 1.7.21"}, 36 + {:phoenix_ecto, "~> 4.5"}, 37 + {:ecto_sql, "~> 3.10"}, 38 + {:postgrex, ">= 0.0.0"}, 39 + {:phoenix_live_dashboard, "~> 0.8.3"}, 40 + {:telemetry_metrics, "~> 1.0"}, 41 + {:telemetry_poller, "~> 1.0"}, 42 + {:jason, "~> 1.2"}, 43 + {:dns_cluster, "~> 0.1.1"}, 44 + {:bandit, "~> 1.5"}, 45 + {:lexgen, "~> 1.0.0", only: [:dev]}, 46 + {:req, "~> 0.5.0"}, 47 + {:typedstruct, "~> 0.5"} 48 + ] 49 + end 50 + 51 + # Aliases are shortcuts or tasks specific to the current project. 52 + # For example, to install project dependencies and perform other setup tasks, run: 53 + # 54 + # $ mix setup 55 + # 56 + # See the documentation for `Mix` for more info on aliases. 57 + defp aliases do 58 + lexicon_paths = Path.wildcard("../../packages/lexicons/defs/**/*.json") 59 + 60 + [ 61 + setup: ["deps.get", "ecto.setup"], 62 + "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 63 + "ecto.reset": ["ecto.drop", "ecto.setup"], 64 + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], 65 + "gen.lexicons": ["lexgen" | lexicon_paths] |> Enum.join(" ") 66 + ] 67 + end 68 + end
+35
apps/backend/mix.lock
··· 1 + %{ 2 + "bandit": {:hex, :bandit, "1.6.11", "2fbadd60c95310eefb4ba7f1e58810aa8956e18c664a3b2029d57edb7d28d410", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "543f3f06b4721619a1220bed743aa77bf7ecc9c093ba9fab9229ff6b99eacc65"}, 3 + "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, 4 + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, 5 + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 6 + "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, 7 + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, 8 + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, 9 + "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, 10 + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, 11 + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 12 + "lexgen": {:hex, :lexgen, "1.0.0", "1ca22ba00b86f9fa97718651b77b87a5965b8a9f71109ac2c11cb573f17499aa", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ff64e0e192645208e7ce1b6468037a6d4ebfb98a506ab15d30fb46ca492ec275"}, 13 + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 14 + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 15 + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 16 + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 17 + "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, 18 + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, 19 + "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, 20 + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, 21 + "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.12", "a37134b9bb3602efbfa5a7a8cb51d50e796f7acff7075af9d9796f30de04c66a", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "058e06e59fd38f1feeca59bbf167bec5d44aacd9b745e4363e2ac342ca32e546"}, 22 + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 23 + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 24 + "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, 25 + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, 26 + "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, 27 + "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, 28 + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 29 + "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, 30 + "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, 31 + "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"}, 32 + "typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"}, 33 + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 34 + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, 35 + }
+4
apps/backend/priv/repo/migrations/.formatter.exs
··· 1 + [ 2 + import_deps: [:ecto_sql], 3 + inputs: ["*.exs"] 4 + ]
+11
apps/backend/priv/repo/seeds.exs
··· 1 + # Script for populating the database. You can run it as: 2 + # 3 + # mix run priv/repo/seeds.exs 4 + # 5 + # Inside the script, you can read and write to any of your 6 + # repositories directly: 7 + # 8 + # Comet.Repo.insert!(%Comet.SomeSchema{}) 9 + # 10 + # We recommend using the bang functions (`insert!`, `update!` 11 + # and so on) as they will fail if something goes wrong.
apps/backend/priv/static/favicon.ico

This is a binary file and will not be displayed.

+5
apps/backend/priv/static/robots.txt
··· 1 + # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 + # 3 + # To ban all spiders from the entire site uncomment the next two lines: 4 + # User-agent: * 5 + # Disallow: /
+12
apps/backend/test/comet_web/controllers/error_json_test.exs
··· 1 + defmodule CometWeb.ErrorJSONTest do 2 + use CometWeb.ConnCase, async: true 3 + 4 + test "renders 404" do 5 + assert CometWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} 6 + end 7 + 8 + test "renders 500" do 9 + assert CometWeb.ErrorJSON.render("500.json", %{}) == 10 + %{errors: %{detail: "Internal Server Error"}} 11 + end 12 + end
+38
apps/backend/test/support/conn_case.ex
··· 1 + defmodule CometWeb.ConnCase do 2 + @moduledoc """ 3 + This module defines the test case to be used by 4 + tests that require setting up a connection. 5 + 6 + Such tests rely on `Phoenix.ConnTest` and also 7 + import other functionality to make it easier 8 + to build common data structures and query the data layer. 9 + 10 + Finally, if the test case interacts with the database, 11 + we enable the SQL sandbox, so changes done to the database 12 + are reverted at the end of every test. If you are using 13 + PostgreSQL, you can even run database tests asynchronously 14 + by setting `use CometWeb.ConnCase, async: true`, although 15 + this option is not recommended for other databases. 16 + """ 17 + 18 + use ExUnit.CaseTemplate 19 + 20 + using do 21 + quote do 22 + # The default endpoint for testing 23 + @endpoint CometWeb.Endpoint 24 + 25 + use CometWeb, :verified_routes 26 + 27 + # Import conveniences for testing with connections 28 + import Plug.Conn 29 + import Phoenix.ConnTest 30 + import CometWeb.ConnCase 31 + end 32 + end 33 + 34 + setup tags do 35 + Comet.DataCase.setup_sandbox(tags) 36 + {:ok, conn: Phoenix.ConnTest.build_conn()} 37 + end 38 + end
+58
apps/backend/test/support/data_case.ex
··· 1 + defmodule Comet.DataCase do 2 + @moduledoc """ 3 + This module defines the setup for tests requiring 4 + access to the application's data layer. 5 + 6 + You may define functions here to be used as helpers in 7 + your tests. 8 + 9 + Finally, if the test case interacts with the database, 10 + we enable the SQL sandbox, so changes done to the database 11 + are reverted at the end of every test. If you are using 12 + PostgreSQL, you can even run database tests asynchronously 13 + by setting `use Comet.DataCase, async: true`, although 14 + this option is not recommended for other databases. 15 + """ 16 + 17 + use ExUnit.CaseTemplate 18 + 19 + using do 20 + quote do 21 + alias Comet.Repo 22 + 23 + import Ecto 24 + import Ecto.Changeset 25 + import Ecto.Query 26 + import Comet.DataCase 27 + end 28 + end 29 + 30 + setup tags do 31 + Comet.DataCase.setup_sandbox(tags) 32 + :ok 33 + end 34 + 35 + @doc """ 36 + Sets up the sandbox based on the test tags. 37 + """ 38 + def setup_sandbox(tags) do 39 + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Comet.Repo, shared: not tags[:async]) 40 + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) 41 + end 42 + 43 + @doc """ 44 + A helper that transforms changeset errors into a map of messages. 45 + 46 + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 47 + assert "password is too short" in errors_on(changeset).password 48 + assert %{password: ["password is too short"]} = errors_on(changeset) 49 + 50 + """ 51 + def errors_on(changeset) do 52 + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 53 + Regex.replace(~r"%{(\w+)}", message, fn _, key -> 54 + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 55 + end) 56 + end) 57 + end 58 + end
+2
apps/backend/test/test_helper.exs
··· 1 + ExUnit.start() 2 + Ecto.Adapters.SQL.Sandbox.mode(Comet.Repo, :manual)
+1 -1
flake.nix
··· 15 15 in { 16 16 devShells = defaultForSystems (pkgs: 17 17 pkgs.mkShell { 18 - nativeBuildInputs = with pkgs; [nodejs_22 bun]; 18 + nativeBuildInputs = with pkgs; [nodejs_22 bun elixir erlang]; 19 19 }); 20 20 }; 21 21 }