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)