Flake Ids for Users and Activities
[akkoma] / lib / pleroma / flake_id.ex
1 defmodule Pleroma.FlakeId do
2 @moduledoc """
3 Flake is a decentralized, k-ordered id generation service.
4
5 Adapted from:
6
7 * [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License,
8 * [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0
9 """
10
11 @type t :: binary
12
13 @behaviour Ecto.Type
14 use GenServer
15 require Logger
16 alias __MODULE__
17 import Kernel, except: [to_string: 1]
18
19 defstruct node: nil, time: 0, sq: 0
20
21 @doc "Converts a binary Flake to a String"
22 def to_string(<<0::integer-size(64), id::integer-size(64)>>) do
23 Kernel.to_string(id)
24 end
25
26 def to_string(flake = <<_::integer-size(64), _::integer-size(48), _::integer-size(16)>>) do
27 encode_base62(flake)
28 end
29
30 def to_string(s), do: s
31
32 def from_string(<<id::integer-size(64)>>) do
33 <<0::integer-size(64), id::integer-size(64)>>
34 end
35
36 for i <- [-1, 0] do
37 def from_string(unquote(i)), do: <<0::integer-size(128)>>
38 def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
39 end
40
41 def from_string(string) when is_binary(string) and byte_size(string) < 18 do
42 case Integer.parse(string) do
43 {id, _} -> <<0::integer-size(64), id::integer-size(64)>>
44 _ -> nil
45 end
46 end
47
48 def from_string(string) do
49 string |> decode_base62 |> from_integer
50 end
51
52 def to_integer(<<integer::integer-size(128)>>), do: integer
53
54 def from_integer(integer) do
55 <<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> =
56 <<integer::integer-size(128)>>
57 end
58
59 @doc "Generates a Flake"
60 @spec get :: binary
61 def get, do: to_string(:gen_server.call(:flake, :get))
62
63 # -- Ecto.Type API
64 @impl Ecto.Type
65 def type, do: :uuid
66
67 @impl Ecto.Type
68 def cast(value) do
69 {:ok, FlakeId.to_string(value)}
70 end
71
72 @impl Ecto.Type
73 def load(value) do
74 {:ok, FlakeId.to_string(value)}
75 end
76
77 @impl Ecto.Type
78 def dump(value) do
79 {:ok, FlakeId.from_string(value)}
80 end
81
82 def autogenerate(), do: get()
83
84 # -- GenServer API
85 def start_link do
86 :gen_server.start_link({:local, :flake}, __MODULE__, [], [])
87 end
88
89 @impl GenServer
90 def init([]) do
91 {:ok, %FlakeId{node: mac(), time: time()}}
92 end
93
94 @impl GenServer
95 def handle_call(:get, _from, state) do
96 {flake, new_state} = get(time(), state)
97 {:reply, flake, new_state}
98 end
99
100 # Matches when the calling time is the same as the state time. Incr. sq
101 defp get(time, %FlakeId{time: time, node: node, sq: seq}) do
102 new_state = %FlakeId{time: time, node: node, sq: seq + 1}
103 {gen_flake(new_state), new_state}
104 end
105
106 # Matches when the times are different, reset sq
107 defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do
108 new_state = %FlakeId{time: newtime, node: node, sq: 0}
109 {gen_flake(new_state), new_state}
110 end
111
112 # Error when clock is running backwards
113 defp get(newtime, %FlakeId{time: time}) when newtime < time do
114 {:error, :clock_running_backwards}
115 end
116
117 defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do
118 <<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>>
119 end
120
121 defp nthchar_base62(n) when n <= 9, do: ?0 + n
122 defp nthchar_base62(n) when n <= 35, do: ?A + n - 10
123 defp nthchar_base62(n), do: ?a + n - 36
124
125 defp encode_base62(<<integer::integer-size(128)>>) do
126 integer
127 |> encode_base62([])
128 |> List.to_string()
129 end
130
131 defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc)
132 defp encode_base62(int, []) when int == 0, do: '0'
133 defp encode_base62(int, acc) when int == 0, do: acc
134
135 defp encode_base62(int, acc) do
136 r = rem(int, 62)
137 id = div(int, 62)
138 acc = [nthchar_base62(r) | acc]
139 encode_base62(id, acc)
140 end
141
142 defp decode_base62(s) do
143 decode_base62(String.to_charlist(s), 0)
144 end
145
146 defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9,
147 do: decode_base62(cs, 62 * acc + (c - ?0))
148
149 defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z,
150 do: decode_base62(cs, 62 * acc + (c - ?A + 10))
151
152 defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z,
153 do: decode_base62(cs, 62 * acc + (c - ?a + 36))
154
155 defp decode_base62([], acc), do: acc
156
157 defp time do
158 {mega_seconds, seconds, micro_seconds} = :erlang.timestamp()
159 1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
160 end
161
162 defp mac do
163 {:ok, addresses} = :inet.getifaddrs()
164
165 ifaces_with_mac =
166 Enum.reduce(addresses, [], fn {iface, attrs}, acc ->
167 if attrs[:hwaddr], do: [iface | acc], else: acc
168 end)
169
170 iface = Enum.at(ifaces_with_mac, :rand.uniform(length(ifaces_with_mac)) - 1)
171 mac(iface)
172 end
173
174 defp mac(name) do
175 {:ok, addresses} = :inet.getifaddrs()
176 proplist = :proplists.get_value(name, addresses)
177 hwaddr = Enum.take(:proplists.get_value(:hwaddr, proplist), 6)
178 <<worker::integer-size(48)>> = :binary.list_to_bin(hwaddr)
179 worker
180 end
181 end