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