static async createHunt (req, res) {
    // make sure all the necessary data is included in the post body
    if (!req.body.totalSessions) { return res.serverError(403, 'This hunt does not apply to any sessions'); }
    if (!ArkimeUtil.isString(req.body.name)) { return res.serverError(403, 'Missing hunt name'); }
    if (!req.body.size) { return res.serverError(403, 'Missing max mumber of packets to examine per session'); }
    if (!ArkimeUtil.isString(req.body.search)) { return res.serverError(403, 'Missing packet search text'); }
    if (!req.body.src && !req.body.dst) {
      return res.serverError(403, 'The hunt must search source or destination packets (or both)');
    }
    if (!req.body.query) { return res.serverError(403, 'Missing query'); }
    if (req.body.query.startTime === undefined || req.body.query.stopTime === undefined) {
      return res.serverError(403, 'Missing fully formed query (must include start time and stop time)');
    }

    const searchTypes = ['ascii', 'asciicase', 'hex', 'regex', 'hexregex'];
    if (!ArkimeUtil.isString(req.body.searchType)) {
      return res.serverError(403, 'Missing packet search text type');
    } else if (searchTypes.indexOf(req.body.searchType) === -1) {
      return res.serverError(403, 'Improper packet search text type. Must be "ascii", "asciicase", "hex", "hexregex", or "regex"');
    }

    if (!ArkimeUtil.isString(req.body.type)) {
      return res.serverError(403, 'Missing packet search type (raw or reassembled packets)');
    } else if (req.body.type !== 'raw' && req.body.type !== 'reassembled') {
      return res.serverError(403, 'Improper packet search type. Must be "raw" or "reassembled"');
    }

    const limit = req.user.hasRole('arkimeAdmin') ? Config.get('huntAdminLimit', 10000000) : Config.get('huntLimit', 1000000);
    if (parseInt(req.body.totalSessions) > limit) {
      return res.serverError(403, `This hunt applies to too many sessions. Narrow down your session search to less than ${limit} first.`);
    }

    if (req.body.roles !== undefined && !ArkimeUtil.isStringArray(req.body.roles)) {
      return res.serverError(403, 'Roles field must be an array of strings');
    }

    if (req.body.users !== undefined && !ArkimeUtil.isString(req.body.users, 0)) {
      return res.serverError(403, 'Users field must be a string');
    }

    const now = Math.floor(Date.now() / 1000);

    req.body.name = req.body.name.replace(/[^-a-zA-Z0-9_: ]/g, '');

    const hunt = {
      name: req.body.name,
      size: req.body.size,
      search: req.body.search,
      searchType: req.body.searchType,
      type: req.body.type,
      src: req.body.src,
      dst: req.body.dst,
      totalSessions: req.body.totalSessions,
      notifier: req.body.notifier,
      created: now,
      status: 'queued', // always starts as queued
      userId: req.user.userId,
      matchedSessions: 0, // start with no matches
      searchedSessions: 0, // start with no sessions searched
      query: { // only use the necessary query items
        expression: req.body.query.expression,
        startTime: req.body.query.startTime,
        stopTime: req.body.query.stopTime,
        view: req.body.query.view
      },
      description: req.body.description,
      roles: req.body.roles
    };

    async function doneCb (doneHunt, invalidUsers) {
      try {
        const { body: result } = await Db.createHunt(doneHunt, req.query.cluster);
        doneHunt.id = result._id;
        HuntAPIs.processHuntJobs(() => {
          const response = {
            success: true,
            hunt: doneHunt
          };

          if (invalidUsers) {
            response.invalidUsers = invalidUsers;
          }

          return res.send(JSON.stringify(response));
        });
      } catch (err) {
        console.log(`ERROR - ${req.method} /api/hunt`, util.inspect(err, false, 50));
        return res.serverError(500, 'Error creating hunt');
      }
    }

    if (!req.body.users || !req.body.users.length) {
      return doneCb(hunt);
    }

    const reqUsers = ArkimeUtil.commaOrNewlineStringToArray(req.body.users);

    try {
      const users = await User.validateUserIds(reqUsers);
      hunt.users = users.validUsers;
      // dedupe the array of users
      hunt.users = [...new Set(hunt.users)];
      return doneCb(hunt, users.invalidUsers);
    } catch (err) {
      return res.serverError(500, err);
    }
  }
POST - /api/hunt

Creates a new hunt.
@name /hunt
@param {SessionsQuery} See_List - This API supports a common set of parameters documented in the SessionsQuery section
@param {number} totalSessions - The number of sessions to search.
@param {string} name - The name of the hunt (not unique).
@param {number} size - The number of packets to search within each session.
@param {boolean} src - Whether to search the source packets. Must search src or dst or both.
@param {boolean} dst - Whether to search the destination packets. Must search src or dst or both.
@param {string} type - Whether to search raw or reassembled packets.
@param {string} search - The search text to search for within packets.
@param {string} searchType - What type of search the text is. Options include:
     ascii - search for case insensitive ascii text.
     asciicase - search for case sensitive ascii text.
     hex - search for hex text.
     regex - search for text using <a href="https://github.com/google/re2/wiki/Syntax">safe regex</a>.
     hexregex - search for text using <a href="https://github.com/google/re2/wiki/Syntax">safe hex regex</a>.
@param {string} notifier - The optional notifier name to fire when there is an error, or there are matches (every 10 minutes), or when the hunt is complete.
@param {string} users - The comma separated list of users to be added to the hunt so they can view the results.
@returns {boolean} success - Whether the creation of the hunt was successful.
@returns {Hunt} hunt - The newly created hunt object.
@returns {array} invalidUsers - The list of users that could not be added to the hunt because they were invalid or nonexitent.