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