Merge remote-tracking branch 'pleroma/develop' into feature/disable-account
[akkoma] / lib / mix / tasks / pleroma / emoji.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 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 @default_manifest Pleroma.Config.get!([:emoji, :default_manifest])
59
60 def run(["ls-packs" | args]) do
61 Application.ensure_all_started(:hackney)
62
63 {options, [], []} = parse_global_opts(args)
64
65 manifest =
66 fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest)
67
68 Enum.each(manifest, fn {name, info} ->
69 to_print = [
70 {"Name", name},
71 {"Homepage", info["homepage"]},
72 {"Description", info["description"]},
73 {"License", info["license"]},
74 {"Source", info["src"]}
75 ]
76
77 for {param, value} <- to_print do
78 IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
79 end
80
81 # A newline
82 IO.puts("")
83 end)
84 end
85
86 def run(["get-packs" | args]) do
87 Application.ensure_all_started(:hackney)
88
89 {options, pack_names, []} = parse_global_opts(args)
90
91 manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest
92
93 manifest = fetch_manifest(manifest_url)
94
95 for pack_name <- pack_names do
96 if Map.has_key?(manifest, pack_name) do
97 pack = manifest[pack_name]
98 src_url = pack["src"]
99
100 IO.puts(
101 IO.ANSI.format([
102 "Downloading ",
103 :bright,
104 pack_name,
105 :normal,
106 " from ",
107 :underline,
108 src_url
109 ])
110 )
111
112 binary_archive = Tesla.get!(client(), src_url).body
113 archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
114
115 sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
116
117 if archive_sha == String.upcase(pack["src_sha256"]) do
118 IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
119 else
120 IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))
121
122 raise "Bad SHA256 for #{pack_name}"
123 end
124
125 # The url specified in files should be in the same directory
126 files_url = Path.join(Path.dirname(manifest_url), pack["files"])
127
128 IO.puts(
129 IO.ANSI.format([
130 "Fetching the file list for ",
131 :bright,
132 pack_name,
133 :normal,
134 " from ",
135 :underline,
136 files_url
137 ])
138 )
139
140 files = Tesla.get!(client(), files_url).body |> Jason.decode!()
141
142 IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
143
144 pack_path =
145 Path.join([
146 Pleroma.Config.get!([:instance, :static_dir]),
147 "emoji",
148 pack_name
149 ])
150
151 files_to_unzip =
152 Enum.map(
153 files,
154 fn {_, f} -> to_charlist(f) end
155 )
156
157 {:ok, _} =
158 :zip.unzip(binary_archive,
159 cwd: pack_path,
160 file_list: files_to_unzip
161 )
162
163 IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
164
165 emoji_txt_str =
166 Enum.map(
167 files,
168 fn {shortcode, path} ->
169 emojo_path = Path.join("/emoji/#{pack_name}", path)
170 "#{shortcode}, #{emojo_path}"
171 end
172 )
173 |> Enum.join("\n")
174
175 File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
176 else
177 IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
178 end
179 end
180 end
181
182 def run(["gen-pack", src]) do
183 Application.ensure_all_started(:hackney)
184
185 proposed_name = Path.basename(src) |> Path.rootname()
186 name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
187 # If there's no name, use the default one
188 name = if String.length(name) > 0, do: name, else: proposed_name
189
190 license = String.trim(IO.gets("License: "))
191 homepage = String.trim(IO.gets("Homepage: "))
192 description = String.trim(IO.gets("Description: "))
193
194 proposed_files_name = "#{name}.json"
195 files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
196 files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
197
198 default_exts = [".png", ".gif"]
199 default_exts_str = Enum.join(default_exts, " ")
200
201 exts =
202 String.trim(
203 IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
204 )
205
206 exts =
207 if String.length(exts) > 0 do
208 String.split(exts, " ")
209 |> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
210 else
211 default_exts
212 end
213
214 IO.puts("Downloading the pack and generating SHA256")
215
216 binary_archive = Tesla.get!(client(), src).body
217 archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
218
219 IO.puts("SHA256 is #{archive_sha}")
220
221 pack_json = %{
222 name => %{
223 license: license,
224 homepage: homepage,
225 description: description,
226 src: src,
227 src_sha256: archive_sha,
228 files: files_name
229 }
230 }
231
232 tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
233
234 {:ok, _} =
235 :zip.unzip(
236 binary_archive,
237 cwd: tmp_pack_dir
238 )
239
240 emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
241
242 File.write!(files_name, Jason.encode!(emoji_map, pretty: true))
243
244 IO.puts("""
245
246 #{files_name} has been created and contains the list of all found emojis in the pack.
247 Please review the files in the remove those not needed.
248 """)
249
250 if File.exists?("index.json") do
251 existing_data = File.read!("index.json") |> Jason.decode!()
252
253 File.write!(
254 "index.json",
255 Jason.encode!(
256 Map.merge(
257 existing_data,
258 pack_json
259 ),
260 pretty: true
261 )
262 )
263
264 IO.puts("index.json file has been update with the #{name} pack")
265 else
266 File.write!("index.json", Jason.encode!(pack_json, pretty: true))
267
268 IO.puts("index.json has been created with the #{name} pack")
269 end
270 end
271
272 defp fetch_manifest(from) do
273 Jason.decode!(
274 if String.starts_with?(from, "http") do
275 Tesla.get!(client(), from).body
276 else
277 File.read!(from)
278 end
279 )
280 end
281
282 defp parse_global_opts(args) do
283 OptionParser.parse(
284 args,
285 strict: [
286 manifest: :string
287 ],
288 aliases: [
289 m: :manifest
290 ]
291 )
292 end
293
294 defp client do
295 middleware = [
296 {Tesla.Middleware.FollowRedirects, [max_redirects: 3]}
297 ]
298
299 Tesla.client(middleware)
300 end
301 end