diff --git a/mmcli.py b/mmcli.py index d3c219f..9983c69 100755 --- a/mmcli.py +++ b/mmcli.py @@ -31,16 +31,13 @@ def yes_no(x): return "yes" if x else "no" -def get_posts_for_channel(self, channel_id: str, **kwargs) -> Generator[Dict, None, None]: +def get_posts_for_channel(self, channel_id: str, progress=lambda x: None, **kwargs) -> Generator[Dict, None, None]: """ - Generator: Get a page of posts in a channel. Use the query parameters to modify the behaviour of this endpoint. - - @param channel_id: The channel ID to iterate over. - - Raises: - ApiException: Passed on from lower layers. + @raises ApiException: Passed on from lower layers. """ page = 0 + posts = [] + while True: data_page = self._get(f"/v4/channels/{channel_id}/posts", params={"page":str(page), "per_page":200, **kwargs}) @@ -48,11 +45,15 @@ def get_posts_for_channel(self, channel_id: str, **kwargs) -> Generator[Dict, No break page += 1 - for order in data_page["order"]: - yield data_page["posts"][order] - + posts.extend(data_page["posts"][order] for order in data_page["order"]) + progress(len(posts)) sleep(0.1) + # Mattermost gives newest first, so reverse order + posts.reverse() + + return posts + ID_PREFIX = "id:" @@ -105,15 +106,17 @@ def login(mm_api, parsed): f"TOTP token provided: {yes_no(parsed.totp)}", file=sys.stderr) mm_api.login(parsed.user, parsed.password, parsed.totp) - return mm_api._bearer + + if parsed.format == "json": + print(json.dumps({"token": mm_api._bearer})) + elif parsed.format == "tsv": + print(mm_api._bearer) + else: + assert False def cat(mm_api: mattermost.MMApi, parsed): - - # FIXME Wrong order - - channels = [ resolve_team_channel(mm_api, query) for query in parsed.channels @@ -135,9 +138,40 @@ def cat(mm_api: mattermost.MMApi, parsed): def attribute(key_value): return key_value - for post in get_posts_for_channel(mm_api, channel["id"], after=parsed.after): + posts = get_posts_for_channel(mm_api, channel["id"], after=parsed.after) + for post in posts: print(post_str(attribute, post, parsed)) - print(parsed.after) + + +def send(mm_api: mattermost.MMApi, parsed): + read_stdin = parsed.message is None or parsed.channel is None + + team, channel = resolve_team_channel(mm_api, parsed.channel) if parsed.channel is not None else (None, None) + + if read_stdin: + if sys.stdin.isatty(): + print("Reading from tty. (You can type the message objects below. Or maybe you meant to redirect something to stdin.)", file=sys.stderr) + + for line in sys.stdin: + msg = json.loads(line) + + if "channel_id" in msg: + channel_id = msg["channel_id"] + elif "channel" in msg: + _, local_channel = resolve_team_channel(mm_api, msg["channel"]) + channel_id = local_channel["id"] + elif channel is not None: + channel_id = channel["id"] + else: + print(f"Illegal message, missing channel: {line.strip()}", file=sys.stderr) + raise ValueError("Illegal message, missing channel") + + sent = mm_api.create_post(channel_id, msg["message"], props={"from_mmcli": "true"}, filepaths=msg.get("attachments", msg.get("attachments"))) + print(sent) + + else: + sent = mm_api.create_post(channel["id"], parsed.message, props={"from_mmcli": "true"}, filepaths=parsed.attach) + print(sent) def post_str(attribute, post, parsed): @@ -152,14 +186,16 @@ def post_str(attribute, post, parsed): if parsed.format == "tsv": msg = obj.get("message", "").replace("\\", "\\\\").replace("\t", r"\t").replace("\n", r"\n") return f"{obj['id']}\t{obj['create_at']}\t{obj.get('username') or obj['user_id']}\t{msg}" + assert False ACTIONS = { "login": {"function": login, "accesstoken_required": False}, "cat": {"function": cat}, + "send": {"function": send}, } -FORMATTERS = { "json", "tsv", "csv" } +FORMATTERS = { "json", "tsv" } ENVVAR_SERVER = "MM_SERVER" ENVVAR_USERNAME = "MM_USERNAME" @@ -200,13 +236,26 @@ Hint: JSON output can be filtered on the command line with jq(1). parser_login.add_argument("--password", default=os.getenv(ENVVAR_PASSWORD)) parser_login.add_argument("--totp", default=os.getenv(ENVVAR_TOTP)) - parser_cat = subparsers.add_parser("cat", help="list messages in channel(s)") - parser_cat.add_argument( - "channels", nargs="+", help="URL names of team and channel: '/'") + # TODO support multiple channels + # parser_cat = subparsers.add_parser("cat", help="list messages in channel(s)") + # parser_cat.add_argument( + # "channels", nargs="+", help="URL names of team and channel: '/'") + parser_cat = subparsers.add_parser("cat", help="list messages in channel") + parser_cat.add_argument("channel", help="URL names of team and channel: '/'") + # --- parser_cat.add_argument("--after", help="all after post with ID") parser_cat.add_argument("--since", help="all after timestamp") parser_cat.add_argument("-f", "--follow", help="keep running, printing new posts as they come in") + parser_send = subparsers.add_parser("send", help="send message(s)") + parser_send.add_argument( + "--channel", help="URL names of team and channel: '/'; if not provided, " + "messages must be provided on stdin and each must specify channel") + parser_send.add_argument( + "--message", help="message; if not provided, messages will be expected on stdin") + parser_send.add_argument( + "--attach", nargs="+", help="filename of file to attach") + parsed = argparser.parse_args() if not parsed.server: