Mercurial > libervia-backend
comparison sat_frontends/jp/cmd_blog.py @ 4024:4941cd102f93
jp (blog): new `--attachment` argument to attach files or external data to a blog post
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 23 Mar 2023 15:43:48 +0100 |
parents | 570254d5a798 |
children | 524856bd7b19 |
comparison
equal
deleted
inserted
replaced
4023:78b5f356900c | 4024:4941cd102f93 |
---|---|
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 17 # You should have received a copy of the GNU Affero General Public License |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 | 20 |
21 import json | |
22 import sys | |
23 import os.path | |
24 import os | |
25 import tempfile | |
26 import subprocess | |
27 import asyncio | 21 import asyncio |
28 from asyncio.subprocess import DEVNULL | 22 from asyncio.subprocess import DEVNULL |
23 from configparser import NoOptionError, NoSectionError | |
24 import json | |
25 import os | |
26 import os.path | |
29 from pathlib import Path | 27 from pathlib import Path |
28 import re | |
29 import subprocess | |
30 import sys | |
31 import tempfile | |
32 from urllib.parse import urlparse | |
33 | |
34 from sat.core.i18n import _ | |
35 from sat.tools import config | |
36 from sat.tools.common import uri | |
37 from sat.tools.common import data_format | |
38 from sat.tools.common.ansi import ANSI as A | |
39 from sat_frontends.jp import common | |
40 from sat_frontends.jp.constants import Const as C | |
41 | |
30 from . import base, cmd_pubsub | 42 from . import base, cmd_pubsub |
31 from sat.core.i18n import _ | |
32 from sat_frontends.jp.constants import Const as C | |
33 from sat_frontends.jp import common | |
34 from sat.tools.common.ansi import ANSI as A | |
35 from sat.tools.common import uri | |
36 from sat.tools import config | |
37 from configparser import NoSectionError, NoOptionError | |
38 from sat.tools.common import data_format | |
39 | 43 |
40 __commands__ = ["Blog"] | 44 __commands__ = ["Blog"] |
41 | 45 |
42 SYNTAX_XHTML = "xhtml" | 46 SYNTAX_XHTML = "xhtml" |
43 # extensions to use with known syntaxes | 47 # extensions to use with known syntaxes |
82 "title", | 86 "title", |
83 "title_xhtml", | 87 "title_xhtml", |
84 "extra" | 88 "extra" |
85 ) | 89 ) |
86 OUTPUT_OPT_NO_HEADER = "no-header" | 90 OUTPUT_OPT_NO_HEADER = "no-header" |
91 RE_ATTACHMENT_METADATA = re.compile(r"^(?P<key>[a-z_]+)=(?P<value>.*)") | |
92 ALLOWER_ATTACH_MD_KEY = ("desc", "media_type", "external") | |
87 | 93 |
88 | 94 |
89 async def guessSyntaxFromPath(host, sat_conf, path): | 95 async def guessSyntaxFromPath(host, sat_conf, path): |
90 """Return syntax guessed according to filename extension | 96 """Return syntax guessed according to filename extension |
91 | 97 |
102 | 108 |
103 # if not found, we use current syntax | 109 # if not found, we use current syntax |
104 return await host.bridge.getParamA("Syntax", "Composition", "value", host.profile) | 110 return await host.bridge.getParamA("Syntax", "Composition", "value", host.profile) |
105 | 111 |
106 | 112 |
107 class BlogPublishCommon(object): | 113 class BlogPublishCommon: |
108 """handle common option for publising commands (Set and Edit)""" | 114 """handle common option for publising commands (Set and Edit)""" |
109 | 115 |
110 async def get_current_syntax(self): | 116 async def get_current_syntax(self): |
111 """Retrieve current_syntax | 117 """Retrieve current_syntax |
112 | 118 |
146 "-l", | 152 "-l", |
147 "--language", | 153 "--language", |
148 help=_("language of the item (ISO 639 code)"), | 154 help=_("language of the item (ISO 639 code)"), |
149 ) | 155 ) |
150 | 156 |
157 self.parser.add_argument( | |
158 "-a", | |
159 "--attachment", | |
160 dest="attachments", | |
161 nargs="+", | |
162 help=_( | |
163 "attachment in the form URL [metadata_name=value]" | |
164 ) | |
165 ) | |
166 | |
151 comments_group = self.parser.add_mutually_exclusive_group() | 167 comments_group = self.parser.add_mutually_exclusive_group() |
152 comments_group.add_argument( | 168 comments_group.add_argument( |
153 "-C", | 169 "-C", |
154 "--comments", | 170 "--comments", |
155 action="store_const", | 171 action="store_const", |
190 "--sign", | 206 "--sign", |
191 action="store_true", | 207 action="store_true", |
192 help=_("cryptographically sign the blog post") | 208 help=_("cryptographically sign the blog post") |
193 ) | 209 ) |
194 | 210 |
195 async def setMbDataContent(self, content, mb_data): | 211 async def set_mb_data_content(self, content, mb_data): |
196 if self.default_syntax_used: | 212 if self.default_syntax_used: |
197 # default syntax has been used | 213 # default syntax has been used |
198 mb_data["content_rich"] = content | 214 mb_data["content_rich"] = content |
199 elif self.current_syntax == SYNTAX_XHTML: | 215 elif self.current_syntax == SYNTAX_XHTML: |
200 mb_data["content_xhtml"] = content | 216 mb_data["content_xhtml"] = content |
201 else: | 217 else: |
202 mb_data["content_xhtml"] = await self.host.bridge.syntaxConvert( | 218 mb_data["content_xhtml"] = await self.host.bridge.syntaxConvert( |
203 content, self.current_syntax, SYNTAX_XHTML, False, self.profile | 219 content, self.current_syntax, SYNTAX_XHTML, False, self.profile |
204 ) | 220 ) |
205 | 221 |
206 def setMbDataFromArgs(self, mb_data): | 222 def handle_attachments(self, mb_data: dict) -> None: |
223 """Check, validate and add attachments to mb_data""" | |
224 if self.args.attachments: | |
225 attachments = [] | |
226 attachment = {} | |
227 for arg in self.args.attachments: | |
228 m = RE_ATTACHMENT_METADATA.match(arg) | |
229 if m is None: | |
230 # we should have an URL | |
231 url_parsed = urlparse(arg) | |
232 if url_parsed.scheme not in ("http", "https"): | |
233 self.parser.error( | |
234 "invalid URL in --attachment (only http(s) scheme is " | |
235 f" accepted): {arg}" | |
236 ) | |
237 if attachment: | |
238 # if we hae a new URL, we have a new attachment | |
239 attachments.append(attachment) | |
240 attachment = {} | |
241 attachment["url"] = arg | |
242 else: | |
243 # we should have a metadata | |
244 if "url" not in attachment: | |
245 self.parser.error( | |
246 "you must to specify an URL before any metadata in " | |
247 "--attachment" | |
248 ) | |
249 key = m.group("key") | |
250 if key not in ALLOWER_ATTACH_MD_KEY: | |
251 self.parser.error( | |
252 f"invalid metadata key in --attachment: {key!r}" | |
253 ) | |
254 value = m.group("value").strip() | |
255 if key == "external": | |
256 if not value: | |
257 value=True | |
258 else: | |
259 value = C.bool(value) | |
260 attachment[key] = value | |
261 if attachment: | |
262 attachments.append(attachment) | |
263 if attachments: | |
264 mb_data.setdefault("extra", {})["attachments"] = attachments | |
265 | |
266 def set_mb_data_from_args(self, mb_data): | |
207 """set microblog metadata according to command line options | 267 """set microblog metadata according to command line options |
208 | 268 |
209 if metadata already exist, it will be overwritten | 269 if metadata already exist, it will be overwritten |
210 """ | 270 """ |
211 if self.args.comments is not None: | 271 if self.args.comments is not None: |
220 mb_data["encrypted"] = True | 280 mb_data["encrypted"] = True |
221 if self.args.sign: | 281 if self.args.sign: |
222 mb_data["signed"] = True | 282 mb_data["signed"] = True |
223 if self.args.encrypt_for: | 283 if self.args.encrypt_for: |
224 mb_data["encrypted_for"] = {"targets": self.args.encrypt_for} | 284 mb_data["encrypted_for"] = {"targets": self.args.encrypt_for} |
285 self.handle_attachments(mb_data) | |
225 | 286 |
226 | 287 |
227 class Set(base.CommandBase, BlogPublishCommon): | 288 class Set(base.CommandBase, BlogPublishCommon): |
228 def __init__(self, host): | 289 def __init__(self, host): |
229 base.CommandBase.__init__( | 290 base.CommandBase.__init__( |
241 | 302 |
242 async def start(self): | 303 async def start(self): |
243 self.current_syntax = await self.get_current_syntax() | 304 self.current_syntax = await self.get_current_syntax() |
244 self.pubsub_item = self.args.item | 305 self.pubsub_item = self.args.item |
245 mb_data = {} | 306 mb_data = {} |
246 self.setMbDataFromArgs(mb_data) | 307 self.set_mb_data_from_args(mb_data) |
247 if self.pubsub_item: | 308 if self.pubsub_item: |
248 mb_data["id"] = self.pubsub_item | 309 mb_data["id"] = self.pubsub_item |
249 content = sys.stdin.read() | 310 content = sys.stdin.read() |
250 await self.setMbDataContent(content, mb_data) | 311 await self.set_mb_data_content(content, mb_data) |
251 | 312 |
252 try: | 313 try: |
253 item_id = await self.host.bridge.mbSend( | 314 item_id = await self.host.bridge.mbSend( |
254 self.args.service, | 315 self.args.service, |
255 self.args.node, | 316 self.args.node, |
531 try: | 592 try: |
532 del mb_data[key] | 593 del mb_data[key] |
533 except KeyError: | 594 except KeyError: |
534 pass | 595 pass |
535 # and override metadata with command-line arguments | 596 # and override metadata with command-line arguments |
536 self.setMbDataFromArgs(mb_data) | 597 self.set_mb_data_from_args(mb_data) |
537 | 598 |
538 if self.args.no_publish: | 599 if self.args.no_publish: |
539 mb_data["publish"] = False | 600 mb_data["publish"] = False |
540 | 601 |
541 # then we create the file and write metadata there, as JSON dict | 602 # then we create the file and write metadata there, as JSON dict |
595 ) | 656 ) |
596 | 657 |
597 await asyncio.gather(*coroutines) | 658 await asyncio.gather(*coroutines) |
598 | 659 |
599 async def publish(self, content, mb_data): | 660 async def publish(self, content, mb_data): |
600 await self.setMbDataContent(content, mb_data) | 661 await self.set_mb_data_content(content, mb_data) |
601 | 662 |
602 if self.pubsub_item: | 663 if self.pubsub_item: |
603 mb_data["id"] = self.pubsub_item | 664 mb_data["id"] = self.pubsub_item |
604 | 665 |
605 mb_data = data_format.serialise(mb_data) | 666 mb_data = data_format.serialise(mb_data) |