First
[anni] / lib / mix / tasks / pleroma / emoji.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Mix.Tasks.Pleroma.Emoji do
6   use Mix.Task
7   import Mix.Pleroma
8
9   @shortdoc "Manages emoji packs"
10   @moduledoc File.read!("docs/administration/CLI_tasks/emoji.md")
11
12   def run(["ls-packs" | args]) do
13     start_pleroma()
14
15     {options, [], []} = parse_global_opts(args)
16
17     url_or_path = options[:manifest] || default_manifest()
18     manifest = fetch_and_decode!(url_or_path)
19
20     Enum.each(manifest, fn {name, info} ->
21       to_print = [
22         {"Name", name},
23         {"Homepage", info["homepage"]},
24         {"Description", info["description"]},
25         {"License", info["license"]},
26         {"Source", info["src"]}
27       ]
28
29       for {param, value} <- to_print do
30         IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
31       end
32
33       # A newline
34       IO.puts("")
35     end)
36   end
37
38   def run(["get-packs" | args]) do
39     start_pleroma()
40
41     {options, pack_names, []} = parse_global_opts(args)
42
43     url_or_path = options[:manifest] || default_manifest()
44
45     manifest = fetch_and_decode!(url_or_path)
46
47     for pack_name <- pack_names do
48       if Map.has_key?(manifest, pack_name) do
49         pack = manifest[pack_name]
50         src = pack["src"]
51
52         IO.puts(
53           IO.ANSI.format([
54             "Downloading ",
55             :bright,
56             pack_name,
57             :normal,
58             " from ",
59             :underline,
60             src
61           ])
62         )
63
64         {:ok, binary_archive} = fetch(src)
65         archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
66
67         sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
68
69         if archive_sha == String.upcase(pack["src_sha256"]) do
70           IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
71         else
72           IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))
73
74           raise "Bad SHA256 for #{pack_name}"
75         end
76
77         # The location specified in files should be in the same directory
78         files_loc =
79           url_or_path
80           |> Path.dirname()
81           |> Path.join(pack["files"])
82
83         IO.puts(
84           IO.ANSI.format([
85             "Fetching the file list for ",
86             :bright,
87             pack_name,
88             :normal,
89             " from ",
90             :underline,
91             files_loc
92           ])
93         )
94
95         files = fetch_and_decode!(files_loc)
96
97         IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
98
99         pack_path =
100           Path.join([
101             Pleroma.Config.get!([:instance, :static_dir]),
102             "emoji",
103             pack_name
104           ])
105
106         files_to_unzip =
107           Enum.map(
108             files,
109             fn {_, f} -> to_charlist(f) end
110           )
111
112         {:ok, _} =
113           :zip.unzip(binary_archive,
114             cwd: pack_path,
115             file_list: files_to_unzip
116           )
117
118         IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name]))
119
120         pack_json = %{
121           pack: %{
122             "license" => pack["license"],
123             "homepage" => pack["homepage"],
124             "description" => pack["description"],
125             "fallback-src" => pack["src"],
126             "fallback-src-sha256" => pack["src_sha256"],
127             "share-files" => true
128           },
129           files: files
130         }
131
132         File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true))
133       else
134         IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
135       end
136     end
137   end
138
139   def run(["gen-pack" | args]) do
140     start_pleroma()
141
142     {opts, [src], []} =
143       OptionParser.parse(
144         args,
145         strict: [
146           name: :string,
147           license: :string,
148           homepage: :string,
149           description: :string,
150           files: :string,
151           extensions: :string
152         ]
153       )
154
155     proposed_name = Path.basename(src) |> Path.rootname()
156     name = get_option(opts, :name, "Pack name:", proposed_name)
157     license = get_option(opts, :license, "License:")
158     homepage = get_option(opts, :homepage, "Homepage:")
159     description = get_option(opts, :description, "Description:")
160
161     proposed_files_name = "#{name}_files.json"
162     files_name = get_option(opts, :files, "Save file list to:", proposed_files_name)
163
164     default_exts = [".png", ".gif"]
165
166     custom_exts =
167       get_option(
168         opts,
169         :extensions,
170         "Emoji file extensions (separated with spaces):",
171         Enum.join(default_exts, " ")
172       )
173       |> String.split(" ", trim: true)
174
175     exts =
176       if MapSet.equal?(MapSet.new(default_exts), MapSet.new(custom_exts)) do
177         default_exts
178       else
179         custom_exts
180       end
181
182     IO.puts("Using #{Enum.join(exts, " ")} extensions")
183
184     IO.puts("Downloading the pack and generating SHA256")
185
186     {:ok, %{body: binary_archive}} = Pleroma.HTTP.get(src)
187     archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
188
189     IO.puts("SHA256 is #{archive_sha}")
190
191     pack_json = %{
192       name => %{
193         license: license,
194         homepage: homepage,
195         description: description,
196         src: src,
197         src_sha256: archive_sha,
198         files: files_name
199       }
200     }
201
202     tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
203
204     {:ok, _} = :zip.unzip(binary_archive, cwd: String.to_charlist(tmp_pack_dir))
205
206     emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)
207
208     File.write!(files_name, Jason.encode!(emoji_map, pretty: true))
209
210     IO.puts("""
211
212     #{files_name} has been created and contains the list of all found emojis in the pack.
213     Please review the files in the pack and remove those not needed.
214     """)
215
216     pack_file = "#{name}.json"
217
218     if File.exists?(pack_file) do
219       existing_data = File.read!(pack_file) |> Jason.decode!()
220
221       File.write!(
222         pack_file,
223         Jason.encode!(
224           Map.merge(
225             existing_data,
226             pack_json
227           ),
228           pretty: true
229         )
230       )
231
232       IO.puts("#{pack_file} has been updated with the #{name} pack")
233     else
234       File.write!(pack_file, Jason.encode!(pack_json, pretty: true))
235
236       IO.puts("#{pack_file} has been created with the #{name} pack")
237     end
238   end
239
240   def run(["reload"]) do
241     start_pleroma()
242     Pleroma.Emoji.reload()
243     IO.puts("Emoji packs have been reloaded.")
244   end
245
246   defp fetch_and_decode!(from) do
247     with {:ok, json} <- fetch(from) do
248       Jason.decode!(json)
249     else
250       {:error, error} -> raise "#{from} cannot be fetched. Error: #{error} occur."
251     end
252   end
253
254   defp fetch("http" <> _ = from) do
255     with {:ok, %{body: body}} <- Pleroma.HTTP.get(from) do
256       {:ok, body}
257     end
258   end
259
260   defp fetch(path), do: File.read(path)
261
262   defp parse_global_opts(args) do
263     OptionParser.parse(
264       args,
265       strict: [
266         manifest: :string
267       ],
268       aliases: [
269         m: :manifest
270       ]
271     )
272   end
273
274   defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])
275 end