Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Docs

on:
push:
branches: ["main"]
pull_request:
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build:
name: Build documentation
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Install gomarkdoc
run: |
go install github.com/princjef/gomarkdoc/cmd/gomarkdoc@latest

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install MkDocs dependencies
run: |
python -m pip install --upgrade pip
if [ -f docs/requirements.txt ]; then
pip install -r docs/requirements.txt
elif [ -f requirements.txt ]; then
pip install -r requirements.txt
else
pip install mkdocs mkdocs-material
fi

- name: Configure GitHub Pages
if: github.event_name != 'pull_request'
uses: actions/configure-pages@v5

- name: Generate Go API docs (gomarkdoc -> docs/*.md)
run: |
bash docs/generate_docs.sh

- name: Build MkDocs site
run: |
mkdocs build --strict --site-dir site

- name: Upload Pages artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
path: site

deploy:
name: Deploy to GitHub Pages
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.vscode
.env
/site
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 PZSP2 Z1 Teams

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
130 changes: 130 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Teams API wrapper Lib

[![Go Reference](https://pkg.go.dev/badge/github.com/pzsp-teams/lib.svg)](https://pkg.go.dev/github.com/pzsp-teams/lib)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

<br>

High-level Go (Golang) library that simplifies interaction with **Microsoft Graph API**. Provides abstraction over operations related to Teams, Channels, and Chats, adding a layer of automatic caching and name resolution.

## 🚀 Key Features

- **Simplified Authentication**: Built-in MSAL token support.
- **Intelligent Cache**: Automatic mapping of team names to IDs (e.g., "DevOps Team" -> `UUID`), reducing API queries.
- **Facade Architecture**: One main `Client` providing access to all services (`Teams`, `Channels`, `Chats`).
- **Type Safety**: All operations return strongly typed models.

## 📦 Installation

```bash
go get https://github.com/pzsp-teams/lib
```

## 🛠️ Architecture & Concepts

The library uses a **Facade Pattern**. The Client struct aggregates domain-specific services:

- **client.Teams**: Manage teams lifecycles and members.
- **client.Channels**: Manage standard and private channels.
- **client.Chats**: Handle messages and chat members.

### The "Reference" concept

Many methods accept a `_Ref` argument. This allows you to pass:

- **UUID**
- **Display Name** (**email** address in case of UserRefs) - this provides convenient usage in interactive applications.
Library will automatically resolve refs to IDs.

## 💻 Quick Start

Full example usage is showcased [HERE](https://github.com/pzsp-teams/lib/tree/example-cmd-usage/cmd)
Below is a simple example showing how to initialize the client and list the current user's teams.

### 1. Client initialization

```go
import (
"context"
"time"
"github.com/pzsp-teams/lib"
"github.com/pzsp-teams/lib/config"
)

func main() {
ctx := context.Background()

// Auth config (Azure AD)
authCfg := &config.AuthConfig{
ClientID: "your-client-id",
Tenant: "your-tenant-id",
Email: "your-email",
Scopes: []string{"[https://graph.microsoft.com/.default](https://graph.microsoft.com/.default)"},
AuthMethod: "DEVICE_CODE", // Or "INTERACTIVE"
}

// Cache config
cacheCfg := &config.CacheConfig{
Mode: config.CacheAsync,
Provider: config.CacheProviderJSONFile, // Local file cache
}

// Client init
client, err := lib.NewClient(ctx, authCfg, nil, cacheCfg)
if err != nil {
panic(err)
}
defer lib.Close() // Important if using cache
}
```

### 2. Example usage

```go
// List joined teams
teams, _ := client.Teams.ListMyJoined(ctx)
for _, t := range teams {
fmt.Printf("Team: %s (ID: %s)\n", t.DisplayName, t.ID)
}

// Create a new team
newTeam, _ := client.Teams.CreateViaGroup(ctx, "Project Alpha", "project-alpha", "public")
```

## Authentication

The library uses `config.AuthConfig` to establish the connection. Ensure your Azure App Registration has the necessary **API Permissions** (e.g., `Team.ReadBasic.All`, `Channel.ReadBasic.All`) granted in the Azure Portal.
Complete list of scopes required by all functions is available [HERE](https://github.com/pzsp-teams/lib/blob/example-cmd-usage/.env.template)

There are two available ways to authenticate:

- **INTERACTIVE** - log in window will automatically be opened within your browser.
- **DEVICE CODE** - library will provide you the **URL** and code, which need to be manually opened with browser of your choice.

## Cache

If enabled, stores metadata and non-sensitive mappings (e.g., `TeamRef` -> `UUID`) to provide efficient reference resolution.

<br>

### ⚠️ Important:

Because the cache might run background goroutines to keep data fresh, you **must** call lib.Close() when your application shuts down. This ensures all background operations complete and prevents memory leaks or race conditions.

```go
defer lib.Close() // Important: closes global cache/background workers
```

## 📚 Documentation

Full API reference, architecture details, and configuration guides are available here:

👉 [Read the Documentation](https://pzsp-teams.github.io/lib/)

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Ports

This library is also available in [Python](https://github.com/pzsp-teams/lib-python)
44 changes: 42 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
// Package lib acts as the primary entry point for the Microsoft Teams API client library.
// It adopts a Facade pattern, aggregating specialized services (Teams, Channels, Chats)
// into a single, cohesive Client.
//
// The package manages the complexity of:
// - Authentication (via MSAL and Graph Token Providers).
// - Dependency Injection (wiring APIs, Caches, and Resolvers).
// - Caching strategies (transparently wrapping operations with caching layers).
//
// Usage:
// Initialize the Client using NewClient for a standard setup.
// Alternatively, if you need only specific services, use:
// - NewTeamServiceFromGraphClient for Teams service.
// - NewChannelServiceFromGraphClient for Channels service.
// - NewChatServiceFromGraphClient for Chats service.
//
// Always ensure to call Close() upon application shutdown to flush any background
// cache operations.
package lib

import (
Expand All @@ -15,14 +33,21 @@ import (
"github.com/pzsp-teams/lib/teams"
)

// Client is the central hub for interacting with the Microsoft Teams ecosystem.
// It aggregates access to specific domains: Channels, Teams, and Chats, hiding
// the complexity of underlying Graph API calls and caching mechanisms.
type Client struct {
Channels channels.Service
Teams teams.Service
Chats chats.Service
}

// graphClient is a package-level singleton to hold the authenticated Graph client.
// Note: This approach assumes a single identity per application instance.
var graphClient *graph.GraphServiceClient

// getGraphClient ensures a singleton instance of the GraphServiceClient is created.
// It initializes the MSAL token provider using the provided authentication config.
func getGraphClient(authCfg *config.AuthConfig) (*graph.GraphServiceClient, error) {
if graphClient == nil {
tokenProvider, err := auth.GetMSALTokenProvider(authCfg)
Expand All @@ -38,7 +63,9 @@ func getGraphClient(authCfg *config.AuthConfig) (*graph.GraphServiceClient, erro
return graphClient, nil
}

// NewClient will be used later
// NewClient initializes a new Client instance with fully configured internal services.
// It handles the authentication handshake using the provided authCfg and sets up
// sending and caching behaviors based on senderCfg and cacheCfg.
func NewClient(ctx context.Context, authCfg *config.AuthConfig, senderCfg *config.SenderConfig, cacheCfg *config.CacheConfig) (*Client, error) {
cl, err := getGraphClient(authCfg)
if err != nil {
Expand All @@ -48,6 +75,11 @@ func NewClient(ctx context.Context, authCfg *config.AuthConfig, senderCfg *confi
return NewClientFromGraphClient(cl, senderCfg, cacheCfg)
}

// NewClientFromGraphClient creates a Client using an existing, pre-configured GraphServiceClient.
// This is a separated exported constructor mainly for external testing purposes (via mocking Teams API by injection of GraphServiceClient).
//
// It wires up all internal dependencies, including API clients, caching layers, and
// entity resolvers (e.g., resolving team names to IDs).
func NewClientFromGraphClient(graphClient *graph.GraphServiceClient, senderCfg *config.SenderConfig, cacheCfg *config.CacheConfig) (*Client, error) {
teamsAPI := api.GetTeamAPI(graphClient, senderCfg)
searchAPI := api.GetSearchAPI(graphClient, senderCfg)
Expand Down Expand Up @@ -82,6 +114,8 @@ func NewClientFromGraphClient(graphClient *graph.GraphServiceClient, senderCfg *
}, nil
}

// NewChannelServiceFromGraphClient creates a standalone service for Channel operations.
// Use this if you do not need the full Client wrapper and only want to interact with Channels.
func NewChannelServiceFromGraphClient(ctx context.Context, authCfg *config.AuthConfig, senderCfg *config.SenderConfig, cacheCfg *config.CacheConfig) (channels.Service, error) {
cl, err := getGraphClient(authCfg)
if err != nil {
Expand All @@ -105,6 +139,8 @@ func NewChannelServiceFromGraphClient(ctx context.Context, authCfg *config.AuthC
return channelSvc, nil
}

// NewTeamServiceFromGraphClient creates a standalone service for Team operations.
// Use this if you do not need the full Client wrapper and only want to interact with Teams.
func NewTeamServiceFromGraphClient(ctx context.Context, authCfg *config.AuthConfig, senderCfg *config.SenderConfig, cacheCfg *config.CacheConfig) (teams.Service, error) {
cl, err := getGraphClient(authCfg)
if err != nil {
Expand All @@ -124,6 +160,8 @@ func NewTeamServiceFromGraphClient(ctx context.Context, authCfg *config.AuthConf
return teamSvc, nil
}

// NewChatServiceFromGraphClient creates a standalone service for Chat operations.
// Use this if you do not need the full Client wrapper and only want to interact with Chats.
func NewChatServiceFromGraphClient(ctx context.Context, authCfg *config.AuthConfig, senderCfg *config.SenderConfig, cacheCfg *config.CacheConfig) (chats.Service, error) {
cl, err := getGraphClient(authCfg)
if err != nil {
Expand All @@ -145,7 +183,9 @@ func NewChatServiceFromGraphClient(ctx context.Context, authCfg *config.AuthConf
return chatSvc, nil
}

// Close waits for all background operations to complete.
// Close ensures a graceful shutdown of the library.
// It waits for any pending background operations (such as asynchronous cache updates)
// to complete before returning, preventing data loss or race conditions.
func Close() {
if cacher.Singleton != nil {
cacher.Singleton.Runner.Wait()
Expand Down
Loading