Export Channel Messages from Teams for Slack CSV Import
- Create an entra App in
App Registrations
. - Go to
API permissions
and give all the necesary permissions via Microsoft Graph. Some permissions might be superfluous. You'll figure it out. :)
Permission Name Type
------------------------------- ---------
Channel.ReadBasic.All Application
ChannelMember.Read.All Delegated
ChannelMessage.Read.All Delegated
ChannelMessage.Read.All Application
ChannelSettings.Read.All Application
Chat.Read Delegated
Chat.ReadBasic Delegated
ChatMember.Read Delegated
ChatMessage.Read Delegated
Directory.Read.All Application
Group.Read.All Delegated
Group.Read.All Application
Team.ReadBasic.All Delegated
Team.ReadBasic.All Application
TeamSettings.Read.All Delegated
TeamSettings.Read.All Application
User.Read Delegated
-
Go to
Certificates and Secrets
and create a new secret. -
Go to OneDrive and download your channel's files. You'll find them in their corresponding folder.
-
Upload your files to a web accessible storage (web server).
-
Complete the variables in the CONFIG section below
-
Create a virtual environment, activate it and install the required packages:
E.g.
python3 -m venv .venv
source .venv/bin/activate
pip install requests python-dateutil beautifulsoup4
- Execute the python file. You should get a list of teams, and then a list of channels. You'll get a JSON and a CSV file. Use the CSV file to import your data in Slack (https://{your_company_name}.slack.com/services/import).
export.py
import requests
import json
import csv
import os
from dateutil import parser
from bs4 import BeautifulSoup
from urllib.parse import quote
# === CONFIG ===
CLIENT_ID = ""
CLIENT_SECRET = ""
TENANT_ID = ""
ATTACHMENT_BASE_URL = "https://example.com/attachments/" # Replace with your actual base URL for attachments
def get_access_token():
url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
data = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "https://graph.microsoft.com/.default",
"grant_type": "client_credentials"
}
r = requests.post(url, data=data)
r.raise_for_status()
return r.json()["access_token"]
def graph_get(url, token):
headers = { "Authorization": f"Bearer {token}" }
r = requests.get(url, headers=headers)
r.raise_for_status()
return r.json()
def select_from_list(items, label_key):
for i, item in enumerate(items):
print(f"{i+1}. {item[label_key]}")
choice = int(input("Select number: ")) - 1
return items[choice]
def fetch_all_messages(access_token, team_id, channel_id):
headers = { "Authorization": f"Bearer {access_token}" }
url = f"https://graph.microsoft.com/v1.0/teams/{team_id}/channels/{channel_id}/messages"
all_messages = []
while url:
response = requests.get(url, headers=headers)
data = response.json()
all_messages.extend(data.get("value", []))
url = data.get("@odata.nextLink")
return all_messages
def build_user_email_map(user_ids, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
email_map = {}
for uid in user_ids:
url = f"https://graph.microsoft.com/v1.0/users/{uid}"
try:
r = requests.get(url, headers=headers)
if r.status_code == 404:
continue # silently ignore missing users
r.raise_for_status()
data = r.json()
email = data.get("userPrincipalName")
if email:
email_map[uid] = email
except Exception as e:
print(f"Skipped user {uid}: {e}")
continue
return email_map
def clean_html(content):
return BeautifulSoup(content or "", "html.parser").get_text()
def convert_to_slack_format(messages, email_map):
slack_messages = []
id_to_ts = {}
for msg in messages:
reply_to = msg.get("replyToId")
is_reply = reply_to is not None
from_field = msg.get("from") or {}
user_info = from_field.get("user") or {}
user_id = user_info.get("id")
user = email_map.get(user_id) or user_info.get("displayName") or from_field.get("application", {}).get("displayName") or "unknown"
text = clean_html(msg.get("body", {}).get("content", "")).strip()
# Generate encoded attachment URLs
attachment_links = []
for att in msg.get("attachments", []):
name = att.get("name")
if name:
encoded_name = quote(name)
attachment_links.append(f"{ATTACHMENT_BASE_URL}{encoded_name}")
for content in msg.get("hostedContents", []):
content_type = content.get("contentType", "file")
fallback_name = f"{msg['id']}_{content_type.replace('/', '_')}"
encoded_fallback = quote(fallback_name)
attachment_links.append(f"{ATTACHMENT_BASE_URL}{encoded_fallback}")
full_text = "\n".join(filter(None, [text] + attachment_links))
if not full_text.strip():
continue
timestamp = parser.parse(msg["createdDateTime"]).timestamp()
ts_string = f"{timestamp:.6f}"
slack_msg = {
"type": "message",
"user": user,
"text": full_text,
"ts": ts_string
}
if is_reply:
parent_ts = id_to_ts.get(reply_to)
slack_msg["thread_ts"] = parent_ts or ts_string
slack_messages.append(slack_msg)
if not is_reply:
id_to_ts[msg["id"]] = ts_string
return slack_messages
def save_slack_json(slack_messages, filename="general.json"):
with open(filename, "w", encoding="utf-8") as f:
json.dump(slack_messages, f, indent=2)
def save_csv(slack_messages, channel_name, filename="slack_messages.csv"):
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f, quoting=csv.QUOTE_ALL)
writer.writerow(["timestamp", "channel", "username", "text"])
for msg in sorted(slack_messages, key=lambda x: float(x["ts"])):
ts = int(float(msg["ts"]))
channel = channel_name
username = msg["user"]
text = msg["text"].replace("\r", "")
writer.writerow([ts, channel, username, text])
def main():
token = get_access_token()
teams = graph_get("https://graph.microsoft.com/v1.0/groups?$filter=resourceProvisioningOptions/Any(x:x eq 'Team')", token)["value"]
team = select_from_list(teams, "displayName")
team_id = team["id"]
channels = graph_get(f"https://graph.microsoft.com/v1.0/teams/{team_id}/channels", token)["value"]
channel = select_from_list(channels, "displayName")
messages = fetch_all_messages(token, team_id, channel["id"])
user_ids = set()
for msg in messages:
from_field = msg.get("from")
if isinstance(from_field, dict):
user_info = from_field.get("user")
if isinstance(user_info, dict):
user_id = user_info.get("id")
if user_id:
user_ids.add(user_id)
email_map = build_user_email_map(user_ids, token)
slack_messages = convert_to_slack_format(messages, email_map)
json_file = f"{channel['displayName'].replace(' ', '_').lower()}.json"
csv_file = f"{channel['displayName'].replace(' ', '_').lower()}.csv"
save_slack_json(slack_messages, json_file)
print(f"Saved to {json_file}")
save_csv(slack_messages, channel["displayName"], csv_file)
print(f"Saved to {csv_file}")
if __name__ == "__main__":
main()