defmodule Pleroma.FlakeId do @moduledoc """ Flake is a decentralized, k-ordered id generation service. Adapted from: * [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License, * [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0 """ @type t :: binary @behaviour Ecto.Type use GenServer require Logger alias __MODULE__ import Kernel, except: [to_string: 1] defstruct node: nil, time: 0, sq: 0 @doc "Converts a binary Flake to a String" def to_string(<<0::integer-size(64), id::integer-size(64)>>) do Kernel.to_string(id) end def to_string(flake = <<_::integer-size(64), _::integer-size(48), _::integer-size(16)>>) do encode_base62(flake) end def to_string(s), do: s def from_string(<>) do <<0::integer-size(64), id::integer-size(64)>> end for i <- [-1, 0] do def from_string(unquote(i)), do: <<0::integer-size(128)>> def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>> end def from_string(string) when is_binary(string) and byte_size(string) < 18 do case Integer.parse(string) do {id, _} -> <<0::integer-size(64), id::integer-size(64)>> _ -> nil end end def from_string(string) do string |> decode_base62 |> from_integer end def to_integer(<>), do: integer def from_integer(integer) do <<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> = <> end @doc "Generates a Flake" @spec get :: binary def get, do: to_string(:gen_server.call(:flake, :get)) # -- Ecto.Type API @impl Ecto.Type def type, do: :uuid @impl Ecto.Type def cast(value) do {:ok, FlakeId.to_string(value)} end @impl Ecto.Type def load(value) do {:ok, FlakeId.to_string(value)} end @impl Ecto.Type def dump(value) do {:ok, FlakeId.from_string(value)} end def autogenerate(), do: get() # -- GenServer API def start_link do :gen_server.start_link({:local, :flake}, __MODULE__, [], []) end @impl GenServer def init([]) do {:ok, %FlakeId{node: mac(), time: time()}} end @impl GenServer def handle_call(:get, _from, state) do {flake, new_state} = get(time(), state) {:reply, flake, new_state} end # Matches when the calling time is the same as the state time. Incr. sq defp get(time, %FlakeId{time: time, node: node, sq: seq}) do new_state = %FlakeId{time: time, node: node, sq: seq + 1} {gen_flake(new_state), new_state} end # Matches when the times are different, reset sq defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do new_state = %FlakeId{time: newtime, node: node, sq: 0} {gen_flake(new_state), new_state} end # Error when clock is running backwards defp get(newtime, %FlakeId{time: time}) when newtime < time do {:error, :clock_running_backwards} end defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do <> end defp nthchar_base62(n) when n <= 9, do: ?0 + n defp nthchar_base62(n) when n <= 35, do: ?A + n - 10 defp nthchar_base62(n), do: ?a + n - 36 defp encode_base62(<>) do integer |> encode_base62([]) |> List.to_string() end defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc) defp encode_base62(int, []) when int == 0, do: '0' defp encode_base62(int, acc) when int == 0, do: acc defp encode_base62(int, acc) do r = rem(int, 62) id = div(int, 62) acc = [nthchar_base62(r) | acc] encode_base62(id, acc) end defp decode_base62(s) do decode_base62(String.to_charlist(s), 0) end defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9, do: decode_base62(cs, 62 * acc + (c - ?0)) defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z, do: decode_base62(cs, 62 * acc + (c - ?A + 10)) defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z, do: decode_base62(cs, 62 * acc + (c - ?a + 36)) defp decode_base62([], acc), do: acc defp time do {mega_seconds, seconds, micro_seconds} = :erlang.timestamp() 1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000) end defp mac do {:ok, addresses} = :inet.getifaddrs() ifaces_with_mac = Enum.reduce(addresses, [], fn {iface, attrs}, acc -> if attrs[:hwaddr], do: [iface | acc], else: acc end) iface = Enum.at(ifaces_with_mac, :rand.uniform(length(ifaces_with_mac)) - 1) mac(iface) end defp mac(name) do {:ok, addresses} = :inet.getifaddrs() proplist = :proplists.get_value(name, addresses) hwaddr = Enum.take(:proplists.get_value(:hwaddr, proplist), 6) <> = :binary.list_to_bin(hwaddr) worker end end