Mercurial > libervia-backend
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 | 0d7bb4df2343 |
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 ) |