"):
content = "
" + content + "
"
try:
from lxml import etree
except ImportError:
self.disp(_("You need lxml to edit pretty XHTML"))
else:
parser = etree.XMLParser(remove_blank_text=True)
root = etree.fromstring(content, parser)
content = etree.tostring(root, encoding=str, pretty_print=True)
return content, item, item["id"]
async def start(self):
# if there are user defined extension, we use them
SYNTAX_EXT.update(self.host.cli_settings.syntax_ext)
self.current_syntax = await self.get_current_syntax()
(
self.pubsub_service,
self.pubsub_node,
self.pubsub_item,
content_file_path,
content_file_obj,
mb_data,
) = await self.get_item_path()
await self.edit(content_file_path, content_file_obj, mb_data=mb_data)
self.host.quit()
class Rename(base.CommandBase):
def __init__(self, host):
base.CommandBase.__init__(
self,
host,
"rename",
use_pubsub=True,
pubsub_flags={C.SINGLE_ITEM},
help=_("rename an blog item"),
)
def add_parser_options(self):
self.parser.add_argument("new_id", help=_("new item id to use"))
async def start(self):
try:
await self.host.bridge.mb_rename(
self.args.service,
self.args.node,
self.args.item,
self.args.new_id,
self.profile,
)
except Exception as e:
self.disp(f"can't rename item: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp("Item renamed")
self.host.quit(C.EXIT_OK)
class Repeat(base.CommandBase):
def __init__(self, host):
super().__init__(
host,
"repeat",
use_pubsub=True,
pubsub_flags={C.SINGLE_ITEM},
help=_("repeat (re-publish) a blog item"),
)
def add_parser_options(self):
pass
async def start(self):
try:
repeat_id = await self.host.bridge.mb_repeat(
self.args.service,
self.args.node,
self.args.item,
"",
self.profile,
)
except Exception as e:
self.disp(f"can't repeat item: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
if repeat_id:
self.disp(f"Item repeated at ID {str(repeat_id)!r}")
else:
self.disp("Item repeated")
self.host.quit(C.EXIT_OK)
class Preview(base.CommandBase, common.BaseEdit):
# TODO: need to be rewritten with template output
def __init__(self, host):
base.CommandBase.__init__(
self, host, "preview", use_verbose=True, help=_("preview a blog content")
)
common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True)
def add_parser_options(self):
self.parser.add_argument(
"--inotify",
type=str,
choices=("auto", "true", "false"),
default="auto",
help=_("use inotify to handle preview"),
)
self.parser.add_argument(
"file",
nargs="?",
default="current",
help=_("path to the content file"),
)
async def show_preview(self):
# we implement show_preview here so we don't have to import webbrowser and urllib
# when preview is not used
url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path))
self.webbrowser.open_new_tab(url)
async def _launch_preview_ext(self, cmd_line, opt_name):
url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path))
args = common.parse_args(
self.host, cmd_line, url=url, preview_file=self.preview_file_path
)
if not args:
self.disp(
'Couln\'t find command in "{name}", abording'.format(name=opt_name),
error=True,
)
self.host.quit(1)
subprocess.Popen(args)
async def open_preview_ext(self):
await self._launch_preview_ext(self.open_cb_cmd, "blog_preview_open_cmd")
async def update_preview_ext(self):
await self._launch_preview_ext(self.update_cb_cmd, "blog_preview_update_cmd")
async def update_content(self):
with self.content_file_path.open("rb") as f:
content = f.read().decode("utf-8-sig")
if content and self.syntax != SYNTAX_XHTML:
# we use safe=True because we want to have a preview as close as possible
# to what the people will see
content = await self.host.bridge.syntax_convert(
content, self.syntax, SYNTAX_XHTML, True, self.profile
)
xhtml = (
f''
f'
'
f""
f"{content}"
f""
)
with open(self.preview_file_path, "wb") as f:
f.write(xhtml.encode("utf-8"))
async def start(self):
import webbrowser
import urllib.request, urllib.parse, urllib.error
self.webbrowser, self.urllib = webbrowser, urllib
if self.args.inotify != "false":
try:
import aionotify
except ImportError:
if self.args.inotify == "auto":
aionotify = None
self.disp(
f"aionotify module not found, deactivating feature. You can "
f"install it with {AIONOTIFY_INSTALL}"
)
else:
self.disp(
f"aioinotify not found, can't activate the feature! Please "
f"install it with {AIONOTIFY_INSTALL}",
error=True,
)
self.host.quit(1)
else:
aionotify = None
SYNTAX_EXT.update(self.host.cli_settings.syntax_ext)
self.open_cb_cmd = self.cli_settings.blog_preview_open_cmd
if self.open_cb_cmd is None:
open_cb = self.show_preview
else:
open_cb = self.open_preview_ext
self.update_cb_cmd = self.cli_settings.blog_preview_update_cmd
if self.update_cb_cmd is None:
update_cb = self.show_preview
else:
update_cb = self.update_preview_ext
# which file do we need to edit?
if self.args.file == "current":
self.content_file_path = self.get_current_file(self.profile)
else:
try:
self.content_file_path = Path(self.args.file).resolve(strict=True)
except FileNotFoundError:
self.disp(_('File "{file}" doesn\'t exist!').format(file=self.args.file))
self.host.quit(C.EXIT_NOT_FOUND)
self.syntax = await guess_syntax_from_path(self.host, self.content_file_path)
# at this point the syntax is converted, we can display the preview
preview_file = tempfile.NamedTemporaryFile(suffix=".xhtml", delete=False)
self.preview_file_path = preview_file.name
preview_file.close()
await self.update_content()
if aionotify is None:
# XXX: we don't delete file automatically because browser needs it
# (and webbrowser.open can return before it is read)
self.disp(
f"temporary file created at {self.preview_file_path}\nthis file will NOT "
f"BE DELETED AUTOMATICALLY, please delete it yourself when you have "
f"finished"
)
await open_cb()
else:
await open_cb()
watcher = aionotify.Watcher()
watcher_kwargs = {
# Watcher don't accept Path so we convert to string
"path": str(self.content_file_path),
"alias": "content_file",
"flags": aionotify.Flags.CLOSE_WRITE
| aionotify.Flags.DELETE_SELF
| aionotify.Flags.MOVE_SELF,
}
watcher.watch(**watcher_kwargs)
loop = asyncio.get_event_loop()
await watcher.setup(loop)
try:
while True:
event = await watcher.get_event()
self.disp("Content updated", 1)
if event.flags & (
aionotify.Flags.DELETE_SELF | aionotify.Flags.MOVE_SELF
):
self.disp(
"DELETE/MOVE event catched, changing the watch",
2,
)
try:
watcher.unwatch("content_file")
except IOError as e:
self.disp(
f"Can't remove the watch: {e}",
2,
)
watcher = aionotify.Watcher()
watcher.watch(**watcher_kwargs)
try:
await watcher.setup(loop)
except OSError:
# if the new file is not here yet we can have an error
# as a workaround, we do a little rest and try again
await asyncio.sleep(1)
await watcher.setup(loop)
await self.update_content()
await update_cb()
except FileNotFoundError:
self.disp("The file seems to have been deleted.", error=True)
self.host.quit(C.EXIT_NOT_FOUND)
finally:
os.unlink(self.preview_file_path)
try:
watcher.unwatch("content_file")
except IOError as e:
self.disp(
f"Can't remove the watch: {e}",
2,
)
class Import(base.CommandBase):
def __init__(self, host):
super().__init__(
host,
"import",
use_pubsub=True,
use_progress=True,
help=_("import an external blog"),
)
def add_parser_options(self):
self.parser.add_argument(
"importer",
nargs="?",
help=_("importer name, nothing to display importers list"),
)
self.parser.add_argument("--host", help=_("original blog host"))
self.parser.add_argument(
"--no-images-upload",
action="store_true",
help=_("do *NOT* upload images (default: do upload images)"),
)
self.parser.add_argument(
"--upload-ignore-host",
help=_("do not upload images from this host (default: upload all images)"),
)
self.parser.add_argument(
"--ignore-tls-errors",
action="store_true",
help=_("ignore invalide TLS certificate for uploads"),
)
self.parser.add_argument(
"-o",
"--option",
action="append",
nargs=2,
default=[],
metavar=("NAME", "VALUE"),
help=_("importer specific options (see importer description)"),
)
self.parser.add_argument(
"location",
nargs="?",
help=_(
"importer data location (see importer description), nothing to show "
"importer description"
),
)
async def on_progress_started(self, metadata):
self.disp(_("Blog upload started"), 2)
async def on_progress_finished(self, metadata):
self.disp(_("Blog uploaded successfully"), 2)
redirections = {
k[len(URL_REDIRECT_PREFIX) :]: v
for k, v in metadata.items()
if k.startswith(URL_REDIRECT_PREFIX)
}
if redirections:
conf = "\n".join(
[
"url_redirections_dict = {}".format(
# we need to add ' ' before each new line
# and to double each '%' for ConfigParser
"\n ".join(
json.dumps(redirections, indent=1, separators=(",", ": "))
.replace("%", "%%")
.split("\n")
)
),
]
)
self.disp(
_(
"\nTo redirect old URLs to new ones, put the following lines in your"
" sat.conf file, in [libervia] section:\n\n{conf}"
).format(conf=conf)
)
async def on_progress_error(self, error_msg):
self.disp(
_("Error while uploading blog: {error_msg}").format(error_msg=error_msg),
error=True,
)
async def start(self):
if self.args.location is None:
for name in ("option", "service", "no_images_upload"):
if getattr(self.args, name):
self.parser.error(
_(
"{name} argument can't be used without location argument"
).format(name=name)
)
if self.args.importer is None:
self.disp(
"\n".join(
[
f"{name}: {desc}"
for name, desc in await self.host.bridge.blogImportList()
]
)
)
else:
try:
short_desc, long_desc = await self.host.bridge.blogImportDesc(
self.args.importer
)
except Exception as e:
msg = [l for l in str(e).split("\n") if l][
-1
] # we only keep the last line
self.disp(msg)
self.host.quit(1)
else:
self.disp(f"{self.args.importer}: {short_desc}\n\n{long_desc}")
self.host.quit()
else:
# we have a location, an import is requested
options = {key: value for key, value in self.args.option}
if self.args.host:
options["host"] = self.args.host
if self.args.ignore_tls_errors:
options["ignore_tls_errors"] = C.BOOL_TRUE
if self.args.no_images_upload:
options["upload_images"] = C.BOOL_FALSE
if self.args.upload_ignore_host:
self.parser.error(
"upload-ignore-host option can't be used when no-images-upload "
"is set"
)
elif self.args.upload_ignore_host:
options["upload_ignore_host"] = self.args.upload_ignore_host
try:
progress_id = await self.host.bridge.blogImport(
self.args.importer,
self.args.location,
options,
self.args.service,
self.args.node,
self.profile,
)
except Exception as e:
self.disp(
_("Error while trying to import a blog: {e}").format(e=e),
error=True,
)
self.host.quit(1)
else:
await self.set_progress_id(progress_id)
class AttachmentGet(cmd_pubsub.AttachmentGet):
def __init__(self, host):
super().__init__(host)
self.override_pubsub_flags({C.SERVICE, C.SINGLE_ITEM})
async def start(self):
if not self.args.node:
namespaces = await self.host.bridge.namespaces_get()
try:
ns_microblog = namespaces["microblog"]
except KeyError:
self.disp("XEP-0277 plugin is not loaded", error=True)
self.host.quit(C.EXIT_MISSING_FEATURE)
else:
self.args.node = ns_microblog
return await super().start()
class AttachmentSet(cmd_pubsub.AttachmentSet):
def __init__(self, host):
super().__init__(host)
self.override_pubsub_flags({C.SERVICE, C.SINGLE_ITEM})
async def start(self):
if not self.args.node:
namespaces = await self.host.bridge.namespaces_get()
try:
ns_microblog = namespaces["microblog"]
except KeyError:
self.disp("XEP-0277 plugin is not loaded", error=True)
self.host.quit(C.EXIT_MISSING_FEATURE)
else:
self.args.node = ns_microblog
return await super().start()
class Attachments(base.CommandBase):
subcommands = (AttachmentGet, AttachmentSet)
def __init__(self, host):
super().__init__(
host,
"attachments",
use_profile=False,
help=_("set or retrieve blog attachments"),
)
class Blog(base.CommandBase):
subcommands = (Set, Get, Edit, Rename, Repeat, Preview, Import, Attachments)
def __init__(self, host):
super(Blog, self).__init__(
host, "blog", use_profile=False, help=_("blog/microblog management")
)