diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index ca0aa05..96af91a 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -2,10 +2,10 @@ name: Python Integration Tests
on:
pull_request:
- branches: [ main ]
+ branches: [main]
types: [review_requested, ready_for_review]
push:
- branches: [ main ]
+ branches: [main]
repository_dispatch:
types: [verify-new-go-lib-version]
@@ -237,6 +237,8 @@ jobs:
# clean up unneeded files
rm -rf python_release/tests
+ rm -rf python_release/docs
+ rm python_release/mkdocs.yml
find python_release -type d -name "__pycache__" -exec rm -rf {} +
rm -rf python_release/.venv
diff --git a/README.md b/README.md
index 7727354..a9913ec 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,123 @@
-# lib-python
+# Teams API Python Wrapper
+
+[](https://www.python.org/)
+[](LICENSE)
+[](https://pzsp-teams.github.io/lib-python/)
+
+
+
+A high-performance **Python wrapper** for the Microsoft Teams Graph API. It leverages a compiled **Go (Golang)** backend to handle heavy lifting, concurrency, and caching, while providing a clean, fully typed Pythonic interface.
+
+## 🚀 Key Features
+
+- **Performance of Go**: Uses a compiled Go subprocess for efficient API communication and state management.
+- **Pythonic Interface**: Fully typed using `dataclasses` and `Enums`. Native support for IDE autocompletion and type checkers (`mypy`).
+- **Intelligent Cache**: Background caching of Team/Channel IDs to minimize API throttling and latency (handled transparently by the backend).
+- **Simplified Auth**: Wraps MSAL authentication flows configuration.
+
+## 📦 Installation
+
+```bash
+pip install teams-lib-pzsp2-z1
+```
+
+## 🛠️ Architecture
+
+This library acts as a bridge. When initialized, it spawns a dedicated Go subprocess (`teamsClientLib`) and communicates via JSON-RPC over `stdin`/`stdout`.
+
+The `TeamsClient` aggregates domain-specific services:
+
+- **client.teams**: Manage team lifecycles (create, list, delete).
+
+- **client.channels**: Manage standard and private channels.
+
+- **client.chats**: Handle messages, mentions, and chat participants.
+
+## 💻 Quick Start
+
+### 1. Configuration
+
+Create a `.env` in your project with your Azure AD credentials:
+
+```ini
+CLIENT_ID=your-client-id-uuid
+TENANT_ID=your-tenant-id-uuid
+EMAIL=user@example.com
+SCOPES=User.Read,Team.ReadBasic.All,Channel.ReadBasic.All
+AUTH_METHOD=DEVICE_CODE [or INTERACTIVE]
+```
+
+Scopes needed by all functions could be found in [.env.template](https://github.com/pzsp-teams/lib-python/tree/main/.env.template).
+
+### 2. Basic usage
+
+Here is a minimal example showing how to initialize the client and list joined teams:
+
+```python
+from teams_lib_pzsp2_z1.client import TeamsClient
+from teams_lib_pzsp2_z1.config import CacheMode
+
+def main():
+ # Initialize the client.
+ # This spawns the Go subprocess and performs authentication.
+ client = TeamsClient()
+
+ try:
+ print("Fetching teams...")
+ teams = client.teams.list_my_joined()
+
+ for team in teams:
+ print(f"Team: {team.display_name} | ID: {team.id}")
+
+ # Example: List channels in the first team
+ channels = client.channels.list_channels(team.id)
+ print(f" -> Found {len(channels)} channels.")
+
+ except RuntimeError as e:
+ print(f"An error occurred: {e}")
+
+ finally:
+ # Crucial: Close the client to ensure background cache writes finish
+ # and the Go subprocess is terminated gracefully.
+ client.close()
+
+if __name__ == "__main__":
+ main()
+```
+
+## 📚 Documentation
+
+Full API reference, architecture details, and configuration guides are available here:
+
+👉 [Read the Documentation](https://pzsp-teams.github.io/lib-python/)
+
+## Authentication
+
+The library loads credentials from environment variables. Ensure your Azure App Registration has the necessary **API Permissions** (e.g., `Team.ReadBasic.All`, `Channel.ReadBasic.All`) granted in the Azure Portal.
+
+Supported Authentication Methods (`AUTH_METHOD`):
+
+- **INTERACTIVE**: Opens a browser window automatically.
+
+- **DEVICE_CODE**: Prints a code to the console to login via microsoft.com/devicelogin.
+
+## Cache & Lifecycle
+
+If caching is enabled (SYNC or ASYNC), the Go backend stores metadata locally (e.g., `teams_cache.json`) to speed up reference resolution.
+
+⚠️ Important: client.close()
+Because the Go backend might be running background goroutines (especially for Async Cache), you **must** call `client.close()` before your script exits. This ensures:
+
+1. Pending cache writes are flushed to disk.
+
+2. The Go subprocess is terminated correctly.
+
+```python
+client.close()
+```
+
+## 📄 License
+This project is licensed under the MIT License - see the [LICENSE](https://github.com/pzsp-teams/lib-python/blob/main/LICENSE) file for details.
+
+## Port
+This lib is also available in [GO](https://github.com/pzsp-teams/lib)
diff --git a/go/bridge/main.go b/go/bridge/main.go
index ec5052b..128be65 100644
--- a/go/bridge/main.go
+++ b/go/bridge/main.go
@@ -5,9 +5,11 @@ import (
"encoding/json"
"fmt"
"os"
+ "context"
jsonClientLib "github.com/pzsp-teams/lib-python/internal/json-client"
jsonModel "github.com/pzsp-teams/lib-python/internal/json-model"
+ "github.com/pzsp-teams/lib"
)
var client *jsonClientLib.TeamsJSONClient
@@ -51,19 +53,24 @@ func main() {
continue
}
- switch req.Method {
- case "listChannels":
- channels, err := client.ListChannels(req.Params)
- if err != nil {
- respondError(writer, err)
- } else {
- respondResult(writer, channels)
- }
- default:
+ handler, exists := jsonClientLib.Handlers[req.Method]
+ if !exists {
respondError(writer, fmt.Errorf("unknown method"))
+ continue
+ }
+
+ result, err := handler(client, context.Background(), req.Params)
+ if err != nil {
+ respondError(writer, err)
}
+ respondResult(writer, result)
continue
}
- respondError(writer, fmt.Errorf("unknown request type"))
+
+ if req.Type == "close" {
+ lib.Close()
+ respondResult(writer, "closed")
+ return
+ }
}
}
diff --git a/go/bridge/responses.go b/go/bridge/responses.go
index ff2c4f2..72dcedc 100644
--- a/go/bridge/responses.go
+++ b/go/bridge/responses.go
@@ -6,7 +6,7 @@ import (
)
type response struct {
- Result interface{} `json:"result,omitempty"`
+ Result any `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
@@ -29,7 +29,7 @@ func respondError(writer *bufio.Writer, err error) {
writer.Flush()
}
-func respondResult(writer *bufio.Writer, result interface{}) {
+func respondResult(writer *bufio.Writer, result any) {
response := response{
Result: result,
}
diff --git a/go/go.mod b/go/go.mod
index 75df08b..a736666 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -4,23 +4,25 @@ go 1.25.5
require (
github.com/microsoft/kiota-abstractions-go v1.9.3
- github.com/microsoftgraph/msgraph-sdk-go v1.90.0
- github.com/pzsp-teams/lib v0.0.0-20251213104555-e41d3adf599f
+ github.com/microsoftgraph/msgraph-sdk-go v1.93.0
+ github.com/mitchellh/mapstructure v1.5.0
+ github.com/pzsp-teams/lib v0.0.0-20260112132712-1335fb93c11f
)
require (
- al.essio.dev/pkg/shellescape v1.5.1 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
+ al.essio.dev/pkg/shellescape v1.6.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
- github.com/danieljoos/wincred v1.2.2 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/danieljoos/wincred v1.2.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/godbus/dbus/v5 v5.1.0 // indirect
- github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
+ github.com/godbus/dbus/v5 v5.2.2 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
- github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 // indirect
+ github.com/keybase/go-keychain v0.0.1 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/microsoft/kiota-authentication-azure-go v1.3.1 // indirect
github.com/microsoft/kiota-http-go v1.5.4 // indirect
@@ -30,13 +32,14 @@ require (
github.com/microsoft/kiota-serialization-text-go v1.1.3 // indirect
github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
- github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 // indirect
+ github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.8 // indirect
github.com/zalando/go-keyring v0.2.6 // indirect
- go.opentelemetry.io/auto/sdk v1.1.0 // indirect
- go.opentelemetry.io/otel v1.37.0 // indirect
- go.opentelemetry.io/otel/metric v1.37.0 // indirect
- go.opentelemetry.io/otel/trace v1.37.0 // indirect
- golang.org/x/net v0.43.0 // indirect
- golang.org/x/sys v0.35.0 // indirect
- golang.org/x/text v0.28.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/otel v1.39.0 // indirect
+ go.opentelemetry.io/otel/metric v1.39.0 // indirect
+ go.opentelemetry.io/otel/trace v1.39.0 // indirect
+ golang.org/x/net v0.48.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
)
diff --git a/go/go.sum b/go/go.sum
index e345e5c..6790c62 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -1,15 +1,17 @@
-al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
-al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
+al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
+al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
-github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
-github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
+github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -17,16 +19,18 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
-github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
+github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 h1:rG3VnJUnAWyiv7qYmmdOdSapzz6HM+zb9/uRFr0T5EM=
-github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ=
+github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
+github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -47,41 +51,73 @@ github.com/microsoft/kiota-serialization-multipart-go v1.1.2 h1:1pUyA1QgIeKslQwb
github.com/microsoft/kiota-serialization-multipart-go v1.1.2/go.mod h1:j2K7ZyYErloDu7Kuuk993DsvfoP7LPWvAo7rfDpdPio=
github.com/microsoft/kiota-serialization-text-go v1.1.3 h1:8z7Cebn0YAAr++xswVgfdxZjnAZ4GOB9O7XP4+r5r/M=
github.com/microsoft/kiota-serialization-text-go v1.1.3/go.mod h1:NDSvz4A3QalGMjNboKKQI9wR+8k+ih8UuagNmzIRgTQ=
-github.com/microsoftgraph/msgraph-sdk-go v1.90.0 h1:ygVeWfGB8TMO4rTFxtrYueZmj1mLqtDKW5UZ4iJwczU=
-github.com/microsoftgraph/msgraph-sdk-go v1.90.0/go.mod h1:UdZWxbZiFvjPug9DYayD90JNiHjXyNRA39lEpcy3Kms=
+github.com/microsoftgraph/msgraph-sdk-go v1.92.0 h1:LOTZ5un7tsSki0S072D+TIHsiOIpmGJnDkNps9Em6cw=
+github.com/microsoftgraph/msgraph-sdk-go v1.92.0/go.mod h1:sue5+4Z9FCOon6pHgvC1djjybs9ZYB3LZaAGYI1Qcfo=
+github.com/microsoftgraph/msgraph-sdk-go v1.93.0 h1:H4WgYetjWYpr79S2ylNJchFDQTBE0ldKx8sR0qxyRGQ=
+github.com/microsoftgraph/msgraph-sdk-go v1.93.0/go.mod h1:sue5+4Z9FCOon6pHgvC1djjybs9ZYB3LZaAGYI1Qcfo=
github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0 h1:0SrIoFl7TQnMRrsi5TFaeNe0q8KO5lRzRp4GSCCL2So=
github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0/go.mod h1:A1iXs+vjsRjzANxF6UeKv2ACExG7fqTwHHbwh1FL+EE=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/pzsp-teams/lib v0.0.0-20251208073120-ca1606f2b27d h1:hFdPrkbnUngxrGdMkPVZpvY76T6QgnXo87u9wzXgNWY=
-github.com/pzsp-teams/lib v0.0.0-20251208073120-ca1606f2b27d/go.mod h1:uGrgoO+KmBc55Le/AeysA0d24HOCQHO55Uw5Z7naQo4=
-github.com/pzsp-teams/lib v0.0.0-20251213104555-e41d3adf599f h1:tmXKJEWYl0f/H4n91wnfW1asgkHbftKgX5nbpafNHmU=
-github.com/pzsp-teams/lib v0.0.0-20251213104555-e41d3adf599f/go.mod h1:EPtQVPz+HYZNsSpcoQ3QWLbMEz1O2qeIH5ojlYJLEic=
+github.com/pzsp-teams/lib v0.0.0-20260101113908-bafa8ca37eae h1:Q0NnGhGtyUbPzDHxLsBc39cHMjZBCG4yPCzU3ocNY+s=
+github.com/pzsp-teams/lib v0.0.0-20260101113908-bafa8ca37eae/go.mod h1:OUTZq+jo2AAzjV5p0w7c9VVuOKiHVWxuZhdzOhjOq5s=
+github.com/pzsp-teams/lib v0.0.0-20260101185821-fa4110d82a33 h1:snmBQwIaJV1MBynhm8OKzvaKtpUva4wem3VKmyaVU0Q=
+github.com/pzsp-teams/lib v0.0.0-20260101185821-fa4110d82a33/go.mod h1:OUTZq+jo2AAzjV5p0w7c9VVuOKiHVWxuZhdzOhjOq5s=
+github.com/pzsp-teams/lib v0.0.0-20260107201101-e9e6f48e39e8 h1:1ifLBNVdw8TCLPzHnlQfgxlL8pbj/9MVquTLNiX2t0Q=
+github.com/pzsp-teams/lib v0.0.0-20260107201101-e9e6f48e39e8/go.mod h1:BQwULf+u8TtbKR0U23MlZXgm2CVcLBJn2cS5gmR7L8M=
+github.com/pzsp-teams/lib v0.0.0-20260107211439-3d301706e02c h1:ZtUkM66y/jg5g5kEQhteQeACuwKl+cOkRuGPlX1mkGg=
+github.com/pzsp-teams/lib v0.0.0-20260107211439-3d301706e02c/go.mod h1:BQwULf+u8TtbKR0U23MlZXgm2CVcLBJn2cS5gmR7L8M=
+github.com/pzsp-teams/lib v0.0.0-20260111170324-f66ca4f5d369 h1:TNB4hQ6u8aH0cekG1xbITdkAxgrY4FmwSWgzfI1e21Q=
+github.com/pzsp-teams/lib v0.0.0-20260111170324-f66ca4f5d369/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
+github.com/pzsp-teams/lib v0.0.0-20260112132712-1335fb93c11f h1:MOnTg+X51/lukcWWjHyw9OPx9IzC6Q6TJsZXiaH/KnU=
+github.com/pzsp-teams/lib v0.0.0-20260112132712-1335fb93c11f/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
+github.com/pzsp-teams/lib v0.0.0-20260113134422-e5326c621551 h1:w4nwFMJLwLYolig9U5r5HpVL0FhrPTP14NggM5/UX9M=
+github.com/pzsp-teams/lib v0.0.0-20260113134422-e5326c621551/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
+github.com/pzsp-teams/lib v0.0.0-20260113134659-c00a30df42d1 h1:ykdfDBsQzNu0DCKA+bm20n3o6EtpVPPhXZNRUbhCsuI=
+github.com/pzsp-teams/lib v0.0.0-20260113134659-c00a30df42d1/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
+github.com/pzsp-teams/lib v0.0.0-20260113135020-602a62d68535 h1:fWsO7MUGT+wblfPy0Cubpew9vWblm7+V0UdOUeqbEKU=
+github.com/pzsp-teams/lib v0.0.0-20260113135020-602a62d68535/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
+github.com/pzsp-teams/lib v0.0.0-20260113135200-1428146acf2e h1:ir/+PHMWaP4x2hZnQB4L6IDz42//Enf/RDA6Ryvzo70=
+github.com/pzsp-teams/lib v0.0.0-20260113135200-1428146acf2e/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
+github.com/pzsp-teams/lib v0.0.0-20260113135504-3f606125f3a8 h1:Ck+6z5keqy6aHZEH7Cpe+ozzQm7rv5dg0tRZf2016Zg=
+github.com/pzsp-teams/lib v0.0.0-20260113135504-3f606125f3a8/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
+github.com/pzsp-teams/lib v0.0.0-20260113135852-d15a162b495f h1:iuSQEjA6JrHLPoNJHPhloQlo8lAvqQXFqcxR3CJTn5s=
+github.com/pzsp-teams/lib v0.0.0-20260113135852-d15a162b495f/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
+github.com/pzsp-teams/lib v0.0.0-20260113152600-e9075fa2951a h1:Y/I0MRE60rwsutOcj1FlnyrsrrL6jdJIcxCnL2R80V4=
+github.com/pzsp-teams/lib v0.0.0-20260113152600-e9075fa2951a/go.mod h1:bpgptBb8PaJV03buetvHnq6plkQHpF0WnY1K5iU31U8=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
-github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 h1:7hth9376EoQEd1hH4lAp3vnaLP2UMyxuMMghLKzDHyU=
-github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3/go.mod h1:Z5KcoM0YLC7INlNhEezeIZ0TZNYf7WSNO0Lvah4DSeQ=
+github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.8 h1:gMBdYMTHt2mmTdXW8YfvRjRUZ0GhyGV+IqSH9H15bGw=
+github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.8/go.mod h1:Z5KcoM0YLC7INlNhEezeIZ0TZNYf7WSNO0Lvah4DSeQ=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
-go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
-go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
-go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
-go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
-go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
-golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
-golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
+go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
+go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
-golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
-golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/go/internal/json-client/decoders/decode-chatRef.go b/go/internal/json-client/decoders/decode-chatRef.go
new file mode 100644
index 0000000..1bc85e7
--- /dev/null
+++ b/go/internal/json-client/decoders/decode-chatRef.go
@@ -0,0 +1,28 @@
+package decoders
+
+import (
+ "github.com/pzsp-teams/lib/chats"
+)
+
+type ChatRefDTO struct {
+ Ref string `json:"ref"`
+ Type string `json:"type"`
+}
+
+const (
+ ChatTypeGroup = "group"
+ ChatTypeOneOnOne = "oneOnOne"
+)
+
+func GetChatRef(dto ChatRefDTO) chats.ChatRef {
+ switch dto.Type {
+ case ChatTypeGroup:
+ return chats.GroupChatRef{Ref: dto.Ref}
+ case ChatTypeOneOnOne:
+ return chats.OneOnOneChatRef{Ref: dto.Ref}
+ default:
+ return nil
+ }
+}
+
+
diff --git a/go/internal/json-client/decoders/decode-messageBody.go b/go/internal/json-client/decoders/decode-messageBody.go
new file mode 100644
index 0000000..d9fc39b
--- /dev/null
+++ b/go/internal/json-client/decoders/decode-messageBody.go
@@ -0,0 +1,6 @@
+package decoders
+
+type MessageBodyDTO struct {
+ ContentType string `json:"contentType"`
+ Content string `json:"content"`
+}
\ No newline at end of file
diff --git a/go/internal/json-client/decoders/decode-nextLink.go b/go/internal/json-client/decoders/decode-nextLink.go
new file mode 100644
index 0000000..584689d
--- /dev/null
+++ b/go/internal/json-client/decoders/decode-nextLink.go
@@ -0,0 +1,12 @@
+package decoders
+
+type NextLinkDTO struct {
+ NextLink string `json:"nextLink"`
+}
+
+func GetNextLink(dto *NextLinkDTO) *string {
+ if dto.NextLink == "" {
+ return nil
+ }
+ return &dto.NextLink
+}
\ No newline at end of file
diff --git a/go/internal/json-client/decoders/decode-params.go b/go/internal/json-client/decoders/decode-params.go
new file mode 100644
index 0000000..c6a18ef
--- /dev/null
+++ b/go/internal/json-client/decoders/decode-params.go
@@ -0,0 +1,27 @@
+package decoders
+
+import (
+ "github.com/mitchellh/mapstructure"
+)
+
+func DecodeParams[T any](p any) (*T, error) {
+ var result T
+
+ config := &mapstructure.DecoderConfig{
+ Metadata: nil,
+ Result: &result,
+ TagName: "json",
+ WeaklyTypedInput: true,
+ }
+
+ decoder, err := mapstructure.NewDecoder(config)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := decoder.Decode(p); err != nil {
+ return nil, err
+ }
+
+ return &result, nil
+}
\ No newline at end of file
diff --git a/go/internal/json-client/decoders/decode-searchOptions.go b/go/internal/json-client/decoders/decode-searchOptions.go
new file mode 100644
index 0000000..5958b81
--- /dev/null
+++ b/go/internal/json-client/decoders/decode-searchOptions.go
@@ -0,0 +1,103 @@
+package decoders
+
+import (
+ "time"
+
+ "github.com/pzsp-teams/lib/search"
+)
+
+type SearchConfigDTO struct {
+ MaxWorkers int `json:"maxWorkers"`
+}
+
+type SearchPageDTO struct {
+ From *int `json:"from"`
+ Size *int `json:"size"`
+}
+
+type SearchMessageOptionsDTO struct {
+ Query string `json:"query"`
+ SearchPage SearchPageDTO `json:"searchPage"`
+ From []string `json:"from"`
+ NotFrom []string `json:"notFrom"`
+ IsRead *bool `json:"isRead"`
+ IsMentioned *bool `json:"isMentioned"`
+ To []string `json:"to"`
+ NotTo []string `json:"notTo"`
+ StartTime time.Time `json:"startTime"`
+ EndTime time.Time `json:"endTime"`
+ Interval string `json:"interval"`
+ NotFromMe *bool `json:"notFromMe"`
+ NotToMe *bool `json:"notToMe"`
+ FromMe *bool `json:"fromMe"`
+ ToMe *bool `json:"toMe"`
+}
+
+func DecodeSearchMessageOptions(dto *SearchMessageOptionsDTO) *search.SearchMessagesOptions {
+ if dto == nil {
+ return nil
+ }
+
+ interval := search.TimeInterval(dto.Interval)
+
+ options := &search.SearchMessagesOptions{}
+ if dto.Query != "" {
+ options.Query = &dto.Query
+ }
+ options.SearchPage = DecodeSearchPage(&dto.SearchPage)
+ if len(dto.From) > 0 {
+ options.From = dto.From
+ }
+ if len(dto.NotFrom) > 0 {
+ options.NotFrom = dto.NotFrom
+ }
+ options.IsRead = dto.IsRead
+ options.IsMentioned = dto.IsMentioned
+ if len(dto.To) > 0 {
+ options.To = dto.To
+ }
+ if len(dto.NotTo) > 0 {
+ options.NotTo = dto.NotTo
+ }
+ if !dto.StartTime.IsZero() {
+ options.StartTime = &dto.StartTime
+ }
+ if !dto.EndTime.IsZero() {
+ options.EndTime = &dto.EndTime
+ }
+ if dto.Interval != "" {
+ options.Interval = &interval
+ }
+ if dto.NotFromMe != nil {
+ options.NotFromMe = *dto.NotFromMe
+ }
+ if dto.NotToMe != nil {
+ options.NotToMe = *dto.NotToMe
+ }
+ if dto.FromMe != nil {
+ options.FromMe = *dto.FromMe
+ }
+ if dto.ToMe != nil {
+ options.ToMe = *dto.ToMe
+ }
+
+ return options
+}
+
+func DecodeSearchPage(dto *SearchPageDTO) *search.SearchPage {
+ if dto == nil {
+ return nil
+ }
+
+ page := &search.SearchPage{}
+ if dto.From != nil {
+ from := int32(*dto.From)
+ page.From = &from
+ }
+ if dto.Size != nil {
+ size := int32(*dto.Size)
+ page.Size = &size
+ }
+
+ return page
+}
diff --git a/go/internal/json-client/decoders/decode-timeRange.go b/go/internal/json-client/decoders/decode-timeRange.go
new file mode 100644
index 0000000..e6f74b6
--- /dev/null
+++ b/go/internal/json-client/decoders/decode-timeRange.go
@@ -0,0 +1,33 @@
+package decoders
+
+import (
+ "fmt"
+ "time"
+)
+
+func DecodeTimeRange(startTime string, endTime string) (*time.Time, *time.Time, error) {
+ parseTime := func(s string) (time.Time, error) {
+ t, err := time.Parse(time.RFC3339, s)
+ if err == nil {
+ return t, nil
+ }
+ layoutNaive := "2006-01-02T15:04:05"
+ if len(s) > 19 {
+ layoutNaive = "2006-01-02T15:04:05.999999"
+ }
+
+ return time.Parse(layoutNaive, s)
+ }
+
+ parsedStartTime, err := parseTime(startTime)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error parsing startTime ('%s'): %v", startTime, err)
+ }
+
+ parsedEndTime, err := parseTime(endTime)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error parsing endTime ('%s'): %v", endTime, err)
+ }
+
+ return &parsedStartTime, &parsedEndTime, nil
+}
diff --git a/go/internal/json-client/decoders/decode-updateTeam.go b/go/internal/json-client/decoders/decode-updateTeam.go
new file mode 100644
index 0000000..b5a1eea
--- /dev/null
+++ b/go/internal/json-client/decoders/decode-updateTeam.go
@@ -0,0 +1,27 @@
+package decoders
+
+import (
+ "github.com/pzsp-teams/lib/models"
+)
+
+type UpdateTeamDTO struct {
+ DisplayName string `json:"displayName"`
+ Description string `json:"description"`
+ Visibility string `json:"visibility"`
+}
+
+func GetUpdateTeam(dto *UpdateTeamDTO) models.TeamUpdate {
+ update := models.TeamUpdate{}
+
+ if dto.DisplayName != "" {
+ update.DisplayName = &dto.DisplayName
+ }
+ if dto.Description != "" {
+ update.Description = &dto.Description
+ }
+ if dto.Visibility != "" {
+ update.Visibility = &dto.Visibility
+ }
+
+ return update
+}
diff --git a/go/internal/json-client/fake-client.go b/go/internal/json-client/fake-client.go
index 2d731ca..2882bdc 100644
--- a/go/internal/json-client/fake-client.go
+++ b/go/internal/json-client/fake-client.go
@@ -12,14 +12,15 @@ import (
azcore "github.com/microsoft/kiota-abstractions-go"
graph "github.com/microsoftgraph/msgraph-sdk-go"
- lib "github.com/pzsp-teams/lib"
+ "github.com/pzsp-teams/lib/config"
+ "github.com/pzsp-teams/lib"
jsonModel "github.com/pzsp-teams/lib-python/internal/json-model"
)
// --- 1. Fake Auth Provider ---
type FakeAuthProvider struct{}
-func (f *FakeAuthProvider) AuthenticateRequest(ctx context.Context, request *azcore.RequestInformation, additionalAuthenticationContext map[string]interface{}) error {
+func (f *FakeAuthProvider) AuthenticateRequest(ctx context.Context, request *azcore.RequestInformation, additionalAuthenticationContext map[string]any) error {
if request.Headers == nil {
request.Headers = azcore.NewRequestHeaders()
}
@@ -54,14 +55,17 @@ func NewJSONClient(req jsonModel.Request) (*TeamsJSONClient, error) {
return nil, fmt.Errorf("invalid mockServerUrl parameter")
}
- senderConfig := lib.SenderConfig{
+ senderConfig := config.SenderConfig{
MaxRetries: 3,
NextRetryDelay: 2,
Timeout: 5,
}
- cacheEnabled := false
- var cachePath *string = nil
+ cacheConfig := config.CacheConfig{
+ Mode: config.CacheDisabled,
+ Provider: config.CacheProviderJSONFile,
+ Path: nil,
+ }
hijackedHttpClient := &http.Client{
Transport: &HijackTransport{
@@ -85,7 +89,7 @@ func NewJSONClient(req jsonModel.Request) (*TeamsJSONClient, error) {
graphClient := graph.NewGraphServiceClient(adapter)
- client, err := lib.NewClientFromGraphClient(graphClient, &senderConfig, cacheEnabled, cachePath)
+ client, err := lib.NewClientFromGraphClient(graphClient, &senderConfig, &cacheConfig)
if err != nil {
return nil, err
}
diff --git a/go/internal/json-client/real-client.go b/go/internal/json-client/real-client.go
index a00178c..1711f7a 100644
--- a/go/internal/json-client/real-client.go
+++ b/go/internal/json-client/real-client.go
@@ -5,7 +5,8 @@ package jsonclient
import (
"context"
- lib "github.com/pzsp-teams/lib"
+ "github.com/pzsp-teams/lib"
+ "github.com/pzsp-teams/lib/config"
jsonModel "github.com/pzsp-teams/lib-python/internal/json-model"
)
@@ -20,22 +21,26 @@ func NewJSONClient(req jsonModel.Request) (*TeamsJSONClient, error) {
return nil, err
}
- cacheEnabled, err := jsonModel.ParseCacheEnabled(req.Params["cacheEnabled"])
+ cacheMode, err := jsonModel.ParseCacheMode(req.Config.CacheMode)
if err != nil {
return nil, err
}
-
- var cachePath *string
- if cacheEnabled {
- path, err := jsonModel.ParseCachePath(req.Params["cachePath"])
- if err != nil {
+ cachePath, err := jsonModel.ParseCachePath(req.Config.CachePath)
+ if err != nil {
+ if cacheMode == config.CacheDisabled {
+ cachePath = nil
+ } else {
return nil, err
}
- cachePath = path
+ }
+ cacheConfig := config.CacheConfig{
+ Mode: config.CacheDisabled,
+ Provider: config.CacheProviderJSONFile,
+ Path: cachePath,
}
- client, err := lib.NewClient(context.TODO(), authConfig, senderConfig, cacheEnabled, cachePath)
+ client, err := lib.NewClient(context.TODO(), authConfig, senderConfig, &cacheConfig)
if err != nil {
return nil, err
}
diff --git a/go/internal/json-client/request-map.go b/go/internal/json-client/request-map.go
new file mode 100644
index 0000000..ffb4a1e
--- /dev/null
+++ b/go/internal/json-client/request-map.go
@@ -0,0 +1,189 @@
+package jsonclient
+
+import (
+ "context"
+)
+
+type HandlerFunc func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error)
+
+var Handlers = map[string]HandlerFunc{
+
+ // TEAMS
+ "getTeam": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.GetTeam(ctx, p)
+ },
+
+ "updateTeam": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.UpdateTeam(ctx, p)
+ },
+
+ "listMyJoined": func(c *TeamsJSONClient, ctx context.Context, _ map[string]any) (any, error) {
+ return c.ListMyJoined(ctx)
+ },
+
+ "createTeamViaGroup": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.CreateTeamViaGroup(ctx, p)
+ },
+
+ "createTeamFromTemplate": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.CreateTeamFromTemplate(ctx, p)
+ },
+
+ "archiveTeam": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ArchiveTeam(ctx, p)
+ },
+
+ "unarchiveTeam": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.UnarchiveTeam(ctx, p)
+ },
+
+ "deleteTeam": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.DeleteTeam(ctx, p)
+ },
+
+ "restoreDeletedTeam": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.RestoreDeletedTeam(ctx, p)
+ },
+
+ // CHANNELS
+ "listChannels": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListChannels(ctx, p)
+ },
+
+ "getChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.GetChannel(ctx, p)
+ },
+
+ "createStandardChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.CreateStandardChannel(ctx, p)
+ },
+
+ "createPrivateChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.CreatePrivateChannel(ctx, p)
+ },
+
+ "deleteChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.DeleteChannel(ctx, p)
+ },
+
+ "sendMessageToChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.SendMessageToChannel(ctx, p)
+ },
+
+ "sendReplyToChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.SendReplyToChannel(ctx, p)
+ },
+
+ "listMessagesInChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListMessagesInChannel(ctx, p)
+ },
+
+ "getMessageInChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.GetMessageInChannel(ctx, p)
+ },
+
+ "listMessageRepliesInChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListMessageRepliesInChannel(ctx, p)
+ },
+
+ "getMessageReplyInChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.GetMessageReplyInChannel(ctx, p)
+ },
+
+ "listChannelMembers": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListChannelMembers(ctx, p)
+ },
+
+ "addMemberToChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.AddMemberToChannel(ctx, p)
+ },
+
+ "removeMemberFromChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.RemoveMemberFromChannel(ctx, p)
+ },
+
+ "updateMemberRoleInChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.UpdateMemberInChannel(ctx, p)
+ },
+
+ "getMentionsInChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.GetMentionsInChannel(ctx, p)
+ },
+
+ "searchMessagesInChannel": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.SearchMessagesInChannel(ctx, p)
+ },
+
+ // CHATS
+ "createOneOnOneChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.CreateOneToOneChat(ctx, p)
+ },
+
+ "createGroupChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.CreateGroupChat(ctx, p)
+ },
+
+ "getChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.GetChat(ctx, p)
+ },
+
+ "addMemberToGroupChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.AddMemberToGroupChat(ctx, p)
+ },
+
+ "removeMemberFromGroupChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.RemoveMemberFromGroupChat(ctx, p)
+ },
+
+ "listMembersInGroupChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListGroupChatMembers(ctx, p)
+ },
+
+ "updateGroupChatTopic": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.UpdateGroupChatTopic(ctx, p)
+ },
+
+ "listMessagesInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListMessagesInChat(ctx, p)
+ },
+
+ "sendMessageInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.SendMessageInChat(ctx, p)
+ },
+
+ "deleteMessageInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.DeleteMessageInChat(ctx, p)
+ },
+
+ "getMessageInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.GetMessageInChat(ctx, p)
+ },
+
+ "listMyChats": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListMyChats(ctx, p)
+ },
+
+ "listMyChatMessages": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListMyChatMessages(ctx, p)
+ },
+
+ "listPinnedMessagesInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.ListPinnedMessagesInChat(ctx, p)
+ },
+
+ "pinMessageInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.PinMessageInChat(ctx, p)
+ },
+
+ "unpinMessageInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.UnpinMessageInChat(ctx, p)
+ },
+
+ "getMentionsInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.GetMentionsInChat(ctx, p)
+ },
+
+ "searchMessagesInChat": func(c *TeamsJSONClient, ctx context.Context, p map[string]any) (any, error) {
+ return c.SearchMessagesInChat(ctx, p)
+ },
+}
diff --git a/go/internal/json-client/requests-channels.go b/go/internal/json-client/requests-channels.go
index cdfb18a..1f3a2fa 100644
--- a/go/internal/json-client/requests-channels.go
+++ b/go/internal/json-client/requests-channels.go
@@ -2,22 +2,226 @@ package jsonclient
import (
"context"
- "fmt"
+
+ "github.com/pzsp-teams/lib-python/internal/json-client/decoders"
+ "github.com/pzsp-teams/lib/models"
+ "github.com/pzsp-teams/lib/search"
)
-type ListChannelsParams struct {
+type listChannelsParams struct {
+ TeamRef string `json:"teamRef"`
+}
+
+func (c *TeamsJSONClient) ListChannels(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params listChannelsParams) (any, error) {
+ return c.client.Channels.ListChannels(ctx, params.TeamRef)
+ })
+}
+
+type baseChannelParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+}
+
+func (c *TeamsJSONClient) GetChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params baseChannelParams) (any, error) {
+ return c.client.Channels.Get(ctx, params.TeamRef, params.ChannelRef)
+ })
+}
+
+type createChannelParams struct {
TeamRef string `json:"teamRef"`
+ Name string `json:"name"`
+}
+
+func (c *TeamsJSONClient) CreateStandardChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params createChannelParams) (any, error) {
+ return c.client.Channels.CreateStandardChannel(ctx, params.TeamRef, params.Name)
+ })
+}
+
+type createPrivateChannelParams struct {
+ TeamRef string `json:"teamRef"`
+ Name string `json:"name"`
+ MemberRefs []string `json:"memberRefs"`
+ OwnerRefs []string `json:"ownerRefs"`
+}
+
+func (c *TeamsJSONClient) CreatePrivateChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params createPrivateChannelParams) (any, error) {
+ return c.client.Channels.CreatePrivateChannel(ctx, params.TeamRef, params.Name, params.MemberRefs, params.OwnerRefs)
+ })
+}
+
+func (c *TeamsJSONClient) DeleteChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params baseChannelParams) (any, error) {
+ err := c.client.Channels.Delete(ctx, params.TeamRef, params.ChannelRef)
+ return "deleted", err
+ })
+}
+
+type sendMessageToChannelParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ Body decoders.MessageBodyDTO `json:"body"`
+}
+
+func (c *TeamsJSONClient) SendMessageToChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params sendMessageToChannelParams) (any, error) {
+ body, err := decoders.DecodeParams[models.MessageBody](¶ms.Body)
+ if err != nil {
+ return nil, err
+ }
+ return c.client.Channels.SendMessage(ctx, params.TeamRef, params.ChannelRef, *body)
+ })
+}
+
+type sendReplyToChannelParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ MessageID string `json:"messageId"`
+ Body decoders.MessageBodyDTO `json:"body"`
+}
+
+func (c *TeamsJSONClient) SendReplyToChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params sendReplyToChannelParams) (any, error) {
+ body, err := decoders.DecodeParams[models.MessageBody](¶ms.Body)
+ if err != nil {
+ return nil, err
+ }
+ return c.client.Channels.SendReply(ctx, params.TeamRef, params.ChannelRef, params.MessageID, *body)
+ })
+}
+
+type listMessagesOptionsDTO struct {
+ Top *int32 `json:"top"`
+ ExpandReplies bool `json:"expandReplies"`
+}
+
+type listMessagesParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ Options listMessagesOptionsDTO `json:"options"`
+ IncludeSystem bool `json:"includeSystem"`
+ NextLink decoders.NextLinkDTO `json:"nextLink"`
+}
+
+func (c *TeamsJSONClient) ListMessagesInChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params listMessagesParams) (any, error) {
+ options, err := decoders.DecodeParams[models.ListMessagesOptions](params.Options)
+ if err != nil {
+ return nil, err
+ }
+ nextLink := decoders.GetNextLink(¶ms.NextLink)
+ return c.client.Channels.ListMessages(ctx, params.TeamRef, params.ChannelRef, options, params.IncludeSystem, nextLink)
+ })
+}
+
+type getMessageParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ MessageID string `json:"messageId"`
+}
+
+func (c *TeamsJSONClient) GetMessageInChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params getMessageParams) (any, error) {
+ return c.client.Channels.GetMessage(ctx, params.TeamRef, params.ChannelRef, params.MessageID)
+ })
+}
+
+type listRepliesParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ MessageID string `json:"messageId"`
+ Top *int32 `json:"top"`
+ IncludeSystem bool `json:"includeSystem"`
+ NextLink decoders.NextLinkDTO `json:"nextLink"`
+}
+
+func (c *TeamsJSONClient) ListMessageRepliesInChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params listRepliesParams) (any, error) {
+ nextLink := decoders.GetNextLink(¶ms.NextLink)
+ return c.client.Channels.ListReplies(ctx, params.TeamRef, params.ChannelRef, params.MessageID, params.Top, params.IncludeSystem, nextLink)
+ })
+}
+
+type getReplyParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ MessageID string `json:"messageId"`
+ ReplyID string `json:"replyId"`
+}
+
+func (c *TeamsJSONClient) GetMessageReplyInChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params getReplyParams) (any, error) {
+ return c.client.Channels.GetReply(ctx, params.TeamRef, params.ChannelRef, params.MessageID, params.ReplyID)
+ })
+}
+
+func (c *TeamsJSONClient) ListChannelMembers(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params baseChannelParams) (any, error) {
+ return c.client.Channels.ListMembers(ctx, params.TeamRef, params.ChannelRef)
+ })
+}
+
+type addOrUpdateMemberToChannelParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ UserRef string `json:"userRef"`
+ IsOwner bool `json:"isOwner"`
+}
+
+func (c *TeamsJSONClient) AddMemberToChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params addOrUpdateMemberToChannelParams) (any, error) {
+ return c.client.Channels.AddMember(ctx, params.TeamRef, params.ChannelRef, params.UserRef, params.IsOwner)
+ })
+}
+
+func (c *TeamsJSONClient) UpdateMemberInChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params addOrUpdateMemberToChannelParams) (any, error) {
+ return c.client.Channels.UpdateMemberRoles(ctx, params.TeamRef, params.ChannelRef, params.UserRef, params.IsOwner)
+ })
+}
+
+type removeMemberFromChannelParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ UserRef string `json:"userRef"`
+}
+
+func (c *TeamsJSONClient) RemoveMemberFromChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params removeMemberFromChannelParams) (any, error) {
+ err := c.client.Channels.RemoveMember(ctx, params.TeamRef, params.ChannelRef, params.UserRef)
+ return "removed", err
+ })
+}
+
+type getMentionsInChannelParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ RawMentions []string `json:"rawMentions"`
+}
+
+func (c *TeamsJSONClient) GetMentionsInChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params getMentionsInChannelParams) (any, error) {
+ return c.client.Channels.GetMentions(ctx, params.TeamRef, params.ChannelRef, params.RawMentions)
+ })
+}
+
+type searchMessagesInChannelParams struct {
+ TeamRef string `json:"teamRef"`
+ ChannelRef string `json:"channelRef"`
+ SearchOptions decoders.SearchMessageOptionsDTO `json:"searchMessagesOptions"`
+ SearchConfig decoders.SearchConfigDTO `json:"searchConfig"`
}
-func (jsonclient *TeamsJSONClient) ListChannels(params map[string]interface{}) (interface{}, error) {
- teamRef, ok := params["teamRef"].(string)
- if !ok || teamRef == "" {
- return nil, fmt.Errorf("invalid teamRef parameter")
- }
- channels, err := jsonclient.client.Channels.ListChannels(context.TODO(), teamRef)
- if err != nil {
- return nil, err
- } else {
- return channels, nil
- }
+func (c *TeamsJSONClient) SearchMessagesInChannel(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params searchMessagesInChannelParams) (any, error) {
+ searchOptions := decoders.DecodeSearchMessageOptions(¶ms.SearchOptions)
+ searchConfig, err := decoders.DecodeParams[search.SearchConfig](¶ms.SearchConfig)
+ if err != nil {
+ return nil, err
+ }
+ return c.client.Channels.SearchMessages(ctx, ¶ms.TeamRef, ¶ms.ChannelRef, searchOptions, searchConfig)
+ })
}
diff --git a/go/internal/json-client/requests-chats.go b/go/internal/json-client/requests-chats.go
new file mode 100644
index 0000000..4708e76
--- /dev/null
+++ b/go/internal/json-client/requests-chats.go
@@ -0,0 +1,215 @@
+package jsonclient
+
+import (
+ "context"
+
+ "github.com/pzsp-teams/lib-python/internal/json-client/decoders"
+ "github.com/pzsp-teams/lib/chats"
+ "github.com/pzsp-teams/lib/models"
+ "github.com/pzsp-teams/lib/search"
+)
+
+type createOneToOneChatParams struct {
+ RecipientRef string `json:"recipientRef"`
+}
+
+func (c *TeamsJSONClient) CreateOneToOneChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params createOneToOneChatParams) (any, error) {
+ return c.client.Chats.CreateOneOnOne(ctx, params.RecipientRef)
+ })
+}
+
+type createGroupChatParams struct {
+ RecipientRefs []string `json:"recipientRefs"`
+ Topic string `json:"topic"`
+ IncludeMe bool `json:"includeMe"`
+}
+
+func (c *TeamsJSONClient) CreateGroupChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params createGroupChatParams) (any, error) {
+ return c.client.Chats.CreateGroup(ctx, params.RecipientRefs, params.Topic, params.IncludeMe)
+ })
+}
+
+type baseChatParams struct {
+ ChatRef decoders.ChatRefDTO `json:"chatRef"`
+}
+
+func (c *TeamsJSONClient) GetChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params baseChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ return c.client.Chats.GetChat(ctx, chatRef)
+ })
+}
+
+type groupChatMemberParams struct {
+ GroupChatRef string `json:"groupChatRef"`
+ UserRef string `json:"userRef"`
+}
+
+func (c *TeamsJSONClient) AddMemberToGroupChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params groupChatMemberParams) (any, error) {
+ return c.client.Chats.AddMemberToGroupChat(ctx, chats.GroupChatRef{Ref: params.GroupChatRef}, params.UserRef)
+ })
+}
+
+func (c *TeamsJSONClient) RemoveMemberFromGroupChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params groupChatMemberParams) (any, error) {
+ err := c.client.Chats.RemoveMemberFromGroupChat(ctx, chats.GroupChatRef{Ref: params.GroupChatRef}, params.UserRef)
+ return "removed", err
+ })
+}
+
+type listMembersInChatParams struct {
+ GroupChatRef string `json:"groupChatRef"`
+}
+
+func (c *TeamsJSONClient) ListGroupChatMembers(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params listMembersInChatParams) (any, error) {
+ return c.client.Chats.ListGroupChatMembers(ctx, chats.GroupChatRef{Ref: params.GroupChatRef})
+ })
+}
+
+type updateGroupChatTopicParams struct {
+ GroupChatRef string `json:"groupChatRef"`
+ Topic string `json:"topic"`
+}
+
+func (c *TeamsJSONClient) UpdateGroupChatTopic(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params updateGroupChatTopicParams) (any, error) {
+ return c.client.Chats.UpdateGroupChatTopic(ctx, chats.GroupChatRef{Ref: params.GroupChatRef}, params.Topic)
+ })
+}
+
+type listMessagesInChatParams struct {
+ ChatRef decoders.ChatRefDTO `json:"chatRef"`
+ IncludeSystem bool `json:"includeSystem"`
+ NextLink decoders.NextLinkDTO `json:"nextLink"`
+}
+
+func (c *TeamsJSONClient) ListMessagesInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params listMessagesInChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ nextLink := decoders.GetNextLink(¶ms.NextLink)
+ return c.client.Chats.ListMessages(ctx, chatRef, params.IncludeSystem, nextLink)
+ })
+}
+
+type sendMessageInChatParams struct {
+ ChatRef decoders.ChatRefDTO `json:"chatRef"`
+ Body decoders.MessageBodyDTO `json:"body"`
+}
+
+func (c *TeamsJSONClient) SendMessageInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params sendMessageInChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ body, err := decoders.DecodeParams[models.MessageBody](params.Body)
+ if err != nil {
+ return nil, err
+ }
+ return c.client.Chats.SendMessage(ctx, chatRef, *body)
+ })
+}
+
+type messageInChatParams struct {
+ ChatRef decoders.ChatRefDTO `json:"chatRef"`
+ MessageID string `json:"messageId"`
+}
+
+func (c *TeamsJSONClient) DeleteMessageInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params messageInChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ err := c.client.Chats.DeleteMessage(ctx, chatRef, params.MessageID)
+ return "deleted", err
+ })
+}
+
+func (c *TeamsJSONClient) GetMessageInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params messageInChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ return c.client.Chats.GetMessage(ctx, chatRef, params.MessageID)
+ })
+}
+
+type ListMyChatsParams struct {
+ ChatType string `json:"chatType"`
+}
+
+func (c *TeamsJSONClient) ListMyChats(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params ListMyChatsParams) (any, error) {
+ var chatType models.ChatType
+ if params.ChatType != "" {
+ chatType = models.ChatType(params.ChatType)
+ }
+ return c.client.Chats.ListChats(ctx, &chatType)
+ })
+}
+
+type listChatMessagesParams struct {
+ StartTime string `json:"startTime"`
+ EndTime string `json:"endTime"`
+ Top *int32 `json:"top"`
+}
+
+func (c *TeamsJSONClient) ListMyChatMessages(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params listChatMessagesParams) (any, error) {
+ parsedStartTime, parsedEndTime, err := decoders.DecodeTimeRange(params.StartTime, params.EndTime)
+ if err != nil {
+ return nil, err
+ }
+ return c.client.Chats.ListAllMessages(ctx, parsedStartTime, parsedEndTime, params.Top)
+ })
+}
+
+func (c *TeamsJSONClient) ListPinnedMessagesInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params baseChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ return c.client.Chats.ListPinnedMessages(ctx, chatRef)
+ })
+}
+
+func (c *TeamsJSONClient) PinMessageInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params messageInChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ err := c.client.Chats.PinMessage(ctx, chatRef, params.MessageID)
+ return "pinned", err
+ })
+}
+
+func (c *TeamsJSONClient) UnpinMessageInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params messageInChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ err := c.client.Chats.UnpinMessage(ctx, chatRef, params.MessageID)
+ return "unpinned", err
+ })
+}
+
+type mentionInChatParams struct {
+ ChatRef decoders.ChatRefDTO `json:"chatRef"`
+ RawMentions []string `json:"rawMentions"`
+}
+
+func (c *TeamsJSONClient) GetMentionsInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params mentionInChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ return c.client.Chats.GetMentions(ctx, chatRef, params.RawMentions)
+ })
+}
+
+type searchMessagesInChatParams struct {
+ ChatRef decoders.ChatRefDTO `json:"chatRef"`
+ SearchOptions decoders.SearchMessageOptionsDTO `json:"searchMessagesOptions"`
+ SearchConfig decoders.SearchConfigDTO `json:"searchConfig"`
+}
+
+func (c *TeamsJSONClient) SearchMessagesInChat(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params searchMessagesInChatParams) (any, error) {
+ chatRef := decoders.GetChatRef(params.ChatRef)
+ searchOptions := decoders.DecodeSearchMessageOptions(¶ms.SearchOptions)
+ searchConfig, err := decoders.DecodeParams[search.SearchConfig](¶ms.SearchConfig)
+ if err != nil {
+ return nil, err
+ }
+ return c.client.Chats.SearchMessages(ctx, chatRef, searchOptions, searchConfig)
+ })
+}
diff --git a/go/internal/json-client/requests-teams.go b/go/internal/json-client/requests-teams.go
new file mode 100644
index 0000000..f8608a8
--- /dev/null
+++ b/go/internal/json-client/requests-teams.go
@@ -0,0 +1,104 @@
+package jsonclient
+
+import (
+ "context"
+
+ "github.com/pzsp-teams/lib-python/internal/json-client/decoders"
+)
+
+type getTeamParams struct {
+ TeamRef string `json:"teamRef"`
+}
+
+func (c *TeamsJSONClient) GetTeam(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params getTeamParams) (any, error) {
+ return c.client.Teams.Get(ctx, params.TeamRef)
+ })
+}
+
+func (c *TeamsJSONClient) ListMyJoined(ctx context.Context) (any, error) {
+ return c.client.Teams.ListMyJoined(ctx)
+}
+
+type updateTeamParams struct {
+ TeamRef string `json:"teamRef"`
+ TeamUpdate decoders.UpdateTeamDTO `json:"team"`
+}
+
+func (c *TeamsJSONClient) UpdateTeam(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params updateTeamParams) (any, error) {
+ updateTeam := decoders.GetUpdateTeam(¶ms.TeamUpdate)
+ return c.client.Teams.UpdateTeam(ctx, params.TeamRef, &updateTeam)
+ })
+}
+
+type createViaGroupParams struct {
+ DisplayName string `json:"displayName"`
+ MailNickname string `json:"mailNickname"`
+ Visibility string `json:"visibility"`
+}
+
+func (c *TeamsJSONClient) CreateTeamViaGroup(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params createViaGroupParams) (any, error) {
+ return c.client.Teams.CreateViaGroup(ctx, params.DisplayName, params.MailNickname, params.Visibility)
+ })
+}
+
+type createFromTemplateParams struct {
+ DisplayName string `json:"displayName"`
+ Description string `json:"description"`
+ Owners []string `json:"owners"`
+ Members []string `json:"members"`
+ Visibility string `json:"visibility"`
+ IncludeMe bool `json:"includeMe"`
+}
+
+func (c *TeamsJSONClient) CreateTeamFromTemplate(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params createFromTemplateParams) (any, error) {
+ return c.client.Teams.CreateFromTemplate(ctx, params.DisplayName, params.Description, params.Owners, params.Members, params.Visibility, params.IncludeMe)
+ })
+}
+
+type archiveTeamParams struct {
+ TeamRef string `json:"teamRef"`
+ SpoReadOnlyFromMembers *bool `json:"spoReadOnlyFromMembers,omitempty"`
+}
+
+func (c *TeamsJSONClient) ArchiveTeam(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params archiveTeamParams) (any, error) {
+ err := c.client.Teams.Archive(ctx, params.TeamRef, params.SpoReadOnlyFromMembers)
+ return "archived", err
+ })
+}
+
+type unarchiveTeamParams struct {
+ TeamRef string `json:"teamRef"`
+}
+
+func (c *TeamsJSONClient) UnarchiveTeam(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params unarchiveTeamParams) (any, error) {
+ err := c.client.Teams.Unarchive(ctx, params.TeamRef)
+ return "unarchived", err
+ })
+}
+
+type deleteTeamParams struct {
+ TeamRef string `json:"teamRef"`
+}
+
+func (c *TeamsJSONClient) DeleteTeam(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params deleteTeamParams) (any, error) {
+ err := c.client.Teams.Delete(ctx, params.TeamRef)
+ return "deleted", err
+ })
+}
+
+type restoreTeamParams struct {
+ DeletedGroupID string `json:"deletedGroupId"`
+}
+
+func (c *TeamsJSONClient) RestoreDeletedTeam(ctx context.Context, p map[string]any) (any, error) {
+ return execute(p, func(params restoreTeamParams) (any, error) {
+ return c.client.Teams.RestoreDeleted(ctx, params.DeletedGroupID)
+ })
+}
diff --git a/go/internal/json-client/teams-json-client.go b/go/internal/json-client/teams-json-client.go
index afe9430..7ef7360 100644
--- a/go/internal/json-client/teams-json-client.go
+++ b/go/internal/json-client/teams-json-client.go
@@ -1,9 +1,19 @@
package jsonclient
import (
- lib "github.com/pzsp-teams/lib"
+ "github.com/pzsp-teams/lib"
+
+ "github.com/pzsp-teams/lib-python/internal/json-client/decoders"
)
type TeamsJSONClient struct {
client *lib.Client
}
+
+func execute[T any](p map[string]any, fn func(params T) (any, error)) (any, error) {
+ params, err := decoders.DecodeParams[T](p)
+ if err != nil {
+ return nil, err
+ }
+ return fn(*params)
+}
diff --git a/go/internal/json-model/json-model.go b/go/internal/json-model/json-model.go
index 3d76018..05f24bc 100644
--- a/go/internal/json-model/json-model.go
+++ b/go/internal/json-model/json-model.go
@@ -3,20 +3,20 @@ package jsonmodel
import (
"fmt"
- "github.com/pzsp-teams/lib"
+ "github.com/pzsp-teams/lib/config"
)
type Request struct {
Type string `json:"type"`
Method string `json:"method,omitempty"`
Config Config `json:"config,omitempty"`
- Params map[string]interface{} `json:"params,omitempty"`
+ Params map[string]any `json:"params,omitempty"`
}
type Config struct {
SenderConfigMap SenderConfigMap `json:"senderConfig"`
AuthConfigMap AuthConfigMap `json:"authConfig"`
- CacheEnabled bool `json:"cacheEnabled"`
+ CacheMode string `json:"cacheMode,omitempty"`
CachePath string `json:"cachePath,omitempty"`
}
@@ -26,7 +26,7 @@ type SenderConfigMap struct {
Timeout int `json:"timeout"`
}
-func (scm SenderConfigMap) ToSenderConfig() (*lib.SenderConfig, error) {
+func (scm SenderConfigMap) ToSenderConfig() (*config.SenderConfig, error) {
if scm.MaxRetries < 0 {
return nil, fmt.Errorf("maxRetries cannot be negative")
}
@@ -37,7 +37,7 @@ func (scm SenderConfigMap) ToSenderConfig() (*lib.SenderConfig, error) {
return nil, fmt.Errorf("timeout must be > 0")
}
- return &lib.SenderConfig{
+ return &config.SenderConfig{
MaxRetries: scm.MaxRetries,
NextRetryDelay: scm.NextRetryDelay,
Timeout: scm.Timeout,
@@ -52,7 +52,7 @@ type AuthConfigMap struct {
AuthMethod string `json:"authMethod"`
}
-func (acm AuthConfigMap) ToAuthConfig() (*lib.AuthConfig, error) {
+func (acm AuthConfigMap) ToAuthConfig() (*config.AuthConfig, error) {
if acm.ClientID == "" {
return nil, fmt.Errorf("clientId is required")
}
@@ -74,33 +74,34 @@ func (acm AuthConfigMap) ToAuthConfig() (*lib.AuthConfig, error) {
return nil, err
}
- return &lib.AuthConfig{
+ return &config.AuthConfig{
ClientID: acm.ClientID,
Tenant: acm.Tenant,
Email: acm.Email,
Scopes: acm.Scopes,
- AuthMethod: authMethod,
+ AuthMethod: config.Method(authMethod),
}, nil
}
-func ParseCacheEnabled(value interface{}) (bool, error) {
- if value == nil {
- return false, nil // default
- }
-
- enabled, ok := value.(bool)
- if !ok {
- return false, fmt.Errorf("cacheEnabled must be boolean")
- }
- return enabled, nil
+func ParseCachePath(value any) (*string, error) {
+ path, ok := value.(string)
+ if !ok || path == "" {
+ return nil, fmt.Errorf("cachePath must be a non-empty string")
+ }
+ return &path, nil
}
-func ParseCachePath(value interface{}) (*string, error) {
- path, ok := value.(string)
- if !ok || path == "" {
- return nil, fmt.Errorf("cachePath must be a non-empty string")
- }
- return &path, nil
+func ParseCacheMode(value any) (config.CacheMode, error) {
+ switch value {
+ case "DISABLED":
+ return config.CacheDisabled, nil
+ case "SYNC":
+ return config.CacheSync, nil
+ case "ASYNC":
+ return config.CacheAsync, nil
+ default:
+ return config.CacheDisabled, fmt.Errorf("invalid cacheMode: %s", value)
+ }
}
func validateAuthMethod(method string) (string, error) {
diff --git a/python/.env.template b/python/.env.template
new file mode 100644
index 0000000..e1fca19
--- /dev/null
+++ b/python/.env.template
@@ -0,0 +1,5 @@
+CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxx
+TENANT_ID=xxxxxxxxxxxxxxxxxxxxxxxxx
+EMAIL=xxxxxxxxxxxxxxxxxxxxxxxxx
+SCOPES=openid,profile,User.Read.All,Team.ReadBasic.All,Channel.ReadBasic.All,ChannelSettings.ReadWrite.All,Channel.Create,Channel.Delete.All,ChannelMessage.Send,ChannelMember.ReadWrite.All,Group.ReadWrite.All,TeamMember.ReadWrite.All
+AUTH_METHOD=INTERACTIVE
\ No newline at end of file
diff --git a/python/.gitignore b/python/.gitignore
new file mode 100644
index 0000000..45ddf0a
--- /dev/null
+++ b/python/.gitignore
@@ -0,0 +1 @@
+site/
diff --git a/python/README.md b/python/README.md
index e69de29..1ccd0ae 100644
--- a/python/README.md
+++ b/python/README.md
@@ -0,0 +1,120 @@
+# Teams API Python Wrapper
+
+[](https://www.python.org/)
+[](LICENSE)
+[](https://pzsp-teams.github.io/lib-python/)
+
+
+
+A high-performance **Python wrapper** for the Microsoft Teams Graph API. It leverages a compiled **Go (Golang)** backend to handle heavy lifting, concurrency, and caching, while providing a clean, fully typed Pythonic interface.
+
+## 🚀 Key Features
+
+- **Performance of Go**: Uses a compiled Go subprocess for efficient API communication and state management.
+- **Pythonic Interface**: Fully typed using `dataclasses` and `Enums`. Native support for IDE autocompletion and type checkers (`mypy`).
+- **Intelligent Cache**: Background caching of Team/Channel IDs to minimize API throttling and latency (handled transparently by the backend).
+- **Simplified Auth**: Wraps MSAL authentication flows configuration.
+
+## 📦 Installation
+
+```bash
+pip install teams-lib-pzsp2-z1
+```
+
+## 🛠️ Architecture
+
+This library acts as a bridge. When initialized, it spawns a dedicated Go subprocess (`teamsClientLib`) and communicates via JSON-RPC over `stdin`/`stdout`.
+
+The `TeamsClient` aggregates domain-specific services:
+
+- **client.teams**: Manage team lifecycles (create, list, delete).
+
+- **client.channels**: Manage standard and private channels.
+
+- **client.chats**: Handle messages, mentions, and chat participants.
+
+## 💻 Quick Start
+
+### 1. Configuration
+
+Create a `.env` in your project with your Azure AD credentials:
+
+```ini
+CLIENT_ID=your-client-id-uuid
+TENANT_ID=your-tenant-id-uuid
+EMAIL=user@example.com
+SCOPES=User.Read,Team.ReadBasic.All,Channel.ReadBasic.All
+AUTH_METHOD=DEVICE_CODE [or INTERACTIVE]
+```
+
+Scopes needed by all functions could be found in [.env.template](https://github.com/pzsp-teams/lib-python/tree/main/.env.template).
+
+### 2. Basic usage
+
+Here is a minimal example showing how to initialize the client and list joined teams:
+
+```python
+from teams_lib_pzsp2_z1.client import TeamsClient
+from teams_lib_pzsp2_z1.config import CacheMode
+
+def main():
+ # Initialize the client.
+ # This spawns the Go subprocess and performs authentication.
+ client = TeamsClient()
+
+ try:
+ print("Fetching teams...")
+ teams = client.teams.list_my_joined()
+
+ for team in teams:
+ print(f"Team: {team.display_name} | ID: {team.id}")
+
+ # Example: List channels in the first team
+ channels = client.channels.list_channels(team.id)
+ print(f" -> Found {len(channels)} channels.")
+
+ except RuntimeError as e:
+ print(f"An error occurred: {e}")
+
+ finally:
+ # Crucial: Close the client to ensure background cache writes finish
+ # and the Go subprocess is terminated gracefully.
+ client.close()
+
+if __name__ == "__main__":
+ main()
+```
+
+## 📚 Documentation
+
+Full API reference, architecture details, and configuration guides are available here:
+
+👉 [Read the Documentation](https://pzsp-teams.github.io/lib-python/)
+
+## Authentication
+
+The library loads credentials from environment variables. Ensure your Azure App Registration has the necessary **API Permissions** (e.g., `Team.ReadBasic.All`, `Channel.ReadBasic.All`) granted in the Azure Portal.
+
+Supported Authentication Methods (`AUTH_METHOD`):
+
+- **INTERACTIVE**: Opens a browser window automatically.
+
+- **DEVICE_CODE**: Prints a code to the console to login via microsoft.com/devicelogin.
+
+## Cache & Lifecycle
+
+If caching is enabled (SYNC or ASYNC), the Go backend stores metadata locally (e.g., `teams_cache.json`) to speed up reference resolution.
+
+⚠️ Important: client.close()
+Because the Go backend might be running background goroutines (especially for Async Cache), you **must** call `client.close()` before your script exits. This ensures:
+
+1. Pending cache writes are flushed to disk.
+
+2. The Go subprocess is terminated correctly.
+
+```python
+client.close()
+```
+
+## 📄 License
+This project is licensed under the MIT License - see the [LICENSE](https://github.com/pzsp-teams/lib-python/blob/main/LICENSE) file for details.
diff --git a/python/docs/index.md b/python/docs/index.md
new file mode 100644
index 0000000..dd570f2
--- /dev/null
+++ b/python/docs/index.md
@@ -0,0 +1,128 @@
+# Teams Lib Wrapper
+
+Welcome to the documentation for the **Teams API library**.
+This library provides a high-level, Pythonic interface for managing Microsoft Teams, leveraging a compiled Go backend for performance, authentication, and caching.
+
+---
+
+## Overview
+
+This project bridges the gap between Python's ease of use and Go's concurrency model.
+It acts as a wrapper around a Go binary (`teamsClientLib`), communicating via a JSON-based IPC (Inter-Process Communication) protocol.
+
+---
+
+## Key Features
+
+- **Pythonic Data Models**
+ Uses standard Python `dataclasses` and `Enum`s instead of raw JSON or complex Microsoft Graph SDK objects.
+
+- **Automatic Caching**
+ Reduces API calls by caching Team and Channel references (handled transparently by the Go backend).
+
+- **Simple Authentication**
+ Thin wrappers around MSAL (Microsoft Authentication Library) configuration.
+
+- **Type Hinting**
+ Full support for static type checkers like `mypy` and IDE autocompletion.
+
+---
+
+## Installation
+
+Library is available fully via pip
+
+```bash
+pip install teams_lib_pzsp2_z1
+```
+
+## Quick start
+
+### 1. Configuration
+
+Create a `.env` in your project with your Azure AD credentials:
+
+```ini
+CLIENT_ID=your-client-id-uuid
+TENANT_ID=your-tenant-id-uuid
+EMAIL=user@example.com
+SCOPES=User.Read,Team.ReadBasic.All,Channel.ReadBasic.All
+AUTH_METHOD=DEVICE_CODE [or INTERACTIVE]
+```
+
+Scopes needed by all functions could be found in [.env.template](https://github.com/pzsp-teams/lib-python/tree/main/.env.template).
+
+### 2. Basic usage
+
+Here is a minimal example showing how to initialize the client and list joined teams:
+
+```python
+from teams_lib_pzsp2_z1.client import TeamsClient
+from teams_lib_pzsp2_z1.config import CacheMode
+
+def main():
+ # Initialize the client.
+ # This spawns the Go subprocess and performs authentication.
+ client = TeamsClient()
+
+ try:
+ print("Fetching teams...")
+ teams = client.teams.list_my_joined()
+
+ for team in teams:
+ print(f"Team: {team.display_name} | ID: {team.id}")
+
+ # Example: List channels in the first team
+ channels = client.channels.list_channels(team.id)
+ print(f" -> Found {len(channels)} channels.")
+
+ except RuntimeError as e:
+ print(f"An error occurred: {e}")
+
+ finally:
+ # Crucial: Close the client to ensure background cache writes finish
+ # and the Go subprocess is terminated gracefully.
+ client.close()
+
+if __name__ == "__main__":
+ main()
+```
+
+## Architecture
+
+The library relies on a compiled Go backend to handle performance-critical tasks. When `TeamsClient` is initialized, the following sequence occurs:
+
+1. **OS Detection**: The library checks the host operating system (Windows/Linux).
+2. **Process Launch**: It launches the corresponding Go binary as a background subprocess.
+3. **Initialization**: It sends an `init` command via `stdin` to configure the MSAL authentication provider and the caching engine.
+4. **Request Handling**: Subsequent method calls (e.g., `client.teams.get(...)`) follow this flow:
+ - Arguments are **serialized to JSON**.
+ - The payload is sent to the Go process via **stdin**.
+ - The Go process executes the request against the **Microsoft Graph API**.
+ - The JSON response is read from **stdout** and mapped back to native **Python objects**.
+
+### Process Flow Diagram
+
+```mermaid
+sequenceDiagram
+ participant Py as Python Client
+ participant Go as Go Subprocess
+ participant MS as Microsoft Graph
+
+ rect rgba(230, 240, 255, 0.7)
+ note right of Py: Phase 1: Initialization
+ Py->>Py: Detect OS & Spawn Process
+ Py->>Go: Send {"type": "init", ...}
+ Go->>Go: Setup MSAL & Cache
+ Go-->>Py: {"result": "initialized"}
+ end
+
+ rect rgba(240, 255, 240, 0.7)
+ note right of Py: Phase 2: Normal Operation Loop
+ Py->>Go: Send {"type": "request", "method": "getTeam", ...}
+ Go->>MS: GET /v1.0/teams/{id}
+ MS-->>Go: HTTP 200 OK (JSON)
+ Go-->>Py: {"result": {...}}
+ Py->>Py: Map JSON to Python DataClass
+ end
+```
diff --git a/python/docs/reference/client.md b/python/docs/reference/client.md
new file mode 100644
index 0000000..00d7b9d
--- /dev/null
+++ b/python/docs/reference/client.md
@@ -0,0 +1,8 @@
+# Client Reference
+
+Main lib entry point.
+
+::: teams_lib_pzsp2_z1.client.TeamsClient
+ options:
+ show_root_heading: true
+ show_source: true
\ No newline at end of file
diff --git a/python/docs/reference/config.md b/python/docs/reference/config.md
new file mode 100644
index 0000000..b6ccc47
--- /dev/null
+++ b/python/docs/reference/config.md
@@ -0,0 +1,5 @@
+# Configuration Reference
+
+Config structures.
+
+::: teams_lib_pzsp2_z1.config
\ No newline at end of file
diff --git a/python/docs/reference/models/channel.md b/python/docs/reference/models/channel.md
new file mode 100644
index 0000000..8346b97
--- /dev/null
+++ b/python/docs/reference/models/channel.md
@@ -0,0 +1,3 @@
+# Channel Model
+
+::: teams_lib_pzsp2_z1.model.channel
\ No newline at end of file
diff --git a/python/docs/reference/models/chat.md b/python/docs/reference/models/chat.md
new file mode 100644
index 0000000..b4eb115
--- /dev/null
+++ b/python/docs/reference/models/chat.md
@@ -0,0 +1,3 @@
+# Chat Model
+
+::: teams_lib_pzsp2_z1.model.chat
\ No newline at end of file
diff --git a/python/docs/reference/models/member.md b/python/docs/reference/models/member.md
new file mode 100644
index 0000000..6056a2c
--- /dev/null
+++ b/python/docs/reference/models/member.md
@@ -0,0 +1,3 @@
+# Member Model
+
+::: teams_lib_pzsp2_z1.model.member
\ No newline at end of file
diff --git a/python/docs/reference/models/message.md b/python/docs/reference/models/message.md
new file mode 100644
index 0000000..90ec708
--- /dev/null
+++ b/python/docs/reference/models/message.md
@@ -0,0 +1,5 @@
+# Message Models
+
+::: teams_lib_pzsp2_z1.model.message
+
+::: teams_lib_pzsp2_z1.model.mention
\ No newline at end of file
diff --git a/python/docs/reference/models/search.md b/python/docs/reference/models/search.md
new file mode 100644
index 0000000..306e71a
--- /dev/null
+++ b/python/docs/reference/models/search.md
@@ -0,0 +1,3 @@
+# Search messages model
+
+::: teams_lib_pzsp2_z1.model.search
\ No newline at end of file
diff --git a/python/docs/reference/models/team.md b/python/docs/reference/models/team.md
new file mode 100644
index 0000000..b1176db
--- /dev/null
+++ b/python/docs/reference/models/team.md
@@ -0,0 +1,3 @@
+# Team Model
+
+::: teams_lib_pzsp2_z1.model.team
\ No newline at end of file
diff --git a/python/docs/reference/services/channels.md b/python/docs/reference/services/channels.md
new file mode 100644
index 0000000..a0748e0
--- /dev/null
+++ b/python/docs/reference/services/channels.md
@@ -0,0 +1,5 @@
+# Channels Service
+
+Channels in team management.
+
+::: teams_lib_pzsp2_z1.services.channels.ChannelsService
\ No newline at end of file
diff --git a/python/docs/reference/services/chats.md b/python/docs/reference/services/chats.md
new file mode 100644
index 0000000..de4182a
--- /dev/null
+++ b/python/docs/reference/services/chats.md
@@ -0,0 +1,5 @@
+# Chats Service
+
+Handling one-to-one and group chats.
+
+::: teams_lib_pzsp2_z1.services.chats.ChatsService
\ No newline at end of file
diff --git a/python/docs/reference/services/teams.md b/python/docs/reference/services/teams.md
new file mode 100644
index 0000000..d62308d
--- /dev/null
+++ b/python/docs/reference/services/teams.md
@@ -0,0 +1,5 @@
+# Teams Service
+
+Team lifecycle management.
+
+::: teams_lib_pzsp2_z1.services.teams.TeamsService
\ No newline at end of file
diff --git a/python/mkdocs.yml b/python/mkdocs.yml
new file mode 100644
index 0000000..a861f32
--- /dev/null
+++ b/python/mkdocs.yml
@@ -0,0 +1,60 @@
+site_name: Teams Lib Wrapper Documentation
+site_url: https://pzsp2z1teams.com/
+repo_url: https://github.com/pzsp-teams/lib-python
+edit_uri: edit/main/docs/
+
+theme:
+ name: material
+ features:
+ - content.code.copy
+ - navigation.expand
+ palette:
+ - scheme: default
+ primary: indigo
+ accent: indigo
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ - scheme: slate
+ primary: indigo
+ accent: indigo
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
+
+plugins:
+ - search
+ - mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ paths: [.]
+ options:
+ docstring_style: google
+ show_source: true
+ show_root_heading: true
+ show_category_heading: true
+
+markdown_extensions:
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+
+nav:
+ - Home: index.md
+ - API Reference:
+ - Client: reference/client.md
+ - Config: reference/config.md
+ - Services:
+ - Teams: reference/services/teams.md
+ - Channels: reference/services/channels.md
+ - Chats: reference/services/chats.md
+ - Models:
+ - Team: reference/models/team.md
+ - Channel: reference/models/channel.md
+ - Message: reference/models/message.md
+ - Member: reference/models/member.md
+ - Chat: reference/models/chat.md
+ - Search: reference/models/search.md
\ No newline at end of file
diff --git a/python/pyproject.toml b/python/pyproject.toml
index fcf29b1..d709581 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -23,6 +23,7 @@ fix = true
show-fixes = true
src = ["teams_lib_pzsp2_z1"]
exclude = ["test_*.py", "tests/*"]
+ignore = ["E501"]
[tool.ruff.lint]
diff --git a/python/teams_lib_pzsp2_z1/client.py b/python/teams_lib_pzsp2_z1/client.py
index 055ac45..b2f6ec1 100644
--- a/python/teams_lib_pzsp2_z1/client.py
+++ b/python/teams_lib_pzsp2_z1/client.py
@@ -2,39 +2,93 @@
import pathlib
import platform
import subprocess
+import sys
import threading
from typing import Any
from teams_lib_pzsp2_z1 import config
from teams_lib_pzsp2_z1.services.channels import ChannelsService
+from teams_lib_pzsp2_z1.services.chats import ChatsService
+from teams_lib_pzsp2_z1.services.teams import TeamsService
class TeamsClient:
+ """The main entry point for interacting with the Microsoft Teams library.
+
+ This class manages the lifecycle of the underlying Go subprocess, handles
+ Inter-Process Communication (IPC) via JSON over stdin/stdout, and exposes
+ high-level services for Teams, Channels, and Chats.
+
+ **Architecture:**
+ The Python library acts as a frontend wrapper. It spawns a compiled Go binary
+ (`teamsClientLib`) as a subprocess. Commands are serialized to JSON and sent
+ to the Go process, which executes the actual Microsoft Graph API calls and
+ returns the results.
+
+ Attributes:
+ channels (ChannelsService): Service for managing channels.
+ teams (TeamsService): Service for managing teams.
+ chats (ChatsService): Service for managing chats and messages.
+ """
+
+
def __init__(
self,
auto_init: bool = True,
env_path: str | None = None,
- cache_enabled: bool = False,
+ cache_mode: config.CacheMode = config.CacheMode.DISABLED,
cache_path: str | None = None,
):
+ """Initializes the TeamsClient and spawns the Go subprocess.
+
+ Args:
+ auto_init (bool, optional): If True, automatically calls `init_client`
+ using configuration loaded from the environment. Defaults to True.
+ env_path (str | None, optional): Path to the `.env` file containing
+ credentials (CLIENT_ID, TENANT_ID, etc.). If None, defaults are used.
+ cache_mode (config.CacheMode, optional): The caching strategy to use
+ (e.g., DISABLED, IN_MEMORY, DISK). Defaults to DISABLED.
+ cache_path (str | None, optional): File path for the cache (required if
+ cache_mode is DISK). Defaults to None.
+
+ Raises:
+ RuntimeError: If the operating system is not supported (only Windows/Linux).
+ """
+
self._lock = threading.Lock()
+ # Spawn the Go binary in a subprocess with piped IO
self.proc = subprocess.Popen( # noqa: S603
[str(self._binary())],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL,
+ stderr=sys.stderr, # Pass Go logs directly to stderr
text=True,
bufsize=1,
)
self.env_path = env_path
+
+ # Initialize services with a reference to self (to access .execute())
self.channels = ChannelsService(self)
+ self.teams = TeamsService(self)
+ self.chats = ChatsService(self)
+
+ self.is_cache_enabled = cache_mode != config.CacheMode.DISABLED
if auto_init:
- self.init_client(cache_enabled, cache_path)
+ self.init_client(cache_mode, cache_path)
def _binary(self):
+ """Resolves the path to the correct Go binary for the current OS.
+
+ Returns:
+ pathlib.Path: The absolute path to the executable.
+
+ Raises:
+ RuntimeError: If the OS is not Windows or Linux.
+ """
+
base = pathlib.Path(__file__).parent / "bin"
osname = platform.system()
@@ -46,8 +100,26 @@ def _binary(self):
raise RuntimeError("Unsupported OS")
def init_client(
- self, cache_enabled: bool = False, cache_path: str | None = None
+ self,
+ cache_mode: config.CacheMode = config.CacheMode.DISABLED,
+ cache_path: str | None = None,
) -> Any:
+ """Initializes the Go backend with authentication and cache configuration.
+
+ This method sends the 'init' command to the Go process, which sets up the
+ MSAL token provider and the Graph Service Client.
+
+ Args:
+ cache_mode (config.CacheMode): The caching strategy.
+ cache_path (str | None): Path to the cache file (if applicable).
+
+ Returns:
+ Any: The initialization result from the Go process.
+
+ Raises:
+ RuntimeError: If the Go process reports an initialization error.
+ """
+
sender_config = config.SenderConfig()
auth_config = config.load_auth_config(self.env_path)
return self.execute(
@@ -65,12 +137,24 @@ def init_client(
"scopes": auth_config.scopes,
"authMethod": auth_config.auth_method,
},
- "cacheEnabled": cache_enabled,
+ "cacheMode": cache_mode.value,
"cachePath": cache_path,
},
)
def init_fake_client(self, mock_server_url: str) -> Any:
+ """Initializes the Go backend in test mode using a mock server.
+
+ This bypasses real MSAL authentication and directs Graph API calls to
+ the provided local URL. [For testing purposes only]
+
+ Args:
+ mock_server_url (str): The URL of the mock HTTP server.
+
+ Returns:
+ Any: The result of the initialization.
+ """
+
return self.execute(
cmd_type="init",
params={
@@ -85,6 +169,27 @@ def execute(
config: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
) -> Any:
+ """Executes a command on the Go subprocess via JSON-IPC.
+
+ This is the low-level bridge method used by services to communicate with the backend.
+ It handles serialization, thread-safe writing to stdin, reading from stdout,
+ and error propagation.
+
+ Args:
+ cmd_type (str): The type of command (e.g., "init", "request").
+ method (str | None): The specific API method to call (e.g., "listChannels").
+ Required if cmd_type is "request".
+ config (dict | None): Configuration payload (used primarily for initialization).
+ params (dict | None): Parameters for the method call (e.g., teamRef, body).
+
+ Returns:
+ Any: The 'result' field from the Go response.
+
+ Raises:
+ RuntimeError: If the Go process crashes, closes the pipe, returns an empty response,
+ or explicitly reports an error in the "error" field.
+ """
+
payload = {"type": cmd_type}
if method:
payload["method"] = method
@@ -95,13 +200,19 @@ def execute(
json_payload = json.dumps(payload)
- # Critical section to avoid interleaving requests/responses
+ # Critical section to avoid interleaving requests/responses#
+ # (e.g., thread A writes request A, thread B writes request B,
+ # then thread A reads response B).
with self._lock:
try:
self.proc.stdin.write(json_payload + "\n")
self.proc.stdin.flush()
+ print("Sent to Go process:", json_payload) # Debug print
+
raw_response = self.proc.stdout.readline()
+
+ print("Received from Go process:", raw_response.strip()) # Debug print
except BrokenPipeError:
raise RuntimeError("Go process crashed or closed connection") # noqa: B904
@@ -116,4 +227,35 @@ def execute(
return res.get("result")
def close(self):
- self.proc.terminate()
+ """Gracefully closes the Go backend, ensuring cache is synced.
+
+ Sends a 'close' command to the Go process, which triggers `lib.Close()`
+ to wait for background cache operations. Then terminates the process.
+ """
+
+ if self.is_cache_enabled:
+ with self._lock:
+ if self.proc.poll() is None:
+ try:
+ payload = json.dumps({"type": "close"}) + "\n"
+ self.proc.stdin.write(payload)
+ self.proc.stdin.flush()
+ except (BrokenPipeError, OSError):
+ pass
+
+ try:
+ self.proc.wait(timeout=3)
+ except subprocess.TimeoutExpired:
+ self.proc.kill()
+ self.proc.wait()
+
+ try:
+ if self.proc.stdin:
+ self.proc.stdin.close()
+ if self.proc.stdout:
+ self.proc.stdout.close()
+ except (OSError, ValueError):
+ pass
+ else:
+ self.proc.terminate()
+ self.proc.wait()
diff --git a/python/teams_lib_pzsp2_z1/config.py b/python/teams_lib_pzsp2_z1/config.py
index 286aa89..fa6f40f 100644
--- a/python/teams_lib_pzsp2_z1/config.py
+++ b/python/teams_lib_pzsp2_z1/config.py
@@ -1,15 +1,36 @@
+"""
+Package config holds configuration structures used across the application.
+
+This module defines data classes for authentication, request sending, and caching,
+as well as utility functions to load and validate these configurations from
+environment variables.
+"""
+
import os
from dataclasses import dataclass
+from enum import Enum
from dotenv import load_dotenv
class AuthConfigurationError(Exception):
+ """Raised when the authentication configuration is missing or invalid."""
pass
@dataclass
class SenderConfig:
+ """Defines configuration for the HTTP request sender connecting to Microsoft Graph API.
+
+ Attributes:
+ max_retries (int): The maximum number of retry attempts for failed requests.
+ Defaults to 3.
+ next_retry_delay (int): The delay between retry attempts in **seconds**.
+ Defaults to 2.
+ timeout (int): The maximum time to wait for a request to complete in **seconds**.
+ Defaults to 10.
+ """
+
max_retries: int = 3
next_retry_delay: int = 2
timeout: int = 10
@@ -17,6 +38,21 @@ class SenderConfig:
@dataclass
class AuthConfig:
+ """Holds configuration required for authentication via MSAL.
+
+ All fields are required to successfully acquire tokens.
+
+ Attributes:
+ client_id (str): The Application (client) ID assigned by the Azure portal.
+ tenant (str): The Directory (tenant) ID or domain.
+ email (str): The email address of the user to authenticate.
+ scopes (list[str]): A list of Graph API permission scopes (e.g., "User.Read").
+ auth_method (str): The flow used to acquire tokens.
+ Valid values are:
+ * **"DEVICE_CODE"**: Prints a code to console; user visits a URL.
+ * **"INTERACTIVE"**: Opens a local browser window.
+ """
+
client_id: str
tenant: str
email: str
@@ -24,7 +60,55 @@ class AuthConfig:
auth_method: str
+class CacheMode(Enum):
+ """Defines the caching strategy used by the application.
+
+ Attributes:
+ DISABLED: Caching is turned off completely.
+ SYNC: Cache operations (read/write) are performed synchronously.
+ Safe but may block the main thread.
+ ASYNC: Cache operations are performed asynchronously in the background.
+ Faster for the main thread but requires `close()` to ensure data consistency.
+ """
+
+ DISABLED = "DISABLED"
+ SYNC = "SYNC"
+ ASYNC = "ASYNC"
+
+@dataclass
+class CacheConfig:
+ """Holds configuration for the caching layer.
+
+ Attributes:
+ mode (CacheMode): The caching strategy to use.
+ path (str | None): The file path for the cache storage (e.g., JSON file).
+ Required if mode is not DISABLED. Defaults to None.
+ """
+
+ mode: CacheMode
+ path: str | None = None
+
def load_auth_config(env_path: str | None = None) -> AuthConfig:
+ """Loads authentication configuration from environment variables or a .env file.
+
+ It looks for the following environment variables:
+ * `CLIENT_ID`
+ * `TENANT_ID`
+ * `EMAIL`
+ * `SCOPES` (comma-separated, defaults to "https://graph.microsoft.com/.default")
+ * `AUTH_METHOD` (defaults to "DEVICE_CODE")
+
+ Args:
+ env_path (str | None, optional): Path to the .env file. If None,
+ loads from the current directory.
+
+ Returns:
+ AuthConfig: A populated and validated configuration object.
+
+ Raises:
+ AuthConfigurationError: If required variables are missing or values are invalid.
+ """
+
load_dotenv(env_path)
cfg = AuthConfig(
@@ -40,10 +124,24 @@ def load_auth_config(env_path: str | None = None) -> AuthConfig:
def get_env(key: str, fallback: str) -> str:
+ """Retrieves an environment variable with a fallback value."""
+
return os.getenv(key, fallback)
def validate(cfg: AuthConfig):
+ """Validates the authentication configuration object.
+
+ Checks for the presence of required fields and the validity of the auth method.
+
+ Args:
+ cfg (AuthConfig): The configuration object to validate.
+
+ Raises:
+ AuthConfigurationError: If `client_id`, `tenant`, or `email` are empty,
+ or if `auth_method` is not supported.
+ """
+
missing = []
if not cfg.client_id:
missing.append("CLIENT_ID")
diff --git a/python/teams_lib_pzsp2_z1/model/__init__.py b/python/teams_lib_pzsp2_z1/model/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/python/teams_lib_pzsp2_z1/model/channel.py b/python/teams_lib_pzsp2_z1/model/channel.py
index ace8276..32959ce 100644
--- a/python/teams_lib_pzsp2_z1/model/channel.py
+++ b/python/teams_lib_pzsp2_z1/model/channel.py
@@ -3,6 +3,18 @@
@dataclass
class Channel:
- ID: str
- Name: str
- IsGeneral: bool
+ """Represents a Microsoft Teams channel.
+
+ This model serves as a data container for channel properties returned
+ by the underlying Go library.
+
+ Attributes:
+ id (str): The unique identifier of the channel.
+ name (str): The display name of the channel (e.g., "General", "Development").
+ is_general (bool): Indicates whether this is the default 'General' channel.
+ The General channel typically cannot be deleted or renamed.
+ """
+
+ id: str
+ name: str
+ is_general: bool
diff --git a/python/teams_lib_pzsp2_z1/model/chat.py b/python/teams_lib_pzsp2_z1/model/chat.py
new file mode 100644
index 0000000..fe99164
--- /dev/null
+++ b/python/teams_lib_pzsp2_z1/model/chat.py
@@ -0,0 +1,54 @@
+from dataclasses import dataclass
+from enum import Enum
+
+
+class ChatType(Enum):
+ """Represents the type of chat in Microsoft Teams.
+
+ Attributes:
+ ONE_ON_ONE: Represents a direct conversation between two users.
+ GROUP: Represents a conversation with multiple participants.
+ """
+
+ ONE_ON_ONE = "one-on-one"
+ GROUP = "group"
+
+
+@dataclass
+class Chat:
+ """Represents a chat in Microsoft Teams.
+
+ Attributes:
+ id (str): The unique identifier of the chat.
+ chat_type (ChatType): The type of the conversation (One-on-One or Group).
+ is_hidden (bool): Indicates whether the chat is hidden for the user.
+ topic (Optional[str]): The subject or topic of the chat.
+ This is typically None for one-on-one chats.
+ """
+
+ id: str
+ type: ChatType
+ is_hidden: bool
+ topic: str | None
+
+
+@dataclass
+class ChatRef:
+ """Represents a polymorphic reference to a chat used for API lookups.
+
+ This class corresponds to `GroupChatRef` and `OneOnOneChatRef` in the Go library.
+ The interpretation of the `ref` field depends on the `chat_type`.
+
+ Attributes:
+ ref (str): The identifier string.
+ * If **chat_type is GROUP**: Can be a unique Chat ID or a Chat Topic.
+ **Note**: Using a topic may lead to ambiguities if multiple group chats
+ share the same name.
+ * If **chat_type is ONE_ON_ONE**: Can be a unique Chat ID or the
+ Recipient's reference (User ID or Email).
+ **Note**: For resolution by User ID/Email to work, the chat must
+ already exist between the logged-in user and the recipient.
+ chat_type (ChatType): Determines how the `ref` string is interpreted.
+ """
+ ref: str
+ type: ChatType
diff --git a/python/teams_lib_pzsp2_z1/model/member.py b/python/teams_lib_pzsp2_z1/model/member.py
new file mode 100644
index 0000000..09b721b
--- /dev/null
+++ b/python/teams_lib_pzsp2_z1/model/member.py
@@ -0,0 +1,22 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class Member:
+ """Represents a member of a Microsoft Teams channel, direct chat, or team.
+
+ Attributes:
+ id (str): The unique identifier of the membership object itself.
+ **Note:** This is distinct from the `user_id`.
+ user_id (str): The unique Azure Active Directory (AAD) identifier of the user.
+ display_name (str): The visible name of the user.
+ role (str): The role of the user within the specific context
+ (e.g., "owner", "member").
+ email (str): The email address of the user.
+ """
+
+ id: str
+ user_id: str
+ display_name: str
+ role: str
+ email: str
diff --git a/python/teams_lib_pzsp2_z1/model/mention.py b/python/teams_lib_pzsp2_z1/model/mention.py
new file mode 100644
index 0000000..950f49f
--- /dev/null
+++ b/python/teams_lib_pzsp2_z1/model/mention.py
@@ -0,0 +1,43 @@
+from dataclasses import dataclass
+from enum import Enum
+
+
+class MentionKind(Enum):
+ """Represents the kind of mention in a Microsoft Teams message.
+
+ Attributes:
+ USER: Represents a mention of a specific user.
+ CHANNEL: Represents a mention of the current channel.
+ TEAM: Represents a mention of the entire team.
+ EVERYONE: Represents a mention of all participants.
+ **Note:** This is applicable to Group Chats only.
+ """
+
+ USER = "user"
+ CHANNEL = "channel"
+ TEAM = "team"
+ EVERYONE = "everyone"
+
+
+@dataclass
+class Mention:
+ """Represents a mention entity within a Microsoft Teams message.
+
+ This object maps a specific entity (User, Channel, etc.) to a placeholder
+ in the message content. Can be obtained by get_mentions() method in the Chats and Channels service.
+
+ Attributes:
+ kind (MentionKind): The type of the mention.
+ at_id (int): The internal identifier used to link this mention to the
+ message content.
+ *Example:* If the message body contains `@John`,
+ this field should be `0`.
+ text (str): The text to display in the message (e.g., "@John Doe").
+ target_id (str): The unique identifier (UUID) of the entity being mentioned
+ (User ID, Channel ID, or Team ID).
+ """
+
+ kind: MentionKind
+ at_id: int
+ text: str
+ target_id: str
diff --git a/python/teams_lib_pzsp2_z1/model/message.py b/python/teams_lib_pzsp2_z1/model/message.py
new file mode 100644
index 0000000..08d87c0
--- /dev/null
+++ b/python/teams_lib_pzsp2_z1/model/message.py
@@ -0,0 +1,116 @@
+from dataclasses import dataclass
+from datetime import datetime
+from enum import Enum
+
+from teams_lib_pzsp2_z1.model.mention import Mention
+
+
+class MessageContentType(Enum):
+ """Represents the type of content in a Microsoft Teams message.
+
+ Attributes:
+ TEXT: Plain text content.
+ HTML: HTML formatted content.
+ """
+
+ TEXT = "text"
+ HTML = "html"
+
+
+@dataclass
+class MessageFrom:
+ """Represents the sender of a message in Microsoft Teams.
+
+ Attributes:
+ user_id (str): The unique Azure Active Directory (AAD) identifier of the sender.
+ display_name (str): The visible name of the sender.
+ """
+
+ user_id: str
+ display_name: str
+
+
+@dataclass
+class Message:
+ """Represents a Microsoft Teams chat message.
+
+ Used in both direct chats and channels.
+
+ Attributes:
+ id (str): The unique identifier of the message.
+ content (str): The actual body text or HTML of the message.
+ content_type (MessageContentType): The format of the content (Text or HTML).
+ created_date_time (datetime): The timestamp when the message was created.
+ sender (MessageFrom): The user who sent the message.
+ **Note:** Mapped from the `From` field in the API/Go model to avoid
+ conflict with the Python `from` keyword.
+ reply_count (int): The number of replies to this message.
+ """
+
+ id: str
+ content: str
+ content_type: MessageContentType
+ created_date_time: datetime
+ sender: MessageFrom
+ reply_count: int
+
+
+@dataclass
+class MessageBody:
+ """Represents the body of a message to be sent.
+
+ This class contains the payload structure required to send a new message
+ or reply.
+
+ Attributes:
+ content_type (MessageContentType): The format of the content.
+ content (str): The text or HTML content.
+ mentions (List[Mention]): A list of mentions to include in the message.
+ """
+
+ content_type: MessageContentType
+ content: str
+ mentions: list[Mention]
+
+ def __dict__(self):
+ """Serializes the object keys to PascalCase for the Go backend."""
+
+ return {
+ "ContentType": self.content_type.value,
+ "Content": self.content,
+ }
+
+ def __iter__(self):
+ """Allows iteration over fields with PascalCase keys."""
+
+ yield "ContentType", self.content_type.value
+ yield "Content", self.content
+
+
+@dataclass
+class ListMessagesOptions:
+ """Contains options for listing messages (pagination and expansion).
+
+ Attributes:
+ top (Optional[int]): The maximum number of messages to return in one page.
+ Defaults to None (server default).
+ expand_replies (bool): If True, the response will include replies
+ nested within the messages. Defaults to False.
+ """
+
+ top: int | None = None
+ expand_replies: bool = False
+
+
+@dataclass
+class MessageCollection:
+ """Represents a paginated collection of messages.
+
+ Attributes:
+ messages (List[Message]): The list of message objects in the current page.
+ next_link (Optional[str]): The URL to retrieve the next page of results.
+ If None, there are no more messages.
+ """
+
+ messages: list[Message]
+ next_link: str | None = None
diff --git a/python/teams_lib_pzsp2_z1/model/search.py b/python/teams_lib_pzsp2_z1/model/search.py
new file mode 100644
index 0000000..94ee72f
--- /dev/null
+++ b/python/teams_lib_pzsp2_z1/model/search.py
@@ -0,0 +1,169 @@
+from dataclasses import dataclass
+from enum import Enum
+
+from teams_lib_pzsp2_z1.model.message import Message
+
+
+@dataclass
+class SearchConfig:
+ """
+ Configuration parameters for search operations.
+
+ Attributes:
+ max_workers (int): The maximum number of worker threads to use for concurrent search operations. Defaults to 8.
+ """
+
+ max_workers: int = 8
+
+ def __dict__(self):
+ """Serializes the object keys to camelCase for the Go backend."""
+ return {"maxWorkers": self.max_workers}
+
+
+@dataclass
+class SearchPage:
+ """
+ Represents pagination parameters for search results.
+
+ Attributes:
+ from_ (int): The starting index of the search results.
+ size (int): The number of search results to return. Defaults to 25.
+ """
+
+ from_: int = 0
+ size: int = 25
+
+ def __dict__(self):
+ """Serializes the object keys to camelCase for the Go backend."""
+ return {"from": self.from_, "size": self.size}
+
+
+class TimeInterval(Enum):
+ """Enumeration of time intervals for filtering search results.
+
+ Attributes:
+ TODAY: Represents the current day.
+ YESTERDAY: Represents the previous day.
+ THIS_WEEK: Represents the current week.
+ THIS_MONTH: Represents the current month.
+ LAST_MONTH: Represents the previous month.
+ THIS_YEAR: Represents the current year.
+ LAST_YEAR: Represents the previous year.
+ """
+
+ TODAY = "today"
+ YESTERDAY = "yesterday"
+ THIS_WEEK = "this week"
+ THIS_MONTH = "this month"
+ LAST_MONTH = "last month"
+ THIS_YEAR = "this year"
+ LAST_YEAR = "last year"
+
+
+@dataclass
+class SearchMessagesOptions:
+ """
+ Configuration options for searching messages.
+
+ Attributes:
+ query (str | None): The search query string.
+ search_page (SearchPage | None): Pagination options.
+ from_ (list[str]): List of sender email addresses to include.
+ not_from (list[str]): List of sender email addresses to exclude.
+ is_read (bool | None): Filter by read status (True for read, False for unread).
+ is_mentioned (bool | None): Filter by mention status (True for mentioned, False for not mentioned).
+ to (list[str]): List of recipient email addresses to include.
+ not_to (list[str]): List of recipient email addresses to exclude.
+ start_time (datetime | None): Start time for the sent time range filter.
+ end_time (datetime | None): End time for the sent time range filter.
+ interval (TimeInterval | None): Predefined time interval for the sent time filter.
+ not_from_me (bool): Exclude messages sent by the current user.
+ not_to_me (bool): Exclude messages sent to the current user.
+ from_me (bool): Include only messages sent by the current user.
+ to_me (bool): Include only messages sent to the current user.
+
+ Note: If interval is set, it takes precedence over start_time and end_time.
+ Note: Using `to` clauses works only in chats, not in team channels.
+ Note: Currently, queries for is_read may not function as expected due to API limitations.
+ """
+
+ query: str | None = None
+ search_page: SearchPage | None = None
+ from_: list[str] = None
+ not_from: list[str] = None
+ is_read: bool | None = None
+ is_mentioned: bool | None = None
+ to: list[str] = None
+ not_to: list[str] = None
+ start_time: object = None
+ end_time: object = None
+ interval: TimeInterval | None = None
+ not_from_me: bool = False
+ not_to_me: bool = False
+ from_me: bool = False
+ to_me: bool = False
+
+ def __post_init__(self):
+ if self.from_ is None:
+ self.from_ = []
+ if self.not_from is None:
+ self.not_from = []
+ if self.to is None:
+ self.to = []
+ if self.not_to is None:
+ self.not_to = []
+
+ def __dict__(self):
+ """Serializes the object keys to camelCase for the Go backend."""
+
+ result = {
+ "query": self.query,
+ "searchPage": self.search_page,
+ "from": self.from_,
+ "notFrom": self.not_from,
+ "isRead": self.is_read,
+ "isMentioned": self.is_mentioned,
+ "to": self.to,
+ "notTo": self.not_to,
+ "startTime": self.start_time,
+ "endTime": self.end_time,
+ "interval": self.interval.value if self.interval else None,
+ "notFromMe": self.not_from_me,
+ "notToMe": self.not_to_me,
+ "fromMe": self.from_me,
+ "toMe": self.to_me,
+ }
+ return {k: v for k, v in result.items() if v is not None and v != []}
+
+
+@dataclass
+class SearchResult:
+ """
+ SearchResult represents a single search result containing a message and its context.
+
+ Attributes:
+ message (Message): The message that matched the search criteria.
+ channel_id (str | None): The ID of the channel where the message was found (if applicable).
+ team_id (str | None): The ID of the team where the message was found (if applicable).
+ chat_id (str | None): The ID of the chat where the message was found (if applicable).
+ """
+
+ message: Message
+ channel_id: str | None = None
+ team_id: str | None = None
+ chat_id: str | None = None
+
+
+@dataclass
+class SearchResults:
+ """
+ SearchResults represents the results of a message search.
+
+ Attributes:
+ messages (list[SearchResult]): A list of search results.
+ next_from (int | None): The index to use for the next page of results,
+ or None if there are no more results.
+ """
+
+ messages: list[SearchResult]
+ next_from: int | None = None
diff --git a/python/teams_lib_pzsp2_z1/model/team.py b/python/teams_lib_pzsp2_z1/model/team.py
index 9de6e1b..aa690d7 100644
--- a/python/teams_lib_pzsp2_z1/model/team.py
+++ b/python/teams_lib_pzsp2_z1/model/team.py
@@ -3,8 +3,38 @@
@dataclass
class Team:
- ID: str
- DisplayName: str
- Description: str
- IsArchived: bool
- Visibility: str
+ """Represents a Microsoft Teams team.
+
+ Attributes:
+ id (str): The unique identifier (UUID) of the team.
+ display_name (str): The name of the team.
+ description (str): A description of the team's purpose.
+ is_archived (bool): Indicates whether the team is in an archived state.
+ Archived teams are read-only for members.
+ visibility (str): The privacy setting of the team.
+ Typically "public", "private".
+ """
+
+ id: str
+ display_name: str
+ description: str
+ is_archived: bool
+ visibility: str
+
+
+@dataclass
+class UpdateTeam:
+ """Represents the subset of Team fields that can be updated.
+
+ This class is used as a payload for patch/update operations.
+ Only fields that are not None will be sent to the API.
+
+ Attributes:
+ display_name (Optional[str]): The new name for the team.
+ description (Optional[str]): The new description.
+ visibility (Optional[str]): The new visibility setting (e.g., "private").
+ """
+
+ display_name: str | None = None
+ description: str | None = None
+ visibility: str | None = None
diff --git a/python/teams_lib_pzsp2_z1/services/__init__.py b/python/teams_lib_pzsp2_z1/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/python/teams_lib_pzsp2_z1/services/base_service.py b/python/teams_lib_pzsp2_z1/services/base_service.py
index 073288d..b5627b5 100644
--- a/python/teams_lib_pzsp2_z1/services/base_service.py
+++ b/python/teams_lib_pzsp2_z1/services/base_service.py
@@ -6,5 +6,7 @@
from teams_lib_pzsp2_z1.client import TeamsClient
class BaseService:
+ """Base class for all service classes in the Teams library."""
+
def __init__(self, client: TeamsClient):
self.client = client
diff --git a/python/teams_lib_pzsp2_z1/services/channels.py b/python/teams_lib_pzsp2_z1/services/channels.py
index 303e74e..2c80e1d 100644
--- a/python/teams_lib_pzsp2_z1/services/channels.py
+++ b/python/teams_lib_pzsp2_z1/services/channels.py
@@ -1,21 +1,583 @@
+"""
+This module provides the ChannelsService class for managing Microsoft Teams channels.
+"""
+
from teams_lib_pzsp2_z1.model.channel import Channel
+from teams_lib_pzsp2_z1.model.member import Member
+from teams_lib_pzsp2_z1.model.mention import Mention, MentionKind
+from teams_lib_pzsp2_z1.model.message import (
+ Message,
+ MessageBody,
+ MessageCollection,
+ MessageContentType,
+ MessageFrom,
+)
+from teams_lib_pzsp2_z1.model.search import (
+ SearchConfig,
+ SearchMessagesOptions,
+ SearchResult,
+ SearchResults,
+)
from teams_lib_pzsp2_z1.services.base_service import BaseService
class ChannelsService(BaseService):
- def list_channels(self, teamRef: str) -> list[Channel]:
+ """Service for managing Microsoft Teams channels via the Go backend.
+
+ This class acts as a high-level Python wrapper. It delegates logical operations
+ to the underlying Go library through the `self.client.execute` bridge.
+
+ **Architecture & Caching:**
+ The decision to use caching or direct API calls is handled internally by the
+ compiled Go library, based on the configuration provided during Client initialization.
+ This Python class provides a unified interface regardless of the underlying mode.
+
+ **Concepts:**
+ * **References**: Arguments like `team_ref` or `channel_ref` accept either UUIDs
+ or Display Names. The Go backend resolves these automatically.
+ * **Identity**: Users are identified by UUID or Email.
+ """
+
+ def list_channels(self, team_ref: str) -> list[Channel]:
+ """Retrieves all channels associated with a specific team.
+
+ Args:
+ team_ref (str): The reference to the team (UUID or Display Name).
+ Returns:
+ List[Channel]: A list of channel objects found in the team.
+ """
response = self.client.execute(
cmd_type="request",
method="listChannels",
params={
- "teamRef": teamRef,
+ "teamRef": team_ref,
},
)
return [
Channel(
- ID=channel["ID"],
- Name=channel["Name"],
- IsGeneral=(True if channel["IsGeneral"] else False),
+ id=channel["ID"],
+ name=channel["Name"],
+ is_general=(True if channel["IsGeneral"] else False),
)
for channel in response
]
+
+ def get(self, team_ref: str, channel_ref: str) -> Channel:
+ """Retrieves a specific channel by its reference within a team.
+
+ Args:
+ team_ref (str): The reference to the team (UUID or Display Name).
+ channel_ref (str): The reference to the channel (ID or Display Name).
+
+ Returns:
+ Channel: The requested channel object.
+
+ Raises:
+ Exception: If the reference is ambiguous or not found (propagated from Go).
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="getChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ },
+ )
+
+ return Channel(
+ id=response["ID"],
+ name=response["Name"],
+ is_general=(True if response["IsGeneral"] else False),
+ )
+
+ def create_standard(self, team_ref: str, display_name: str) -> Channel:
+ """Creates a standard (public) channel within a team.
+
+ Args:
+ team_ref (str): The reference to the team.
+ display_name (str): The display name for the new channel.
+
+ Returns:
+ Channel: The newly created channel object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="createStandardChannel",
+ params={
+ "teamRef": team_ref,
+ "name": display_name,
+ },
+ )
+
+ return Channel(
+ id=response["ID"],
+ name=response["Name"],
+ is_general=(True if response["IsGeneral"] else False),
+ )
+
+ def create_private(
+ self,
+ team_ref: str,
+ display_name: str,
+ member_refs: list[str],
+ owner_refs: list[str],
+ ) -> Channel:
+ """Creates a private channel restricted to specific members.
+
+ Args:
+ team_ref (str): The reference to the team.
+ display_name (str): The display name for the new private channel.
+ member_refs (list[str]): List of user references (IDs or emails) to add as members.
+ owner_refs (list[str]): List of user references to add as owners.
+ At least one owner is required.
+
+ Returns:
+ Channel: The newly created private channel object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="createPrivateChannel",
+ params={
+ "teamRef": team_ref,
+ "name": display_name,
+ "memberRefs": member_refs,
+ "ownerRefs": owner_refs,
+ },
+ )
+
+ return Channel(
+ id=response["ID"],
+ name=response["Name"],
+ is_general=(True if response["IsGeneral"] else False),
+ )
+
+ def delete(self, team_ref: str, channel_ref: str) -> bool:
+ """Removes a channel from a team.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel to be deleted.
+
+ Returns:
+ bool: True if the operation was successful.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="deleteChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ },
+ )
+ return response == "deleted"
+
+ def send_message(
+ self, team_ref: str, channel_ref: str, body: MessageBody
+ ) -> Message:
+ """Sends a new message to a channel.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ body (MessageBody): The message payload containing content, content type
+ (Text/HTML), and optional mentions.
+
+ Returns:
+ Message: The created message object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="sendMessageToChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "body": dict(body),
+ },
+ )
+
+ return Message(
+ id=response["ID"],
+ content=response["Content"],
+ content_type=MessageContentType(response["ContentType"]),
+ created_date_time=response["CreatedDateTime"],
+ sender=MessageFrom(
+ user_id=response["From"]["UserID"],
+ display_name=response["From"]["DisplayName"],
+ ),
+ reply_count=response["ReplyCount"],
+ )
+
+ def list_messages( # noqa: PLR0913
+ self,
+ team_ref: str,
+ channel_ref: str,
+ top: int | None = 10,
+ expand_replies: bool = False,
+ include_system_messages: bool = False,
+ next_link: str | None = None,
+ ) -> MessageCollection:
+ """Retrieves messages from a channel.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ top (Optional[int]): The maximum number of messages to retrieve. Defaults to 10.
+ expand_replies (bool): If True, system fetches replies for each message. Defaults to False.
+ include_system_messages (bool): If True, includes system-generated messages. Defaults to False.
+ next_link (Optional[str]): A link for pagination to fetch the next set of messages.
+
+ Returns:
+ MessageCollection: An object containing the list of messages and a next link for pagination.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="listMessagesInChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "options": {
+ "top": top,
+ "expandReplies": expand_replies,
+ "includeSystem": include_system_messages,
+ "nextLink": next_link,
+ },
+ },
+ )
+
+ return MessageCollection(
+ messages=[
+ Message(
+ id=msg["ID"],
+ content=msg["Content"],
+ content_type=MessageContentType(msg["ContentType"]),
+ created_date_time=msg["CreatedDateTime"],
+ sender=MessageFrom(
+ user_id=msg["From"]["UserID"],
+ display_name=msg["From"]["DisplayName"],
+ ),
+ reply_count=msg["ReplyCount"],
+ )
+ for msg in response["Messages"]
+ ],
+ next_link=response.get("NextLink"),
+ )
+
+ def get_message(self, team_ref: str, channel_ref: str, message_id: str) -> Message:
+ """Retrieves a specific message by its ID.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ message_id (str): The unique identifier of the message.
+
+ Returns:
+ Message: The requested message object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="getMessageInChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "messageID": message_id,
+ },
+ )
+
+ return Message(
+ id=response["ID"],
+ content=response["Content"],
+ content_type=MessageContentType(response["ContentType"]),
+ created_date_time=response["CreatedDateTime"],
+ sender=MessageFrom(
+ user_id=response["From"]["UserID"],
+ display_name=response["From"]["DisplayName"],
+ ),
+ reply_count=response["ReplyCount"],
+ )
+
+ def list_message_replies( # noqa: PLR0913
+ self,
+ team_ref: str,
+ channel_ref: str,
+ message_id: str,
+ top: int | None = 10,
+ include_system_messages: bool = False,
+ next_link: str | None = None,
+ ) -> MessageCollection:
+ """Retrieves all replies to a specific message thread.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ message_id (str): The ID of the parent message.
+ top (Optional[int]): Max number of replies to fetch. Defaults to 10.
+ include_system_messages (bool): If True, includes system-generated messages. Defaults to False.
+ next_link (Optional[str]): A link for pagination to fetch the next set of replies.
+
+ Returns:
+ MessageCollection: An object containing the list of replies and a next link for pagination.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="listMessageRepliesInChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "messageID": message_id,
+ "top": top,
+ "includeSystem": include_system_messages,
+ "nextLink": next_link,
+ },
+ )
+
+ return MessageCollection(
+ messages=[
+ Message(
+ id=msg["ID"],
+ content=msg["Content"],
+ content_type=MessageContentType(msg["ContentType"]),
+ created_date_time=msg["CreatedDateTime"],
+ sender=MessageFrom(
+ user_id=msg["From"]["UserID"],
+ display_name=msg["From"]["DisplayName"],
+ ),
+ reply_count=msg["ReplyCount"],
+ )
+ for msg in response["Messages"]
+ ],
+ next_link=response.get("NextLink"),
+ )
+
+ def get_message_reply(
+ self,
+ team_ref: str,
+ channel_ref: str,
+ message_id: str,
+ reply_id: str,
+ ) -> Message:
+ """Retrieves a specific reply from a thread.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ message_id (str): The ID of the parent message.
+ reply_id (str): The ID of the reply to retrieve.
+
+ Returns:
+ Message: The requested reply object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="getMessageReplyInChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "messageID": message_id,
+ "replyID": reply_id,
+ },
+ )
+
+ return Message(
+ id=response["ID"],
+ content=response["Content"],
+ content_type=MessageContentType(response["ContentType"]),
+ created_date_time=response["CreatedDateTime"],
+ sender=MessageFrom(
+ user_id=response["From"]["UserID"],
+ display_name=response["From"]["DisplayName"],
+ ),
+ reply_count=response["ReplyCount"],
+ )
+
+ def list_members(self, team_ref: str, channel_ref: str) -> list[Member]:
+ """Lists all members of a channel.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+
+ Returns:
+ List[Member]: A list of channel members.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="listChannelMembers",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ },
+ )
+ return [Member(
+ id=member["ID"],
+ user_id=member["UserID"],
+ display_name=member["DisplayName"],
+ role=member["Role"],
+ email=member["Email"],
+ ) for member in response]
+
+ def add_member(
+ self, team_ref: str, channel_ref: str, user_ref: str, is_owner: bool
+ ) -> Member:
+ """Adds a user to a channel.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ user_ref (str): The user to add (User ID or Email).
+ is_owner (bool): Whether to grant Owner privileges.
+
+ Returns:
+ Member: The newly added member object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="addMemberToChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "userRef": user_ref,
+ "isOwner": is_owner,
+ },
+ )
+ return Member(
+ id=response["ID"],
+ user_id=response["UserID"],
+ display_name=response["DisplayName"],
+ role=response["Role"],
+ email=response["Email"],
+ )
+
+ def update_member_role(
+ self, team_ref: str, channel_ref: str, user_ref: str, is_owner: bool
+ ) -> Member:
+ """Updates the role of an existing channel member.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ user_ref (str): The user reference (ID or Email).
+ is_owner (bool): True for Owner role, False for Member role.
+
+ Returns:
+ Member: The updated member object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="updateMemberRoleInChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "userRef": user_ref,
+ "isOwner": is_owner,
+ },
+ )
+ return Member(
+ id=response["ID"],
+ user_id=response["UserID"],
+ display_name=response["DisplayName"],
+ role=response["Role"],
+ email=response["Email"],
+ )
+
+ def remove_member(self, team_ref: str, channel_ref: str, user_ref: str) -> bool:
+ """Removes a user from a channel.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ user_ref (str): The user reference (ID or Email) to remove.
+
+ Returns:
+ bool: True if the operation was successful.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="removeMemberFromChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "userRef": user_ref,
+ },
+ )
+ return response == "removed"
+
+ def get_mentions(
+ self, team_ref: str, channel_ref: str, raw_mentions: list[str]
+ ) -> list[Mention]:
+ """Resolves raw mention strings into formal Mention objects.
+
+ This is used to construct the `mentions` field for messages before sending.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ raw_mentions (List[str]): A list of raw strings to resolve.
+ Supported formats:
+ * **Email/User ID**: Resolves to a specific user.
+ * **'channel'**: Mentions the current channel.
+ * **'team'**: Mentions the parent team.
+
+ Returns:
+ List[Mention]: A list of resolved Mention objects ready for the API.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="getMentionsInChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "rawMentions": raw_mentions,
+ },
+ )
+ return [Mention(
+ kind=MentionKind(mention["Kind"]),
+ at_id=mention["AtID"],
+ text=mention["Text"],
+ target_id=mention["TargetID"],
+ ) for mention in response]
+
+ def search_messages(self, team_ref: str, channel_ref: str, options: SearchMessagesOptions, config: SearchConfig) -> SearchResults:
+ """Searches for messages in a channel based on various criteria.
+
+ Args:
+ team_ref (str): The reference to the team.
+ channel_ref (str): The reference to the channel.
+ options (SearchMessagesOptions): The search options and filters.
+ config (SearchConfig): Configuration for the search operation.
+
+ Returns:
+ SearchResults: The results of the search operation.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="searchMessagesInChannel",
+ params={
+ "teamRef": team_ref,
+ "channelRef": channel_ref,
+ "searchMessagesOptions": options.__dict__(),
+ "searchConfig": config.__dict__(),
+ },
+ )
+
+ return SearchResults(
+ messages=[
+ SearchResult(
+ message=Message(
+ id=msg["Message"]["ID"],
+ content=msg["Message"]["Content"],
+ content_type=MessageContentType(msg["Message"]["ContentType"]),
+ created_date_time=msg["Message"]["CreatedDateTime"],
+ sender=MessageFrom(
+ user_id=msg["Message"]["From"]["UserID"],
+ display_name=msg["Message"]["From"]["DisplayName"],
+ ),
+ reply_count=msg["Message"]["ReplyCount"],
+ ),
+ channel_id=msg.get("ChannelID"),
+ team_id=msg.get("TeamID"),
+ chat_id=msg.get("ChatID"),
+ )
+ for msg in response["Messages"]
+ ] if response.get("Messages") else [],
+ next_from=response.get("NextFrom"),
+ )
diff --git a/python/teams_lib_pzsp2_z1/services/chats.py b/python/teams_lib_pzsp2_z1/services/chats.py
new file mode 100644
index 0000000..2524569
--- /dev/null
+++ b/python/teams_lib_pzsp2_z1/services/chats.py
@@ -0,0 +1,613 @@
+"""
+This module contains the service for managing Microsoft Teams chats via the Go backend.
+"""
+
+from datetime import datetime
+
+from teams_lib_pzsp2_z1.model.chat import Chat, ChatRef, ChatType
+from teams_lib_pzsp2_z1.model.member import Member
+from teams_lib_pzsp2_z1.model.mention import Mention
+from teams_lib_pzsp2_z1.model.message import (
+ Message,
+ MessageBody,
+ MessageCollection,
+ MessageContentType,
+ MessageFrom,
+)
+from teams_lib_pzsp2_z1.model.search import (
+ SearchConfig,
+ SearchMessagesOptions,
+ SearchResult,
+ SearchResults,
+)
+from teams_lib_pzsp2_z1.services.base_service import BaseService
+
+
+class ChatsService(BaseService):
+ """Service for managing Microsoft Teams chats via the Go backend.
+
+ This class acts as a high-level wrapper, delegating operations to the
+ underlying Go library. It handles One-on-One chats, Group chats, messages,
+ and membership management.
+
+ **Concepts:**
+ * **Chat Types**: Chats are distinct entities, either **One-on-One** (between two users)
+ or **Group** (multiple users, has a topic).
+ * **References**:
+ * `recipient_ref` / `user_ref`: Can be a User ID (UUID) or an Email.
+ * `group_chat_ref`: Can be the Chat ID or the Group Topic (if unique).
+ * `chat_ref` (Object): A specific model `ChatRef` containing the ID/Name and the `ChatType`.
+ * **Permissions**: Some operations (like `list_all_messages`) require Application Permissions
+ and will not work in Delegated (user context) mode.
+ """
+
+ def create_one_on_one(self, recipient_ref: str) -> Chat:
+ """Creates a One-on-One chat with a specific user.
+
+ The authenticated user is automatically added to the chat.
+
+ Args:
+ recipient_ref (str): The reference to the other user (User ID or Email).
+
+ Returns:
+ Chat: The created chat object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="createOneOnOneChat",
+ params={
+ "recipientRef": recipient_ref,
+ },
+ )
+
+ return Chat(
+ id=response["ID"],
+ type=ChatType(response["Type"]),
+ is_hidden=(True if response["IsHidden"] else False),
+ topic=response["Topic"],
+ )
+
+ def create_group_chat(
+ self, recipient_refs: list[str], topic: str, include_me: bool
+ ) -> Chat:
+ """Creates a Group Chat with multiple participants.
+
+ Args:
+ recipient_refs (list[str]): A list of user references (IDs or Emails) to include.
+ topic (str): The subject/topic of the group chat.
+ include_me (bool): If True, the authenticated user is added to the group.
+
+ Returns:
+ Chat: The created group chat object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="createGroupChat",
+ params={
+ "recipientRefs": recipient_refs,
+ "topic": topic,
+ "includeMe": include_me,
+ },
+ )
+
+ return Chat(
+ id=response["ID"],
+ type=ChatType(response["Type"]),
+ is_hidden=(True if response["IsHidden"] else False),
+ topic=response["Topic"],
+ )
+
+ def get_chat(self, chat_ref: ChatRef) -> Chat:
+ """
+ Retrieves a chat by its reference.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object containing the ID/Name and ChatType.
+
+ Returns:
+ Chat: The requested chat object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="getChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ },
+ )
+
+ return Chat(
+ id=response["ID"],
+ type=ChatType(response["Type"]),
+ is_hidden=(True if response["IsHidden"] else False),
+ topic=response["Topic"],
+ )
+
+ def add_member_to_group_chat(self, group_chat_ref: str, user_ref: str) -> Member:
+ """Adds a user to an existing Group Chat.
+
+ Args:
+ group_chat_ref (str): The reference to the group chat (ID or Topic).
+ user_ref (str): The user to add (User ID or Email).
+
+ Returns:
+ Member: The newly added member details.
+ """
+ member = self.client.execute(
+ cmd_type="request",
+ method="addMemberToGroupChat",
+ params={
+ "groupChatRef": group_chat_ref,
+ "userRef": user_ref,
+ },
+ )
+
+ return Member(
+ id=member["ID"],
+ display_name=member["DisplayName"],
+ user_id=member["UserID"],
+ role=member["Role"],
+ email=member["Email"],
+ )
+
+ def remove_member_from_group_chat(
+ self, group_chat_ref: str, member_ref: str
+ ) -> bool:
+ """Removes a member from a Group Chat.
+
+ Args:
+ group_chat_ref (str): The reference to the group chat (ID or Topic).
+ member_ref (str): The user reference to remove (User ID or Email).
+
+ Returns:
+ bool: True if the member was successfully removed.
+ """
+ result = self.client.execute(
+ cmd_type="request",
+ method="removeMemberFromGroupChat",
+ params={
+ "groupChatRef": group_chat_ref,
+ "userRef": member_ref,
+ },
+ )
+
+ return result == "removed"
+
+ def list_group_chat_members(self, group_chat_ref: str) -> list[Member]:
+ """Lists all members of a Group Chat.
+
+ Args:
+ group_chat_ref (str): The reference to the group chat (ID or Topic).
+
+ Returns:
+ list[Member]: A list of members in the chat.
+ """
+ members = self.client.execute(
+ cmd_type="request",
+ method="listMembersInGroupChat",
+ params={
+ "groupChatRef": group_chat_ref,
+ },
+ )
+
+ return [
+ Member(
+ id=member["ID"],
+ display_name=member["DisplayName"],
+ user_id=member["UserID"],
+ role=member["Role"],
+ email=member["Email"],
+ )
+ for member in members
+ ]
+
+ def update_group_chat_topic(self, group_chat_ref: str, new_topic: str) -> Chat:
+ """Updates the topic of a Group Chat.
+
+ Args:
+ group_chat_ref (str): The reference to the group chat (ID or old Topic).
+ new_topic (str): The new topic string.
+
+ Returns:
+ Chat: The updated chat object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="updateGroupChatTopic",
+ params={
+ "groupChatRef": group_chat_ref,
+ "topic": new_topic,
+ },
+ )
+
+ return Chat(
+ id=response["ID"],
+ type=ChatType(response["Type"]),
+ is_hidden=(True if response["IsHidden"] else False),
+ topic=response["Topic"],
+ )
+
+ def list_messages(
+ self,
+ chat_ref: ChatRef,
+ include_system_messages: bool = False,
+ next_link: str | None = None,
+ ) -> list[Message]:
+ """Retrieves messages from a specific chat.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object containing the ID/Name and ChatType.
+ include_system_messages (bool): If True, includes system-generated messages. Defaults to False.
+ next_link (str | None): A link for pagination to fetch the next set of messages. Defaults to None.
+
+ Returns:
+ list[Message]: A list of messages from the chat history.
+ """
+ messages = self.client.execute(
+ cmd_type="request",
+ method="listMessagesInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ "includeSystem": include_system_messages,
+ "nextLink": next_link,
+ },
+ )
+
+ return MessageCollection(
+ messages=[
+ Message(
+ id=msg["ID"],
+ content=msg["Content"],
+ content_type=MessageContentType(msg["ContentType"]),
+ created_date_time=msg["CreatedDateTime"],
+ sender=MessageFrom(
+ user_id=msg["From"]["UserID"],
+ display_name=msg["From"]["DisplayName"],
+ ),
+ reply_count=msg["ReplyCount"],
+ )
+ for msg in messages["Messages"]
+ ],
+ next_link=messages.get("NextLink"),
+ )
+
+ def send_message(self, chat_ref: ChatRef, body: MessageBody) -> Message:
+ """Sends a message to a chat.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object.
+ body (MessageBody): The message payload (content, type, mentions).
+
+ Returns:
+ Message: The created message object.
+ """
+ message = self.client.execute(
+ cmd_type="request",
+ method="sendMessageInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ "body": {
+ "content": body.content,
+ "contentType": body.content_type.value,
+ },
+ },
+ )
+
+ return Message(
+ id=message["ID"],
+ content=message["Content"],
+ content_type=MessageContentType(message["ContentType"]),
+ sender=MessageFrom(
+ user_id=message["From"]["UserID"],
+ display_name=message["From"]["DisplayName"],
+ ),
+ created_date_time=message["CreatedDateTime"],
+ reply_count=message["ReplyCount"],
+ )
+
+ def delete_message(self, chat_ref: ChatRef, message_id: str) -> bool:
+ """Soft-deletes a message from a chat.
+
+ This action is reversible via the Graph API (though not exposed here).
+
+ Args:
+ chat_ref (ChatRef): The chat reference object.
+ message_id (str): The unique identifier of the message to delete.
+
+ Returns:
+ bool: True if the message was successfully deleted.
+ """
+ result = self.client.execute(
+ cmd_type="request",
+ method="deleteMessageInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ "messageID": message_id,
+ },
+ )
+
+ return result == "deleted"
+
+ def get_message(self, chat_ref: ChatRef, message_id: str) -> Message:
+ """Retrieves a single message by its ID.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object.
+ message_id (str): The unique identifier of the message.
+
+ Returns:
+ Message: The requested message object.
+ """
+ message = self.client.execute(
+ cmd_type="request",
+ method="getMessageInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ "messageID": message_id,
+ },
+ )
+
+ return Message(
+ id=message["ID"],
+ content=message["Content"],
+ content_type=MessageContentType(message["ContentType"]),
+ sender=MessageFrom(
+ user_id=message["From"]["UserID"],
+ display_name=message["From"]["DisplayName"],
+ ),
+ created_date_time=message["CreatedDateTime"],
+ reply_count=message["ReplyCount"],
+ )
+
+ def list_my_joined(self, chat_type: ChatType | None = None) -> list[Chat]:
+ """Lists all chats the authenticated user is part of.
+
+ Args:
+ chat_type (ChatType | None, optional): Filter by chat type (e.g., only OneOnOne).
+ Defaults to None (return all).
+
+ Returns:
+ list[Chat]: A list of chat objects.
+ """
+ params = {}
+ if chat_type:
+ params["chatType"] = chat_type.value
+
+ chats = self.client.execute(
+ cmd_type="request",
+ method="listMyChats",
+ params=params,
+ )
+
+ return [
+ Chat(
+ id=chat["ID"],
+ type=ChatType(chat["Type"]),
+ is_hidden=(True if chat["IsHidden"] else False),
+ topic=chat["Topic"],
+ )
+ for chat in chats
+ ]
+
+ def list_all_messages(
+ self, start_time: datetime, end_time: datetime, top: int
+ ) -> list[Message]:
+ """Retrieves messages across ALL chats within a time range.
+
+ Note:
+ This operation typically requires **Application Permissions** and may not
+ work with standard Delegated (User) credentials depending on the organization's policy.
+
+ Args:
+ start_time (datetime): The start of the time range.
+ end_time (datetime): The end of the time range.
+ top (int): Maximum number of messages to retrieve.
+
+ Returns:
+ list[Message]: A list of messages from all chats.
+ """
+ messages = self.client.execute(
+ cmd_type="request",
+ method="listMyChatMessages",
+ params={
+ "startTime": start_time.isoformat(),
+ "endTime": end_time.isoformat(),
+ "top": top,
+ },
+ )
+
+ return [
+ Message(
+ id=message["ID"],
+ content=message["Content"],
+ content_type=MessageContentType(message["ContentType"]),
+ sender=MessageFrom(
+ user_id=message["From"]["UserID"],
+ display_name=message["From"]["DisplayName"],
+ ),
+ created_date_time=message["CreatedDateTime"],
+ reply_count=message["ReplyCount"],
+ )
+ for message in messages
+ ]
+
+ def list_pinned_messages(self, chat_ref: ChatRef) -> list[Message]:
+ """Lists all pinned messages in a specific chat.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object.
+
+ Returns:
+ list[Message]: A list of pinned messages.
+ """
+ messages = self.client.execute(
+ cmd_type="request",
+ method="listPinnedMessagesInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ },
+ )
+
+ return [
+ Message(
+ id=message["ID"],
+ content=message["Content"],
+ content_type=MessageContentType(message["ContentType"]),
+ sender=MessageFrom(
+ user_id=message["From"]["UserID"],
+ display_name=message["From"]["DisplayName"],
+ ),
+ created_date_time=message["CreatedDateTime"],
+ reply_count=message["ReplyCount"],
+ )
+ for message in messages
+ ]
+
+ def pin_message(self, chat_ref: ChatRef, message_id: str) -> bool:
+ """Pins a message in the chat.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object.
+ message_id (str): The ID of the message to pin.
+
+ Returns:
+ bool: True if the message was successfully pinned.
+ """
+ result = self.client.execute(
+ cmd_type="request",
+ method="pinMessageInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ "messageID": message_id,
+ },
+ )
+
+ return result == "pinned"
+
+ def unpin_message(self, chat_ref: ChatRef, message_id: str) -> bool:
+ """Unpins a message in the chat.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object.
+ message_id (str): The ID of the message to unpin.
+
+ Returns:
+ bool: True if the message was successfully unpinned.
+ """
+ result = self.client.execute(
+ cmd_type="request",
+ method="unpinMessageInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ "messageID": message_id,
+ },
+ )
+
+ return result == "unpinned"
+
+ def get_mentions(self, chat_ref: ChatRef, raw_mentions: list[str]) -> list[Mention]:
+ """Resolves raw mention strings into formal Mention objects.
+
+ This helps in processing @mentions within message content before sending.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object.
+ raw_mentions (list[str]): A list of raw strings to resolve.
+ Supported formats:
+ * **Email/User ID**: Resolves to a specific user.
+ * **'Everyone'**: Mentions all members (Group Chats only).
+
+ Returns:
+ list[Mention]: A list of resolved Mention objects.
+ """
+ mentions = self.client.execute(
+ cmd_type="request",
+ method="getMentionsInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ "rawMentions": raw_mentions,
+ },
+ )
+
+ return [
+ Mention(
+ kind=mention["Kind"],
+ at_id=mention["AtID"],
+ text=mention["Text"],
+ target_id=mention["TargetID"],
+ )
+ for mention in mentions
+ ]
+
+ def search_messages(self, chat_ref: ChatRef, options: SearchMessagesOptions, config: SearchConfig) -> SearchResults:
+ """Searches for messages in a chat based on various criteria.
+
+ Args:
+ chat_ref (ChatRef): The chat reference object.
+ options (SearchMessagesOptions): The search options and filters.
+ config (SearchConfig): Configuration for the search operation.
+
+ Returns:
+ SearchResults: The results of the search operation.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="searchMessagesInChat",
+ params={
+ "chatRef": {
+ "ref": chat_ref.ref,
+ "type": chat_ref.type.value,
+ },
+ "searchMessagesOptions": options.__dict__(),
+ "searchConfig": config.__dict__(),
+ },
+ )
+
+ return SearchResults(
+ messages=[
+ SearchResult(
+ message=Message(
+ id=msg["Message"]["ID"],
+ content=msg["Message"]["Content"],
+ content_type=MessageContentType(msg["Message"]["ContentType"]),
+ created_date_time=msg["Message"]["CreatedDateTime"],
+ sender=MessageFrom(
+ user_id=msg["Message"]["From"]["UserID"],
+ display_name=msg["Message"]["From"]["DisplayName"],
+ ),
+ reply_count=msg["Message"]["ReplyCount"],
+ ),
+ channel_id=msg.get("ChannelID"),
+ team_id=msg.get("TeamID"),
+ chat_id=msg.get("ChatID"),
+ )
+ for msg in response["Messages"]
+ ] if response.get("Messages") else [],
+ next_from=response.get("NextFrom"),
+ )
diff --git a/python/teams_lib_pzsp2_z1/services/teams.py b/python/teams_lib_pzsp2_z1/services/teams.py
new file mode 100644
index 0000000..06646c6
--- /dev/null
+++ b/python/teams_lib_pzsp2_z1/services/teams.py
@@ -0,0 +1,239 @@
+"""
+This module contains the service for managing Microsoft Teams via the Go backend.
+"""
+
+from teams_lib_pzsp2_z1.model.team import Team, UpdateTeam
+from teams_lib_pzsp2_z1.services.base_service import BaseService
+
+
+class TeamsService(BaseService):
+ """Service for managing Microsoft Teams lifecycle and properties via the Go backend.
+
+ This class acts as a high-level Python wrapper. It delegates logical operations
+ to the underlying Go library.
+
+ **Concepts:**
+ * **References**: The `teamRef` argument accepts either the Team UUID or the
+ Team Display Name. The Go backend resolves these automatically.
+ * **Context**: Operations are executed on behalf of the authenticated user
+ (derived from the MSAL context).
+ * **Caching**: If the Go client was initialized with cache enabled, this service
+ will transparently use cached team references to reduce API calls.
+ """
+
+ def get(self, team_ref: str) -> Team:
+ """Retrieves a specific team by its reference.
+
+ Args:
+ team_ref (str): The reference to the team (UUID or Display Name).
+ Returns:
+ Team: The requested team object containing details like ID, DisplayName, and Visibility.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="getTeam",
+ params={
+ "teamRef": team_ref,
+ },
+ )
+
+ return Team(
+ id=response["ID"],
+ display_name=response["DisplayName"],
+ description=response["Description"],
+ is_archived=(True if response["IsArchived"] else False),
+ visibility=response["Visibility"],
+ )
+
+ def list_my_joined(self) -> list[Team]:
+ """Lists all teams that the authenticated user has joined.
+
+ Returns:
+ list[Team]: A list of team objects the user is a member of.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="listMyJoined",
+ params={},
+ )
+ return [
+ Team(
+ id=team["ID"],
+ display_name=team["DisplayName"],
+ description=team["Description"],
+ is_archived=(True if team["IsArchived"] else False),
+ visibility=team["Visibility"],
+ )
+ for team in response
+ ]
+
+ def update(self, team_ref: str, update: UpdateTeam) -> Team:
+ """Applies updates to a team's properties.
+
+ Only fields that are not None in the `update` object will be modified.
+
+ Args:
+ team_ref (str): The reference to the team (UUID or Display Name).
+ update (UpdateTeam): An object containing the fields to update (e.g., DisplayName).
+
+ Returns:
+ Team: The updated team object.
+ """
+ params = {"teamRef": team_ref}
+ # Filter out None values to perform a PATCH-like update
+ update_dict = {k: v for k, v in update.__dict__.items() if v is not None}
+ params["team"] = update_dict
+
+ response = self.client.execute(
+ cmd_type="request",
+ method="updateTeam",
+ params=params,
+ )
+ return Team(
+ id=response["ID"],
+ display_name=response["DisplayName"],
+ description=response["Description"],
+ is_archived=(True if response["IsArchived"] else False),
+ visibility=response["Visibility"],
+ )
+
+ def create_via_group(
+ self, display_name: str, mail_nickname: str, visibility: str
+ ) -> Team:
+ """Creates a new team associated with a standard Microsoft 365 group.
+
+ Args:
+ display_name (str): The name of the new team.
+ mail_nickname (str): The mail alias for the underlying group.
+ visibility (str): The privacy level ('Public' or 'Private').
+
+ Returns:
+ Team: The newly created team object.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="createTeamViaGroup",
+ params={
+ "displayName": display_name,
+ "mailNickname": mail_nickname,
+ "visibility": visibility,
+ },
+ )
+ return Team(
+ id=response["ID"],
+ display_name=response["DisplayName"],
+ description=response["Description"],
+ is_archived=(True if response["IsArchived"] else False),
+ visibility=response["Visibility"],
+ )
+
+ def create_from_template( # noqa: PLR0913
+ self,
+ display_name: str,
+ description: str,
+ owners: list[str],
+ members: list[str] | None = None,
+ visibility: str = "public",
+ include_me: bool = True,
+ ) -> str:
+ """Creates a new team based on a template.
+
+ This operation is often asynchronous.
+
+ Args:
+ display_name (str): The name of the team.
+ description (str): A brief description of the team.
+ owners (list[str]): A list of user references (IDs or emails) to be owners.
+ members (list[str] | None, optional): A list of user references to be members. Defaults to None.
+ visibility (str, optional): The privacy level ('public' or 'private'). Defaults to "public".
+ include_me (bool, optional): Whether to add the authenticated user as an owner. Defaults to True.
+
+ Returns:
+ str: The ID of the newly created team (or the async operation ID).
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="createTeamFromTemplate",
+ params={
+ "displayName": display_name,
+ "description": description,
+ "owners": owners,
+ "members": members,
+ "visibility": visibility,
+ "includeMe": include_me,
+ },
+ )
+ return response
+
+ def archive(self, team_ref: str, spo_read_only_from_members: bool) -> bool:
+ """Archives a team.
+
+ Args:
+ team_ref (str): The reference to the team (UUID or Display Name).
+ spo_read_only_from_members (bool): If True, sets the associated SharePoint site
+ to read-only for members.
+
+ Returns:
+ bool: True if the team was successfully archived.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="archiveTeam",
+ params={
+ "teamRef": team_ref,
+ "spoReadOnlyFromMembers": spo_read_only_from_members,
+ },
+ )
+ return response == "archived"
+
+ def unarchive(self, team_ref: str) -> bool:
+ """Restores an archived team to an active state.
+
+ Args:
+ team_ref (str): The reference to the team (UUID or Display Name).
+ Returns:
+ bool: True if the team was successfully unarchived.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="unarchiveTeam",
+ params={
+ "teamRef": team_ref,
+ },
+ )
+ return response == "unarchived"
+
+ def delete(self, team_ref: str) -> bool:
+ """Deletes a team (soft delete).
+
+ Args:
+ team_ref (str): The reference to the team (UUID or Display Name).
+ Returns:
+ bool: True if the delete request was successful.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="deleteTeam",
+ params={
+ "teamRef": team_ref,
+ },
+ )
+ return response == "deleted"
+
+ def restore_deleted(self, deleted_team_ID: str) -> str:
+ """Restores a previously deleted team.
+
+ Args:
+ deleted_team_ID (str): The ID of the deleted group/team.
+
+ Returns:
+ str: The ID of the restored team/group.
+ """
+ response = self.client.execute(
+ cmd_type="request",
+ method="restoreDeletedTeam",
+ params={
+ "deletedGroupID": deleted_team_ID,
+ },
+ )
+ return response
diff --git a/python/tests/fake_server/data.py b/python/tests/fake_server/data.py
index d917f45..f3cb45d 100644
--- a/python/tests/fake_server/data.py
+++ b/python/tests/fake_server/data.py
@@ -1,61 +1,879 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, field
+import re
+import traceback
+from typing import Optional, List, Dict, Any
+from unittest import result
+from urllib import request
+
+from werkzeug import Response
+
from teams_lib_pzsp2_z1.model.team import Team
+from teams_lib_pzsp2_z1.model.chat import Chat, ChatType
from teams_lib_pzsp2_z1.model.channel import Channel
+from teams_lib_pzsp2_z1.model.message import Message, MessageFrom
+from teams_lib_pzsp2_z1.model.member import Member
+
+# --- Constants for OData ---
+ODATA_CONTEXT = "@odata.context"
+ODATA_TYPE = "@odata.type"
+ODATA_COUNT = "@odata.count"
+GRAPH_URL = "https://graph.microsoft.com/v1.0/$metadata"
@dataclass
class FakeServerData:
- teams: list[Team]
- channels: dict[str, list[Channel]]
+ teams: List[Team]
+ channels: Dict[str, List[Channel]]
+
+ # State tracking variables
+ newGroupID: str
+ newTeamName: str
+ newGroupMailNickname: str
+ newTeamVisibility: str
+ potentialTeams: List[Team] = field(default_factory=list)
+ searching: str = "chats"
- def __init__(self) -> FakeServerData:
+ # Initial data containers
+ messages: Dict[str, List[Message]] = field(default_factory=dict)
+ replies: Dict[str, List[Message]] = field(default_factory=dict)
+ members: Dict[str, Dict[str, List[Member]]] = field(default_factory=dict)
+ group_chat_members: Dict[str, List[Member]] = field(default_factory=dict)
+ chat_messages: Dict[str, List[Message]] = field(default_factory=dict)
+ users: List[Member] = field(default_factory=list)
+ me: Member = field(default_factory=Member) # type: ignore
+ group_chats: List[Chat] = field(default_factory=list)
+ oneonone_chats: List[Chat] = field(default_factory=list)
+
+ # Templates and temporary state
+ newChannelName: str = "New Channel"
+ newChannelID: str = "19:newchannelid@thread.tacv2"
+ newMessageTemplate: Message = field(default_factory=lambda: Message(
+ id="new-message-id",
+ content="This is a new message.",
+ content_type="text",
+ sender=MessageFrom(user_id="user-new-001", display_name="New User"),
+ created_date_time="2024-01-02T10:00:00Z",
+ reply_count=0,
+ ))
+ newMemberTemplate: Member = field(default_factory=lambda: Member(
+ id="user-new-002", user_id="new-user-002", display_name="New Member",
+ role="owner", email="newmember@example.com"
+ ))
+ newChatTemplate: Chat = field(default_factory=lambda: Chat(
+ id="new-chat-001", type=ChatType.GROUP, is_hidden=False, topic="New Chat Topic"
+ ))
+ updatedGroupChatTopic: str = "Updated Chat Topic"
+
+ def __init__(self) -> None:
+ # --- Teams ---
self.teams = [
- Team(
- ID="team-123-abc",
- DisplayName="Test Team",
- Description="A team for testing",
- IsArchived=False,
- Visibility="Private",
- ),
+ Team(id="team-123-abc", display_name="Test Team", description="A team for testing", is_archived=False, visibility="private"),
+ Team(id="team-456-def", display_name="Another Team", description="Another team for testing", is_archived=False, visibility="public"),
+ Team(id="archived-team-789-ghi", display_name="Archived Team", description="An archived team for testing", is_archived=True, visibility="private")
]
+ self.potentialTeams = []
+
+ # --- Channels ---
self.channels = {
"team-123-abc": [
- Channel(
- ID="19:123123@thread.tacv2",
- Name="General",
- IsGeneral=True,
- ),
- Channel(
- ID="19:999999@thread.tacv2",
- Name="Development",
- IsGeneral=False,
- ),
+ Channel(id="19:123123@thread.tacv2", name="Something", is_general=False),
+ Channel(id="19:999999@thread.tacv2", name="Development", is_general=False),
+ ],
+ }
+
+ # --- Messages (Channel) ---
+ self.messages = {
+ "19:123123@thread.tacv2": [
+ Message(id="msg-001", content="Hello, team!", content_type="text", sender=MessageFrom(user_id="user-123-abc", display_name="Alice"), created_date_time="2024-01-01T10:00:00Z", reply_count=0),
+ Message(id="msg-002", content="Don't forget the meeting at 3 PM.", content_type="text", sender=MessageFrom(user_id="user-456-def", display_name="Bob"), created_date_time="2024-01-01T11:00:00Z", reply_count=2),
+ Message(id="AAMkAGIwMDA5MmY0LWY5ZTgtNGY5YS04NzczLWNhNjc0ZGIyZDBjYgBGAAAAAADm35sgHbzESapJ8_BjBlhEBwDAYtphe7dsRbDrOT-HAHoKAACmqNsoAADAYtphe7dsRbDrOT-HAHoKAAFsBhyEAAA=", content="Hello, team!", content_type="text", sender=MessageFrom(user_id="user-123-abc", display_name="Alice"), created_date_time="2024-01-01T10:00:00Z", reply_count=0),
+ ],
+ }
+
+ # --- Replies ---
+ self.replies = {
+ "msg-002": [
+ Message(id="msg-002-reply-001", content="Thanks for the reminder!", content_type="text", sender=MessageFrom(user_id="user-789-ghi", display_name="Charlie"), created_date_time="2024-01-01T12:00:00Z", reply_count=0),
+ Message(id="msg-002-reply-002", content="I'll be there.", content_type="text", sender=MessageFrom(user_id="user-123-abc", display_name="Alice"), created_date_time="2024-01-01T12:30:00Z", reply_count=0),
],
}
+ # --- Members (Channel) ---
+ self.members = {
+ "team-123-abc": {
+ "19:123123@thread.tacv2": [
+ Member(id="user-123-abc", user_id="user-123-abc", display_name="Alice", role="owner", email="alice@example.com"),
+ Member(id="user-456-def", user_id="user-456-def", display_name="Bob", role="member", email="bob@example.com"),
+ ],
+ }
+ }
+
+ # --- Users ---
+ self.users = [
+ Member(id="user-123-abc", user_id="user-123-abc", display_name="Alice", role="owner", email="alice@example.com"),
+ Member(id="user-456-def", user_id="user-456-def", display_name="Bob", role="member", email="bob@example.com"),
+ ]
+ self.me = Member(id="user-me-001", user_id="user-me-001", display_name="Current User", role="member", email="me@example.com")
+
+ # --- Chats & Chat Members/Messages ---
+ self.group_chats = [
+ Chat(id="chat-123-abc", type=ChatType.GROUP, is_hidden=False, topic="Project Discussion"),
+ Chat(id="chat-456-def", type=ChatType.GROUP, is_hidden=True, topic="Secret Plans"),
+ ]
+ self.oneonone_chats = [
+ Chat(id="chat-789-ghi", type=ChatType.ONE_ON_ONE, is_hidden=False, topic=""),
+ ]
+
+ self.group_chat_members = {
+ "chat-123-abc": [
+ Member(id="user-123-abc", user_id="user-123-abc", display_name="Alice", role="owner", email="alice@example.com"),
+ Member(id="user-456-def", user_id="user-456-def", display_name="Bob", role="member", email="bob@example.com"),
+ ],
+ }
+
+ self.chat_messages = {
+ "chat-123-abc": [
+ Message(id="AAMkAGIwMDA5MmY0LWY5ZTgtNGY5YS04NzczLWNhNjc0ZGIyZDBjYgBGAAAAAADm35sgHbzESapJ8_BjBlhEBwDAYtphe7dsRbDrOT-HAHoKAACmqNsoAADAYtphe7dsRbDrOT-HAHoKAAFsBhyEAAA=", content="Hello, team!", content_type="text", sender=MessageFrom(user_id="user-123-abc", display_name="Alice"), created_date_time="2024-01-01T10:00:00Z", reply_count=0),
+ Message(id="BAMkAGIwMDA5MmY0LWY5ZTgtNGY5YS04NzczLWNhNjc0ZGIyZDBjYgBGAAAAAADm35sgHbzESapJ8_BjBlhEBwDAYtphe7dsRbDrOT-HAHoKAACmqNsoAADAYtphe7dsRbDrOT-HAHoKAAFsBhyEAAA=", content="Don't forget the meeting at 3 PM.", content_type="text", sender=MessageFrom(user_id="user-456-def", display_name="Bob"), created_date_time="2024-01-01T11:00:00Z", reply_count=2),
+ ],
+ }
+
+ # --- State Variables ---
+ self.newGroupID = "group-789-ghi"
+ self.newTeamName = "New Team"
+ self.newGroupMailNickname = "new-team-nickname"
+ self.newTeamVisibility = "private"
+ self.newTeamID = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
+
+ # Init Templates (re-assigning to ensure freshness if needed, though default_factory handles it)
+ self.newChannelName = "New Channel"
+ self.newChannelID = "19:newchannelid@thread.tacv2"
+ self.updatedGroupChatTopic = "Updated Chat Topic"
+
+ # Initialize templates manually to match original structure exactly if factories behave differently
+ self.newMessageTemplate = Message(
+ id="new-message-id", content="This is a new message.", content_type="text",
+ sender=MessageFrom(user_id="user-new-001", display_name="New User"),
+ created_date_time="2024-01-02T10:00:00Z", reply_count=0
+ )
+ self.newMemberTemplate = Member(
+ id="user-new-002", user_id="new-user-002", display_name="New Member",
+ role="owner", email="newmember@example.com"
+ )
+ self.newChatTemplate = Chat(
+ id="new-chat-001", type=ChatType.GROUP, is_hidden=False, topic="New Chat Topic"
+ )
+
+ # --- Helpers ---
+ def _find_team(self, team_id: str) -> Optional[Team]:
+ return next((t for t in self.teams if t.id == team_id), None)
+
+ def _find_channel(self, team_id: str, channel_id: str) -> Optional[Channel]:
+ return next((c for c in self.channels.get(team_id, []) if c.id == channel_id), None)
+
+ # ==========================================
+ # TEAMS & GROUPS
+ # ==========================================
+
def get_myJoinedTeams_response(self) -> dict:
return {
- "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#teams",
"value": [
{
- "id": team.ID,
- "displayName": team.DisplayName,
- "description": team.Description,
- "isArchived": team.IsArchived,
- "visibility": team.Visibility,
+ "id": team.id,
+ "displayName": team.display_name,
+ "description": team.description,
+ "isArchived": team.is_archived,
+ "visibility": team.visibility,
}
for team in self.teams
],
}
+ def get_team_response(self, team_id: str) -> Optional[dict]:
+ team = self._find_team(team_id)
+ if not team:
+ return None
+ return {
+ "id": team.id,
+ "displayName": team.display_name,
+ "description": team.description,
+ "isArchived": team.is_archived,
+ "visibility": team.visibility,
+ }
+
+ def get_updateTeam_response(self, team_id: str, update_json: dict) -> Optional[dict]:
+ team = self._find_team(team_id)
+ if not team:
+ return None
+
+ if "displayName" in update_json:
+ team.display_name = update_json["displayName"]
+ if "description" in update_json:
+ team.description = update_json["description"]
+ if "visibility" in update_json:
+ team.visibility = update_json["visibility"]
+
+ return {
+ "id": team.id,
+ "displayName": team.display_name,
+ "description": team.description,
+ "isArchived": team.is_archived,
+ "visibility": team.visibility,
+ }
+
+ def get_createGroup_response(self, request_json: dict) -> dict:
+ visibility = request_json.get("visibility", "").lower()
+ if visibility not in ("private", "public"):
+ visibility = "private"
+
+ self.potentialTeams.append(
+ Team(
+ id=self.newGroupID,
+ display_name=request_json.get("displayName"),
+ description=request_json.get("description"),
+ is_archived=False,
+ visibility=visibility,
+ )
+ )
+
+ return {
+ "id": self.newGroupID,
+ "displayName": request_json.get("displayName"),
+ "description": request_json.get("displayName"),
+ "visibility": visibility,
+ "groupTypes": ["Unified"],
+ "mailEnabled": True,
+ "mailNickname": request_json.get("mailNickname"),
+ "securityEnabled": False,
+ "createdDateTime": "2024-01-01T00:00:00Z",
+ }
+
+ def get_createTeamViaGroup_response(self, group_id: str) -> dict:
+ team = next((t for t in self.potentialTeams if t.id == group_id), None)
+ if not team:
+ return {}
+
+ self.teams.append(team)
+
+ return {
+ "id": team.id,
+ "displayName": team.display_name,
+ "description": team.description,
+ "isArchived": team.is_archived,
+ "visibility": team.visibility,
+ }
+
+ def get_createTeamFromTemplate_response(self, request: request.Request) -> Response:
+ request_json = request.json
+ new_team = Team(
+ id=self.newTeamID,
+ display_name=request_json.get("displayName"),
+ description=request_json.get("description"),
+ is_archived=False,
+ visibility="private",
+ )
+ self.teams.append(new_team)
+
+ # self.newTeamID == "f47ac10b-58cc-4372-a567-0e02b2c3d479"
+ op_id = "00000000-0000-0000-0000-000000000000"
+
+ return Response(
+ status= 202,
+ headers= {
+ "Location": f"/teams('{self.newTeamID}')/operations('{op_id}')",
+ "Content-Location": f"/teams('{self.newTeamID}')",
+ "Content-Type": "application/json",
+ "Content-Length": "0"
+ },
+ response= ""
+ )
+
+ def get_archiveTeam_response(self, team_id: str) -> dict:
+ team = self._find_team(team_id)
+ if not team:
+ return {"success": False}
+ team.is_archived = True
+ return {"success": True}
+
+ def get_unarchiveTeam_response(self, team_id: str) -> dict:
+ team = self._find_team(team_id)
+ if not team:
+ return {"success": False}
+ team.is_archived = False
+ return {"success": True}
+
+ def get_deleteTeam_response(self, team_id: str) -> dict:
+ team = self._find_team(team_id)
+ if not team:
+ return {"success": False}
+ self.teams.remove(team)
+ return {"success": True}
+
+ def get_restoreTeam_response(self, team_id: str) -> dict:
+ archived_team = next((t for t in self.teams if t.id == team_id and t.is_archived), None)
+ if not archived_team:
+ return {"success": False}
+
+ archived_team.is_archived = False
+ return {
+ "id": archived_team.id,
+ "displayName": archived_team.display_name,
+ "description": archived_team.description,
+ "isArchived": archived_team.is_archived,
+ "visibility": archived_team.visibility,
+ "mailNickname": self.newGroupMailNickname
+ }
+
+ # ==========================================
+ # CHANNELS
+ # ==========================================
+
def get_listChannels_response(self, team_id: str) -> dict:
return {
- "@odata.context": f"https://graph.microsoft.com/v1.0/$metadata#teams('{team_id}')/channels",
+ ODATA_CONTEXT: f"{GRAPH_URL}#teams('{team_id}')/channels",
"value": [
{
- "id": channel.ID,
- "displayName": channel.Name,
- "isGeneral": channel.IsGeneral,
+ ODATA_TYPE: "#microsoft.graph.channel",
+ "id": channel.id,
+ "displayName": channel.name,
+ "isGeneral": channel.is_general,
+ "membershipType": "standard",
+ "email": ""
}
for channel in self.channels.get(team_id, [])
],
- }
\ No newline at end of file
+ }
+
+ def get_channel_response(self, team_id: str, channel_id: str) -> Optional[dict]:
+ channel = self._find_channel(team_id, channel_id)
+ if not channel:
+ return None
+ return {
+ "id": channel.id,
+ "displayName": channel.name,
+ "isGeneral": channel.is_general,
+ }
+
+ def get_create_channel_response(self, team_id: str, request_json: dict) -> dict:
+ display_name = request_json.get("displayName")
+ description = request_json.get("description", display_name)
+
+ new_channel = Channel(
+ id=self.newChannelID,
+ name=display_name,
+ is_general=False,
+ )
+
+ if team_id not in self.channels:
+ self.channels[team_id] = []
+ self.channels[team_id].append(new_channel)
+
+ return {
+ "id": new_channel.id,
+ "displayName": new_channel.name,
+ "description": description,
+ "isGeneral": new_channel.is_general,
+ "membershipType": "standard"
+ }
+
+ def get_delete_channel_response(self, team_id: str, channel_id: str) -> dict:
+ channel = self._find_channel(team_id, channel_id)
+ if not channel:
+ return {"success": False}
+ self.channels[team_id].remove(channel)
+ return {"success": True}
+
+ # ==========================================
+ # MESSAGES (CHANNEL)
+ # ==========================================
+
+ def get_list_messages_response(self, team_id: str, channel_id: str) -> dict:
+ return {
+ "value": [
+ {
+ "id": message.id,
+ "body": {
+ "content": message.content,
+ "contentType": message.content_type,
+ },
+ "from": {
+ "user": {
+ "id": message.sender.user_id,
+ "displayName": message.sender.display_name,
+ }
+ },
+ "createdDateTime": message.created_date_time,
+ "replies": [{"id": f"dummy-reply-{i}"} for i in range(message.reply_count)]
+ }
+ for message in self.messages.get(channel_id, [])
+ ],
+ }
+
+ def get_message_response(self, team_id: str, channel_id: str, message_id: str) -> Optional[dict]:
+ message = next((m for m in self.messages.get(channel_id, []) if m.id == message_id), None)
+ if not message:
+ return None
+
+ return {
+ "id": message.id,
+ "body": {
+ "content": message.content,
+ "contentType": message.content_type,
+ },
+ "from": {
+ "user": {
+ "id": message.sender.user_id,
+ "displayName": message.sender.display_name,
+ }
+ },
+ "createdDateTime": message.created_date_time,
+ "replies": [{"id": f"dummy-reply-{i}"} for i in range(message.reply_count)]
+ }
+
+ def get_send_message_response(self, team_id: str, channel_id: str, request_json: dict) -> dict:
+ return {
+ "id": self.newMessageTemplate.id,
+ "body": {
+ "content": request_json.get("body", {}).get("content"),
+ "contentType": request_json.get("body", {}).get("contentType"),
+ },
+ "from": {
+ "user": {
+ "id": self.newMessageTemplate.sender.user_id,
+ "displayName": self.newMessageTemplate.sender.display_name,
+ }
+ },
+ "createdDateTime": self.newMessageTemplate.created_date_time,
+ }
+
+ def get_list_replies_response(self, team_id: str, channel_id: str, message_id: str) -> dict:
+ return {
+ "value": [
+ {
+ "id": reply.id,
+ "body": {
+ "content": reply.content,
+ "contentType": reply.content_type,
+ },
+ "from": {
+ "user": {
+ "id": reply.sender.user_id,
+ "displayName": reply.sender.display_name,
+ }
+ },
+ "createdDateTime": reply.created_date_time,
+ }
+ for reply in self.replies.get(message_id, [])
+ ],
+ }
+
+ def get_reply_response(self, team_id: str, channel_id: str, message_id: str, reply_id: str) -> Optional[dict]:
+ reply = next((r for r in self.replies.get(message_id, []) if r.id == reply_id), None)
+ if not reply:
+ return None
+ return {
+ "id": reply.id,
+ "body": {
+ "content": reply.content,
+ "contentType": reply.content_type,
+ },
+ "from": {
+ "user": {
+ "id": reply.sender.user_id,
+ "displayName": reply.sender.display_name,
+ }
+ },
+ "createdDateTime": reply.created_date_time,
+ }
+
+ # ==========================================
+ # MEMBERS (CHANNEL)
+ # ==========================================
+
+ def get_list_members_response(self, team_id: str, channel_id: str) -> dict:
+ return {
+ ODATA_CONTEXT: f"{GRAPH_URL}#teams('{team_id}')/channels('{channel_id}')/members",
+ "value": [
+ {
+ ODATA_TYPE: "#microsoft.graph.aadUserConversationMember",
+ "id": member.id,
+ "userId": member.user_id,
+ "displayName": member.display_name,
+ "roles": [member.role] if member.role == "owner" else [],
+ "email": member.email,
+ }
+ for member in self.members.get(team_id, {}).get(channel_id, [])
+ ],
+ }
+
+ def get_add_member_response(self, team_id: str, request_json: dict) -> dict:
+ roles = ["owner"] if self.newMemberTemplate.role == "owner" else []
+ return {
+ ODATA_CONTEXT: f"{GRAPH_URL}#teams('{team_id}')/members/$entity",
+ ODATA_TYPE: "#microsoft.graph.aadUserConversationMember",
+ "id": self.newMemberTemplate.id,
+ "roles": roles,
+ "displayName": self.newMemberTemplate.display_name,
+ "userId": self.newMemberTemplate.user_id,
+ "email": self.newMemberTemplate.email,
+ }
+
+ def get_update_member_role_response(self, team_id: str, channel_id: str, member_id: str, request_json: dict) -> Optional[dict]:
+ channel_members = self.members.get(team_id, {}).get(channel_id, [])
+ member = next((m for m in channel_members if m.id == member_id), None)
+ if not member:
+ return None
+
+ if "roles" in request_json:
+ member.role = "owner" if "owner" in request_json["roles"] else "member"
+
+ return {
+ ODATA_CONTEXT: f"{GRAPH_URL}#teams('{team_id}')/channels('{channel_id}')/members/$entity",
+ ODATA_TYPE: "#microsoft.graph.aadUserConversationMember",
+ "id": member.id,
+ "userId": member.user_id,
+ "displayName": member.display_name,
+ "roles": [member.role] if member.role == "owner" else [],
+ "email": member.email,
+ }
+
+ def get_remove_member_response(self, team_id: str, channel_id: str, member_id: str) -> dict:
+ channel_members = self.members.get(team_id, {}).get(channel_id, [])
+ member = next((m for m in channel_members if m.id == member_id), None)
+ if not member:
+ return {"success": False}
+ channel_members.remove(member)
+ return {"success": True}
+
+ # ==========================================
+ # CHATS
+ # ==========================================
+
+ def get_chat_responses(self, chat_id: str) -> dict:
+ chat = next((c for c in self.group_chats + self.oneonone_chats if c.id == chat_id), None)
+ if not chat:
+ return {}
+
+ return {
+ ODATA_CONTEXT: "https://graph.microsoft.com/v1.0/$metadata#chats/$entity",
+ "id": chat.id,
+ "chatType": "oneOnOne" if chat.type == ChatType.ONE_ON_ONE else "group",
+ "isHiddenForAllMembers": chat.is_hidden,
+ "topic": chat.topic,
+ }
+
+ def get_list_chats_response(self, chat_type: ChatType) -> dict:
+ chats = self.oneonone_chats if chat_type == ChatType.ONE_ON_ONE else self.group_chats
+ return {
+ ODATA_CONTEXT: f"{GRAPH_URL}#chats",
+ ODATA_COUNT: len(chats),
+ "value": [
+ {
+ "id": chat.id,
+ "chatType": "oneOnOne" if chat.type == ChatType.ONE_ON_ONE else "group",
+ "isHiddenForAllMembers": chat.is_hidden,
+ "topic": chat.topic,
+ }
+ for chat in chats
+ ],
+ }
+
+ def get_create_chat_response(self, request_json: dict) -> dict:
+ is_one_on_one = request_json["chatType"] == "oneOnOne"
+ response = {
+ ODATA_CONTEXT: f"{GRAPH_URL}#chats/$entity",
+ "id": self.newChatTemplate.id,
+ "chatType": "oneOnOne" if is_one_on_one else "group",
+ "isHiddenForAllMembers": self.newChatTemplate.is_hidden,
+ "topic": None if is_one_on_one else self.newChatTemplate.topic,
+ }
+ return response
+
+ def get_update_group_chat_topic_response(self, chat_id: str, request_json: dict) -> dict:
+ chat = next((c for c in self.group_chats if c.id == chat_id), None)
+ if not chat:
+ return {}
+
+ chat.topic = request_json.get("topic", chat.topic)
+ return {
+ ODATA_CONTEXT: f"{GRAPH_URL}#chats('{chat_id}')",
+ "id": chat.id,
+ "chatType": "group",
+ "isHiddenForAllMembers": chat.is_hidden,
+ "topic": chat.topic,
+ }
+
+ # ==========================================
+ # CHAT MEMBERS
+ # ==========================================
+
+ def get_list_group_chat_members_response(self, chat_id: str) -> dict:
+ return {
+ ODATA_CONTEXT: f"{GRAPH_URL}#chats('{chat_id}')/members",
+ "value": [
+ {
+ ODATA_TYPE: "#microsoft.graph.aadUserConversationMember",
+ "id": member.id,
+ "userId": member.user_id,
+ "displayName": member.display_name,
+ "roles": [member.role] if member.role == "owner" else [],
+ "email": member.email,
+ }
+ for member in self.group_chat_members.get(chat_id, [])
+ ],
+ }
+
+ def get_add_member_to_group_chat_response(self, chat_id: str, request_json: dict) -> dict:
+ return {
+ ODATA_CONTEXT: f"{GRAPH_URL}#chats('{chat_id}')/members/$entity",
+ ODATA_TYPE: "#microsoft.graph.aadUserConversationMember",
+ "id": self.newMemberTemplate.id,
+ "roles": [self.newMemberTemplate.role] if self.newMemberTemplate.role == "owner" else [],
+ "displayName": self.newMemberTemplate.display_name,
+ "userId": self.newMemberTemplate.user_id,
+ "email": self.newMemberTemplate.email,
+ }
+
+ def get_remove_member_from_group_chat_response(self, chat_id: str, member_id: str) -> dict:
+ return {"success": True}
+
+ # ==========================================
+ # CHAT MESSAGES
+ # ==========================================
+
+
+ def get_list_messages_in_chat_response(self, chat_id: str) -> dict:
+ return {
+ "value": [
+ {
+ "id": message.id,
+ "body": {
+ "content": message.content,
+ "contentType": message.content_type,
+ },
+ "from": {
+ "user": {
+ "id": message.sender.user_id,
+ "displayName": message.sender.display_name,
+ }
+ },
+ "createdDateTime": message.created_date_time,
+ "replies": [{"id": f"dummy-reply-{i}"} for i in range(message.reply_count)]
+ }
+ for message in self.chat_messages.get(chat_id, [])
+ ],
+ }
+
+ def get_get_message_in_chat_response(self, chat_id: str, message_id: str) -> Optional[dict]:
+ message = next((m for m in self.chat_messages.get(chat_id, []) if m.id == message_id), None)
+ if not message:
+ return None
+
+ return {
+ "id": message.id,
+ "body": {
+ "content": message.content,
+ "contentType": message.content_type,
+ },
+ "from": {
+ "user": {
+ "id": message.sender.user_id,
+ "displayName": message.sender.display_name,
+ }
+ },
+ "createdDateTime": message.created_date_time,
+ "replies": [{"id": f"dummy-reply-{i}"} for i in range(message.reply_count)]
+ }
+
+ def get_send_message_in_chat_response(self, chat_id: str, request_json: dict) -> dict:
+ return {
+ "id": self.newMessageTemplate.id,
+ "body": {
+ "content": request_json.get("body", {}).get("content"),
+ "contentType": request_json.get("body", {}).get("contentType"),
+ },
+ "from": {
+ "user": {
+ "id": self.newMessageTemplate.sender.user_id,
+ "displayName": self.newMessageTemplate.sender.display_name,
+ }
+ },
+ "createdDateTime": self.newMessageTemplate.created_date_time,
+ }
+
+ def get_all_messeges_in_chats_response(self) -> dict:
+ target_chat_id = self.group_chats[0].id
+ return {
+ ODATA_CONTEXT: f"{GRAPH_URL}#Collection(chatMessage)",
+ ODATA_COUNT: len(self.chat_messages),
+ "value": [
+ {
+ ODATA_TYPE: "#microsoft.graph.chatMessage",
+ "id": message.id,
+ "etag": message.id,
+ "messageType": "message",
+ "chatId": target_chat_id,
+ "body": {
+ "content": message.content,
+ "contentType": message.content_type,
+ },
+ "from": {
+ "user": {
+ "id": message.sender.user_id,
+ "displayName": message.sender.display_name,
+ }
+ },
+ "createdDateTime": message.created_date_time,
+ "lastModifiedDateTime": message.created_date_time,
+ }
+ for message in self.chat_messages[target_chat_id]
+ ],
+ }
+
+ def get_list_pinned_messages_in_chat_response(self, chat_id: str) -> dict:
+ return {
+ "value": [
+ {
+ "id": message.id,
+ "message": {
+ "id": message.id,
+ "etag": message.id,
+ "messageType": "message",
+ "chatId": chat_id,
+ "body": {
+ "content": message.content,
+ "contentType": message.content_type,
+ },
+ "from": {
+ "user": {
+ "id": message.sender.user_id,
+ "displayName": message.sender.display_name,
+ }
+ },
+ "createdDateTime": message.created_date_time,
+ "lastModifiedDateTime": message.created_date_time,
+ }
+ }
+ for message in self.chat_messages.get(chat_id, [])
+ ],
+ }
+
+ # ==========================================
+ # USERS
+ # ==========================================
+
+ def get_me_response(self) -> dict:
+ return {
+ "id": self.me.id,
+ "displayName": self.me.display_name,
+ "email": self.me.email,
+ }
+
+ # ==========================================
+ # SEARCH
+ # ==========================================
+
+ def get_search_messages_response(self, request_body) -> dict:
+ hits = []
+ rank = 1
+ messages = []
+
+ raw_query = ""
+ try:
+ if isinstance(request_body, dict):
+ req_list = request_body.get('requests', [])
+ if req_list:
+ raw_query = req_list[0].get('query', {}).get('queryString', "")
+ except Exception:
+ raw_query = ""
+
+ search_term = re.sub(r'\w+:"[^"]+"', '', raw_query)
+
+ search_term = search_term.strip()
+
+
+ try:
+ for chat_id, messages in self.chat_messages.items():
+ for message in messages:
+ if not message.content:
+ continue
+
+ if search_term and search_term.lower() in message.content.lower():
+
+ sender_id = getattr(message.sender, 'user_id', 'unknown') if message.sender else 'unknown'
+ sender_name = getattr(message.sender, 'display_name', 'Unknown') if message.sender else 'Unknown'
+ created_dt = getattr(message, 'created_date_time', "2024-01-01T12:00:00Z")
+
+ if self.searching == "chats":
+ hits.append({
+ "hitId": message.id,
+ "rank": rank,
+ "summary": message.content[:200],
+ "resource": {
+ "id": message.id,
+ "chatId": chat_id,
+ "body": {
+ "content": message.content,
+ "contentType": message.content_type,
+ },
+ "from": {
+ "user": {
+ "id": sender_id,
+ "displayName": sender_name,
+ }
+ },
+ "createdDateTime": created_dt,
+ "lastModifiedDateTime": created_dt,
+ }
+ },
+ )
+ elif self.searching == "channels":
+ hits.append({
+ "hitId": message.id,
+ "rank": rank,
+ "summary": message.content[:200],
+ "resource": {
+ "id": message.id,
+ "channelIdentity": {
+ "teamId": self.teams[0].id,
+ "channelId": self.channels[self.teams[0].id][0].id,
+ },
+ "teamId": self.teams[0].id,
+ "channelId": self.channels[self.teams[0].id][0].id,
+ "body": {
+ "content": message.content,
+ "contentType": message.content_type,
+ },
+ "from": {
+ "user": {
+ "id": sender_id,
+ "displayName": sender_name,
+ }
+ },
+ "createdDateTime": created_dt,
+ "lastModifiedDateTime": created_dt,
+ }
+ },
+ )
+ rank += 1
+
+ except Exception:
+ traceback.print_exc()
+
+ response = {
+ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.searchResponse)",
+ "value": [
+ {
+ "searchTerms": [search_term],
+ "hitsContainers": [
+ {
+ "hits": hits,
+ "total": len(hits),
+ "moreResultsAvailable": False,
+ }
+ ],
+ }
+ ]
+ }
+
+
+ return response
\ No newline at end of file
diff --git a/python/tests/fake_server/setup.py b/python/tests/fake_server/setup.py
index aa92527..34fe21c 100644
--- a/python/tests/fake_server/setup.py
+++ b/python/tests/fake_server/setup.py
@@ -1,21 +1,593 @@
from tests.fake_server.data import FakeServerData
+import json
+import re
+import sys
+from werkzeug.wrappers import Response
+from teams_lib_pzsp2_z1.model.chat import ChatType
+import traceback
def setup_fake_server(httpserver) -> FakeServerData:
"""
Sets up a fake HTTP server to simulate Microsoft Graph API responses.
+ Includes stdout logging for debugging.
"""
data = FakeServerData()
- # Mock response for listing joined teams
+ def make_log_response(request, data_dict: dict, context_msg: str = "", status_code = None) -> Response:
+ print(f"\n[MOCK SERVER] 🟢 REQUEST: {request.method} {request.path}", file=sys.stdout)
+ if context_msg:
+ print(f"[MOCK SERVER] Context: {context_msg}", file=sys.stdout)
+
+ print(f"[MOCK SERVER] Returned body: {data_dict}", file=sys.stdout)
+
+ if request.content_type == "application/json":
+ try:
+ req_data = request.json
+ if req_data:
+ print(f"[MOCK SERVER] Body: {req_data}", file=sys.stdout)
+ except Exception as e:
+ print(f"[MOCK SERVER] ⚠️ Body Error: {e}", file=sys.stdout)
+
+ if not status_code:
+ status_code = 200 if data_dict else 404
+ if request.method in ["POST", "PUT"] and data_dict:
+ status_code = 201
+
+ print(f"[MOCK SERVER] 👉 Response: {status_code}", file=sys.stdout)
+
+ return Response(
+ json.dumps(data_dict),
+ mimetype="application/json",
+ status=status_code
+ )
+
+ # ==========================================
+ # SEARCH
+ # ==========================================
+
+ # POST /search/query
+ httpserver.expect_request(
+ "/v1.0/search/query",
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_search_messages_response(req.json),
+ "Search Messages",
+ status_code=200
+ ))
+
+ # ==========================================
+ # USERS
+ # ==========================================
+
+ # GET /users/me/joinedTeams
httpserver.expect_request(
"/v1.0/users/me-token-to-replace/joinedTeams", method="GET"
- ).respond_with_json(data.get_myJoinedTeams_response())
+ ).respond_with_handler(lambda req: make_log_response(
+ req, data.get_myJoinedTeams_response(), "List Joined Teams"
+ ))
+
+ # GET /users/me/chats/getAllMessages()
+ httpserver.expect_request(
+ "/v1.0/users/me-token-to-replace/chats/getAllMessages()",
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_all_messeges_in_chats_response(),
+ "List All Chat Messages"
+ ))
+
+ # GET users/me/chats/{chat_id}/messages
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/users/me-token-to-replace/chats/([^/]+)/messages"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_list_messages_in_chat_response(
+ re.search(r"/users/me-token-to-replace/chats/([^/]+)/messages", req.path).group(1)
+ ),
+ "List Chat Messages (me)"
+ ))
+
+ # DELETE users/me/chats/{chat_id}/messages/{message_id}/softDelete
+ httpserver.expect_request(
+ re.compile(r"/v1.0/users/[^/]+/chats/[^/]+/messages/[^/]+/softDelete"),
+ method="POST"
+ ).respond_with_response(
+ Response(status=204)
+ )
+
+ def handle_chats_request(req):
+ filter_param = req.args.get('$filter', '')
+ chat_type = None
+ match = re.search(r"chatType\s+eq\s+'([^']+)'", filter_param)
+
+ if match:
+ chat_type = match.group(1)
+
+ if chat_type == "oneOnOne":
+ chat_type = ChatType.ONE_ON_ONE
+ elif chat_type == "group":
+ chat_type = ChatType.GROUP
+
+ return make_log_response(
+ req,
+ data.get_list_chats_response(chat_type),
+ f"List Chats ({chat_type})"
+ )
+
+ # GET /users/me/chats
+ httpserver.expect_request(
+ "/v1.0/users/me-token-to-replace/chats",
+ method="GET"
+ ).respond_with_handler(handle_chats_request)
+
+ # GET /users/me
+ httpserver.expect_request(
+ "/v1.0/users/me-token-to-replace",
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_me_response(),
+ "Get Users"
+ ))
+
+ # ==========================================
+ # TEAMS
+ # ==========================================
+
+ # POST /teams/{id}/channels/{channel_id}/members
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/members"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_add_member_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members", req.path).group(1),
+ req.json
+ ),
+ "Add member to channel"
+ ))
+
+ # PATCH /teams/{tid}/channels/{cid}/members/{mid}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/members/([^/]+)"),
+ method="PATCH"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_update_member_role_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members/([^/]+)", req.path).group(1), # team_id
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members/([^/]+)", req.path).group(2), # channel_id
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members/([^/]+)", req.path).group(3), # member_id
+ req.json
+ ),
+ "Update Member Role"
+ ))
+
+ # DELETE /teams/{team_id}/channels/{channel_id}/members/{member_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/members/([^/]+)"),
+ method="DELETE"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_remove_member_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members/([^/]+)", req.path).group(1), # team_id
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members/([^/]+)", req.path).group(2), # channel_id
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members/([^/]+)", req.path).group(3) # member_id
+ ),
+ "Remove Member from Channel"
+ ))
+
+ # GET /teams/{team_id}/channels/{channel_id}/members
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/members"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_list_members_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members", req.path).group(1),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/members", req.path).group(2)
+ ),
+ "List channel members"
+ ))
+
+ # GET /teams/{team_id}/channels/{channel_id}/messages/{message_id}/replies/{reply_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies/([^/]+)"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_reply_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies/([^/]+)", req.path).group(1),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies/([^/]+)", req.path).group(2),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies/([^/]+)", req.path).group(3),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies/([^/]+)", req.path).group(4)
+ ),
+ "Get Message Reply"
+ ))
+
+ # GET /teams/{team_id}/channels/{channel_id}/messages/{message_id}/replies
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_list_replies_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies", req.path).group(1),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies", req.path).group(2),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)/replies", req.path).group(3)
+ ),
+ "List Message Replies"
+ ))
+
+ # GET /teams/{team_id}/channels/{channel_id}/messages/{message_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_message_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)", req.path).group(1),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)", req.path).group(2),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages/([^/]+)", req.path).group(3)
+ ),
+ "Get Channel Message"
+ ))
+
+ # POST /teams/{team_id}/channels/{channel_id}/messages
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/messages"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_send_message_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages", req.path).group(1),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages", req.path).group(2),
+ req.json
+ ),
+ "Create Channel Message"
+ ))
+
+ # GET /teams/{team_id}/channels/{channel_id}/messages
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)/messages"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_list_messages_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages", req.path).group(1),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)/messages", req.path).group(2)
+ ),
+ "List Channel Messages"
+ ))
+
+ # DELETE /teams/{team_id}/channels/{channel_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)"),
+ method="DELETE"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_delete_channel_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)", req.path).group(1),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)", req.path).group(2)
+ ),
+ "Delete Channel (DELETE)"
+ ))
+
+ # GET /teams/{team_id}/channels/{channel_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels/([^/]+)"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_channel_response(
+ re.search(r"/teams/([^/]+)/channels/([^/]+)", req.path).group(1),
+ re.search(r"/teams/([^/]+)/channels/([^/]+)", req.path).group(2)
+ ),
+ "Get Channel Details"
+ ))
+
+ # POST /teams/{id}/channels
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_create_channel_response(
+ re.search(r"/teams/([^/]+)/channels", req.path).group(1),
+ req.json
+ ),
+ "Create Channel (POST)"
+ ))
+
+ # GET /teams/{id}/channels
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/channels"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_listChannels_response(re.search(r"/teams/([^/]+)/channels", req.path).group(1)),
+ "List Channels"
+ ))
+
+ # POST /teams/{id}/archive
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/archive"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_archiveTeam_response(re.search(r"/teams/([^/]+)/archive", req.path).group(1)),
+ "Archive Team (POST)"
+ ))
+
+ # POST /teams/{id}/unarchive
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/unarchive"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_unarchiveTeam_response(re.search(r"/teams/([^/]+)/unarchive", req.path).group(1)),
+ "Unarchive Team (POST)"
+ ))
+
+ # PATCH /teams/{id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)"),
+ method="PATCH"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_updateTeam_response(
+ re.search(r"/teams/([^/]+)", req.path).group(1),
+ req.json
+ ),
+ "Update Team (PATCH)"
+ ))
+
+ # PATCH /groups/{id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/groups/([^/]+)"),
+ method="PATCH"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_updateTeam_response(
+ re.search(r"/groups/([^/]+)", req.path).group(1),
+ req.json
+ ),
+ "Update Team (PATCH)"
+ ))
+
+ # GET /teams/{id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)(?:$|\?)"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_team_response(re.search(r"/teams/([^/?]+)", req.path).group(1)),
+ "Get Team Details"
+ ))
+
+ # POST /teams
+ httpserver.expect_request(
+ "/v1.0/teams",
+ method="POST"
+ ).respond_with_handler(data.get_createTeamFromTemplate_response)
+
+ # POST /teams/{team_id}/members
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/teams/([^/]+)/members"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_add_member_response(
+ re.search(r"/teams/([^/]+)/members", req.path).group(1),
+ req.json
+ ),
+ "Add Team Member"
+ ))
+
+ # ==========================================
+ # GROUPS
+ # ==========================================
+
+ # PUT /groups/{id}/team
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/groups/([^/]+)/team"),
+ method="PUT"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_createTeamViaGroup_response(re.search(r"/groups/([^/]+)/team", req.path).group(1)),
+ "Teamify Group (PUT)"
+ ))
+
+ # DELETE /groups/{id} (Delete Team uses group endpoint)
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/groups/([^/]+)"),
+ method="DELETE"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_deleteTeam_response(re.search(r"/groups/([^/]+)", req.path).group(1)),
+ "Delete Team (DELETE)"
+ ))
+
+ # POST /groups
+ httpserver.expect_request(
+ "/v1.0/groups",
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_createGroup_response(req.json),
+ "Create Group"
+ ))
+
+ # ==========================================
+ # DIRECTORY / OTHER
+ # ==========================================
+
+ # POST /directory/deletedItems/{id}/restore
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/directory/deletedItems/([^/]+)/restore"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_restoreTeam_response(re.search(r"/directory/deletedItems/([^/]+)/restore", req.path).group(1)),
+ "Restore Deleted Team (POST)"
+ ))
- # Mock response for listing channels in the fake team
+ # ==========================================
+ # CHATS
+ # ==========================================
+
+
+ # DELETE /chats/{chat_id}/pinnedMessages/{message_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)/pinnedMessages/([^/]+)"),
+ method="DELETE"
+ ).respond_with_response(
+ Response(status=204)
+ )
+
+ # POST /chats/{chat_id}/pinnedMessages
httpserver.expect_request(
- f"/v1.0/teams/{data.teams[0].ID}/channels", method="GET"
- ).respond_with_json(data.get_listChannels_response(data.teams[0].ID))
+ re.compile(r"^/v1.0/chats/([^/]+)/pinnedMessages"),
+ method="POST"
+ ).respond_with_response(
+ Response(status=204)
+ )
+
+ # GET /chats/{chat_id}/pinnedMessages
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)/pinnedMessages"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_list_pinned_messages_in_chat_response(
+ re.search(r"/chats/([^/]+)/pinnedMessages", req.path).group(1)
+ ),
+ "List Pinned Chat Messages"
+ ))
+
+ # DELETE /chats/{chat_id}/members/{member_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)/members/([^/]+)"),
+ method="DELETE"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_remove_member_from_group_chat_response(
+ re.search(r"/chats/([^/]+)/members/([^/]+)", req.path).group(1),
+ re.search(r"/chats/([^/]+)/members/([^/]+)", req.path).group(2)
+ ),
+ "Remove Chat Member"
+ ))
+
+ # POST /chats/{chat_id}/members
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)/members"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_add_member_to_group_chat_response(
+ re.search(r"/chats/([^/]+)/members", req.path).group(1),
+ req.json
+ ),
+ "Add Chat Member"
+ ))
+
+ # GET /chats/{chat_id}/members
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)/members"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_list_group_chat_members_response(
+ re.search(r"/chats/([^/]+)/members", req.path).group(1)
+ ),
+ "List Chat Members"
+ ))
+
+ # GET /chats/{chat_id}/messages/{message_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)/messages/([^/]+)"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_get_message_in_chat_response(
+ re.search(r"/chats/([^/]+)/messages/([^/]+)", req.path).group(1),
+ re.search(r"/chats/([^/]+)/messages/([^/]+)", req.path).group(2)
+ ),
+ "Get Chat Message"
+ ))
+
+ # POST /chats/{chat_id}/messages
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)/messages"),
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_send_message_in_chat_response(
+ re.search(r"/chats/([^/]+)/messages", req.path).group(1),
+ req.json
+ ),
+ "Send Chat Message"
+ ))
+
+ # GET /chats/{chat_id}/messages
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)/messages"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_list_messages_in_chat_response(
+ re.search(r"/chats/([^/]+)/messages", req.path).group(1)
+ ),
+ "List Chat Messages"
+ ))
+
+ # PATCH /chats/{chat_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)"),
+ method="PATCH"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_update_group_chat_topic_response(
+ re.search(r"/chats/([^/]+)", req.path).group(1),
+ req.json
+ ),
+ "Update Chat"
+ ))
+
+ # POST /chats
+ httpserver.expect_request(
+ "/v1.0/chats",
+ method="POST"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_create_chat_response(req.json),
+ "Create Chat"
+ ))
+
+ # GET /chats/{chat_id}
+ httpserver.expect_request(
+ re.compile(r"^/v1.0/chats/([^/]+)"),
+ method="GET"
+ ).respond_with_handler(lambda req: make_log_response(
+ req,
+ data.get_chat_responses(re.search(r"/chats/([^/]+)", req.path).group(1)),
+ "Get Chat"
+ ))
+
- return data
+ # Fallback for unmatched routes
+ httpserver.expect_request(re.compile(".*")).respond_with_handler(
+ lambda req: (
+ print(f"[MOCK SERVER] ⚠️ UNMATCHED: {req.method} {req.path}", file=sys.stdout),
+ print(f"[MOCK SERVER] Body: {req.get_data(as_text=True)}", file=sys.stdout),
+ Response(
+ json.dumps({"error": f"Route not matched in Mock: {req.method} {req.path}"}),
+ status=404,
+ mimetype="application/json"
+ )
+ )[-1]
+ )
+ return data
\ No newline at end of file
diff --git a/python/tests/test_channels.py b/python/tests/test_channels.py
index 121f315..98323eb 100644
--- a/python/tests/test_channels.py
+++ b/python/tests/test_channels.py
@@ -1,6 +1,9 @@
from teams_lib_pzsp2_z1.client import TeamsClient
from tests.init_fake_client import init_fake_client
from tests.fake_server.setup import setup_fake_server
+from teams_lib_pzsp2_z1.model.message import MessageContentType, MessageBody
+from teams_lib_pzsp2_z1.model.mention import MentionKind
+from teams_lib_pzsp2_z1.model.search import SearchConfig, SearchMessagesOptions
def test_list_channels_integration(httpserver):
@@ -15,17 +18,421 @@ def test_list_channels_integration(httpserver):
try:
init_fake_client(client, httpserver.url_for(""))
- channels = client.channels.list_channels(data.teams[0].DisplayName)
+ channels = client.channels.list_channels(data.teams[0].display_name)
- assert len(channels) == len(data.channels[data.teams[0].ID])
+ assert len(channels) == len(data.channels[data.teams[0].id])
- assert channels[0].Name == data.channels[data.teams[0].ID][0].Name
- assert channels[0].ID == data.channels[data.teams[0].ID][0].ID
- assert channels[0].IsGeneral == data.channels[data.teams[0].ID][0].IsGeneral
+ assert channels[0].name == data.channels[data.teams[0].id][0].name
+ assert channels[0].id == data.channels[data.teams[0].id][0].id
+ assert channels[0].is_general == data.channels[data.teams[0].id][0].is_general
- assert channels[1].Name == data.channels[data.teams[0].ID][1].Name
- assert channels[1].ID == data.channels[data.teams[0].ID][1].ID
- assert channels[1].IsGeneral == data.channels[data.teams[0].ID][1].IsGeneral
+ assert channels[1].name == data.channels[data.teams[0].id][1].name
+ assert channels[1].id == data.channels[data.teams[0].id][1].id
+ assert channels[1].is_general == data.channels[data.teams[0].id][1].is_general
finally:
client.close()
+
+
+def test_get_channel_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ channel = client.channels.get(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][1].name,
+ )
+
+ assert channel.name == data.channels[data.teams[0].id][1].name
+ assert channel.id == data.channels[data.teams[0].id][1].id
+ assert channel.is_general == data.channels[data.teams[0].id][1].is_general
+
+ finally:
+ client.close()
+
+
+def test_create_standard_channel_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ channel = client.channels.create_standard(
+ team_ref=data.teams[0].display_name,
+ display_name=data.newChannelName,
+ )
+
+ assert channel.name == data.newChannelName
+ assert channel.id == data.newChannelID
+ assert channel.is_general is False
+
+ finally:
+ client.close()
+
+
+def test_create_private_channel_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ channel = client.channels.create_private(
+ team_ref=data.teams[0].display_name,
+ display_name=data.newChannelName,
+ member_refs=[],
+ owner_refs=[],
+ )
+
+ assert channel.name == data.newChannelName
+ assert channel.id == data.newChannelID
+ assert channel.is_general is False
+
+ finally:
+ client.close()
+
+
+def test_delete_channel_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ success = client.channels.delete(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][1].name,
+ )
+
+ assert success is True
+
+ finally:
+ client.close()
+
+
+def test_send_message_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ message = client.channels.send_message(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ body=MessageBody(
+ content=data.newMessageTemplate.content,
+ content_type=MessageContentType.TEXT,
+ mentions=[],
+ )
+ )
+
+ assert message.id == data.newMessageTemplate.id
+ assert message.content == data.newMessageTemplate.content
+ assert message.content_type == MessageContentType(data.newMessageTemplate.content_type)
+ assert message.sender.user_id == data.newMessageTemplate.sender.user_id
+ assert message.sender.display_name == data.newMessageTemplate.sender.display_name
+ assert message.reply_count == data.newMessageTemplate.reply_count
+ assert message.created_date_time == data.newMessageTemplate.created_date_time
+
+ finally:
+ client.close()
+
+
+def test_list_messages_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ collection = client.channels.list_messages(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ )
+
+ assert len(collection.messages) == len(data.messages[data.channels[data.teams[0].id][0].id])
+ assert collection.messages[0].id == data.messages[data.channels[data.teams[0].id][0].id][0].id
+ assert collection.messages[0].content == data.messages[data.channels[data.teams[0].id][0].id][0].content
+ assert collection.messages[0].content_type == MessageContentType(data.messages[data.channels[data.teams[0].id][0].id][0].content_type)
+ assert collection.messages[0].sender.user_id == data.messages[data.channels[data.teams[0].id][0].id][0].sender.user_id
+ assert collection.messages[0].sender.display_name == data.messages[data.channels[data.teams[0].id][0].id][0].sender.display_name
+ assert collection.messages[0].reply_count == data.messages[data.channels[data.teams[0].id][0].id][0].reply_count
+ assert collection.messages[0].created_date_time == data.messages[data.channels[data.teams[0].id][0].id][0].created_date_time
+
+ assert collection.messages[1].id == data.messages[data.channels[data.teams[0].id][0].id][1].id
+ assert collection.messages[1].content == data.messages[data.channels[data.teams[0].id][0].id][1].content
+ assert collection.messages[1].content_type == MessageContentType(data.messages[data.channels[data.teams[0].id][0].id][1].content_type)
+ assert collection.messages[1].sender.user_id == data.messages[data.channels[data.teams[0].id][0].id][1].sender.user_id
+ assert collection.messages[1].sender.display_name == data.messages[data.channels[data.teams[0].id][0].id][1].sender.display_name
+ assert collection.messages[1].reply_count == data.messages[data.channels[data.teams[0].id][0].id][1].reply_count
+ assert collection.messages[1].created_date_time == data.messages[data.channels[data.teams[0].id][0].id][1].created_date_time
+ finally:
+ client.close()
+
+
+def test_get_message_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ message = client.channels.get_message(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ message_id=data.messages[data.channels[data.teams[0].id][0].id][0].id,
+ )
+
+ assert message.id == data.messages[data.channels[data.teams[0].id][0].id][0].id
+ assert message.content == data.messages[data.channels[data.teams[0].id][0].id][0].content
+ assert message.content_type == MessageContentType(data.messages[data.channels[data.teams[0].id][0].id][0].content_type)
+ assert message.sender.user_id == data.messages[data.channels[data.teams[0].id][0].id][0].sender.user_id
+ assert message.sender.display_name == data.messages[data.channels[data.teams[0].id][0].id][0].sender.display_name
+ assert message.reply_count == data.messages[data.channels[data.teams[0].id][0].id][0].reply_count
+ assert message.created_date_time == data.messages[data.channels[data.teams[0].id][0].id][0].created_date_time
+
+ finally:
+ client.close()
+
+
+def test_list_replies_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ replies = client.channels.list_message_replies(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ message_id=data.messages[data.channels[data.teams[0].id][0].id][1].id,
+ )
+
+ assert len(replies.messages) == len(data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id])
+ assert replies.messages[0].id == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].id
+ assert replies.messages[0].content == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].content
+ assert replies.messages[0].content_type == MessageContentType(data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].content_type)
+ assert replies.messages[0].sender.user_id == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].sender.user_id
+ assert replies.messages[0].sender.display_name == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].sender.display_name
+ assert replies.messages[0].created_date_time == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].created_date_time
+
+ assert replies.messages[1].id == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][1].id
+ assert replies.messages[1].content == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][1].content
+ assert replies.messages[1].content_type == MessageContentType(data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][1].content_type)
+ assert replies.messages[1].sender.user_id == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][1].sender.user_id
+ assert replies.messages[1].sender.display_name == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][1].sender.display_name
+ assert replies.messages[1].created_date_time == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][1].created_date_time
+
+ finally:
+ client.close()
+
+
+def test_get_reply_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ reply = client.channels.get_message_reply(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ message_id=data.messages[data.channels[data.teams[0].id][0].id][1].id,
+ reply_id=data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].id,
+ )
+
+ assert reply.id == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].id
+ assert reply.content == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].content
+ assert reply.content_type == MessageContentType(data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].content_type)
+ assert reply.sender.user_id == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].sender.user_id
+ assert reply.sender.display_name == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].sender.display_name
+ assert reply.created_date_time == data.replies[data.messages[data.channels[data.teams[0].id][0].id][1].id][0].created_date_time
+
+ finally:
+ client.close()
+
+
+def test_list_members_integration(httpserver):
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ team_id = data.teams[0].id
+ channel = data.channels[team_id][0]
+ channel_id = channel.id
+
+ members = client.channels.list_members(
+ team_ref=data.teams[0].display_name,
+ channel_ref=channel.name,
+ )
+
+ expected_members = data.members[team_id][channel_id]
+
+ assert len(members) == len(expected_members)
+
+ assert members[0].user_id == expected_members[0].user_id
+ assert members[0].display_name == expected_members[0].display_name
+ expected_role_0 = "owner" if expected_members[0].role == "owner" else ""
+ assert members[0].role == expected_role_0
+ assert members[0].email == expected_members[0].email
+ assert members[0].id == expected_members[0].id
+
+ assert members[1].user_id == expected_members[1].user_id
+ assert members[1].display_name == expected_members[1].display_name
+ expected_role_1 = "owner" if expected_members[1].role == "owner" else ""
+ assert members[1].role == expected_role_1
+ assert members[1].email == expected_members[1].email
+ assert members[1].id == expected_members[1].id
+
+ finally:
+ client.close()
+
+
+def test_add_member_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ member = client.channels.add_member(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][1].name,
+ user_ref=data.newMemberTemplate.display_name,
+ is_owner=True if data.newMemberTemplate.role == "owner" else False,
+ )
+
+ assert member.user_id == data.newMemberTemplate.user_id
+ assert member.display_name == data.newMemberTemplate.display_name
+ assert member.role == data.newMemberTemplate.role
+ assert member.email == data.newMemberTemplate.email
+ assert member.id == data.newMemberTemplate.id
+
+ finally:
+ client.close()
+
+
+def test_update_member_role_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ member = client.channels.update_member_role(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ user_ref=data.members[data.teams[0].id][data.channels[data.teams[0].id][0].id][1].email,
+ is_owner=True
+ )
+
+ assert member.user_id == data.members[data.teams[0].id][data.channels[data.teams[0].id][0].id][1].user_id
+ assert member.display_name == data.members[data.teams[0].id][data.channels[data.teams[0].id][0].id][1].display_name
+ assert member.role == "owner"
+ assert member.email == data.members[data.teams[0].id][data.channels[data.teams[0].id][0].id][1].email
+ assert member.id == data.members[data.teams[0].id][data.channels[data.teams[0].id][0].id][1].id
+ finally:
+ client.close()
+
+
+def test_remove_member_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ success = client.channels.remove_member(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ user_ref=data.members[data.teams[0].id][data.channels[data.teams[0].id][0].id][1].email,
+ )
+
+ assert success is True
+
+ finally:
+ client.close()
+
+def test_get_mention_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ mention = client.channels.get_mentions(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ raw_mentions=MentionKind.TEAM.value,
+ )
+
+ assert mention[0].kind == MentionKind.TEAM
+ assert mention[0].at_id == 0
+ assert mention[0].text == data.teams[0].display_name
+ assert mention[0].target_id == data.teams[0].id
+
+ finally:
+ client.close()
+
+
+def test_search_messages_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+ data.searching = "channels"
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ results = client.channels.search_messages(
+ team_ref=data.teams[0].display_name,
+ channel_ref=data.channels[data.teams[0].id][0].name,
+ options=SearchMessagesOptions(
+ query="Hello",
+ ),
+ config=SearchConfig(
+ max_workers=4,
+ )
+ )
+
+ assert len(results.messages) == 1
+
+ assert results.messages[0].team_id == data.teams[0].id
+ assert results.messages[0].channel_id == data.channels[data.teams[0].id][0].id
+ assert results.messages[0].chat_id is None
+
+ assert results.messages[0].message.id == data.messages[results.messages[0].channel_id][2].id
+ assert results.messages[0].message.content == data.messages[results.messages[0].channel_id][2].content
+ assert results.messages[0].message.content_type == MessageContentType(data.messages[results.messages[0].channel_id][2].content_type)
+ assert results.messages[0].message.sender.user_id == data.messages[results.messages[0].channel_id][2].sender.user_id
+ assert results.messages[0].message.sender.display_name == data.messages[results.messages[0].channel_id][2].sender.display_name
+ assert results.messages[0].message.reply_count == data.messages[results.messages[0].channel_id][2].reply_count
+ assert results.messages[0].message.created_date_time == data.messages[results.messages[0].channel_id][0].created_date_time
+
+ finally:
+ client.close()
+
+
+
+
+
diff --git a/python/tests/test_chats.py b/python/tests/test_chats.py
new file mode 100644
index 0000000..d71275b
--- /dev/null
+++ b/python/tests/test_chats.py
@@ -0,0 +1,511 @@
+from teams_lib_pzsp2_z1.client import TeamsClient
+from tests.init_fake_client import init_fake_client
+from tests.fake_server.setup import setup_fake_server
+from teams_lib_pzsp2_z1.model.chat import ChatType, ChatRef
+from teams_lib_pzsp2_z1.model.message import MessageContentType, MessageBody
+from teams_lib_pzsp2_z1.model.mention import MentionKind
+from teams_lib_pzsp2_z1.model.search import SearchConfig, SearchMessagesOptions
+from datetime import datetime
+
+
+def test_list_my_group_chats_integration(httpserver):
+ """
+ Integration test: Python -> Go Binary -> Fake HTTP -> Python Mock Server
+ """
+
+ data = setup_fake_server(httpserver)
+
+ # Init fake client
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ chats = client.chats.list_my_joined(ChatType.GROUP)
+
+ assert len(chats) == len(data.group_chats)
+ assert chats[0].id == data.group_chats[0].id
+ assert chats[0].type == data.group_chats[0].type
+ assert chats[0].topic == data.group_chats[0].topic
+ assert chats[0].is_hidden == data.group_chats[0].is_hidden
+
+ assert chats[1].id == data.group_chats[1].id
+ assert chats[1].type == data.group_chats[1].type
+ assert chats[1].topic == data.group_chats[1].topic
+ assert chats[1].is_hidden == data.group_chats[1].is_hidden
+
+ finally:
+ client.close()
+
+def test_list_my_one_on_one_chats_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ chats = client.chats.list_my_joined(ChatType.ONE_ON_ONE)
+
+ assert len(chats) == len(data.oneonone_chats)
+ assert chats[0].id == data.oneonone_chats[0].id
+ assert chats[0].type == data.oneonone_chats[0].type
+ assert chats[0].topic == data.oneonone_chats[0].topic
+ assert chats[0].is_hidden == data.oneonone_chats[0].is_hidden
+
+ finally:
+ client.close()
+
+
+def test_create_group_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ chat = client.chats.create_group_chat(
+ topic=data.newChatTemplate.topic,
+ recipient_refs=[
+ data.users[0].email,
+ data.users[1].email,
+ ],
+ include_me=True,
+ )
+
+ assert chat.id == data.newChatTemplate.id
+ assert chat.type == ChatType.GROUP
+ assert chat.topic == data.newChatTemplate.topic
+ assert chat.is_hidden == data.newChatTemplate.is_hidden
+
+ finally:
+ client.close()
+
+def test_create_one_on_one_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ chat = client.chats.create_one_on_one(
+ recipient_ref=data.users[0].email,
+ )
+
+ assert chat.id == data.newChatTemplate.id
+ assert chat.type == ChatType.ONE_ON_ONE
+ assert chat.topic == None
+ assert chat.is_hidden == data.newChatTemplate.is_hidden
+
+ finally:
+ client.close()
+
+def test_get_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ chat = client.chats.get_chat(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ )
+ )
+
+ assert chat.id == data.group_chats[0].id
+ assert chat.type == data.group_chats[0].type
+ assert chat.topic == data.group_chats[0].topic
+ assert chat.is_hidden == data.group_chats[0].is_hidden
+
+ finally:
+ client.close()
+
+
+def test_list_group_chat_members_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ members = client.chats.list_group_chat_members(data.group_chats[0].topic)
+
+ assert len(members) == len(data.group_chat_members[data.group_chats[0].id])
+
+ assert members[0].id == data.group_chat_members[data.group_chats[0].id][0].id
+ assert members[0].user_id == data.group_chat_members[data.group_chats[0].id][0].user_id
+ assert members[0].display_name == data.group_chat_members[data.group_chats[0].id][0].display_name
+ if data.group_chat_members[data.group_chats[0].id][0].role == "owner":
+ assert members[0].role == "owner"
+ assert members[0].email == data.group_chat_members[data.group_chats[0].id][0].email
+
+ assert members[1].id == data.group_chat_members[data.group_chats[0].id][1].id
+ assert members[1].user_id == data.group_chat_members[data.group_chats[0].id][1].user_id
+ assert members[1].display_name == data.group_chat_members[data.group_chats[0].id][1].display_name
+ if data.group_chat_members[data.group_chats[0].id][1].role == "owner":
+ assert members[1].role == "owner"
+ assert members[1].email == data.group_chat_members[data.group_chats[0].id][1].email
+
+ finally:
+ client.close()
+
+
+def test_add_group_chat_member_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ member = client.chats.add_member_to_group_chat(
+ group_chat_ref=data.group_chats[0].topic,
+ user_ref=data.newMemberTemplate.email,
+ )
+
+ assert member.id == data.newMemberTemplate.id
+ assert member.user_id == data.newMemberTemplate.user_id
+ assert member.display_name == data.newMemberTemplate.display_name
+ assert member.role == data.newMemberTemplate.role
+ assert member.email == data.newMemberTemplate.email
+ finally:
+ client.close()
+
+
+def test_remove_group_chat_member_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ result = client.chats.remove_member_from_group_chat(
+ group_chat_ref=data.group_chats[0].topic,
+ member_ref=data.group_chat_members[data.group_chats[0].id][0].email,
+ )
+
+ assert result is True
+
+ finally:
+ client.close()
+
+
+def test_update_group_chat_topic_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ chat = client.chats.update_group_chat_topic(
+ group_chat_ref=data.group_chats[0].topic,
+ new_topic=data.updatedGroupChatTopic,
+ )
+
+ assert chat.id == data.group_chats[0].id
+ assert chat.type == data.group_chats[0].type
+ assert chat.topic == data.updatedGroupChatTopic
+ assert chat.is_hidden == data.group_chats[0].is_hidden
+
+ finally:
+ client.close()
+
+
+def test_list_messeges_in_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ collection = client.chats.list_messages(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ )
+ )
+
+ assert len(collection.messages) == len(data.chat_messages[data.group_chats[0].id])
+
+ assert collection.messages[0].id == data.chat_messages[data.group_chats[0].id][0].id
+ assert collection.messages[0].content == data.chat_messages[data.group_chats[0].id][0].content
+ assert collection.messages[0].content_type == MessageContentType(data.chat_messages[data.group_chats[0].id][0].content_type)
+ assert collection.messages[0].sender.user_id == data.chat_messages[data.group_chats[0].id][0].sender.user_id
+ assert collection.messages[0].sender.display_name == data.chat_messages[data.group_chats[0].id][0].sender.display_name
+ assert collection.messages[0].reply_count == data.chat_messages[data.group_chats[0].id][0].reply_count
+ assert collection.messages[0].created_date_time == data.chat_messages[data.group_chats[0].id][0].created_date_time
+
+ assert collection.messages[1].id == data.chat_messages[data.group_chats[0].id][1].id
+ assert collection.messages[1].content == data.chat_messages[data.group_chats[0].id][1].content
+ assert collection.messages[1].content_type == MessageContentType(data.chat_messages[data.group_chats[0].id][1].content_type)
+ assert collection.messages[1].sender.user_id == data.chat_messages[data.group_chats[0].id][1].sender.user_id
+ assert collection.messages[1].sender.display_name == data.chat_messages[data.group_chats[0].id][1].sender.display_name
+ assert collection.messages[1].reply_count == data.chat_messages[data.group_chats[0].id][1].reply_count
+ assert collection.messages[1].created_date_time == data.chat_messages[data.group_chats[0].id][1].created_date_time
+
+ finally:
+ client.close()
+
+def test_send_message_in_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ message = client.chats.send_message(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ ),
+ body=MessageBody(
+ content=data.newMessageTemplate.content,
+ content_type=MessageContentType(data.newMessageTemplate.content_type),
+ mentions=[],
+ )
+ )
+
+ assert message.id == data.newMessageTemplate.id
+ assert message.content == data.newMessageTemplate.content
+ assert message.content_type == MessageContentType(data.newMessageTemplate.content_type)
+ assert message.sender.user_id == data.newMessageTemplate.sender.user_id
+ assert message.sender.display_name == data.newMessageTemplate.sender.display_name
+ assert message.reply_count == data.newMessageTemplate.reply_count
+ assert message.created_date_time == data.newMessageTemplate.created_date_time
+
+ finally:
+ client.close()
+
+def test_delete_message_in_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ result = client.chats.delete_message(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ ),
+ message_id=data.chat_messages[data.group_chats[0].id][0].id,
+ )
+
+ assert result is True
+
+ finally:
+ client.close()
+
+def test_get_message_in_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ message = client.chats.get_message(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ ),
+ message_id=data.chat_messages[data.group_chats[0].id][0].id,
+ )
+
+ assert message.id == data.chat_messages[data.group_chats[0].id][0].id
+ assert message.content == data.chat_messages[data.group_chats[0].id][0].content
+ assert message.content_type == MessageContentType(data.chat_messages[data.group_chats[0].id][0].content_type)
+ assert message.sender.user_id == data.chat_messages[data.group_chats[0].id][0].sender.user_id
+ assert message.sender.display_name == data.chat_messages[data.group_chats[0].id][0].sender.display_name
+ assert message.reply_count == data.chat_messages[data.group_chats[0].id][0].reply_count
+ assert message.created_date_time == data.chat_messages[data.group_chats[0].id][0].created_date_time
+ finally:
+ client.close()
+
+
+def test_get_all_messages_in_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ messages = client.chats.list_all_messages(
+ start_time=datetime.min,
+ end_time=datetime.max,
+ top=50,
+ )
+
+ assert len(messages) == len(data.chat_messages[data.group_chats[0].id])
+
+ assert messages[0].id == data.chat_messages[data.group_chats[0].id][0].id
+ assert messages[0].content == data.chat_messages[data.group_chats[0].id][0].content
+ assert messages[0].content_type == MessageContentType(data.chat_messages[data.group_chats[0].id][0].content_type)
+ assert messages[0].sender.user_id == data.chat_messages[data.group_chats[0].id][0].sender.user_id
+ assert messages[0].sender.display_name == data.chat_messages[data.group_chats[0].id][0].sender.display_name
+ assert messages[0].reply_count == 0
+ assert messages[0].created_date_time == data.chat_messages[data.group_chats[0].id][0].created_date_time
+
+ assert messages[1].id == data.chat_messages[data.group_chats[0].id][1].id
+ assert messages[1].content == data.chat_messages[data.group_chats[0].id][1].content
+ assert messages[1].content_type == MessageContentType(data.chat_messages[data.group_chats[0].id][1].content_type)
+ assert messages[1].sender.user_id == data.chat_messages[data.group_chats[0].id][1].sender.user_id
+ assert messages[1].sender.display_name == data.chat_messages[data.group_chats[0].id][1].sender.display_name
+ assert messages[1].reply_count == 0
+ assert messages[1].created_date_time == data.chat_messages[data.group_chats[0].id][1].created_date_time
+
+ finally:
+ client.close()
+
+
+def test_list_pinned_messages_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ messages = client.chats.list_pinned_messages(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ )
+ )
+
+ assert len(messages) == len(data.chat_messages[data.group_chats[0].id])
+
+ assert messages[0].id == data.chat_messages[data.group_chats[0].id][0].id
+ assert messages[0].content == data.chat_messages[data.group_chats[0].id][0].content
+ assert messages[0].content_type == MessageContentType(data.chat_messages[data.group_chats[0].id][0].content_type)
+ assert messages[0].sender.user_id == data.chat_messages[data.group_chats[0].id][0].sender.user_id
+ assert messages[0].sender.display_name == data.chat_messages[data.group_chats[0].id][0].sender.display_name
+ assert messages[0].reply_count == 0
+ assert messages[0].created_date_time == data.chat_messages[data.group_chats[0].id][0].created_date_time
+
+ assert messages[1].id == data.chat_messages[data.group_chats[0].id][1].id
+ assert messages[1].content == data.chat_messages[data.group_chats[0].id][1].content
+ assert messages[1].content_type == MessageContentType(data.chat_messages[data.group_chats[0].id][1].content_type)
+ assert messages[1].sender.user_id == data.chat_messages[data.group_chats[0].id][1].sender.user_id
+ assert messages[1].sender.display_name == data.chat_messages[data.group_chats[0].id][1].sender.display_name
+ assert messages[1].reply_count == 0
+ assert messages[1].created_date_time == data.chat_messages[data.group_chats[0].id][1].created_date_time
+
+ finally:
+ client.close()
+
+
+def test_pin_message_in_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ result = client.chats.pin_message(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ ),
+ message_id=data.chat_messages[data.group_chats[0].id][0].id,
+ )
+
+ assert result is True
+
+ finally:
+ client.close()
+
+
+def test_unpin_message_in_chat_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ result = client.chats.unpin_message(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ ),
+ message_id=data.chat_messages[data.group_chats[0].id][0].id,
+ )
+
+ assert result is True
+
+ finally:
+ client.close()
+
+
+def test_get_mention_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ mention = client.chats.get_mentions(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ ),
+ raw_mentions=MentionKind.EVERYONE.value,
+ )
+
+ assert mention[0].kind == MentionKind.EVERYONE.value
+ assert mention[0].at_id == 0
+ assert mention[0].text == "Everyone"
+ assert mention[0].target_id == data.group_chats[0].id
+
+ finally:
+ client.close()
+
+def test_search_messages_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ results = client.chats.search_messages(
+ chat_ref=ChatRef(
+ ref=data.group_chats[0].topic,
+ type=ChatType.GROUP,
+ ),
+ options=SearchMessagesOptions(
+ query="Hello"
+ ),
+ config=SearchConfig(
+ max_workers=4,
+ )
+ )
+
+ assert len(results.messages) == 1
+
+ assert results.messages[0].chat_id == data.group_chats[0].id
+
+ assert results.messages[0].message.id == data.chat_messages[data.group_chats[0].id][0].id
+ assert results.messages[0].message.content == data.chat_messages[data.group_chats[0].id][0].content
+ assert results.messages[0].message.content_type == MessageContentType(data.chat_messages[data.group_chats[0].id][0].content_type)
+ assert results.messages[0].message.sender.user_id == data.chat_messages[data.group_chats[0].id][0].sender.user_id
+ assert results.messages[0].message.sender.display_name == data.chat_messages[data.group_chats[0].id][0].sender.display_name
+ assert results.messages[0].message.reply_count == data.chat_messages[data.group_chats[0].id][0].reply_count
+ assert results.messages[0].message.created_date_time == data.chat_messages[data.group_chats[0].id][0].created_date_time
+
+ finally:
+ client.close()
diff --git a/python/tests/test_teams.py b/python/tests/test_teams.py
new file mode 100644
index 0000000..be19e85
--- /dev/null
+++ b/python/tests/test_teams.py
@@ -0,0 +1,196 @@
+from teams_lib_pzsp2_z1.model.team import UpdateTeam
+from teams_lib_pzsp2_z1.client import TeamsClient
+from tests.init_fake_client import init_fake_client
+from tests.fake_server.setup import setup_fake_server
+
+
+def test_list_my_teams_integration(httpserver):
+ """
+ Integration test: Python -> Go Binary -> Fake HTTP -> Python Mock Server
+ """
+
+ data = setup_fake_server(httpserver)
+
+ # Init fake client
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ teams = client.teams.list_my_joined()
+
+ assert len(teams) == len(data.teams)
+
+ assert teams[0].display_name == data.teams[0].display_name
+ assert teams[0].id == data.teams[0].id
+ assert teams[0].description == data.teams[0].description
+ assert teams[0].is_archived == data.teams[0].is_archived
+ assert teams[0].visibility == data.teams[0].visibility
+
+ assert teams[1].display_name == data.teams[1].display_name
+ assert teams[1].id == data.teams[1].id
+ assert teams[1].description == data.teams[1].description
+ assert teams[1].is_archived == data.teams[1].is_archived
+ assert teams[1].visibility == data.teams[1].visibility
+
+ finally:
+ client.close()
+
+def test_get_team_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ team = client.teams.get(data.teams[0].display_name)
+
+ print(data.get_team_response(data.teams[0].id))
+
+ assert team.display_name == data.teams[0].display_name
+ assert team.id == data.teams[0].id
+ assert team.description == data.teams[0].description
+ assert team.is_archived == data.teams[0].is_archived
+ assert team.visibility == data.teams[0].visibility
+
+ finally:
+ client.close()
+
+
+def test_update_team_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ updated_description = "Updated team description"
+ team = client.teams.update(
+ team_ref=data.teams[0].display_name,
+ update=UpdateTeam(
+ description=updated_description,
+ ),
+ )
+
+ assert team.display_name == data.teams[0].display_name
+ assert team.id == data.teams[0].id
+ assert team.description == updated_description
+ assert team.is_archived == data.teams[0].is_archived
+ assert team.visibility == data.teams[0].visibility
+
+ finally:
+ client.close()
+
+
+def test_create_team_via_group_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ team = client.teams.create_via_group(
+ display_name=data.newTeamName,
+ mail_nickname=data.newGroupMailNickname,
+ visibility=data.newTeamVisibility,
+ )
+
+ assert team.id == data.newGroupID
+ assert team.display_name == data.newTeamName
+ assert team.visibility == data.newTeamVisibility
+
+ finally:
+ client.close()
+
+
+def test_archive_team_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ success = client.teams.archive(
+ team_ref=data.teams[0].display_name,
+ spo_read_only_from_members=True,
+ )
+
+ assert success is True
+
+ finally:
+ client.close()
+
+
+def test_unarchive_team_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ success = client.teams.unarchive(
+ team_ref=data.teams[2].display_name,
+ )
+
+ assert success is True
+
+ finally:
+ client.close()
+
+
+def test_delete_team_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ success = client.teams.delete(
+ team_ref=data.teams[0].display_name,
+ )
+
+ assert success is True
+
+ finally:
+ client.close()
+
+
+def test_restore_deleted_team_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ msg = client.teams.restore_deleted(
+ deleted_team_ID=data.teams[2].id,
+ )
+
+ assert msg == data.teams[2].id
+
+ finally:
+ client.close()
+
+
+def test_error_handling_integration(httpserver):
+
+ data = setup_fake_server(httpserver)
+
+ client = TeamsClient(auto_init=False)
+ try:
+ init_fake_client(client, httpserver.url_for(""))
+
+ try:
+ client.teams.get("non-existent-team")
+ assert False, "Expected an exception for non-existent team"
+ except Exception as e:
+ assert "Go Error" in str(e)
+
+ finally:
+ client.close()
\ No newline at end of file