comparison libervia/cli/cmd_message.py @ 4075:47401850dec6

refactoring: rename `libervia.frontends.jp` to `libervia.cli`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 14:54:26 +0200
parents libervia/frontends/jp/cmd_message.py@26b7ed2817da
children
comparison
equal deleted inserted replaced
4074:26b7ed2817da 4075:47401850dec6
1 #!/usr/bin/env python3
2
3
4 # Libervia CLI
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
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/>.
19
20 from pathlib import Path
21 import sys
22
23 from twisted.python import filepath
24
25 from libervia.backend.core.i18n import _
26 from libervia.backend.tools.common import data_format
27 from libervia.backend.tools.common.ansi import ANSI as A
28 from libervia.backend.tools.utils import clean_ustr
29 from libervia.cli import base
30 from libervia.cli.constants import Const as C
31 from libervia.frontends.tools import jid
32
33
34 __commands__ = ["Message"]
35
36
37 class Send(base.CommandBase):
38 def __init__(self, host):
39 super(Send, self).__init__(host, "send", help=_("send a message to a contact"))
40
41 def add_parser_options(self):
42 self.parser.add_argument(
43 "-l", "--lang", type=str, default="", help=_("language of the message")
44 )
45 self.parser.add_argument(
46 "-s",
47 "--separate",
48 action="store_true",
49 help=_(
50 "separate xmpp messages: send one message per line instead of one "
51 "message alone."
52 ),
53 )
54 self.parser.add_argument(
55 "-n",
56 "--new-line",
57 action="store_true",
58 help=_(
59 "add a new line at the beginning of the input"
60 ),
61 )
62 self.parser.add_argument(
63 "-S",
64 "--subject",
65 help=_("subject of the message"),
66 )
67 self.parser.add_argument(
68 "-L", "--subject-lang", type=str, default="", help=_("language of subject")
69 )
70 self.parser.add_argument(
71 "-t",
72 "--type",
73 choices=C.MESS_TYPE_STANDARD + (C.MESS_TYPE_AUTO,),
74 default=C.MESS_TYPE_AUTO,
75 help=_("type of the message"),
76 )
77 self.parser.add_argument("-e", "--encrypt", metavar="ALGORITHM",
78 help=_("encrypt message using given algorithm"))
79 self.parser.add_argument(
80 "--encrypt-noreplace",
81 action="store_true",
82 help=_("don't replace encryption algorithm if an other one is already used"))
83 self.parser.add_argument(
84 "-a", "--attach", dest="attachments", action="append", metavar="FILE_PATH",
85 help=_("add a file as an attachment")
86 )
87 syntax = self.parser.add_mutually_exclusive_group()
88 syntax.add_argument("-x", "--xhtml", action="store_true", help=_("XHTML body"))
89 syntax.add_argument("-r", "--rich", action="store_true", help=_("rich body"))
90 self.parser.add_argument(
91 "jid", help=_("the destination jid")
92 )
93
94 async def send_stdin(self, dest_jid):
95 """Send incomming data on stdin to jabber contact
96
97 @param dest_jid: destination jid
98 """
99 header = "\n" if self.args.new_line else ""
100 # FIXME: stdin is not read asynchronously at the moment
101 stdin_lines = [
102 stream for stream in sys.stdin.readlines()
103 ]
104 extra = {}
105 if self.args.subject is None:
106 subject = {}
107 else:
108 subject = {self.args.subject_lang: self.args.subject}
109
110 if self.args.xhtml or self.args.rich:
111 key = "xhtml" if self.args.xhtml else "rich"
112 if self.args.lang:
113 key = f"{key}_{self.args.lang}"
114 extra[key] = clean_ustr("".join(stdin_lines))
115 stdin_lines = []
116
117 to_send = []
118
119 error = False
120
121 if self.args.separate:
122 # we send stdin in several messages
123 if header:
124 # first we sent the header
125 try:
126 await self.host.bridge.message_send(
127 dest_jid,
128 {self.args.lang: header},
129 subject,
130 self.args.type,
131 profile_key=self.profile,
132 )
133 except Exception as e:
134 self.disp(f"can't send header: {e}", error=True)
135 error = True
136
137 to_send.extend({self.args.lang: clean_ustr(l.replace("\n", ""))}
138 for l in stdin_lines)
139 else:
140 # we sent all in a single message
141 if not (self.args.xhtml or self.args.rich):
142 msg = {self.args.lang: header + clean_ustr("".join(stdin_lines))}
143 else:
144 msg = {}
145 to_send.append(msg)
146
147 if self.args.attachments:
148 attachments = extra[C.KEY_ATTACHMENTS] = []
149 for attachment in self.args.attachments:
150 try:
151 file_path = str(Path(attachment).resolve(strict=True))
152 except FileNotFoundError:
153 self.disp("file {attachment} doesn't exists, ignoring", error=True)
154 else:
155 attachments.append({"path": file_path})
156
157 for idx, msg in enumerate(to_send):
158 if idx > 0 and C.KEY_ATTACHMENTS in extra:
159 # if we send several messages, we only want to send attachments with the
160 # first one
161 del extra[C.KEY_ATTACHMENTS]
162 try:
163 await self.host.bridge.message_send(
164 dest_jid,
165 msg,
166 subject,
167 self.args.type,
168 data_format.serialise(extra),
169 profile_key=self.host.profile)
170 except Exception as e:
171 self.disp(f"can't send message {msg!r}: {e}", error=True)
172 error = True
173
174 if error:
175 # at least one message sending failed
176 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
177
178 self.host.quit()
179
180 async def start(self):
181 if self.args.xhtml and self.args.separate:
182 self.disp(
183 "argument -s/--separate is not compatible yet with argument -x/--xhtml",
184 error=True,
185 )
186 self.host.quit(C.EXIT_BAD_ARG)
187
188 jids = await self.host.check_jids([self.args.jid])
189 jid_ = jids[0]
190
191 if self.args.encrypt_noreplace and self.args.encrypt is None:
192 self.parser.error("You need to use --encrypt if you use --encrypt-noreplace")
193
194 if self.args.encrypt is not None:
195 try:
196 namespace = await self.host.bridge.encryption_namespace_get(
197 self.args.encrypt)
198 except Exception as e:
199 self.disp(f"can't get encryption namespace: {e}", error=True)
200 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
201
202 try:
203 await self.host.bridge.message_encryption_start(
204 jid_, namespace, not self.args.encrypt_noreplace, self.profile
205 )
206 except Exception as e:
207 self.disp(f"can't start encryption session: {e}", error=True)
208 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
209
210 await self.send_stdin(jid_)
211
212
213 class Retract(base.CommandBase):
214
215 def __init__(self, host):
216 super().__init__(host, "retract", help=_("retract a message"))
217
218 def add_parser_options(self):
219 self.parser.add_argument(
220 "message_id",
221 help=_("ID of the message (internal ID)")
222 )
223
224 async def start(self):
225 try:
226 await self.host.bridge.message_retract(
227 self.args.message_id,
228 self.profile
229 )
230 except Exception as e:
231 self.disp(f"can't retract message: {e}", error=True)
232 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
233 else:
234 self.disp(
235 "message retraction has been requested, please note that this is a "
236 "request which can't be enforced (see documentation for details).")
237 self.host.quit(C.EXIT_OK)
238
239
240 class MAM(base.CommandBase):
241
242 def __init__(self, host):
243 super(MAM, self).__init__(
244 host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True,
245 help=_("query archives using MAM"))
246
247 def add_parser_options(self):
248 self.parser.add_argument(
249 "-s", "--service", default="",
250 help=_("jid of the service (default: profile's server"))
251 self.parser.add_argument(
252 "-S", "--start", dest="mam_start", type=base.date_decoder,
253 help=_(
254 "start fetching archive from this date (default: from the beginning)"))
255 self.parser.add_argument(
256 "-E", "--end", dest="mam_end", type=base.date_decoder,
257 help=_("end fetching archive after this date (default: no limit)"))
258 self.parser.add_argument(
259 "-W", "--with", dest="mam_with",
260 help=_("retrieve only archives with this jid"))
261 self.parser.add_argument(
262 "-m", "--max", dest="rsm_max", type=int, default=20,
263 help=_("maximum number of items to retrieve, using RSM (default: 20))"))
264 rsm_page_group = self.parser.add_mutually_exclusive_group()
265 rsm_page_group.add_argument(
266 "-a", "--after", dest="rsm_after",
267 help=_("find page after this item"), metavar='ITEM_ID')
268 rsm_page_group.add_argument(
269 "-b", "--before", dest="rsm_before",
270 help=_("find page before this item"), metavar='ITEM_ID')
271 rsm_page_group.add_argument(
272 "--index", dest="rsm_index", type=int,
273 help=_("index of the page to retrieve"))
274
275 async def start(self):
276 extra = {}
277 if self.args.mam_start is not None:
278 extra["mam_start"] = float(self.args.mam_start)
279 if self.args.mam_end is not None:
280 extra["mam_end"] = float(self.args.mam_end)
281 if self.args.mam_with is not None:
282 extra["mam_with"] = self.args.mam_with
283 for suff in ('max', 'after', 'before', 'index'):
284 key = 'rsm_' + suff
285 value = getattr(self.args,key)
286 if value is not None:
287 extra[key] = str(value)
288 try:
289 data, metadata_s, profile = await self.host.bridge.mam_get(
290 self.args.service, data_format.serialise(extra), self.profile)
291 except Exception as e:
292 self.disp(f"can't retrieve MAM archives: {e}", error=True)
293 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
294
295 metadata = data_format.deserialise(metadata_s)
296
297 try:
298 session_info = await self.host.bridge.session_infos_get(self.profile)
299 except Exception as e:
300 self.disp(f"can't get session infos: {e}", error=True)
301 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
302
303 # we need to fill own_jid for message output
304 self.host.own_jid = jid.JID(session_info["jid"])
305
306 await self.output(data)
307
308 # FIXME: metadata are not displayed correctly and don't play nice with output
309 # they should be added to output data somehow
310 if self.verbosity:
311 for value in ("rsm_first", "rsm_last", "rsm_index", "rsm_count",
312 "mam_complete", "mam_stable"):
313 if value in metadata:
314 label = value.split("_")[1]
315 self.disp(A.color(
316 C.A_HEADER, label, ': ' , A.RESET, metadata[value]))
317
318 self.host.quit()
319
320
321 class Message(base.CommandBase):
322 subcommands = (Send, Retract, MAM)
323
324 def __init__(self, host):
325 super(Message, self).__init__(
326 host, "message", use_profile=False, help=_("messages handling")
327 )