> ## Documentation Index
> Fetch the complete documentation index at: https://developer.box.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Slackイベントの処理

export const ProgressBar = ({pages = [], ...props}) => {
  const [currentStep, setCurrentStep] = useState(0);
  const [isDarkMode, setIsDarkMode] = useState(false);
  useEffect(() => {
    const checkDarkMode = () => {
      const isDark = document.documentElement.classList.contains('dark');
      console.log('ProgressBar - isDarkMode:', isDark);
      setIsDarkMode(isDark);
    };
    checkDarkMode();
    const observer = new MutationObserver(() => {
      checkDarkMode();
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => {
      observer.disconnect();
    };
  }, []);
  useEffect(() => {
    if (pages.length > 0) {
      const currentPath = window.location.pathname;
      const stepIndex = pages.findIndex(page => {
        const pagePath = page.startsWith('/') ? page : `/${page}`;
        return currentPath.endsWith(pagePath) || currentPath.includes(pagePath);
      });
      if (stepIndex !== -1) {
        setCurrentStep(stepIndex + 1);
      }
    }
  }, [pages]);
  if (!pages || pages.length === 0) {
    return null;
  }
  const step = currentStep;
  const total = pages.length;
  console.log('ProgressBar - Rendering with isDarkMode:', isDarkMode);
  const progressBarContainerStyle = {
    width: '100%',
    marginBottom: '32px',
    display: 'flex',
    alignItems: 'center',
    gap: '16px'
  };
  const stepsContainerStyle = {
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    flexShrink: 0
  };
  const progressBarTrackStyle = {
    flex: 1,
    height: '22px',
    backgroundColor: 'rgba(169, 210, 244, 0.06)',
    border: isDarkMode ? '1px solid rgba(230, 241, 247, 0.67)' : '1px solid #e3ecf3',
    borderRadius: '4px',
    overflow: 'hidden',
    position: 'relative'
  };
  const progressBarFillStyle = {
    height: '100%',
    backgroundColor: 'rgba(113, 192, 248, 0.23)',
    width: `${step / total * 100}%`,
    transition: 'width 0.3s ease'
  };
  const getStepStyle = (stepNumber, isActive) => {
    if (isDarkMode) {
      return {
        width: '22px',
        height: '22px',
        borderRadius: '4px',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: '12px',
        fontWeight: '600',
        position: 'relative',
        zIndex: 1,
        transition: 'all 0.3s ease',
        backgroundColor: isActive ? 'rgba(113, 192, 248, 0.23)' : 'transparent',
        color: isActive ? '#60a5fa' : '#a0aec0',
        border: isActive ? '1px solid #e3ecf3' : '1px solid #e0e6eb',
        cursor: 'pointer',
        textDecoration: 'none'
      };
    }
    if (isActive) {
      return {
        width: '22px',
        height: '22px',
        borderRadius: '4px',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: '12px',
        fontWeight: '600',
        position: 'relative',
        zIndex: 1,
        transition: 'all 0.3s ease',
        backgroundColor: 'rgba(169, 210, 244, 0.32)',
        color: '#374151',
        border: '1px solid #e1eef8',
        cursor: 'pointer',
        textDecoration: 'none'
      };
    }
    return {
      width: '22px',
      height: '22px',
      borderRadius: '4px',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      fontSize: '12px',
      fontWeight: '600',
      position: 'relative',
      zIndex: 1,
      transition: 'all 0.3s ease',
      backgroundColor: '#fbfbfb',
      color: '#9ca3af',
      border: '1px solid #e3ecf3',
      cursor: 'pointer',
      textDecoration: 'none'
    };
  };
  return <div style={progressBarContainerStyle} {...props}>
      <div style={stepsContainerStyle}>
        {Array.from({
    length: total
  }, (_, index) => {
    const stepNumber = index + 1;
    const pageIndex = index;
    const pagePath = pages[pageIndex];
    const fullPath = pagePath.startsWith('/') ? pagePath : `/${pagePath}`;
    const isActive = stepNumber === step;
    return <a key={stepNumber} href={fullPath} style={getStepStyle(stepNumber, isActive)}>
              {stepNumber}
            </a>;
  })}
      </div>
      <div style={progressBarTrackStyle}>
        <div style={progressBarFillStyle}></div>
      </div>
    </div>;
};

export const ChoiceDebug = ({option}) => {
  const [currentValue, setCurrentValue] = useState(null);
  const [allState, setAllState] = useState({});
  const [isDarkMode, setIsDarkMode] = useState(false);
  useEffect(() => {
    const updateState = () => {
      if (window.choiceStateManager) {
        setCurrentValue(window.choiceStateManager.getValue(option));
        setAllState(window.choiceStateManager.getState());
      }
    };
    updateState();
    const unsubscribe = window.listenToChoice?.(option, updateState) || (() => {});
    const handleGlobalUpdate = () => updateState();
    window.addEventListener("choiceStateUpdate", handleGlobalUpdate);
    return () => {
      unsubscribe();
      window.removeEventListener("choiceStateUpdate", handleGlobalUpdate);
    };
  }, [option]);
  useEffect(() => {
    const checkDarkMode = () => {
      if (document.documentElement.classList.contains("dark")) {
        setIsDarkMode(true);
      } else if (document.documentElement.classList.contains("light")) {
        setIsDarkMode(false);
      } else {
        setIsDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches);
      }
    };
    checkDarkMode();
    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    const handleMediaChange = e => {
      if (!document.documentElement.classList.contains("dark") && !document.documentElement.classList.contains("light")) {
        setIsDarkMode(e.matches);
      }
    };
    mediaQuery.addEventListener("change", handleMediaChange);
    const observer = new MutationObserver(() => {
      checkDarkMode();
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => {
      mediaQuery.removeEventListener("change", handleMediaChange);
      observer.disconnect();
    };
  }, []);
  return <div style={{
    padding: "10px",
    backgroundColor: isDarkMode ? "#2d3748" : "#f5f5f5",
    border: isDarkMode ? "1px solid #4a5568" : "1px solid #ddd",
    borderRadius: "4px",
    fontSize: "12px",
    fontFamily: "monospace",
    marginTop: "20px",
    color: isDarkMode ? "#e2e8f0" : "inherit"
  }}>
      <strong>{translate("Choice Debug:")}</strong>
      <br />
      {translate("Option:")} {option}
      <br />
      {translate("Current Value:")} {currentValue || "undefined"}
      <br />
      {translate("All State:")} {JSON.stringify(allState, null, 2)}
    </div>;
};

export const Observe = ({option, value, children, ...props}) => {
  const [shouldShow, setShouldShow] = useState(false);
  useEffect(() => {
    const updateVisibility = () => {
      const matches = window.matchesChoiceValues?.(option, value) || false;
      setShouldShow(matches);
    };
    updateVisibility();
    const unsubscribe = window.listenToChoice?.(option, updateVisibility) || (() => {});
    return unsubscribe;
  }, [option, value]);
  if (!shouldShow) {
    return null;
  }
  return <div {...props}>{children}</div>;
};

export const Trigger = ({option, value, children, ...props}) => {
  const handleClick = () => {
    window.triggerChoice?.(option, value);
  };
  return <div onClick={handleClick} style={{
    cursor: "pointer"
  }} {...props}>
      {children}
    </div>;
};

export const Grid = ({columns = 2, compact = false, children, ...props}) => {
  const gridStyles = {
    display: "grid",
    gridTemplateColumns: `repeat(${columns}, 1fr)`,
    gap: compact ? "8px" : "16px",
    marginBottom: compact ? "10px" : "20px"
  };
  return <div style={gridStyles} {...props}>
      {children}
    </div>;
};

export const Choice = ({option, value, color = "", unset = false, lazy = false, children, ...props}) => {
  const [shouldShow, setShouldShow] = useState(false);
  const [hasEverShown, setHasEverShown] = useState(false);
  const [isDarkMode, setIsDarkMode] = useState(false);
  useEffect(() => {
    const updateVisibility = () => {
      const hasOptionValue = window.hasChoiceValue?.(option) || false;
      const matchesValue = window.matchesChoiceValues?.(option, value) || false;
      let show = false;
      if (unset && !hasOptionValue) {
        show = true;
      } else if (!unset && matchesValue) {
        show = true;
      }
      setShouldShow(show);
      if (show && !hasEverShown) {
        setHasEverShown(true);
      }
    };
    updateVisibility();
    const unsubscribe = window.listenToChoice?.(option, updateVisibility) || (() => {});
    return unsubscribe;
  }, [option, value, unset, hasEverShown]);
  useEffect(() => {
    const checkDarkMode = () => {
      if (document.documentElement.classList.contains("dark")) {
        setIsDarkMode(true);
      } else if (document.documentElement.classList.contains("light")) {
        setIsDarkMode(false);
      } else {
        setIsDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches);
      }
    };
    checkDarkMode();
    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    const handleMediaChange = e => {
      if (!document.documentElement.classList.contains("dark") && !document.documentElement.classList.contains("light")) {
        setIsDarkMode(e.matches);
      }
    };
    mediaQuery.addEventListener("change", handleMediaChange);
    const observer = new MutationObserver(() => {
      checkDarkMode();
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => {
      mediaQuery.removeEventListener("change", handleMediaChange);
      observer.disconnect();
    };
  }, []);
  const getColorStyles = () => {
    const baseStyles = {
      border: isDarkMode ? "1px dashed #4a5568" : "1px dashed #e1e5e9",
      padding: "20px",
      marginBottom: "20px",
      borderRadius: "8px",
      backgroundColor: isDarkMode ? "#1a202c" : "#ffffff"
    };
    const colorMap = {
      green: {
        light: {
          backgroundColor: "#d4edda",
          borderColor: "#28a745"
        },
        dark: {
          backgroundColor: "#1a3a2a",
          borderColor: "#66bb6a"
        }
      },
      red: {
        light: {
          backgroundColor: "#f8d7da",
          borderColor: "#dc3545"
        },
        dark: {
          backgroundColor: "#3a1a1a",
          borderColor: "#ef5350"
        }
      },
      blue: {
        light: {
          backgroundColor: "#d1ecf1",
          borderColor: "#0c5460"
        },
        dark: {
          backgroundColor: "#1a2a3a",
          borderColor: "#42a5f5"
        }
      },
      none: {
        backgroundColor: "transparent",
        padding: "0",
        margin: "0",
        border: "none"
      }
    };
    const colorStyles = color !== "none" ? colorMap[color]?.[isDarkMode ? "dark" : "light"] || ({}) : colorMap.none;
    return {
      ...baseStyles,
      ...colorStyles
    };
  };
  if (lazy && !hasEverShown && !shouldShow) {
    return null;
  }
  return <div style={{
    ...getColorStyles(),
    display: shouldShow ? "block" : "none"
  }} className="choice-content" {...props}>
      {children}
    </div>;
};

export const Choose = ({option, value, color = "", children, ...props}) => {
  const [isSelected, setIsSelected] = useState(false);
  const [hasOptionTriggered, setHasOptionTriggered] = useState(false);
  const [isDarkMode, setIsDarkMode] = useState(false);
  useEffect(() => {
    const currentValue = window.getChoiceValue?.(option);
    const optionTriggered = window.hasChoiceValue?.(option) || false;
    setIsSelected(currentValue === value);
    setHasOptionTriggered(optionTriggered);
    const unsubscribe = window.listenToChoice?.(option, newValue => {
      setIsSelected(newValue === value);
      setHasOptionTriggered(true);
    }) || (() => {});
    return unsubscribe;
  }, [option, value]);
  useEffect(() => {
    const checkDarkMode = () => {
      if (document.documentElement.classList.contains("dark")) {
        setIsDarkMode(true);
      } else if (document.documentElement.classList.contains("light")) {
        setIsDarkMode(false);
      } else {
        setIsDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches);
      }
    };
    checkDarkMode();
    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    const handleMediaChange = e => {
      if (!document.documentElement.classList.contains("dark") && !document.documentElement.classList.contains("light")) {
        setIsDarkMode(e.matches);
      }
    };
    mediaQuery.addEventListener("change", handleMediaChange);
    const observer = new MutationObserver(() => {
      checkDarkMode();
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => {
      mediaQuery.removeEventListener("change", handleMediaChange);
      observer.disconnect();
    };
  }, []);
  const handleClick = () => {
    window.triggerChoice?.(option, value);
  };
  const handleKeyDown = e => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      handleClick();
    }
  };
  const getColorStyles = () => {
    const baseStyles = {
      border: isDarkMode ? "1px dashed #4a5568" : "1px dashed #e1e5e9",
      cursor: "pointer",
      padding: "20px",
      position: "relative",
      backgroundColor: isDarkMode ? "#2d3748" : "#f8f9fa",
      outline: "none",
      height: "100%",
      borderRadius: "8px",
      transition: "all 0.2s ease",
      display: "flex",
      flexDirection: "column"
    };
    const colorMap = {
      green: {
        light: {
          backgroundColor: "#d4edda",
          borderColor: "#28a745"
        },
        dark: {
          backgroundColor: "#1a3a2a",
          borderColor: "#66bb6a"
        }
      },
      red: {
        light: {
          backgroundColor: "#f8d7da",
          borderColor: "#dc3545"
        },
        dark: {
          backgroundColor: "#3a1a1a",
          borderColor: "#ef5350"
        }
      },
      blue: {
        light: {
          backgroundColor: "#d1ecf1",
          borderColor: "#0c5460"
        },
        dark: {
          backgroundColor: "#1a2a3a",
          borderColor: "#42a5f5"
        }
      }
    };
    const colorStyles = colorMap[color]?.[isDarkMode ? "dark" : "light"] || ({});
    if (isSelected) {
      return {
        ...baseStyles,
        ...colorStyles,
        borderStyle: "solid",
        borderWidth: "3px",
        borderColor: colorStyles.borderColor || (isDarkMode ? "#42a5f5" : "#0061d5"),
        backgroundColor: colorStyles.backgroundColor || (isDarkMode ? "#1a2a3a" : "#e3f2fd"),
        boxShadow: isDarkMode ? "0 2px 8px rgba(66, 165, 245, 0.3)" : "0 2px 8px rgba(0, 97, 213, 0.3)",
        transform: "scale(1.02)"
      };
    }
    if (hasOptionTriggered && !isSelected) {
      return {
        ...baseStyles,
        ...colorStyles,
        opacity: 0.5
      };
    }
    return {
      ...baseStyles,
      ...colorStyles
    };
  };
  const iconStyles = {
    float: "left",
    position: "relative",
    top: "2px",
    marginRight: "12px",
    width: "20px",
    height: "20px",
    color: isSelected ? isDarkMode ? "#42a5f5" : "#0061d5" : isDarkMode ? "#a0aec0" : "#666"
  };
  return <div onClick={handleClick} style={getColorStyles()} tabIndex={0} onKeyDown={handleKeyDown} {...props}>
      <div style={iconStyles}>
        {isSelected ? <svg viewBox="0 0 24 24" fill="currentColor" style={{
    width: "100%",
    height: "100%"
  }}>
            <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
          </svg> : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{
    width: "100%",
    height: "100%"
  }}>
            <circle cx="12" cy="12" r="10" />
          </svg>}
      </div>
      <div style={{
    flex: 1
  }} className="choose-content">
        {children}
      </div>
    </div>;
};

<ProgressBar
  pages={[
                    "guides/collaborations/connect-slack-to-group-collabs/configure-slack",
                    "guides/collaborations/connect-slack-to-group-collabs/configure-box",
                    "guides/collaborations/connect-slack-to-group-collabs/scaffold-application-code",
                    "guides/collaborations/connect-slack-to-group-collabs/handle-slack-events",
                    "guides/collaborations/connect-slack-to-group-collabs/connect-box-functions",
                    "guides/collaborations/connect-slack-to-group-collabs/test-bot"
                  ]}
/>

アプリケーションのスキャフォールドを設定したら、次に、User Eventの処理機能と、Slackから送信されるスラッシュコマンドの処理機能を構築します。最終的に、これらの機能はそれぞれBox APIエンドポイントに渡されて、グループおよびコンテンツのコラボレーションタスクを実行します。

この手順では、直前の手順で作成した空の関数を拡張します。これらの関数では、以下のタスクを実行します。

* Slackからの新しいイベントとスラッシュコマンドをリッスンする
* これらのイベントとコマンドを処理して適切な関数に送る
* ボットが初めてチャンネルに追加されたときにBoxグループに追加されるようにチャンネル内のすべてのSlackユーザーを処理する
* Slackユーザーのプロフィール情報を取得してそのメールアドレスを取得する

## Slackイベントのリッスン

Slackアプリケーションを構成したときに、3つのイベントのアプリケーションコードにイベントを送信するようSlackアプリケーションに指示しました。

* ユーザーがチャンネルに参加したとき。
* ユーザーがチャンネルから退出したとき。
* ユーザーが`/boxadd`スラッシュコマンドを入力したとき。

このアプリケーションには、Slackからのこれらのメッセージをリッスンする公開ルートが必要です。このメッセージのペイロードは、次のようになります。

<CodeGroup>
  ```json "/boxadd"-command theme={null}
  {
    "token": "cF1PwB1eIMcRHZWwFHJR1tgs",
    "team_id": "T932DQSV12P",
    "team_domain": "slacktest",
    "channel_id": "C078N43MFHU",
    "channel_name": "bottest",
    "user_id": "U016JCDPN56",
    "user_name": "testuser",
    "command": "/boxadd",
    "text": "file 123456",
    "response_url": "https://hooks.slack.com/commands/T541DQSV12P/3977594927231/ankvsRb42WKnKPRp002FeyTx",
    "trigger_id": "1189442196855.1183332180295.cca20c3ca1ea193dab432ad8e9e95431"
  }
  ```

  ```json "member_joined_channel"-event theme={null}
  {
    "token": "cF1PwB1eIMcRHZWwFHJR1tgs",
    "team_id": "T932DQSV12P",
    "api_app_id": "A321V573PQT",
    "event": {
      "type": "member_joined_channel",
      "user": "U0431JM4RLZ",
      "channel": "C078N43MFHU",
      "channel_type": "C",
      "team": "T932DQSV12P",
      "inviter": "U016JCDPN56",
      "event_ts": "1592858788.000700"
    },
    "type": "event_callback",
    "event_id": "Ev032NRJYASJ",
    "event_time": 1592858788,
    "authed_users": [ "U0431JM4RLZ" ]
  }
  ```

  ```json "member_left_channel"-event theme={null}
  {
    "token": "cF1PwB1eIMcRHZWwFHJR1tgs",
    "team_id": "T932DQSV12P",
    "api_app_id": "A321V573PQT",
    "event": {
      "type": "member_left_channel",
      "user": "U0431JM4RLZ",
      "channel": "C078N43MFHU",
      "channel_type": "C",
      "team": "T932DQSV12P",
      "event_ts": "1593033236.000600"
    },
    "type": "event_callback",
    "event_id": "Ev032NRJYASJ",
    "event_time": 1593033236,
    "authed_users": [ "U0431JM4RLZ" ]
  }
  ```
</CodeGroup>

<Choice option="programming.platform" value="node" color="none">
  これらのイベントの処理を開始するには、任意のエディタに`process.js`を読み込み、`app.post("/event" ...`リスナーを次の内容に置き換えます。

  ```js theme={null}
  app.post("/event", (req, res) => {
      if (req.body.token !== slackConfig.verificationToken) {
          res.send("Slack Verification Failed");
      }

      handler.process(res, req.body);
  });
  ```

  イベントが成功すると、リスナーではSlackアプリケーションからの確認トークンを使用して、メッセージがSlackから届いたことを確認します。メッセージが有効なリクエストであれば、イベントペイロードがイベント処理関数に送信されます。
</Choice>

<Choice option="programming.platform" value="java" color="none">
  任意のエディタに`Application.java`を読み込み、`@PostMapping("/event")`ブロックを次の内容に置き換えます。

  ```java theme={null}
  @PostMapping("/event")
  @ResponseBody
  public void handleEvent(@RequestBody String data, @RequestHeader("Content-Type") String contentType, HttpServletResponse response) throws Exception {
      int code = HttpServletResponse.SC_OK;
      java.io.PrintWriter wr = response.getWriter();
      response.setStatus(code);

      if (contentType.startsWith(MediaType.APPLICATION_JSON_VALUE)) {
          wr.write("Adding content to group");
      } else {
          wr.print(response);
      }

      wr.flush();
      wr.close();

      if (! contentType.startsWith(MediaType.APPLICATION_JSON_VALUE)) {
          JSONObject returnJSON = new JSONObject();
          String[] inputParts = data.split("&");

          for (String part: inputParts) {
              String[] keyval = part.split("=");

              try {
                  keyval[1] = java.net.URLDecoder.decode(keyval[1], StandardCharsets.UTF_8.name());
              } catch (UnsupportedEncodingException e) {
                  System.err.println(e);
              }

              returnJSON.put(keyval[0], keyval[1]);
          }

          data = returnJSON.toString();
      }

      processEvent(data);
  }
  ```

  イベントが成功すると、ハンドラは、コードを処理する前に、直ちにHTTP200レスポンスを返します。スラッシュコマンドはURLでエンコードされた文字列として送信されるのに対し、メンバーの参加/退出イベントはJSONとして送信されます。スラッシュコマンドが検出されると、処理中のメッセージで応答します。それ以外の場合は、`HttpServletResponse`レスポンスを送信します。

  <Note>
    この例では、イベントがすべて処理される前に`HTTP 200`レスポンスが送信されます。その理由は、Slackではイベントの送信後3秒以内にレスポンスを必要とするためです。コードの実行時間が3秒を超える場合は、重複したイベントがSlackによって送信されます。
  </Note>

  イベント処理を容易にするには、すべてのイベントオブジェクトをJSONに標準化します。コンテンツタイプがJSONでない場合は、URLでエンコードされた文字列になります。それが検出されると、その文字列は、JSONオブジェクトに変換されてから`processEvent`に送信されます。

  `processEvent`を以下の内容に置き換えます。

  ```java theme={null}
  @Async
  public void processEvent(String data) throws Exception {
      Object dataObj = new JSONParser().parse(data);
      JSONObject inputJSON = (JSONObject) dataObj;
      String token = (String) inputJSON.get("token");

      if (token.equals(slackConfig.verificationToken)) {
          // INSTANTIATE BOX CLIENT

          process(inputJSON);
      } else {
          System.err.println("Invalid event source");
      }
  }
  ```

  このメソッドは、JSONイベント文字列をJSONオブジェクトに変換した後、確認トークンを比較して、イベントがSlackから送信されたかどうかを確認します。有効な場合は、イベントが`process`に転送されます。
</Choice>

<Choice option="programming.platform" unset color="none">
  <Danger>
    **前の手順が完了していません**最初に、手順1でお好みの言語/フレームワークを選択してください。
  </Danger>
</Choice>

## Slackイベントの処理

次に、受信したイベントを判定し、アプリケーションの適切な機能にそのイベントを渡します。

<Choice option="programming.platform" value="node" color="none">
  `process`関数を次の内容に置き換えます。

  ```js theme={null}
  function process(res, data) {
      if (data.type && data.type === "event_callback") {
          const eventType = data.event.type;
          const channel = data.event.channel;
          const userId = data.event.user;

          getSlackUser(userId, function (user) {
              processUser(user, eventType, channel);
          });

          res.send();
      } else if (data.command && data.command === "/boxadd") {
          const [itemType, itemId] = data.text.split(" ");
          if (["file", "folder"].includes(itemType) && !isNaN(itemId)) {
              const userId = data.user_id;

              getSlackUser(userId, function (user) {
                  processContent(user, data.channel_id, itemType, itemId);
              });
              res.send("Adding content");
          } else {
              res.send("Invalid input. Example usage: /boxadd file 123456");
          }
      } else {
          res.send("Invalid action");
      }
  }
  ```

  この関数の目的は、SlackからのペイロードがUser Eventとスラッシュコマンドのどちらであるかを判断し、必要な情報をすべて取得して、結果を処理するために適切な関数に転送することです。

  ペイロードがUser Eventの場合 (`event_callback`に設定されている`data.type`によって示されます)、いくつかの情報を抽出します。

  * `eventType`: ユーザーがチャンネルから退出する (`member_left_channel`) かチャンネルに参加する (`member_joined_channel`) かを決定するイベントのタイプ。
  * `channel`: チャンネルID。Boxグループ名として使用されます。
  * `userId`: ユーザーのID。同じメールアドレスを使用するBoxのユーザープロフィールにバインドされるプロフィールのメールアドレスを検索するためのものです。

  その後、process関数は`getSlackUser`を呼び出してユーザーのプロフィールを取得します。取得したユーザープロフィールは`processUser`関数に送信され、Boxグループでユーザーが追加または削除されます。

  ペイロードがスラッシュコマンドの場合 (`/boxadd`に設定されている`data.command`によって示されます)、`file 1234`のように、Box IDとファイルかフォルダかを表すコマンドのコンテンツは抽出され、個々の値を取得するために分割されます。これらの値は、適切なコンテンツであるかどうかが検証されます。

  検証後、Slackユーザーのプロフィールは、メールアドレスを取得するために取得されます。その後、このユーザープロフィールは、BoxグループとBoxコンテンツでコラボレーションするために`processContent`に送信され、すべてのユーザーにアクセス権限が付与されます。

  <Note>
    この手順でSlackユーザーのメールアドレスを取得する理由は、ファイルまたはフォルダの所有者がアプリケーションのサービスアカウントではなくユーザーであるためです。(コラボレーションの作成によって) コンテンツを共有する際は、そのファイルまたはフォルダに対して共有権限を持つユーザーが操作を行う必要があります。そのため、Slackユーザーの代理でコラボレーションを作成できるように、SlackユーザーのメールアドレスをBoxユーザーのメールアドレスと照合する必要があります。
  </Note>
</Choice>

<Choice option="programming.platform" value="java" color="none">
  `process`メソッドを次の内容に置き換えます。

  ```java theme={null}
  public void process(JSONObject inputJSON) throws Exception {
      if (inputJSON.containsKey("event")) {
          JSONObject event = (JSONObject) inputJSON.get("event");
          String eventType = (String) event.get("type");
          String eventUserId = (String) event.get("user");
          String eventChannel = (String) event.get("channel");

          processUser(getSlackUser(eventUserId), eventType, eventChannel);
      } else if (inputJSON.containsKey("command")) {
          String eventCommand = (String) inputJSON.get("command");
          if (eventCommand.equals("/boxadd")) {
              String eventChannelId = (String) inputJSON.get("channel_id");
              String eventUserId = (String) inputJSON.get("user_id");
              String cInput = (String) inputJSON.get("text");
              String[] cInputParts = cInput.split(" ");

              if (cInputParts[0].matches("file|folder")) {
                  processContent(getSlackUser(eventUserId), eventChannelId, cInputParts[0], cInputParts[1]);
              }
          }
      } else {
          System.err.println("Invalid event action");
      }
  }
  ```

  このメソッドの目的は、SlackからのペイロードがUser Eventとスラッシュコマンドのどちらであるかを判断し、必要な情報をすべて取得して、結果を処理するために適切なメソッドに転送することです。

  ペイロードがUser Eventの場合 (JSONペイロードに存在するイベントノードによって示されます)、いくつかの情報を抽出します。

  * `eventType`: ユーザーがチャンネルから退出する (`member_left_channel`) かチャンネルに参加する (`member_joined_channel`) かを決定するイベントのタイプ。
  * `eventUserId`: ユーザーのID。同じメールアドレスを使用するBoxのユーザープロフィールにバインドされるプロフィールのメールアドレスを検索するためのものです。
  * `eventChannel`: チャンネルID。Boxグループ名として使用されます。

  その後、`processUser`に転送し、`getSlackUser`メソッドからの戻り値 (ユーザーオブジェクト)、イベントのタイプ、チャンネルを渡します。

  ペイロードがスラッシュコマンドの場合 (JSONペイロードに存在する`command`ノードによって示されます)、いくつかの情報を抽出します。

  * `eventChannelId`: Boxグループ名として使用するSlackチャンネルID。
  * `eventUserId`: コマンドを発行したユーザーのID。
  * `cInputParts`: `file 1234`などの文字列からのコマンド入力のタイプとID。

  その後、`processContent`に転送し、`getSlackUser`メソッドからの戻り値 (ユーザーオブジェクト)、チャンネルID、コンテンツタイプ (ファイルまたはフォルダ)、およびBoxに保存されているファイルまたはフォルダのコンテンツIDを渡します。
</Choice>

<Choice option="programming.platform" unset color="none">
  <Danger>
    **前の手順が完了していません**最初に、手順1でお好みの言語/フレームワークを選択してください。
  </Danger>
</Choice>

## Slackユーザーの処理

次に、User Eventの処理方法を定義する必要があります。ここで説明すべきイベントは以下の3つです。

* ボットがチャンネルに追加された。
* 通常のユーザーがチャンネルに参加した。
* 通常のユーザーがチャンネルから退出した。

<Choice option="programming.platform" value="node" color="none">
  `processUser`関数を次の内容に置き換えます。

  ```js theme={null}
  function processUser(user, event, channel) {
      getGroupId(channel, function (groupId) {
          // if bot was added, add all channel users
          if (user.is_bot) {
              processSlackChannel(channel, groupId);
          } else if (
              user.profile &&
              user.profile.email &&
              event === "member_joined_channel"
          ) {
              addGroupUser(groupId, user.profile.email);
          } else if (
              user.profile &&
              user.profile.email &&
              event === "member_left_channel"
          ) {
              removeGroupUser(groupId, user.profile.email);
          }
      });
  }
  ```
</Choice>

<Choice option="programming.platform" value="java" color="none">
  `processUser`メソッドを次の内容に置き換えます。

  ```java theme={null}
  public void processUser(JSONObject userResponse, String event, String channel) throws Exception {
      String groupId = getGroupId(channel);

      JSONObject userObj = (JSONObject) userResponse.get("user");

      Boolean isBot = (Boolean) userObj.get("is_bot");
      JSONObject userProfile = (JSONObject) userObj.get("profile");
      String userEmail = (String) userProfile.get("email");

      if (isBot) {
          processSlackChannel(channel, groupId);
      } else if (event.equals("member_joined_channel")) {
          addGroupUser(groupId, userEmail);
      } else if (event.equals("member_left_channel")) {
          removeGroupUser(groupId, userEmail);
      }
  }
  ```
</Choice>

<Choice option="programming.platform" unset color="none">
  <Danger>
    **前の手順が完了していません**最初に、手順1でお好みの言語/フレームワークを選択してください。
  </Danger>
</Choice>

このコードでは、最初に、この次の手順で定義するチャンネルのBoxグループIDを取得します。取得後、以下のようにユーザーが処理されます。

* ユーザーがボットの場合は、Boxグループを初期化し、チャンネルの現在のユーザーをすべてBoxユーザーとしてグループに追加する必要があります。これは、既存のチャンネルに追加されるボットを構成するためです。この処理は、以前ユーザーが存在していたチャンネルにボットが再度追加される場合には無視されます。
* ユーザーがチャンネルに参加した場合は、グループにユーザーを追加する必要があります。
* ユーザーがチャンネルから退出した場合は、グループからユーザーを削除する必要があります。

## Slackチャンネルユーザーの処理

ボットは、初めてチャンネルに追加されたときに、現在チャンネルに含まれている全ユーザーのリストを取得し、そのユーザーを含むBoxグループを作成してチャンネルの基礎を作成する必要があります。

<Choice option="programming.platform" value="node" color="none">
  `processSlackChannel`関数を次の内容に置き換えます。

  ```js theme={null}
  function processSlackChannel(channel, groupId) {
      const limit = 100;
      const channelUsersPath = `https://slack.com/api/conversations.members?token=${slackConfig.botToken}&channel=${channel}&limit=${limit}`;

      axios.get(channelUsersPath).then((response) => {
          response.data.members.forEach((uid) => {
              getSlackUser(uid, function (user) {
                  if (user.profile.email && !user.is_bot) {
                      addGroupUser(groupId, user.profile.email);
                  }
              });
          });
      });
  }
  ```
</Choice>

<Choice option="programming.platform" value="java" color="none">
  `processSlackChannel`メソッドを次の内容に置き換えます。

  ```java theme={null}
  public void processSlackChannel(String channel, String groupId) throws Exception {
      String limit = "100";
      String channelUsersPath = String.format("%s/conversations.members?token=%s&channel=%s&limit=%s", slackConfig.slackApiUrl, slackConfig.botToken, channel, limit);

      JSONObject channelUserList = sendGETRequest(channelUsersPath);
      JSONArray channelUserIds = (JSONArray) channelUserList.get("members");

      @SuppressWarnings("rawtypes")
      Iterator i = channelUserIds.iterator();
      while(i.hasNext()) {
          String uid = (String)i.next();

          JSONObject userResponse = (JSONObject) getSlackUser(uid.toString());
          JSONObject userObj = (JSONObject) userResponse.get("user");
          JSONObject userProfile = (JSONObject) userObj.get("profile");
          Boolean isBot = (Boolean) userObj.get("is_bot");

          String userEmail = new String();
          if (!isBot) {
              userEmail = (String) userProfile.get("email");
          }

          if (!userEmail.isEmpty() && !isBot) {
              addGroupUser(groupId, userEmail);
          }
      }
  }
  ```
</Choice>

<Choice option="programming.platform" unset color="none">
  <Danger>
    **前の手順が完了していません**最初に、手順1でお好みの言語/フレームワークを選択してください。
  </Danger>
</Choice>

このコードは、複数の処理を順番に実行します。

* 最初に、Slack APIを呼び出し、チャンネルのすべてのメンバーを取得します。
* `limit`を調整して、チャネルのユーザーをさらに収集できます。
* 見つかったユーザーごとに、`getSlackUser`を呼び出してそのユーザーのプロフィールを取得し、メールアドレスをBoxユーザーのメールアドレスにマップできます。
* 各ユーザーは`addGroupUser`に送信され、グループに追加されます。

## Slackユーザープロフィールの取得

Slackに関連した最後の関数は、他の関数によって使用されるユーティリティメカニズムです。この関数は、Slack APIを呼び出して、Slackイベント/コマンドが提供するユーザーIDまたはチャンネルユーザーのリストを取得したときに提供されるユーザーIDが指定されたユーザープロフィールを取得します。メールアドレスを使用してSlackユーザーをBoxユーザーと照合しているため、ユーザープロフィールの検索では、メールアドレスのフィールドに注意します。

<Note>
  Boxのメールアドレスは一意であり、複数のアカウントに使用することはできません。つまり、ユーザーアカウントの検索に使用すると効果的です。
</Note>

<Choice option="programming.platform" value="node" color="none">
  `getSlackUser`関数を次の内容に置き換えます。

  ```js theme={null}
  function getSlackUser(userId, callback) {
      const userPath = `https://slack.com/api/users.info?token=${slackConfig.botToken}&user=${userId}`;

      axios.get(userPath).then((response) => {
          if (response.data.user && response.data.user.profile) {
              callback(response.data.user);
          } else {
              console.log("No user data found");
          }
      });
  }
  ```

  この関数では、Slackユーザープロフィールエンドポイントを呼び出した後、指定したコールバックにユーザープロフィール情報 (有効な場合) を送信します。
</Choice>

<Choice option="programming.platform" value="java" color="none">
  `getSlackUser`メソッドを次の内容に置き換えます。

  ```java theme={null}
  public JSONObject getSlackUser(String userId) throws Exception {
      String usersPath = String.format("%s/users.info?token=%s&user=%s", slackConfig.slackApiUrl, slackConfig.botToken, userId);
      return sendGETRequest(usersPath);
  }
  ```

  このメソッドでは、ユーザープロフィールを取得するようSlackにリクエストを送信した後、そのリクエストのレスポンスを返します。このレスポンスはユーザープロフィールJSONオブジェクトになります。
</Choice>

<Choice option="programming.platform" unset color="none">
  <Danger>
    **前の手順が完了していません**最初に、手順1でお好みの言語/フレームワークを選択してください。
  </Danger>
</Choice>

## まとめ

* 受信イベントを確認し、処理するために転送しました。
* イベントを処理し、適切な関数に転送しました。
* チャンネル内のすべてのユーザーを処理する関数と1人のユーザーのSlackプロフィールを取得する関数を実装しました。

<Observe option="programming.platform" value="node,java">
  <Next>Slackの関数を設定しました</Next>
</Observe>
