Merge branch 'develop' into tests/mastodon_api_controller.ex
[akkoma] / lib / mix / tasks / pleroma / emoji.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 Mix.Tasks.Pleroma.Emoji do
6 use Mix.Task
7
8 @shortdoc "Manages emoji packs"
9 @moduledoc """
10 Manages emoji packs
11
12 ## ls-packs
13
14 mix pleroma.emoji ls-packs [OPTION...]
15
16 Lists the emoji packs and metadata specified in the manifest.
17
18 ### Options
19
20 - `-m, --manifest PATH/URL` - path to a custom manifest, it can
21 either be an URL starting with `http`, in that case the
22 manifest will be fetched from that address, or a local path
23
24 ## get-packs
25
26 mix pleroma.emoji get-packs [OPTION...] PACKS
27
28 Fetches, verifies and installs the specified PACKS from the
29 manifest into the `STATIC-DIR/emoji/PACK-NAME`
30
31 ### Options
32
33 - `-m, --manifest PATH/URL` - same as ls-packs
34
35 ## gen-pack
36
37 mix pleroma.emoji gen-pack PACK-URL
38
39 Creates a new manifest entry and a file list from the specified
40 remote pack file. Currently, only .zip archives are recognized
41 as remote pack files and packs are therefore assumed to be zip
42 archives. This command is intended to run interactively and will
43 first ask you some basic questions about the pack, then download
44 the remote file and generate an SHA256 checksum for it, then
45 generate an emoji file list for you.
46
47 The manifest entry will either be written to a newly created
48 `index.json` file or appended to the existing one, *replacing*
49 the old pack with the same name if it was in the file previously.
50
51 The file list will be written to the file specified previously,
52 *replacing* that file. You _should_ check that the file list doesn't
53 contain anything you don't need in the pack, that is, anything that is
54 not an emoji (the whole pack is downloaded, but only emoji files
55 are extracted).
56 """
57
58 def run(["ls-packs" | args]) do
59 Application.ensure_all_started(:hackney)
60
61 {options, [], []} = parse_global_opts(args)
62
63 manifest =
64 fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest())
65
66 Enum.each(manifest, fn {name, info} ->
67 to_print = [
68 {"Name", name},
69 {"Homepage", info["homepage"]},
70 {"Description", info["description"]},
71 {"License", info["license"]},
72 {"Source", info["src"]}
73 ]
74
75 for {param, value} <- to_print do
76 IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
77 end
78
79 # A newline
80 IO.puts("")
81 end)
82 end
83
84 def run(["get-packs" | args]) do
85 Application.ensure_all_started(:hackney)
86
87 {options, pack_names, []} = parse_global_opts(args)
88
89 manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest()
90
91 manifest = fetch_manifest(manifest_url)
92
93 for pack_name <- pack_names do
94 if Map.has_key?(manifest, pack_name) do
95 pack = manifest[pack_name]
96 src_url = pack["src"]
97
98 IO.puts(
99 IO.ANSI.format([
100 "Downloading ",
101 :bright,
102 pack_name,
103 :normal,
104 " from ",
105 :underline,
106 src_url
107 ])
108 )
109
110 binary_archive = Tesla.get!(client(), src_url).body
111 archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
112
113 sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
114
115 if archive_sha == String.upcase(pack["src_sha256"]) do
116 IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
117 else
118 IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))
119
120 raise "Bad SHA256 for #{pack_name}"
121 end
122
123 # The url specified in files should be in the same directory
124 files_url = Path.join(Path.dirname(manifest_url), pack["files"])
125
126 IO.puts(
127 IO.ANSI.format([
128 "Fetching the file list for ",
129 :bright,
130 pack_name,
131 :normal,
132 " from ",
133 :underline,
134 files_url
135 ])
136 )
137
138 files = Tesla.get!(client(), files_url).body |> Jason.decode!()
139
140 IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
141
142 pack_path =
143 Path.join([
144 Pleroma.Config.get!([:instance, :static_dir]),
145 "emoji",
146 pack_name
147 ])
148
149 files_to_unzip =
150 Enum.map(
151 files,
152 fn {_, f} -> to_charlist(f) end
153 )
154
155 {:ok, _} =
156 :zip.unzip(binary_archive,
157 cwd: pack_path,
158 file_list: files_to_unzip
159 )
160
161 IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
162
163 emoji_txt_str =
164 Enum.map(
165 files,
166 fn {shortcode, path} ->
167 emojo_path = Path.join("/emoji/#{pack_name}", path)
168 "#{shortcode}, #{emojo_path}"
169 end
170 )
171 |> Enum.join("\n")
172
173 File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
174 else
175 IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
176 end
177 end
178 end
179
180 def run(["gen-pack", src]) do
181 Application.ensure_all_started(:hackney)
182
183 proposed_name = Path.basename(src) |> Path.rootname()
184 name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
185 # If there's no name, use the default one
186 name = if String.length(name) > 0, do: name, else: proposed_name
187
188 license = String.trim(IO.gets("License: "))
189 homepage = String.trim(IO.gets("Homepage: "))
190 description = String.trim(IO.gets("Description: "))
191
192 proposed_files_name = "#{name}.json"
193 files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
194 files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
195
196 default_exts = [".png", ".gif"]
197 default_exts_str = Enum.join(default_exts, " ")
198
199 exts =
200 String.trim(
201 IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
202 )
203
204 exts =
205 if String.length(exts) > 0 do
206 String.split(exts, " ")
207 |> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
208 else
209 default_exts
210 end
211
212 IO.puts("Downloading the pack and generating SHA256")
213
214 binary_archive = Tesla.get!(client(), src).body
215 archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
216
217 IO.puts("SHA256 is #{archive_sha}")
218
219 pack_json = %{
220 name => %{
221 license: license,
222 homepage: homepage,
223 description: description,
224 src: src,
225 src_sha256: archive_sha,
226 files: files_name
227 }
228 }
229
230 tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
231
232 {:ok, _} =
233 :zip.unzip(
234 binary_archive,
235 cwd: tmp_pack_dir
236 )
237
238 emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
239
240 File.write!(files_name, Jason.encode!(emoji_map, pretty: true))
241
242 IO.puts("""
243
244 #{files_name} has been created and contains the list of all found emojis in the pack.
245 Please review the files in the remove those not needed.
246 """)
247
248 if File.exists?("index.json") do
249 existing_data = File.read!("index.json") |> Jason.decode!()
250
251 File.write!(
252 "index.json",
253 Jason.encode!(
254 Map.merge(
255 existing_data,
256 pack_json
257 ),
258 pretty: true
259 )
260 )
261
262 IO.puts("index.json file has been update with the #{name} pack")
263 else
264 File.write!("index.json", Jason.encode!(pack_json, pretty: true))
265
266 IO.puts("index.json has been created with the #{name} pack")
267 end
268 end
269
270 defp fetch_manifest(from) do
271 Jason.decode!(
272 if String.starts_with?(from, "http") do
273 Tesla.get!(client(), from).body
274 else
275 File.read!(from)
276 end
277 )
278 end
279
280 defp parse_global_opts(args) do
281 OptionParser.parse(
282 args,
283 strict: [
284 manifest: :string
285 ],
286 aliases: [
287 m: :manifest
288 ]
289 )
290 end
291
292 defp client do
293 middleware = [
294 {Tesla.Middleware.FollowRedirects, [max_redirects: 3]}
295 ]
296
297 Tesla.client(middleware)
298 end
299
300 defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])
301 end