diceline-chartmagnifiermouse-upquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

Export Channel Messages from Teams for Slack CSV Import

  1. Create an entra App in App Registrations.
  2. 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
  1. Go to Certificates and Secrets and create a new secret.

  2. Go to OneDrive and download your channel's files. You'll find them in their corresponding folder.

  3. Upload your files to a web accessible storage (web server).

  4. Complete the variables in the CONFIG section below

  5. 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

  1. 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()