/*
-- Tiny XMPP Client --
txmppc reads commands from stdin and prints to stdout.
It is a KISS client for command line or automation.

Out of scope:
roster -> there is a presence list -> no name (alias), group or subscription state
MUC management -> basic works: join (with password), write and leave -> no administration, invitation, register, disco, ...
no disco, bookmark, pubsub, vcard, files, voice, ...
OMEMO - encryption is good, but too complicated for a tiny client:
  There is one lib ( https://github.com/gkdr/libomemo ) and profanity has a good implementation, but the code of omemo is 5x larger than txmppc.
  -> profanity (tui - OMEMO, OTR and PGP and plugins) -> https://profanity-im.github.io/ (c uses libstrophe)
  -> poezio (tui - OMEMO and OTR) -> https://poez.io/ (python uses slixmpp lib)
  -> mcabber (tui - No OMEMO, but OTR and PGP, external scripts + fifo) -> https://mcabber.com/ (c uses lib loudmouth -> https://github.com/mcabber/loudmouth )
  -> xmppc (cli, which can send messages (chat + PGP), monitor and query some data) -> https://codeberg.org/Anoxinon_e.V./xmppc (c uses libstrophe)
  -> smplxmpp -> https://codeberg.org/tropf/smplxmpp (c++ uses gloox lib)
  -> freetalk -> https://www.gnu.org/software/freetalk/ (c uses loudmouth)
  -> jj (creates filesystem (fifo) structure) -> https://23.fi/jj/ (c uses loudmouth)
  -> https://wiki.xmpp.org/web/User:MDosch/Sendxmpp_incarnations

Build:
Depends only on libstophe
> gcc txmppc.c -lstrophe -o txmppc
Static compile (musl)
> gcc txmppc.c -lstrophe -lc -lssl -lcrypto -lexpat -static -o txmppc_static
  (depending on libstrophe, it can be xml2 instead of expat and/or gnutls instead of openssl)
optional: > strip txmppc_static

Use:
[echo "command jid message" |] txmppc[_static] <jid> <pass> [[=]<server>[(:|=)<port>]]
- jid and password are mandatory
- optional is a third parameter with server (and :port or =port -> ignores if tls startup (self-signed cert) fails)
- if arg "-" is provided the first line read from stdin need to be a list of NULL separated args (JID\0PASS[\0SERVER_PORT]\0\n)
  -> security feature password will not show up in ps / proc listing

txmppc logs incomming messages to stdout and reads commands from stdin
- just start as a slim client:
  > txmppc JID PASS
- Sending single message:
  > echo "[mM] jid message" | txmppc
    -> txmppc executes the command and exits (on close of stdin)
- Using fifo for commands:
  > tail -f fifo | txmppc
    -> executes the commands from fifo and prints to stdout
    > echo "q" >> fifo # ! tail will still wait - so it doesn't exit -> send another line
      -> Hint: Instead of tail, use txmppc < fifo (will block until first write) and echo "D" > fifo (will disable exit on EOF)
- tmux horizontal split + rlwrap client example:
  (mkfifo fifo; echo "contact@example.com" >> jid_file)
  90% pane> txmppc JID PASS < fifo | while read line; do printf '%s: %s\n' "$(date "+%Y-%m-%d %H:%M")" "${line}"; done
  10% pane> rlwrap -s -10 -H /dev/null -b " " -f jid_file awk 'BEGIN {print("i");}; {print}; /^q$/ {exit}' >> fifo
  -> readline editing with 10 lines history (not saved) and jid completion from fid_file, output will have a date prepended
  -> BEGIN {print("i");} will activate stdin echo feedback - reading from fifo will block until first write
  -> Hint: In that case the fifo can't be used for other processes, because sending an EOF will end the client (or use "D" command).

Commands:
j[ muc [password]] -> join multi user conference (without muc "join" presence status changes - default on)
J muc [password] -> join multi user conference - no history
l[ muc] -> leave multi user conference (without muc "leave" presence status changes)
[m ]jid[ message] -> send (chat) message to jid (without message enter multi line mode)
h jid message -> send headline message to jid
n jid message -> send normal message to jid
M muc_jid message -> send (groupchat) message to multi user conference (if not in the muc, it will be joined and left)
.[ message] -> send (chat|headline|normal|groupchat) message to last jid (without message enter multi line mode)
p[ [priority] presence [status]] -> priority: -128 / 127, presence: on[line]|of[fline]|aw[ay]|ch[at]|dn[d]|xa
  -> without arg: show presence list; without priority: change presence only
P[ jid [priority] presence [status]] -> send presence to jid, extra presence: subscribe|subscribed|unsubscribe|unsubscribed
  -> without arg show presence list including unavailable/offline
q -> end/exit
r[ stanza] -> send raw stanza (without stanza enter multi line mode)
  -> invalid data may result in immediate stream termination by the XMPP server
R[ key] -> show raw (message, iq.result) stanza, if it contains key - disabled without key
  -> "<" (will match every stanza), "iq", "message", "carbons", "openpgp", "juliet@example.org", ...
  (to see every stanza compile with -DDEBUG, which will enable libstrophe XMPP_LEVEL_DEBUG)
Extra commands:
i -> stdin echo feedback
I -> stdin no feedback (default)
d -> exit on EOF (default)
D -> ignored EOF (using multiple sender at the same time and mutliline mode might mess up commands and messages)

Output:
> -> stdin echo feedback
i -> info
I -> Info offline, lost connection
p -> presence (join/leave status changes with j/l command)
P -> presence list
m -> message chat ("m from: message", if carbon from self "m -> to: message")
n -> message normal
h -> message headline
M -> message muc
H -> message muc history
r -> raw message stanza
e -> error
W -> Warn - command error
E -> (stderr) startup error -> will exit

End-to-end encryption:
- It is possible to send PGP encrypted messages (sending a raw stanza):
  > BASE64_OPENPGP_MESSAGE=$(echo "message" | gpg | base64)"
  > echo "r <message to='juliet@example.org'><openpgp xmlns='urn:xmpp:openpgp:0'>${BASE64_OPENPGP_MESSAGE}</openpgp></message>" | txmppc
  (might be supported by other clients: https://xmpp.org/extensions/xep-0373.html -> Be aware that most clients have implemented the obsolete: https://xmpp.org/extensions/xep-0027.html )
  -> For receiving raw message stanza filtering with "R openpgp" ("R encrypted") is an option.
- A non XMPP conformant way, if both clients use txmppc can be scripted with any encryption. Example with age (don't use password protected key):
  sender:
  > ENCRYPTED_MESSAGE="$(printf 'message' | age -r age1urredcd3g8dfachch0d6gzt5katx3tsfesz35zc6jn3fl23jrfmq027uu9 | base64 -w0)"
  > printf 'm juliet@example.org MYCRYPT:%s' "${ENCRYPTED_MESSAGE}" | txmppc
  receiver:
  > txmppc | while read line; do
    ENCRYPTED_MESSAGE="${line#*MYCRYPT:}"
    if [ "${line}" = "${ENCRYPTED_MESSAGE}" ]; then
      printf '%s\n' "${line}"
    else
      MESSAGE="$(printf '%s' "${ENCRYPTED_MESSAGE}" | base64 -d | age -d -i juliet.key)"
      printf '%s%s\n' "${line%MYCRYPT:*}" "${MESSAGE}"
    fi
  done


License:
https://holmeinbuch.de/repo/txmppc/ - matthias@

Copyright (c) ISC License

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#define _XOPEN_SOURCE 700
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
// https://www.rfc-editor.org/rfc/rfc6120 - core
// https://www.rfc-editor.org/rfc/rfc6121 - IM + presence
#include <strophe.h>

static unsigned short ignore_cert_fail = 0;
// signal-safety.7
volatile sig_atomic_t reconnect = 1;
// https://www.rfc-editor.org/rfc/rfc6120
// -> 4.6.4 Use of Checking Methods: RECOMMENDED not more than once every 5 minutes
#define PING_INTERVAL 60
static char ping_id[32];
#define BUFIN_MAX 4096
// https://www.rfc-editor.org/rfc/rfc6121
// -> don't mess with priority, negative might get no messages
static short conn_priority = 0;
static unsigned short show_presence_status_change = 1;
#define PRESENCE_MAX 5
#define PRESENCE_JID_MAX 9
static char presence_type[10][13] = {"offline", "online", "away", "chat", "dnd", "xa",
                                     "subscribe", "subscribed", "unsubscribe", "unsubscribed"};
#define PRESENCE_OFFLINE 0
#define PRESENCE_ONLINE 1
#define PRESENCE_ONLINE_MUC -1
#define PRESENCE_ONLINE_MUC_NO_HISTORY -101
#define PRESENCE_OFFLINE_MUC -100
// https://www.rfc-editor.org/rfc/rfc7622 - address (jid)
#define JID_MAX 3071 + 1
static char last_sent_type_jid[1 + JID_MAX];
struct list_node {
  char jid[JID_MAX];
  short presence;
  short priority;
  struct list_node *next;
};
static struct list_node *jid_list = NULL;
#define JID_BARE_MAX 2047 + 1
static char jid_bare[JID_BARE_MAX];
// https://xmpp.org/extensions/xep-0172.html - nick -> nickname length not defined
// https://xmpp.org/extensions/xep-0045.html - muc -> nickname is resource part of jid
#define NICK_MAX 1023 + 1
static char nick[NICK_MAX];
#define MULTI_LINE_OFF 0
#define MULTI_LINE_ON 1
#define MULTI_LINE_MUC 2
#define MULTI_LINE_RAW 3
static short multi_line = MULTI_LINE_OFF;
#define SHOW_RAW_STANZA_MAX 32
static char show_raw_stanza[SHOW_RAW_STANZA_MAX];
static unsigned short stdin_echo_feedback = 0;
static unsigned short exit_on_eof = 1;

unsigned short get_start_pos(const char *buf, unsigned short start_pos) {
  unsigned short buf_len = (unsigned short)strlen(&buf[start_pos]) + start_pos;

  for (unsigned short i = start_pos; i < buf_len; i++) {
    if (!isspace(buf[i])) {
      return i;
    }
  }
  return buf_len;
}

void update_jid_list(const char *jid, short presence, short priority) {
  struct list_node *list_before = NULL;
  struct list_node *list_current = jid_list;
  struct list_node *list_new = NULL;
  size_t jid_len = strlen(jid);

  while (list_current) {
    int jid_match = strncmp(jid, list_current->jid, jid_len);

    if (jid_match == 0) {
      if (presence == PRESENCE_OFFLINE_MUC) {
        // remove jid, of who leaves the muc from list
        // if self leaves muc, called with bare jid -> remove every muc entry
        while (list_current && (strncmp(jid, list_current->jid, jid_len) == 0)) {
          if (list_before) {
            list_before->next = list_current->next;
            free(list_current);
            list_current = list_before->next;
          } else {
            jid_list = list_current->next;
            free(list_current);
            list_current = jid_list->next;
          }
        }
        return;
      }
      // update jid state for self and subscription (offline too)
      list_current->presence = presence;
      list_current->priority = priority;
      return;
    }
    if (jid_match < 0) {
      // insert new jid here
      break;
    }
    list_before = list_current;
    list_current = list_current->next;
  }
  list_new = (struct list_node*) malloc(sizeof(struct list_node));
  if (list_new == NULL) {
    fprintf(stderr, "alloc failed\n");
    fflush(stderr);
    exit(1);
  }
  memccpy(list_new->jid, jid, '\0', JID_MAX - 1);
  list_new->presence = presence;
  list_new->priority = priority;
  if (list_before) {
    list_before->next = list_new;
  } else {
    jid_list = list_new;
  }
  list_new->next = list_current;
}

void send_presence(xmpp_conn_t *conn, short presence_index,
                   const char *status_text, const char *jid, const char *priority_text) {
  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
  xmpp_stanza_t *presence_stanza = xmpp_presence_new(ctx);

  if (jid) {
    xmpp_stanza_set_to(presence_stanza, jid);
  }
  if (presence_index == PRESENCE_OFFLINE) {
    xmpp_stanza_set_type(presence_stanza, "unavailable");
  } else if (PRESENCE_MAX < presence_index) {
    xmpp_stanza_set_type(presence_stanza, presence_type[presence_index]);
  // inverse presence for muc
  } else if (presence_index < 0) {
    // MUC special
    xmpp_stanza_t *extension_stanza;
    unsigned short jid_len = (unsigned short)strlen(jid);
    unsigned short i = 0;

    for (;;) {
      // muc_jid is only muc, no nick
      if (i == jid_len) {
        char muc_jid_nick[JID_MAX];

        if (JID_MAX - 1 - NICK_MAX < strlen(jid)) {
          fprintf(stdout, "W invalid muc jid\n");
          fflush(stdout);
          xmpp_stanza_release(presence_stanza);
          return;
        }
        sprintf(muc_jid_nick, "%s/%s", jid, nick);
        xmpp_stanza_set_to(presence_stanza, muc_jid_nick);
        break;
      }
      // muc_jid contains nick
      if (jid[i++] == '/') {
        break;
      }
    }
    extension_stanza = xmpp_stanza_new(ctx);
    xmpp_stanza_set_name(extension_stanza, "x");
    xmpp_stanza_set_ns(extension_stanza, "http://jabber.org/protocol/muc");
    if (presence_index == PRESENCE_ONLINE_MUC_NO_HISTORY) {
      xmpp_stanza_t *history_stanza;

      history_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_name(history_stanza, "history");
      xmpp_stanza_set_attribute(history_stanza, "maxstanzas", "0");
      xmpp_stanza_add_child_ex(extension_stanza, history_stanza, 0);
    }
    if (status_text) {
      xmpp_stanza_t *password_stanza, *value_stanza;

      password_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_name(password_stanza, "password");
      value_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_text(value_stanza, status_text);
      xmpp_stanza_add_child_ex(password_stanza, value_stanza, 0);
      xmpp_stanza_add_child_ex(extension_stanza, password_stanza, 0);
    }
    xmpp_stanza_add_child_ex(presence_stanza, extension_stanza, 0);
  } else {
    if (1 < presence_index) {
      xmpp_stanza_t *show_stanza, *value_stanza;

      show_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_name(show_stanza, "show");
      value_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_text(value_stanza, presence_type[presence_index]);
      xmpp_stanza_add_child_ex(show_stanza, value_stanza, 0);
      xmpp_stanza_add_child_ex(presence_stanza, show_stanza, 0);
    }
    if (priority_text || (conn_priority && -128 < conn_priority && conn_priority < 127)) {
      xmpp_stanza_t *priority_stanza, *value_stanza;

      priority_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_name(priority_stanza, "priority");
      value_stanza = xmpp_stanza_new(ctx);
      if (!priority_text) {
        char conn_priority_text[5];
        sprintf(conn_priority_text, "%d", conn_priority);
        xmpp_stanza_set_text(value_stanza, conn_priority_text);
      } else {
        xmpp_stanza_set_text(value_stanza, priority_text);
      }
      xmpp_stanza_add_child_ex(priority_stanza, value_stanza, 0);
      xmpp_stanza_add_child_ex(presence_stanza, priority_stanza, 0);
    }
    if (status_text) {
      xmpp_stanza_t *status_stanza, *value_stanza;

      status_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_name(status_stanza, "status");
      value_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_text(value_stanza, status_text);
      xmpp_stanza_add_child_ex(status_stanza, value_stanza, 0);
      xmpp_stanza_add_child_ex(presence_stanza, status_stanza, 0);
    }
  }
  xmpp_send(conn, presence_stanza);
  xmpp_stanza_release(presence_stanza);
}

void disconnect(xmpp_conn_t *conn) {
  if (reconnect == 1) {
    reconnect = 0;
  }
  send_presence(conn, PRESENCE_OFFLINE, NULL, NULL, NULL);
  xmpp_disconnect(conn);
}

void send_message(xmpp_conn_t *conn, const char *jid, const char command, const char *text) {
  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
  xmpp_stanza_t *message_stanza;
  xmpp_stanza_t *extension_stanza = NULL;
  const char *type = "chat";

  switch (command) {
    case 'x':
      // x: m -> jid in muc
      extension_stanza = xmpp_stanza_new(ctx);
      break;
    case 'M':
      type = "groupchat";
      break;
    case 'h':
      type = "headline";
      break;
    case 'N':
      // N: n -> jid in muc
      extension_stanza = xmpp_stanza_new(ctx);
      // fall through
    case 'n':
      type = "normal";
      break;
  }
  message_stanza = xmpp_message_new(ctx, type, jid, NULL);
  xmpp_message_set_body(message_stanza, text);
  if (extension_stanza) {
    xmpp_stanza_set_name(extension_stanza, "x");
    xmpp_stanza_set_ns(extension_stanza, "http://jabber.org/protocol/muc#user");
    xmpp_stanza_add_child_ex(message_stanza, extension_stanza, 0);
  }
  xmpp_send(conn, message_stanza);
  xmpp_stanza_release(message_stanza);
}

short muc_in_jid_list(const char *jid) {
  struct list_node *list_current = jid_list;
  unsigned short jid_len = (unsigned short)strlen(jid);

  while (list_current) {
    // inverse presence for muc jid
    if (list_current->presence < 0 && strncmp(jid, list_current->jid, jid_len) == 0) {
      return 1;
    }
    list_current = list_current->next;
  }
  return 0;
}

int input_handler(xmpp_conn_t *conn, void *userdata) {
  char bufin[BUFIN_MAX];
  unsigned short index, bufin_len;
  // starting with a jid -> default (chat) [m]essage to jid
  char command = 'm';
  char *jid = NULL;

  if (reconnect < 0) {
    // signal received
    disconnect(conn);
    return 1;
  }
  if (!fgets(bufin, BUFIN_MAX, stdin)) {
    if (exit_on_eof == 0 && feof(stdin)) {
      clearerr(stdin);
    } else if (feof(stdin) || errno != EAGAIN) {
      disconnect(conn);
    }
    return 1;
  }
  bufin_len = (unsigned short)strlen(bufin);
  if (bufin_len == BUFIN_MAX - 1 && bufin[BUFIN_MAX - 2] != '\n') {
    // read/remove everything from input
    while (fgetc(stdin) != '\n');
    fprintf(stdout, "W too much input\n");
    fflush(stdout);
    return 1;
  }
  while (0 < bufin_len && isspace(bufin[bufin_len - 1])) {
    // trim end of input
    bufin_len--;
  }
  bufin[bufin_len] = 0;
  if (stdin_echo_feedback && (multi_line || bufin[0] != 0)) {
    fprintf(stdout, "> %s\n", bufin);
    fflush(stdout);
  }
  if (multi_line) {
    if (bufin[0] == '.' && bufin[1] == 0) {
      if (multi_line == MULTI_LINE_MUC) {
        send_presence(conn, PRESENCE_OFFLINE, NULL, &last_sent_type_jid[1], NULL);
      }
      multi_line = MULTI_LINE_OFF;
      fprintf(stdout, "i end multi line\n");
      fflush(stdout);
      return 1;
    }
    if (multi_line == MULTI_LINE_RAW) {
      xmpp_send_raw_string(conn, "%s", bufin);
      return 1;
    }
    send_message(conn, &last_sent_type_jid[1], last_sent_type_jid[0], bufin);
    return 1;
  }
  if (bufin[0] == 0) {
    // ignore empty lines
    return 1;
  }
  // trim start
  index = get_start_pos(bufin, 0);

  if (bufin[index + 1] == ' ') {
    // get command (if second char is empty it is no jid)
    command = bufin[index];
    index = get_start_pos(bufin, index + 2);
  } else if (bufin[index + 1] == 0) {
    // just single char command
    struct list_node *list_current;
    char priority_text[8];
    unsigned short show_offline = 0;

    command = bufin[index];
    switch (command) {
      case '.':
        if (last_sent_type_jid[0] == 0) {
          fprintf(stdout, "W no last jid\n");
          fflush(stdout);
          return 1;
        }
        multi_line = MULTI_LINE_ON;
        if (last_sent_type_jid[0] == 'M') {
          if (!muc_in_jid_list(&last_sent_type_jid[1])) {
            multi_line = MULTI_LINE_MUC;
          }
        }
        fprintf(stdout, "i multi line - end with \".\"\n");
        fflush(stdout);
        return 1;
      case 'd':
        exit_on_eof = 1;
        return 1;
      case 'D':
        exit_on_eof = 0;
        return 1;
      case 'i':
        stdin_echo_feedback = 1;
        return 1;
      case 'I':
        stdin_echo_feedback = 0;
        return 1;
      case 'j':
        show_presence_status_change = 1;
        return 1;
      case 'l':
        show_presence_status_change = 0;
        return 1;
      case 'P':
        show_offline = 1;
        // fall through
      case 'p':
        list_current = jid_list;
        while (list_current) {
          if (list_current->priority) {
            sprintf(priority_text, " (%d)", list_current->priority);
          } else {
            priority_text[0] = 0;
          }
          if (list_current->presence < 0) {
            // use inverse presence for muc
            fprintf(stdout, "P %s (M) - %s%s\n",
                    list_current->jid, presence_type[-list_current->presence], priority_text);
          } else if (show_offline || list_current->presence != PRESENCE_OFFLINE) {
            fprintf(stdout, "P %s - %s%s\n",
                    list_current->jid, presence_type[list_current->presence], priority_text);
          }
          list_current = list_current->next;
        }
        fflush(stdout);
        return 1;
      case 'q':
        disconnect(conn);
        return 1;
      case 'r':
        multi_line = MULTI_LINE_RAW;
        fprintf(stdout, "i multi line - end with \".\"\n");
        fflush(stdout);
        return 1;
      case 'R':
        show_raw_stanza[0] = 0;
        return 1;
    }
  }
  if (command == 'r') {
    xmpp_send_raw_string(conn, "%s", &bufin[index]);
    return 1;
  }
  if (command == 'R') {
    if (memccpy(show_raw_stanza, &bufin[index], '\0', SHOW_RAW_STANZA_MAX - 1) == NULL) {
      show_raw_stanza[SHOW_RAW_STANZA_MAX - 1] = 0;
    }
    return 1;
  }
  if (command != '.' && command != 'm' && command != 'M' && command != 'j' && command != 'J'
      && command != 'l' && command != 'p' && command != 'P' && command != 'n' && command != 'h') {
    fprintf(stdout, "W invalid command\n");
    fflush(stdout);
    return 1;
  }

  if (command != '.' && command != 'p') {
    // get jid (if not message to last jid or global presence)
    unsigned short jid_local_and_domain = 0;

    for (unsigned short i = index; i < bufin_len; i++) {
      if (bufin[i] == '@') {
        // simple identification of jid (no real validation)
        jid_local_and_domain++;
      } else if (jid_local_and_domain == 1 && bufin[i] == ' ') {
        bufin[i] = 0;
        jid = &bufin[index];
        index = i + 1;
        break;
      } else if (jid_local_and_domain == 1 && bufin[i + 1] == 0) {
        jid = &bufin[index];
        index = i + 1;
        break;
      }
    }
    if (!jid || JID_MAX < strlen(jid)) {
      fprintf(stdout, "W invalid jid\n");
      fflush(stdout);
      return 1;
    }
  }

  if (command == 'j' || command == 'J') {
    // join room
    const char *password = &bufin[get_start_pos(bufin, index)];

    send_presence(conn, command == 'j' ? PRESENCE_ONLINE_MUC : PRESENCE_ONLINE_MUC_NO_HISTORY,
                  password[0] == 0 ? NULL : password, jid, NULL);
    return 1;
  }
  if (command == 'l') {
    // leave room
    send_presence(conn, PRESENCE_OFFLINE, NULL, jid, NULL);
    return 1;
  }
  if (command == 'p' || command == 'P') {
    // sending presence
    char *presence_text = &bufin[index];
    char *status_text = NULL;
    char *priority_text = NULL;
    short priority = 0;

    index = get_start_pos(bufin, index);
    while (index < bufin_len) {
      if (bufin[index] != ' ') {
        index++;
        continue;
      }
      bufin[index] = 0;
      index = get_start_pos(bufin, index + 1);
      priority = (short)atoi(presence_text);
      if (priority == 0 && (presence_text[0] != '0' || presence_text[1] != 0)) {
        status_text = &bufin[index];
        break;
      } else {
        priority_text = presence_text;
        if (command == 'p') {
          // keep priority (if global presence)
          conn_priority = priority;
        }
        presence_text = &bufin[index];
      }
    }
    if (presence_text[0] == 0) {
      fprintf(stdout, "W no presence\n");
      fflush(stdout);
      return 1;
    }
    if (strcmp(presence_text, "unavailable") == 0) {
      // special check for word unavailable instead of offline
      send_presence(conn, PRESENCE_OFFLINE, status_text, jid, NULL);
      return 1;
    }
    for (short i = 0; i <= PRESENCE_JID_MAX; i++) {
      if (!jid && PRESENCE_MAX < i) {
        fprintf(stdout, "W subscription with invalid jid\n");
        fflush(stdout);
        return 1;
      }
      if ((i <= PRESENCE_MAX && strncmp(presence_text, presence_type[i], 2) == 0)
          || strcmp(presence_text, presence_type[i]) == 0) {
        send_presence(conn, i, status_text, jid, priority_text);
        return 1;
      }
    }
    fprintf(stdout, "W invalid presence\n");
    fflush(stdout);
    return 1;
  }

  if (command == 'm' || command == 'M' || command == 'h' || command == 'n' || command == '.') {
    // sending message
    unsigned short muc_leave = 0;

    if (command == '.') {
      // sending with last command to last jid -> restore values
      if (last_sent_type_jid[0] == 0) {
        fprintf(stdout, "W no last jid\n");
        fflush(stdout);
        return 1;
      }
      command = last_sent_type_jid[0];
      jid = &last_sent_type_jid[1];
    } else {
      // save values for '.' command next time
      last_sent_type_jid[0] = command;
      memccpy(&last_sent_type_jid[1], jid, '\0', JID_MAX - 1);
    }
    if (command == 'm' || command == 'n') {
      if (muc_in_jid_list(jid)) {
        // marker for sending to jid in muc
        command = command == 'm' ? 'x' : 'N';
        last_sent_type_jid[0] = command;
      }
    } else if (command == 'M') {
      if (!muc_in_jid_list(jid)) {
        send_presence(conn, PRESENCE_ONLINE_MUC_NO_HISTORY, NULL, jid, NULL);
        muc_leave = 1;
      }
    }
    if (bufin[index] == 0) {
      // no message text -> use multi line mode
      multi_line = muc_leave ? MULTI_LINE_MUC : MULTI_LINE_ON;
      fprintf(stdout, "i multi line - end with \".\"\n");
      fflush(stdout);
      return 1;
    }
    send_message(conn, jid, command, &bufin[index]);
    if (muc_leave) {
      send_presence(conn, PRESENCE_OFFLINE, NULL, jid, NULL);
    }
    return 1;
  }
  // should not get here...
  fprintf(stdout, "W invalid input\n");
  fflush(stdout);
  return 1;
}

void print_stanza_error(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char* error_from) {
  xmpp_stanza_t *error_stanza, *defined_condition_stanza, *text_stanza;
  const char *error_name = NULL;

  error_stanza = xmpp_stanza_get_child_by_name(stanza, "error");
  if (!error_stanza) {
    return;
  }
  defined_condition_stanza = xmpp_stanza_get_child_by_ns(
                               error_stanza, "urn:ietf:params:xml:ns:xmpp-stanzas");
  while (defined_condition_stanza) {
    // the name is one out of 22 defined conditions, but stanzas in that namespace are only 1 or 2
    const char *error_ns = xmpp_stanza_get_ns(defined_condition_stanza);

    if (error_ns && strcmp(error_ns, "urn:ietf:params:xml:ns:xmpp-stanzas") == 0) {
      error_name = xmpp_stanza_get_name(defined_condition_stanza);
      if (error_name && strcmp(error_name, "text") != 0) {
        break;
      }
    }
    defined_condition_stanza = xmpp_stanza_get_next(defined_condition_stanza);
  }
  if (!error_name) {
    return;
  }
  text_stanza = xmpp_stanza_get_child_by_name(error_stanza, "text");
  if (text_stanza) {
    char *error_text = xmpp_stanza_get_text(text_stanza);

    fprintf(stdout, "e %s -> %s - %s: %s\n",
            error_from, xmpp_stanza_get_type(error_stanza), error_name, error_text);
    xmpp_free(ctx, error_text);
  } else {
    fprintf(stdout, "e %s -> %s - %s\n",
            error_from, xmpp_stanza_get_type(error_stanza), error_name);
  }
  fflush(stdout);
}

int presence_handler(xmpp_conn_t *conn, xmpp_stanza_t *presence_stanza, void *userdata) {
  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
  const char *presence_from = xmpp_stanza_get_from(presence_stanza);
  xmpp_stanza_t *status_stanza = xmpp_stanza_get_child_by_name(presence_stanza, "status");
  const char *type;
  char *status_text = NULL;

  if (!presence_from) {
    presence_from = "";
  }
  if (status_stanza) {
    status_text = xmpp_stanza_get_text(status_stanza);
  }
  type = xmpp_stanza_get_type(presence_stanza);
  if (type) {
    if (strcmp(type, "error") == 0) {
      print_stanza_error(ctx, presence_stanza, presence_from);
    } else if (strcmp(type, "unavailable") == 0) {
      xmpp_stanza_t *extension_stanza = xmpp_stanza_get_child_by_name_and_ns(
                                          presence_stanza, "x", "http://jabber.org/protocol/muc#user");

      if (extension_stanza) {
        // presence from a muc jid
        xmpp_stanza_t *x_status_stanza = xmpp_stanza_get_child_by_name(extension_stanza, "status");
        unsigned short is_self_jid = 0;

        while (x_status_stanza) {
          const char *code_text = xmpp_stanza_get_attribute(x_status_stanza, "code");

          if (code_text) {
            unsigned short code = (unsigned short)atoi(code_text);
            switch (code) {
              case 110:
                // self presence
                is_self_jid++;
                break;
              case 210:
                // nick assigned or modified
                // fall through
              case 303:
                // new nick
                is_self_jid--;
                break;
            }
          }
          x_status_stanza = xmpp_stanza_get_next(x_status_stanza);
        }
        if (is_self_jid == 1) {
          // bare muc jid will remove every room occupant
          char *muc_jid_bare = xmpp_jid_bare(ctx, presence_from);
          update_jid_list(muc_jid_bare, PRESENCE_OFFLINE_MUC, 0);
          xmpp_free(ctx, muc_jid_bare);
        } else {
          // other muc jid - only remove that
          update_jid_list(presence_from, PRESENCE_OFFLINE_MUC, 0);
        }
      } else {
        // no muc jid - change of presence status
        update_jid_list(presence_from, PRESENCE_OFFLINE, 0);
      }
      if (show_presence_status_change) {
        if (status_text) {
          fprintf(stdout, "p %s -> offline: %s\n", presence_from, status_text);
        } else {
          fprintf(stdout, "p %s -> offline\n", presence_from);
        }
      }
    } else {
      // "subscribe", "subscribed", "unsubscribe", "unsubscribed"(, "probe" - server only)
      fprintf(stdout, "p %s -> %s\n", presence_from, type);
    }
  } else {
    char *show_text = NULL;
    short presence = PRESENCE_ONLINE;
    short priority = 0;
    xmpp_stanza_t *priority_stanza, *show_stanza;

    priority_stanza = xmpp_stanza_get_child_by_name(presence_stanza, "priority");
    if (priority_stanza) {
      char *priority_text = xmpp_stanza_get_text(priority_stanza);

      if (priority_text) {
        priority = (short)atoi(priority_text);
      }
      xmpp_free(ctx, priority_text);
    }
    show_stanza = xmpp_stanza_get_child_by_name(presence_stanza, "show");
    if (show_stanza) {
      show_text = xmpp_stanza_get_text(show_stanza);
      if (show_text) {
        // skip 0 offline and 1 online
        for (short i = 2; i <= PRESENCE_MAX; i++) {
          if (strncmp(show_text, presence_type[i], 2) == 0) {
            presence = i;
            break;
          }
        }
      }
    }
    if (xmpp_stanza_get_child_by_name_and_ns(presence_stanza, "x",
                                             "http://jabber.org/protocol/muc#user")) {
      // use inverse presence for muc
      update_jid_list(presence_from, -presence, priority);
    } else {
      update_jid_list(presence_from, presence, priority);
    }
    if (show_presence_status_change) {
      char priority_text[8];

      priority_text[0] = 0;
      if (priority) {
        sprintf(priority_text, " (%d)", priority);
      }
      fprintf(stdout, "p %s -> %s%s",
              presence_from, show_text ? show_text : "online", priority_text);
      if (status_text) {
        fprintf(stdout, ": %s\n", status_text);
      } else {
        fprintf(stdout, "\n");
      }
    }
    if (show_text) {
      xmpp_free(ctx, show_text);
    }
  }
  if (status_text) {
    xmpp_free(ctx, status_text);
  }
  fflush(stdout);
  return 1;
}

void print_raw_stanza(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza) {
  char *text_stanza;
  size_t buflen;

  if (show_raw_stanza[0] == 0) {
    return;
  }
  if (xmpp_stanza_to_text(stanza, &text_stanza, &buflen) == XMPP_EOK) {
    if (strstr(text_stanza, show_raw_stanza)) {
      fprintf(stdout, "r %s\n", text_stanza);
      fflush(stdout);
    }
    xmpp_free(ctx, text_stanza);
  }
}

int message_handler(xmpp_conn_t *conn, xmpp_stanza_t *message_stanza, void *userdata) {
  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
  const char *type = xmpp_stanza_get_type(message_stanza);
  xmpp_stanza_t *carbons_stanza, *body_stanza, *subject_stanza;
  const char *message_from = NULL;
  const char *carbons_sent = "";
  char indicator = 'm';
  char *message_text;

  print_raw_stanza(ctx, message_stanza);
  if (!type) {
    return 1;
  }
  // https://xmpp.org/extensions/xep-0280.html
  carbons_stanza = xmpp_stanza_get_child_by_ns(message_stanza, "urn:xmpp:carbons:2");
  if (carbons_stanza) {
    xmpp_stanza_t *forward_stanza, *forward_message_stanza;
    const char *carbons_name, *sender_jid;

    sender_jid = xmpp_stanza_get_from(message_stanza);
    if (!sender_jid || strcmp(jid_bare, sender_jid) != 0) {
      // fake sender
      return 1;
    }
    carbons_name = xmpp_stanza_get_name(carbons_stanza);
    if (carbons_name && strcmp(carbons_name, "sent") == 0) {
      carbons_sent = "-> ";
    } else if (!carbons_name || strcmp(carbons_name, "received") != 0) {
      // invalid carbon type
      return 1;
    }
    forward_stanza = xmpp_stanza_get_child_by_ns(carbons_stanza, "urn:xmpp:forward:0");
    if (!forward_stanza) {
      return 1;
    }
    forward_message_stanza = xmpp_stanza_get_child_by_name(forward_stanza, "message");
    if (!forward_message_stanza) {
      return 1;
    }
    if (xmpp_stanza_get_child_by_name_and_ns(forward_message_stanza,
                                             "private", "urn:xmpp:carbons:2")) {
      // invalid private -> no carbons
      return 1;
    }
    if (xmpp_stanza_get_child_by_name_and_ns(forward_message_stanza, "x",
                                             "http://jabber.org/protocol/muc#user")) {
      const char *message_to = xmpp_stanza_get_to(forward_message_stanza);
      if (message_to) {
        if (!muc_in_jid_list(message_to)) {
          // invalid message not in muc with this client
          return 1;
        }
      }
    }
    message_stanza = forward_message_stanza;
  }
  if (carbons_sent[0] == 0) {
    message_from = xmpp_stanza_get_from(message_stanza);
  } else {
    // message_from is self jid
    message_from = xmpp_stanza_get_to(message_stanza);
  }
  if (!message_from) {
    message_from = "";
  }
  if (strcmp(type, "error") == 0) {
    print_stanza_error(ctx, message_stanza, message_from);
    return 1;
  }
  if (strcmp(type, "groupchat") == 0) {
    if (xmpp_stanza_get_child_by_ns(message_stanza, "urn:xmpp:delay")) {
      indicator = 'H';
    } else {
      indicator = 'M';
    }
  } else if (strcmp(type, "headline") == 0) {
      indicator = 'h';
  } else if (strcmp(type, "normal") == 0) {
      indicator = 'n';
  }
  subject_stanza = xmpp_stanza_get_child_by_name(message_stanza, "subject");
  if (subject_stanza) {
    message_text = xmpp_stanza_get_text(subject_stanza);
    if (message_text) {
      fprintf(stdout, "%c %s%s -> subject: %s\n",
              indicator, carbons_sent, message_from, message_text);
      fflush(stdout);
    }
    xmpp_free(ctx, message_text);
  }
  body_stanza = xmpp_stanza_get_child_by_name(message_stanza, "body");
  if (!body_stanza) {
    return 1;
  }
  message_text = xmpp_stanza_get_text(body_stanza);
  fprintf(stdout, "%c %s%s: %s\n",
          indicator, carbons_sent, message_from, message_text ? message_text : "");
  fflush(stdout);
  xmpp_free(ctx, message_text);
  return 1;
}

// https://xmpp.org/extensions/xep-0199.html - ping
int ping_send_handler(xmpp_conn_t *conn, void *userdata) {
  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
  xmpp_stanza_t *iq_stanza, *ping_stanza;

  if (ping_id[0]) {
    // already sent -> lost
    xmpp_disconnect(conn);
    return 1;
  }
  sprintf(ping_id, "ping_server_%lu", time(NULL));
  iq_stanza = xmpp_iq_new(ctx, "get", ping_id);
  ping_stanza = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(ping_stanza, "ping");
  xmpp_stanza_set_ns(ping_stanza, "urn:xmpp:ping");
  xmpp_stanza_add_child_ex(iq_stanza, ping_stanza, 0);
  xmpp_send(conn, iq_stanza);
  xmpp_stanza_release(iq_stanza);
  return 1;
}

int iq_handler(xmpp_conn_t *conn, xmpp_stanza_t *iq_stanza, void *userdata) {
  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
  const char *type = xmpp_stanza_get_type(iq_stanza);
  const char *id = xmpp_stanza_get_id(iq_stanza);
  const char *iq_from = xmpp_stanza_get_from(iq_stanza);

  if (!type || !id) {
    return 1;
  }
  if (!iq_from) {
    iq_from = "";
  }
  if (strcmp(type, "result") == 0) {
    if (strcmp(ping_id, id) == 0) {
      ping_id[0] = 0;
      return 1;
    }
    print_raw_stanza(ctx, iq_stanza);
  } else if (strcmp(type, "get") == 0) {
    xmpp_stanza_t *result_stanza;

    if (xmpp_stanza_get_child_by_ns(iq_stanza, "urn:xmpp:ping")) {
      result_stanza = xmpp_iq_new(ctx, "result", id);
    } else {
      xmpp_stanza_t *error_stanza, *defined_condition_stanza;

      result_stanza = xmpp_iq_new(ctx, "error", id);
      error_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_name(iq_stanza, "error");
      xmpp_stanza_set_type(error_stanza, "cancel");
      defined_condition_stanza = xmpp_stanza_new(ctx);
      xmpp_stanza_set_name(defined_condition_stanza, "service-unavailable");
      xmpp_stanza_set_ns(defined_condition_stanza, "urn:ietf:params:xml:ns:xmpp-stanzas");
      xmpp_stanza_add_child_ex(error_stanza, defined_condition_stanza, 0);
      xmpp_stanza_add_child_ex(result_stanza, error_stanza, 0);
    }
    xmpp_stanza_set_to(result_stanza, iq_from ? iq_from : "");
    xmpp_send(conn, result_stanza);
    xmpp_stanza_release(result_stanza);
  } else if (strcmp(type, "error") == 0) {
    if (xmpp_stanza_get_child_by_ns(iq_stanza, "urn:xmpp:ping")) {
      if (strcmp(ping_id, id) == 0) {
        xmpp_timed_handler_delete(conn, ping_send_handler);
        ping_id[0] = 0;
        return 1;
      }
    }
    print_stanza_error(ctx, iq_stanza, iq_from);
  }
  return 1;
}

int certfail_handler(const xmpp_tlscert_t *cert, const char *const error_message) {
  if (ignore_cert_fail) {
    return 1;
  }
  fprintf(stderr, "E %s\n", error_message);
  fflush(stderr);
  reconnect = -1;
  return 0;
}

void conn_handler(xmpp_conn_t *conn, xmpp_conn_event_t status,
                  int error, xmpp_stream_error_t *stream_error, void *userdata) {
  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);

  if (status == XMPP_CONN_CONNECT) {
    xmpp_stanza_t *carbons_stanza, *enable_stanza;

    xmpp_handler_add(conn, iq_handler, NULL, "iq", NULL, NULL);
    xmpp_handler_add(conn, presence_handler, NULL, "presence", NULL, NULL);
    xmpp_handler_add(conn, message_handler, NULL, "message", NULL, NULL);
    xmpp_timed_handler_add(conn, input_handler, 750, NULL);
    xmpp_timed_handler_add(conn, ping_send_handler, PING_INTERVAL * 1000, NULL);
    send_presence(conn, PRESENCE_ONLINE, NULL, NULL, NULL);
    carbons_stanza = xmpp_iq_new(ctx, "set", "carbons_enable1");
    enable_stanza = xmpp_stanza_new(ctx);
    xmpp_stanza_set_name(enable_stanza, "enable");
    xmpp_stanza_set_ns(enable_stanza, "urn:xmpp:carbons:2");
    xmpp_stanza_add_child_ex(carbons_stanza, enable_stanza, 0);
    xmpp_send(conn, carbons_stanza);
    xmpp_stanza_release(carbons_stanza);
    fprintf(stdout, "i online: %s\n", xmpp_conn_get_bound_jid(conn));
    fflush(stdout);
  } else {
    xmpp_timed_handler_delete(conn, ping_send_handler);
    xmpp_timed_handler_delete(conn, input_handler);
    xmpp_handler_delete(conn, message_handler);
    xmpp_handler_delete(conn, presence_handler);
    xmpp_handler_delete(conn, iq_handler);
    xmpp_stop(ctx);
  }
}

void signal_handler(int signum) {
  // inverse signum -> negative reconnect means exit
  reconnect = (sig_atomic_t)-(signum + 128);
}

int main(int argc, char **argv) {
  xmpp_ctx_t *ctx;
  xmpp_conn_t *conn;
  int connected;
  char *jid, *pass;
  char *arg_null = NULL;
  char *server = NULL;
  unsigned short port = 0;
  long flags = 0;
  struct sigaction st_sigaction;

  if (argc == 2 && argv[1][0] == '-') {
    arg_null = (char*) malloc(BUFIN_MAX);
    if (!arg_null) {
      return 1;
    }
    fgets(arg_null, BUFIN_MAX, stdin);
    argv[1] = arg_null;
    for (unsigned short i = 0; i < BUFIN_MAX; i++) {
      if (arg_null[i] == '\0') {
        if (arg_null[i + 1] == '\n') {
          break;
        }
        argv[argc++] = &arg_null[i + 1];
      }
    }
  }
  if (argc < 3) {
    fprintf(stderr, "E Usage: %s <jid> <pass> [[=]<server>[(:|=)<port>]]\n", argv[0]);
    fflush(stderr);
    return 1;
  }
  jid = argv[1];
  if (memccpy(nick, jid, '\0', NICK_MAX - 1) == NULL) {
    nick[NICK_MAX - 1] = 0;
  }
  if (memccpy(jid_bare, jid, '\0', JID_BARE_MAX - 1) == NULL) {
    jid_bare[JID_BARE_MAX - 1] = 0;
  }
  for (unsigned short i = 0; i < JID_BARE_MAX; i++) {
    if (i < NICK_MAX && nick[i] == '@') {
    // default nick from jid (if muc is not joined with muc_jid/nick)
      nick[i] = 0;
    } else if (jid_bare[i] == '/') {
    // no resource, if given
      jid_bare[i] = 0;
      break;
    } else if (jid_bare[i] == 0) {
      break;
    }
  }
  pass = argv[2];
  if (4 == argc) {
    // custom server and port
    for (unsigned short i = 1; i < strlen(argv[3]); i++) {
      if (argv[3][i] == ':' || argv[3][i] == '=') {
        if (argv[3][i] == '=') {
          ignore_cert_fail = 1;
        }
        port = (unsigned short)atoi(&argv[3][i + 1]);
        argv[3][i] = 0;
        break;
      }
    }
    if (argv[3][0] == '=') {
      // TLS
      flags = XMPP_CONN_FLAG_LEGACY_SSL;
      server = &argv[3][1];
    } else {
      server = argv[3];
    }
  }
  last_sent_type_jid[0] = 0;
  show_raw_stanza[0] = 0;
  srand((unsigned int)time(NULL));
  xmpp_initialize();
#ifdef DEBUG
  ctx = xmpp_ctx_new(NULL, xmpp_get_default_logger(XMPP_LEVEL_DEBUG));
#else
  ctx = xmpp_ctx_new(NULL, NULL);
#endif
  if (!ctx) {
    fprintf(stderr, "E cannot initialize ctx\n");
    fflush(stderr);
    return 1;
  }
  conn = xmpp_conn_new(ctx);
  if (!conn) {
    fprintf(stderr, "E cannot initialize conn\n");
    fflush(stderr);
    return 1;
  }
  xmpp_conn_set_jid(conn, jid);
  xmpp_conn_set_pass(conn, pass);
  xmpp_conn_set_certfail_handler(conn, certfail_handler);
  xmpp_conn_set_flags(conn, flags);
  memset(&st_sigaction, 0, sizeof(st_sigaction));
  st_sigaction.sa_handler = &signal_handler;
  sigaction(SIGINT, &st_sigaction, NULL);
  sigaction(SIGTERM, &st_sigaction, NULL);
  fcntl(0, F_SETFL, O_NONBLOCK);
  for (;;) {
    struct list_node *list_current;
    unsigned short unpredictable;

    ping_id[0] = 0;
    connected = xmpp_connect_client(conn, server, port, conn_handler, NULL);
    if (connected != XMPP_EOK) {
      fprintf(stderr, "E connection failed\n");
      fflush(stderr);
      reconnect = -1;
      break;
    }
    xmpp_run(ctx);
    // clean jid presence list
    while (jid_list) {
      list_current = jid_list->next;
      free(jid_list);
      jid_list = list_current;
    }
    if (reconnect <= 0) {
      fprintf(stdout, "i offline\n");
      fflush(stdout);
      break;
    }
    // https://www.rfc-editor.org/rfc/rfc6120 -> 3.3. Reconnection:
    // SHOULD be set to an unpredictable number between 0 and 60 (seconds)
    unpredictable = (unsigned short)rand() % 61;
    fprintf(stdout, "I offline - lost connection - try reconnect in %ds ...\n", unpredictable);
    fflush(stdout);
    sleep(unpredictable);
  }
  xmpp_conn_release(conn);
  xmpp_ctx_free(ctx);
  xmpp_shutdown();
  if (arg_null) {
    free(arg_null);
  }
  // reconnect is 0, -1 on ssl fail or -signum
  return -reconnect;
}

