Microsoft Teams 데이터와 채널 메시지
Microsoft Teams 커넥터는 Microsoft Graph API를 통해 팀, 채널, 채팅, 구성원과 메시지를 관계형 테이블로 제공합니다. 배치 동기화는 이 테이블을 Foundry 데이터셋에 저장합니다. 요청 시 데이터를 읽거나 메시지를 보내는 작업은 함수에서 Graph를 호출하는 경로를 사용합니다.
원천 탐색, 배치 동기화, 증분 수집, OAuth 2.0 인증과 테이블 내보내기는 일반 제공(GA)합니다. CData 테이블 내보내기는 기존 데이터를 비우지 않는 행 추가 모드만 지원하고 키 기준 갱신, 삭제와 upsert는 지원하지 않습니다.
인증 방식과 접근 범위
AuthScheme | 인증 주체 | 사용하는 상황 |
|---|---|---|
AzureAD | 로그인한 사용자의 위임된 권한 | 사용자 접근 범위를 따르고 대화형 승인이 가능한 경우 |
AzureServicePrincipal | client secret을 사용하는 서비스 주체 | 로그인 사용자에 의존하지 않는 앱 접근. 부여한 Graph 권한에 따라 테넌트 전체 또는 리소스별 동의를 받은 팀에 접근합니다. |
AzureServicePrincipalCert | 인증서를 사용하는 서비스 주체 | 서비스 주체 접근이 필요하고 조직 정책상 인증서를 사용하는 경우 |
위임된 사용자 인증
Microsoft Entra 관리 센터에서 앱을 등록하고 client ID, client secret과 테넌트 ID 또는 contoso.onmicrosoft.com 같은 도메인을 준비합니다. 작업에 필요한 위임된 Graph 권한을 부여합니다. 테넌트가 사용자 동의를 제한하면 Global Administrator가 먼저 관리자 동의를 부여해야 합니다.
Foundry 소스에 CallbackURL 속성을 추가하고 자동 생성한 URL을 앱의 Web 리디렉션 URI로 등록합니다. AuthScheme=AzureAD, AzureTenant, OAuthClientId, OAuthClientSecret, InitiateOAuth=REFRESH를 설정합니다. Authorize를 선택하고 실제로 데이터를 읽을 사용자로 로그인하면 Foundry가 대화형 교환과 토큰 갱신을 수행합니다.
서비스 주체 인증
Entra 앱의 API permissions에서 필요한 애플리케이션 권한을 추가하고 Global Administrator가 Grant admin consent를 수행합니다. 관리자 동의 전에는 애플리케이션 권한이 효력을 갖지 않습니다. Certificates & secrets에서 client secret을 생성하거나 인증서를 업로드합니다.
이 방식에는 대화형 로그인이 없어 리디렉션 URI가 필요하지 않습니다. Foundry에서 AuthScheme=AzureServicePrincipal, 테넌트, client ID와 client secret을 설정합니다. Authorize 단계는 없습니다. 인증서를 사용하면 AzureServicePrincipalCert와 드라이버의 인증서 속성을 구성합니다.
작업별 Graph 권한
테넌트 동의 정책이 추가 제한을 적용할 수 있으므로 필요한 작업에 해당하는 권한만 부여합니다.
| 작업 | 위임된 사용자 권한 | 애플리케이션 권한 |
|---|---|---|
| 팀과 채널 탐색 | Team.ReadBasic.All, Channel.ReadBasic.All. 사용자 동의가 필요합니다. | 같은 두 권한이며 관리자 동의가 필요합니다. |
| 채널 메시지 내용 읽기 | ChannelMessage.Read.All. 관리자 동의가 필요합니다. | 테넌트 전체에는 ChannelMessage.Read.All과 관리자 동의가 필요합니다. 설치한 팀으로 제한하려면 ChannelMessage.Read.Group과 해당 팀의 리소스별 동의를 사용합니다. |
| 채팅과 채팅 메시지 읽기 | 이 연결 절차에 별도 위임 권한이 지정되어 있지 않습니다. | Chat.Read.All. 관리자 동의가 필요합니다. |
| 구성원과 소유자 나열 | GroupMember.Read.All, TeamMember.Read.All, ChannelMember.Read.All. 관리자 동의가 필요합니다. | 같은 세 권한과 관리자 동의가 필요합니다. |
| 채널 메시지 전송 | ChannelMessage.Send. 사용자 동의가 필요합니다. | ChannelMessage.Send.Group. 앱이 설치된 각 팀의 리소스별 동의가 필요합니다. |
소스 속성과 네트워크
Data Connection에서 Microsoft Teams 소스를 만들고 새 연결에 권장되는 Foundry 워커를 선택합니다. 사용자 OAuth 인증 코드 흐름은 에이전트 워커에서 지원하지 않습니다.
| 속성 | 요구 수준 | 의미 | 기본값 |
|---|---|---|---|
AuthScheme | 권장 | Teams 인증 방식입니다. 비워 두면 기본 인증을 사용합니다. | AzureAD |
AzureTenant | 권장 | 테넌트 이름 또는 ID입니다. 인증 방식에 따라 필요합니다. | 미지정 |
InitiateOAuth | 권장 | OAuth 토큰 발급과 갱신 절차입니다. 위의 사용자 승인 구성에서는 REFRESH를 지정합니다. | GETANDREFRESH |
OAuthClientId | 권장 | 사용자 정의 OAuth 앱의 client ID, 즉 consumer key입니다. | 미지정 |
OAuthClientSecret | 권장 | 사용자 정의 OAuth 앱의 client secret, 즉 consumer secret입니다. | 미지정 |
사용자 OAuth 승인을 시작하거나 갱신하거나 철회하려면 기본적으로 소스 Owner 역할이 필요합니다. Viewer와 Editor만으로는 수행할 수 없습니다. OAuth 인증 코드 흐름에서는 ConnectOnOpen=true를 사용하지 않습니다.
| 대상 | 필요 조건 |
|---|---|
graph.microsoft.com | AzureEnvironment=GLOBAL일 때 필요합니다. GLOBAL이 기본값입니다. |
login.microsoftonline.com | GLOBAL에서 AzureAD, AzureServicePrincipal, AzureServicePrincipalCert 중 하나를 사용할 때 필요합니다. |
microsoftgraph.chinacloudapi.cn | CHINA 환경의 Graph 대상입니다. |
login.chinacloudapi.cn | CHINA에서 위의 세 인증 방식 중 하나를 사용할 때 필요합니다. |
graph.microsoft.us | USGOVT 환경의 Graph 대상입니다. |
login.microsoftonline.us | USGOVT 또는 USGOVTDOD에서 위의 세 인증 방식 중 하나를 사용할 때 필요합니다. |
dod-graph.microsoft.us | USGOVTDOD 환경의 Graph 대상입니다. |
외부 통신 정책으로 해당 대상을 허용합니다. 직접 도달할 수 없는 경로는 에이전트 프록시 정책을 사용하며 에이전트 호스트의 연결도 확보합니다.
데이터 읽기와 요청 한도
원천 탐색에서 테이블을 선택하고 배치 동기화를 구성합니다. 위임된 인증이면 로그인한 사용자가 볼 수 있는 데이터, 서비스 주체 인증이면 앱에 부여된 테넌트 또는 팀 범위의 데이터를 읽습니다.
채널 메시지 조회 GET /teams/{team-id}/channels/{channel-id}/messages의 한도는 앱당 초당 200회, 앱과 테넌트 조합당 초당 20회, 채널당 초당 1회입니다. 여러 작업이 같은 앱이나 채널을 공유하면 각 경계의 한도를 함께 고려합니다.
코드에서 채널 메시지 읽기
Microsoft Teams 소스를 코드 저장소에 가져오고 서비스 주체 인증과 채널 메시지 읽기 권한을 구성합니다. 팀과 채널 ID는 원천 탐색의 Teams, Channels 테이블에서 확인합니다. 소스, 테넌트, 앱, 팀과 채널 식별자를 실제 값으로 바꿉니다. TypeScript v1의 MicrosoftTeamsSource는 소스 API 이름에서 생성된 식별자에 맞춥니다.
다음 네 구현은 최근 메시지를 최대 20개 읽고 내용이 아닌 id, 생성 시각과 웹 URL을 반환합니다. Python 파이프라인은 이 값을 데이터셋 테이블에 쓰고 함수는 JSON 문자열을 반환합니다. 전체 이력의 페이지를 순회하는 예제는 아닙니다.
서비스 주체는 테넌트 토큰 엔드포인트에 client_credentials와 https://graph.microsoft.com/.default 범위를 전달합니다. 소스에서 읽은 client secret으로 토큰을 발급하고 Graph 요청에 사용합니다. Python은 소스의 프록시 경로가 있으면 세션에 적용합니다.
Python 파이프라인
import pandas as pd
import requests
from transforms.api import Output, transform
from transforms.external.systems import ResolvedSource, Source, external_systems
SOURCE_RID = "<source_rid>"
TENANT_ID = "<tenant_id>"
CLIENT_ID = "<client_id>"
TEAM_ID = "<team_id>"
CHANNEL_ID = "<channel_id>"
OUTPUT_RID = "<output_dataset_rid>"
@external_systems(teams=Source(SOURCE_RID))
@transform.using(output=Output(OUTPUT_RID))
def read_channel_messages(teams: ResolvedSource, output) -> None:
session = requests.Session()
proxy = teams.get_https_proxy_uri()
if proxy:
session.proxies.update({"http": proxy, "https": proxy})
token_response = session.post(
f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token",
data={
"client_id": CLIENT_ID,
"client_secret": teams.get_secret("OAuthClientSecret"),
"scope": "https://graph.microsoft.com/.default",
"grant_type": "client_credentials",
},
timeout=30,
)
token_response.raise_for_status()
response = session.get(
f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/{CHANNEL_ID}/messages",
params={"$top": 20},
headers={"Authorization": f"Bearer {token_response.json()['access_token']}"},
timeout=30,
)
response.raise_for_status()
rows = [
{
"id": message.get("id"),
"created_date_time": message.get("createdDateTime"),
"web_url": message.get("webUrl"),
}
for message in response.json().get("value", [])
]
output.write_table(pd.DataFrame(rows))
Python 함수
import json
import requests
from functions.api import function
from functions.sources import get_source
SOURCE_ALIAS = "<source_alias>"
TENANT_ID = "<tenant_id>"
CLIENT_ID = "<client_id>"
TEAM_ID = "<team_id>"
CHANNEL_ID = "<channel_id>"
@function(sources=[SOURCE_ALIAS])
def list_channel_messages() -> str:
source = get_source(SOURCE_ALIAS)
session = requests.Session()
proxy = source.get_https_proxy_uri()
if proxy:
session.proxies.update({"http": proxy, "https": proxy})
token_response = session.post(
f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token",
data={
"client_id": CLIENT_ID,
"client_secret": source.get_secret("OAuthClientSecret"),
"scope": "https://graph.microsoft.com/.default",
"grant_type": "client_credentials",
},
timeout=30,
)
token_response.raise_for_status()
response = session.get(
f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/{CHANNEL_ID}/messages",
params={"$top": 20},
headers={"Authorization": f"Bearer {token_response.json()['access_token']}"},
timeout=30,
)
response.raise_for_status()
messages = [
{
"id": message.get("id"),
"createdDateTime": message.get("createdDateTime"),
"webUrl": message.get("webUrl"),
}
for message in response.json().get("value", [])
]
return json.dumps(messages)
TypeScript v1 함수
import { ExternalSystems, Function } from "@foundry/functions-api";
import { MicrosoftTeamsSource } from "@foundry/external-systems/sources";
const TENANT_ID = "<tenant_id>";
const CLIENT_ID = "<client_id>";
const TEAM_ID = "<team_id>";
const CHANNEL_ID = "<channel_id>";
export class TeamsGraphFunctions {
@ExternalSystems({ sources: [MicrosoftTeamsSource] })
@Function()
public async listChannelMessages(): Promise<string> {
const tokenResponse = await MicrosoftTeamsSource.fetch(
`https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: MicrosoftTeamsSource.getSecret("OAuthClientSecret"),
scope: "https://graph.microsoft.com/.default",
grant_type: "client_credentials",
}).toString(),
},
);
if (!tokenResponse.ok) throw new Error(`Microsoft token exchange returned ${tokenResponse.status}`);
const token = await tokenResponse.json() as { access_token: string };
const response = await MicrosoftTeamsSource.fetch(
`https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages?$top=20`,
{ headers: { Authorization: `Bearer ${token.access_token}` } },
);
if (!response.ok) throw new Error(`Microsoft Graph returned ${response.status}`);
const payload = await response.json() as {
value?: Array<{ id?: string; createdDateTime?: string; webUrl?: string }>;
};
return JSON.stringify((payload.value ?? []).map((message) => ({
id: message.id,
createdDateTime: message.createdDateTime,
webUrl: message.webUrl,
})));
}
}
TypeScript v2 함수
import { getFetch, getSource } from "@palantir/functions-sources";
const SOURCE_RID = "<source_rid>";
const TENANT_ID = "<tenant_id>";
const CLIENT_ID = "<client_id>";
const TEAM_ID = "<team_id>";
const CHANNEL_ID = "<channel_id>";
export const config = { sources: [SOURCE_RID] };
export default async function listChannelMessages(): Promise<string> {
const source = await getSource({ rid: SOURCE_RID });
const clientSecret = source.secrets.OAuthClientSecret;
if (!clientSecret) throw new Error("Add OAuthClientSecret to the source");
const sourceFetch = await getFetch(source);
const tokenResponse = await sourceFetch(
`https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: clientSecret,
scope: "https://graph.microsoft.com/.default",
grant_type: "client_credentials",
}).toString(),
},
);
if (!tokenResponse.ok) throw new Error(`Microsoft token exchange returned ${tokenResponse.status}`);
const token = (await tokenResponse.json()) as { access_token: string };
const response = await sourceFetch(
`https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages?$top=20`,
{ headers: { Authorization: `Bearer ${token.access_token}` } },
);
if (!response.ok) throw new Error(`Microsoft Graph returned ${response.status}`);
const payload = (await response.json()) as {
value?: Array<{ id?: string; createdDateTime?: string; webUrl?: string }>;
};
return JSON.stringify((payload.value ?? []).map((message) => ({
id: message.id,
createdDateTime: message.createdDateTime,
webUrl: message.webUrl,
})));
}
사용자 권한으로 채널 메시지 보내기
로그인한 사용자로 새 메시지를 보내려면 https://graph.microsoft.com용 별도 REST API 소스를 만들고 외부 인증 애플리케이션을 연결합니다. ChannelMessage.Send 권한을 부여하고 실제 전송 사용자로 승인합니다. 이 REST 소스를 Functions 저장소에 가져와 소스, 팀과 채널 식별자를 설정합니다.
Workshop이나 자동화에서 직접 호출하기 전에 함수 기반 액션의 대화형 절차로 외부 인증 애플리케이션을 승인합니다. 함수는 호출 사용자의 승인을 사용하며 만료되거나 철회되면 다시 승인해야 합니다.
특정 팀에 대한 앱 권한으로 보내려면 ChannelMessage.Send.Group, 팀별 리소스 동의와 위의 애플리케이션 토큰 패턴을 사용합니다. Teamwork.Migrate.All은 과거 메시지 가져오기용이며 새 채널 메시지를 보내는 권한으로 사용하지 않습니다.
다음 함수를 호출하면 채널 구성원에게 보이는 메시지가 실제로 게시됩니다. 입력 내용과 대상 채널을 확인하고 비운영 채널에서 시험합니다. 사용자의 세션 액세스 토큰을 Authorization 헤더에 넣고 {"body":{"content":text}}를 POST로 전송하며 응답의 메시지 ID를 반환합니다.
Python 함수
import requests
from functions.api import function
from functions.sources import get_source
SOURCE_ALIAS = "<source_alias>"
TEAM_ID = "<team_id>"
CHANNEL_ID = "<channel_id>"
@function(sources=[SOURCE_ALIAS])
def send_channel_message(text: str) -> str:
source = get_source(SOURCE_ALIAS)
credentials = source.get_session_credentials().get()
session = requests.Session()
proxy = source.get_https_proxy_uri()
if proxy:
session.proxies.update({"http": proxy, "https": proxy})
response = session.post(
f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/{CHANNEL_ID}/messages",
headers={"Authorization": f"Bearer {credentials.access_token}"},
json={"body": {"content": text}},
timeout=30,
)
response.raise_for_status()
return response.json()["id"]
TypeScript v2 함수
import { getFetch, getSource } from "@palantir/functions-sources";
const SOURCE_RID = "<source_rid>";
const TEAM_ID = "<team_id>";
const CHANNEL_ID = "<channel_id>";
export const config = { sources: [SOURCE_RID] };
export default async function sendChannelMessage(text: string): Promise<string> {
const source = await getSource({ rid: SOURCE_RID });
const credentials = await source.sessionCredentials?.get();
if (credentials?.type !== "oauth") throw new Error("Authorize the source first");
const sourceFetch = await getFetch(source);
const response = await sourceFetch(
`https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${credentials.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ body: { content: text } }),
},
);
if (!response.ok) throw new Error(`Microsoft Graph returned ${response.status}`);
return ((await response.json()) as { id: string }).id;
}