teams

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: MIT Imports: 14 Imported by: 0

README

teams-go

teams-go is a typed Go client for Microsoft Teams over Microsoft Graph. It owns Teams request shapes, identifiers, message models, and permission hints. msgraph-go owns HTTP, paging, retries, and Graph errors; callers supply authentication.

Use

graph, err := msgraph.New(auth.TokenSource("Chat.Read"))
if err != nil { return err }
client, err := teams.New(graph)
if err != nil { return err }

for message, err := range client.Messages(ctx, chatID, teams.MessageQuery{Top: 50}) {
    if err != nil { return err }
    fmt.Println(message.PlainText())
}

auth above can be an msauth-go.Authenticator; any msgraph.TokenSource works. Teams Graph endpoints require a work or school account.

Current API

Area Operations
Chats List and get chats; list members; read and send messages; react; soft-delete and restore messages
Teams and channels List joined teams and channels; read root messages and replies; post root messages and replies; list channel members
Search and presence Search messages; read own or another user's presence

Channel message listings contain roots only. ChannelMessageQuery.WithReplies includes the first page of each thread; follow Client.MoreReplies when RepliesNextLink is set. MessageQuery couples the filter and sort property because Graph otherwise silently discards mismatched filters.

Use Capabilities, ScopesFor, and NeedsAdminConsent before asking users for permissions. Several team, channel, and presence reads need tenant administrator consent. The SDK does not perform sign-in or grant permissions.

The SDK does not currently create chats or teams, manage memberships, download hosted content, or model meetings, calls, tabs, or Shifts. These are separate Graph areas with different contracts and permissions.

Develop

just ci

Tests use msgraph-go/graphmock to exercise service constraints and response shapes. A live tenant check remains necessary before claiming full service compatibility.

Documentation

Overview

Package teams is a Microsoft Teams SDK over Microsoft Graph.

It owns Teams semantics -- paths, request shapes, and the chat and channel message models -- and delegates transport, retries, paging, and error classification to msgraph-go. It does not do auth: build a github.com/albertocavalcante/msgraph-go.Client with a token source, then wrap it with New.

Three Graph behaviors shape the API. First, a $filter on a chat message listing is silently discarded unless $orderby names the same property -- no error, the whole unfiltered collection comes back instead. Second, messageType is an evolvable enum: without a Prefer header asking Graph to include unknown enum members, every system event message -- a member joining, a chat renamed, a call ending -- arrives as unknownFutureValue, which in a real conversation's history is a large minority of it. New sets that header on every request, with no option to turn it off. Third, from is null on system event messages: the event has no author in the ordinary sense, and code that assumes otherwise renders a blank sender for a message that was never really "from" anyone.

Index

Constants

View Source
const (
	// MaxBodyDepth bounds how many levels of tag nesting [ChatMessage.PlainText]
	// tracks before it stops counting further opens. Nesting past the bound
	// is still stripped like any other tag; only the counter itself saturates,
	// so a body built from a huge run of open tags cannot grow unbounded
	// internal state.
	MaxBodyDepth = 64
	// MaxBodyBytes bounds the rendered output of [ItemBody.Text] and
	// [ChatMessage.PlainText]. Content past the bound is truncated, never
	// rejected: a caller rendering a preview would rather see the first
	// megabyte of an oversized message than an error.
	MaxBodyBytes = 1 << 20
)

Bounds on how much of a message body this package will render as text.

View Source
const (
	// CodeBadRequest is what a chat or channel message listing answers for an
	// unsupported $orderby property, an ascending sort, or a $select/$expand
	// it does not serve. Unlike a mismatched $filter -- which the service
	// drops silently -- these shapes are genuinely refused.
	CodeBadRequest = "BadRequest"
	// CodeInvalidSearchRequest is what /search/query answers for a search
	// body missing entityTypes, or one that asks for sortProperties on a
	// chatMessage search, which the service will not sort.
	CodeInvalidSearchRequest = "invalidRequest"
)

Graph error codes Teams returns for conditions worth handling.

View Source
const (
	// MaxMessagePageSize is the largest page a chat or channel message
	// listing serves, whatever $top asks for. A request above it is not
	// refused; it is silently served this many.
	MaxMessagePageSize = 50
	// DefaultChannelMessagePageSize is what a channel message listing
	// returns when $top says nothing. It is well under
	// [MaxMessagePageSize], so leaving Top unset is not the same as asking
	// for the largest page the service will give.
	DefaultChannelMessagePageSize = 20
)
View Source
const (
	FieldCreatedDateTime      = "createdDateTime"
	FieldLastModifiedDateTime = "lastModifiedDateTime"
	// FieldLastMessagePreviewCreated is the property a chat listing orders by
	// when [ChatQuery.ByRecentMessage] is set. It lives one level into the
	// expanded lastMessagePreview, which is why [ChatQuery.Params] adds the
	// expansion automatically rather than leaving a caller to discover that
	// ordering by it silently does nothing without the expand.
	FieldLastMessagePreviewCreated = "lastMessagePreview/createdDateTime"
)

Message property paths, for naming a sort property without misspelling it into a request the service quietly answers wrong instead of rejecting; see MessageQuery in messages.go.

View Source
const MaxChatPageSize = 50

MaxChatPageSize is the largest page the chat listing serves, whatever $top asks for. A request above it is not refused; it is silently served this many.

View Source
const MaxSearchPageSize = 25

MaxSearchPageSize is the largest page a message search serves, whatever Size asks for.

Variables

View Source
var (
	ErrNilGraphClient = errors.New("teams: nil graph client")
	ErrEmptyID        = errors.New("teams: empty identifier")
	ErrEmptyBody      = errors.New("teams: request body has no properties set")
)

Errors returned when a call is malformed before it reaches Graph.

View Source
var ErrEmptySearchTerm = errors.New("teams: search term is empty")

ErrEmptySearchTerm means a SearchQuery carries no search term. The service accepts an empty queryString and answers with a search over nothing that reads as "no matches" rather than as a malformed request.

View Source
var ErrInvertedWindow = errors.New("teams: message window is inverted: Since is after Before")

ErrInvertedWindow means a MessageQuery's Since is after its Before.

The service does not refuse this shape: it answers 200 OK with an empty page, which reads exactly like a quiet chat rather than like a mistake. MessageQuery.Validate exists so the mistake is caught here, with a message that says which end was wrong, instead of silently as "nothing happened here."

Functions

func Granted

func Granted(scopes []string, capability Capability) bool

Granted reports whether the given scopes satisfy a capability.

This delegates to msgraph.ScopeSatisfies rather than a plain membership check, which is what lets a broad grant such as Group.ReadWrite.All stand in for ReadChannelMessages without this package having to repeat the nesting msgraph-go already maintains. Comparison is case-insensitive because the service echoes granted scopes back with inconsistent casing.

func IsForbidden

func IsForbidden(err error) bool

IsForbidden reports whether the caller lacks the permission the request needed, or the tenant has otherwise refused it outright.

func IsMessagingPolicyBlocked

func IsMessagingPolicyBlocked(err error) bool

IsMessagingPolicyBlocked reports whether a send was refused by a tenant messaging policy rather than by a missing permission.

Teams has no error code of its own for this: the response is a plain 403 Forbidden, the same status a missing scope produces, so status alone cannot tell the two apart. The distinguishing text lives in the message, which is why this checks for it -- best-effort, because Microsoft has not documented the message as stable. The distinction still matters: a missing scope is fixed by asking the user to consent to more; a messaging policy is a tenant administrator's choice that the signed-in user cannot act on no matter what they consent to, so retrying after a new sign-in only loops.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether the chat, channel, team, or message does not exist.

func IsReservedScope

func IsReservedScope(scope string) bool

IsReservedScope reports whether a scope is granted implicitly rather than requested.

func IsThrottled

func IsThrottled(err error) bool

IsThrottled reports whether Graph rate-limited the request.

func IsUnauthorized

func IsUnauthorized(err error) bool

IsUnauthorized reports whether the request carried no usable token.

func LeastPrivilegedScope

func LeastPrivilegedScope(capability Capability) (string, bool)

LeastPrivilegedScope returns the narrowest permission that satisfies a capability, which is what to ask for when adding one.

func MostPrivilegedScope

func MostPrivilegedScope(capability Capability) (string, bool)

MostPrivilegedScope returns the broadest permission that satisfies a capability.

It exists because the identity service does not treat permissions as nested. Holding Chat.ReadWrite does not make Chat.Read obtainable: they are unrelated strings, and asking for one the user never consented to is refused outright. A caller that asked for the least privileged scope and was refused should try this before concluding the capability is unavailable, since consenting by capability commonly grants only the broader form.

func NeedsAdminConsent

func NeedsAdminConsent(capability Capability) bool

NeedsAdminConsent reports whether the least privileged scope that satisfies a capability can only be granted by a tenant administrator.

Every Outlook mail permission is user-consentable: a caller missing one can always fix it by signing in again and accepting a consent screen. Half of the Teams permissions are not -- Team.ReadBasic.All, Channel.ReadBasic.All, ChannelMessage.Read.All, and Presence.Read.All are tenant-wide by construction, and a signed-in user is never offered them regardless of how many times they retry sign-in. Telling a caller to grant one is advice they cannot act on, and a caller that treats every missing capability the same way loops forever against a consent screen that will never offer it.

func Require

func Require(scopes []string, capability Capability) error

Require reports an error when the given scopes do not satisfy a capability. It is the check to make before a request rather than after a refusal.

func ScopesFor

func ScopesFor(capability Capability) []string

ScopesFor returns the delegated permissions that satisfy a capability. Any one of them is sufficient; the first is the least privileged.

func ScopesToRequest

func ScopesToRequest(granted []string, capabilities ...Capability) []string

ScopesToRequest returns the scopes to ask for so that the given capabilities are all satisfied, keeping any already granted and adding the least privileged permission for each that is not.

The result is deduplicated and stably ordered, so re-running it produces the same sign-in line rather than a reshuffled one. The granted argument must be what consent actually covers, not the scopes a client is configured with. Passing the configuration instead makes this add the least-privileged scope for a capability the user has already granted in its broader form -- a permission that was never consented to, which the identity service refuses.

Types

type Activity

type Activity string

Activity is a presence activity, such as "InACall" or "Presenting". It is a plain string rather than a closed enum: the service documents this set as subject to change, and an unrecognized value should still round-trip rather than collapse to nothing.

type Availability

type Availability string

Availability is a presence availability state.

These values are Capitalized -- "Available", not "available" -- unlike every other enum in this package, all of which the service spells in camelCase. That is the service's own spelling for this one resource, not an inconsistency this package introduced, and comparing a decoded value against a lowercased literal shows up as "nobody is ever available," silently, since the comparison simply never matches.

const (
	AvailabilityAvailable     Availability = "Available"
	AvailabilityAvailableIdle Availability = "AvailableIdle"
	AvailabilityAway          Availability = "Away"
	AvailabilityBeRightBack   Availability = "BeRightBack"
	AvailabilityBusy          Availability = "Busy"
	AvailabilityBusyIdle      Availability = "BusyIdle"
	AvailabilityDoNotDisturb  Availability = "DoNotDisturb"
	AvailabilityOffline       Availability = "Offline"
	AvailabilityUnknown       Availability = "PresenceUnknown"
)

type BodyType

type BodyType string

BodyType is the content type of an ItemBody.

const (
	BodyTypeText BodyType = "text"
	BodyTypeHTML BodyType = "html"
)

type Capability

type Capability string

Capability is something this client can be asked to do.

Every operation in this package documents the capability it needs, and the capability names the delegated permissions that satisfy it. This exists because the service is unhelpful about permissions: a request made without the right one is refused with a bare error that names nothing, so there is no way to learn from the failure what to ask for. Checking beforehand is the only way to give an answer.

const (
	// ReadIdentity reads the signed-in user's own profile.
	ReadIdentity Capability = "read your profile"
	// ListChats lists chats and their members.
	ListChats Capability = "list chats"
	// ReadChatMessages reads messages in a chat.
	ReadChatMessages Capability = "read chat messages"
	// SendChatMessage sends a chat message.
	SendChatMessage Capability = "send a chat message"
	// ChangeChat reacts to or soft-deletes a chat message.
	ChangeChat Capability = "change chat messages"
	// ListTeams lists the teams the signed-in user belongs to.
	ListTeams Capability = "list your teams"
	// ListChannels lists a team's channels.
	ListChannels Capability = "list channels"
	// ReadChannelMessages reads channel messages and their replies.
	ReadChannelMessages Capability = "read channel messages"
	// SendChannelMessage posts to a channel.
	SendChannelMessage Capability = "send a channel message"
	// SearchMessages runs a full-text message search.
	SearchMessages Capability = "search messages"
	// ReadOwnPresence reads the signed-in user's own presence.
	ReadOwnPresence Capability = "read your presence"
	// ReadPresence reads another user's presence.
	ReadPresence Capability = "read others' presence"
)

func Capabilities

func Capabilities() []Capability

Capabilities lists every capability, narrowest first.

func MissingFor

func MissingFor(scopes []string, capabilities ...Capability) []Capability

MissingFor returns the capabilities the given scopes do not satisfy, in a stable order.

func UserConsentable

func UserConsentable() []Capability

UserConsentable returns the capabilities a signed-in user can grant by consenting, in the stable Capabilities order. It excludes anything NeedsAdminConsent reports true for.

type Channel

type Channel struct {
	ID          ChannelID `json:"id,omitempty"`
	DisplayName string    `json:"displayName,omitempty"`
	Description string    `json:"description,omitempty"`
	// Email is expensive for the service to populate. See [ChannelQuery.Select].
	Email               string            `json:"email,omitempty"`
	WebURL              string            `json:"webUrl,omitempty"`
	Membership          ChannelMembership `json:"membershipType,omitempty"`
	CreatedDateTime     time.Time         `json:"createdDateTime,omitzero"`
	IsArchived          bool              `json:"isArchived,omitempty"`
	IsFavoriteByDefault bool              `json:"isFavoriteByDefault,omitempty"`
	TenantID            string            `json:"tenantId,omitempty"`
}

Channel is a channel within a team.

Membership matters beyond description: a private or shared channel keeps its messages on a back end the team's own message endpoints do not reach. Client.ChannelMessages against one of these returns an empty listing, not a permission error, which reads as "no messages yet" rather than "you are looking in the wrong place."

type ChannelID

type ChannelID string

ChannelID identifies a channel within a team.

func (ChannelID) String

func (i ChannelID) String() string

type ChannelIdentity

type ChannelIdentity struct {
	TeamID    TeamID    `json:"teamId,omitempty"`
	ChannelID ChannelID `json:"channelId,omitempty"`
}

ChannelIdentity names the team and channel a channel message belongs to.

type ChannelMembership

type ChannelMembership string

ChannelMembership is Graph's evolvable channelMembershipType enum.

const (
	ChannelStandard           ChannelMembership = "standard"
	ChannelPrivate            ChannelMembership = "private"
	ChannelShared             ChannelMembership = "shared"
	ChannelUnknownFutureValue ChannelMembership = "unknownFutureValue"
)

type ChannelMessageQuery

type ChannelMessageQuery struct {
	MessageQuery
	// WithReplies expands each root message's first page of replies. This is
	// the one $expand a channel message listing accepts; every other
	// $select or $expand is refused with 400, same as on a chat message
	// listing. A thread longer than the expansion's cap is truncated with a
	// per-message continuation link on the message itself -- see
	// [Client.MoreReplies].
	WithReplies bool
}

ChannelMessageQuery selects, windows, and orders a channel message listing.

func (ChannelMessageQuery) Params

func (q ChannelMessageQuery) Params() msgraph.Params

Params renders the query as Graph query parameters.

type ChannelQuery

type ChannelQuery struct {
	// Select limits the properties Graph returns. Excluding "email" is worth
	// doing deliberately: the service computes it from the channel's
	// underlying group mailbox, which costs measurably more than the rest of
	// the channel's properties combined, and a listing rendering only names
	// and descriptions does not need to pay for it.
	Select []string
	// Where is a typed $filter.
	Where msgraph.Expr
	// Custom carries query parameters this package does not model.
	Custom url.Values
}

ChannelQuery selects and filters a channel listing.

Unlike a chat or channel message listing, a channel listing genuinely supports $filter and $select, which is why -- unlike MessageQuery and ChatQuery -- this has a Select and a Where field instead of a closed set of booleans.

func (ChannelQuery) Params

func (q ChannelQuery) Params() msgraph.Params

Params renders the query as Graph query parameters.

type Chat

type Chat struct {
	ID ChatID `json:"id,omitempty"`
	// Topic is null for a one-on-one chat: it has no name of its own. See
	// [Chat.DisplayName].
	Topic               string    `json:"topic,omitempty"`
	Type                ChatType  `json:"chatType,omitempty"`
	CreatedDateTime     time.Time `json:"createdDateTime,omitzero"`
	LastUpdatedDateTime time.Time `json:"lastUpdatedDateTime,omitzero"`
	WebURL              string    `json:"webUrl,omitempty"`
	TenantID            string    `json:"tenantId,omitempty"`
	// Viewpoint is the signed-in user's own read state, populated only when
	// requested. See [Chat.Unread].
	Viewpoint *ChatViewpoint `json:"viewpoint,omitempty"`
	// LastMessagePreview is populated only when [ChatQuery.WithLastMessagePreview]
	// or [ChatQuery.ByRecentMessage] was set.
	LastMessagePreview *MessagePreview `json:"lastMessagePreview,omitempty"`
	// Members is populated only when [ChatQuery.WithMembers] was set, and is
	// truncated at 25 with no continuation link; see [Client.ChatMembers]
	// for a chat that might have more.
	Members           []ConversationMember `json:"members,omitempty"`
	OnlineMeetingInfo *OnlineMeetingInfo   `json:"onlineMeetingInfo,omitempty"`
}

Chat is a one-on-one, group, or meeting chat.

func (Chat) DisplayName

func (c Chat) DisplayName(self UserID) string

DisplayName returns a name for the chat: Topic when the service sent one, otherwise -- for a one-on-one chat, whose Topic is always null -- the other participant's display name, found by excluding self from Members.

Members is truncated at 25 with no continuation link (see Client.ChatMembers), which does not affect a one-on-one chat -- it only ever has two members -- but does mean this cannot reliably name "the other participants" for a large group chat with no topic of its own; it only ever looks at what ChatQuery.WithMembers already fetched.

func (Chat) Unread

func (c Chat) Unread() (unread, known bool)

Unread reports whether the chat has a message newer than the signed-in user's last read mark.

known is false when either half of that comparison was never asked for: LastMessagePreview was not expanded, or the chat was never opened (a nil ChatViewpoint.LastMessageReadDateTime). Reporting "read" in either case would mark every chat in a listing that simply did not request the preview as read, which is a worse answer than admitting the question cannot be answered from what was fetched.

type ChatID

type ChatID string

ChatID identifies a chat.

func (ChatID) String

func (i ChatID) String() string

type ChatMessage

type ChatMessage struct {
	ID MessageID `json:"id,omitempty"`
	// ETag is the message's version tag, exposed on the wire as @odata.etag.
	ETag                 ETag        `json:"@odata.etag,omitempty"`
	MessageType          MessageType `json:"messageType,omitempty"`
	CreatedDateTime      time.Time   `json:"createdDateTime,omitzero"`
	LastModifiedDateTime time.Time   `json:"lastModifiedDateTime,omitzero"`
	// LastEditedDateTime is set only when a human edited the message after
	// posting it, which is what makes it the right field for
	// [ChatMessage.Edited]. LastModifiedDateTime is the wrong one to use for
	// that: it also moves when someone reacts to the message, so a message
	// nobody has touched would misreport as edited the first time it gets a
	// thumbs up.
	LastEditedDateTime *time.Time `json:"lastEditedDateTime,omitempty"`
	// DeletedDateTime is set when the message was soft-deleted. The message
	// still exists at this ID with its body cleared by the service, rather
	// than the resource disappearing.
	DeletedDateTime *time.Time `json:"deletedDateTime,omitempty"`

	// From is null on a system event message: nothing sent it, the service
	// generated it. Dereferencing this unconditionally is the most common way
	// a Teams client renders a blank sender for a membership change instead
	// of recognizing the message as an event. See [ChatMessage.Sender] and
	// [ChatMessage.IsSystemEvent].
	From *IdentitySet `json:"from,omitempty"`
	Body *ItemBody    `json:"body,omitempty"`

	Subject string `json:"subject,omitempty"`
	Summary string `json:"summary,omitempty"`

	Mentions    []Mention           `json:"mentions,omitempty"`
	Reactions   []Reaction          `json:"reactions,omitempty"`
	Attachments []MessageAttachment `json:"attachments,omitempty"`

	Importance Importance `json:"importance,omitempty"`
	Locale     string     `json:"locale,omitempty"`
	WebURL     string     `json:"webUrl,omitempty"`

	// EventDetail is set only on a system event message, whichever
	// MessageType it decoded to. [ChatMessage.IsSystemEvent] checks this
	// rather than MessageType, which is what keeps it correct even when a
	// proxy strips the Prefer header this package always sends and the type
	// arrives as [MessageTypeUnknownFutureValue] instead.
	EventDetail *EventDetail `json:"eventDetail,omitempty"`

	ChatID          ChatID          `json:"chatId,omitempty"`
	ChannelIdentity ChannelIdentity `json:"channelIdentity,omitempty"`
	ReplyToID       MessageID       `json:"replyToId,omitempty"`

	// Replies is populated only when the request expanded them. An empty
	// slice here does not mean the message has no replies; it means they were
	// not asked for.
	Replies []ChatMessage `json:"replies,omitempty"`
	// RepliesNextLink continues a truncated, expanded replies list. It is a
	// second pagination nested inside the parent collection's own nextLink; a
	// caller following only the outer link truncates every long thread
	// silently, with no error and no gap it can detect.
	RepliesNextLink string `json:"replies@odata.nextLink,omitempty"`
}

ChatMessage is a message in a chat or a channel.

Graph omits any property that was not requested or expanded, so a field being zero can mean either "empty" or "not populated for this request".

func (ChatMessage) Deleted

func (m ChatMessage) Deleted() bool

Deleted reports whether the message was soft-deleted. It still exists at this ID; only its content is gone.

func (ChatMessage) Edited

func (m ChatMessage) Edited() bool

Edited reports whether a human edited the message after posting it. See the field comment on ChatMessage.LastEditedDateTime for why this is not LastModifiedDateTime.

func (ChatMessage) InlineImages

func (m ChatMessage) InlineImages() []HostedContentRef

InlineImages returns the hosted-content images referenced by <img> tags in the message body, in document order.

func (ChatMessage) IsSystemEvent

func (m ChatMessage) IsSystemEvent() bool

IsSystemEvent reports whether the message is service-generated rather than authored. It checks EventDetail rather than MessageType, so it stays correct even when a proxy strips the Prefer header this package always sends and the type decodes to MessageTypeUnknownFutureValue instead of MessageTypeSystemEventMessage.

func (ChatMessage) Mine

func (m ChatMessage) Mine(self UserID) bool

Mine reports whether self authored the message. It is false for a system event message and false when self is empty, rather than matching against a sender the caller did not actually ask about.

func (ChatMessage) PlainText

func (m ChatMessage) PlainText() string

PlainText renders the message body as text, resolving <at id="N"> mentions against m.Mentions, converting <br>, <p>, and <div> to newlines, replacing an inline image with a bracketed marker, and stripping every other tag. It does no I/O and is a pure function of m: everything it needs is already in the value.

func (ChatMessage) Sender

func (m ChatMessage) Sender() (Identity, bool)

Sender returns the message's author, reporting false for a system event message, which has none.

type ChatQuery

type ChatQuery struct {
	// WithMembers expands the chat's members, truncated at 25 with no
	// continuation link. See [Client.ChatMembers] for a chat that might have
	// more.
	WithMembers bool
	// WithLastMessagePreview expands the chat's most recent message.
	WithLastMessagePreview bool
	// ByRecentMessage orders chats by their most recent message, newest
	// first, automatically adding the lastMessagePreview expansion -- see
	// [ChatQuery.Params] for why that addition is not optional.
	ByRecentMessage bool
	// Top asks for a page size, clamped to [MaxChatPageSize].
	Top int
	// Custom carries query parameters this package does not model.
	Custom url.Values
}

ChatQuery selects and expands a chat listing.

There is no Select, Where, or Sort field. The service serves exactly one ordering -- most recently updated, once ByRecentMessage asks for it -- and exactly two expansions, so those are booleans rather than an open-ended filter or sort a caller could aim at a property the service does not index.

func (ChatQuery) Params

func (q ChatQuery) Params() msgraph.Params

Params renders the query as Graph query parameters.

ByRecentMessage always adds the lastMessagePreview expansion, whether or not WithLastMessagePreview was also set. The service refuses to order a chat listing by a property inside an expansion it was not asked to perform -- ordering by FieldLastMessagePreviewCreated without expanding lastMessagePreview answers with an error the caller has no way to spell a request around, short of always remembering to set both fields together. Folding the expansion into ByRecentMessage removes that footgun: asking to sort by recent message is, by construction, also asking for the data the sort needs.

type ChatType

type ChatType string

ChatType is Graph's evolvable chatType enum.

const (
	ChatTypeOneOnOne           ChatType = "oneOnOne"
	ChatTypeGroup              ChatType = "group"
	ChatTypeMeeting            ChatType = "meeting"
	ChatTypeUnknownFutureValue ChatType = "unknownFutureValue"
)

type ChatViewpoint

type ChatViewpoint struct {
	IsHidden bool `json:"isHidden,omitempty"`
	// LastMessageReadDateTime is a pointer because "never opened this chat"
	// and "opened it at the zero time" are different facts. Comparing a
	// plain [time.Time] against a message's CreatedDateTime cannot tell them
	// apart; a nil here can.
	LastMessageReadDateTime *time.Time `json:"lastMessageReadDateTime,omitempty"`
}

ChatViewpoint is the signed-in user's own view of a chat: what nobody else's copy of the same chat resource would show.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a Microsoft Teams client over Microsoft Graph.

It owns Teams semantics -- paths, request shapes, and models -- and delegates transport, retries, paging, and error classification to msgraph.Client.

func New

func New(graph *msgraph.Client, opts ...Option) (*Client, error)

New creates a Teams client over an existing Graph client.

It always appends msgraph.PreferIncludeUnknownEnumMembers to the Prefer directives sent with every request, and there is no option to turn this off. Without it, every system event message -- a member joining, a chat renamed, a call ending -- decodes to MessageTypeUnknownFutureValue instead of MessageTypeSystemEventMessage, silently and with no error; in a real chat's history that is a large minority of it. There is no legitimate reason to want that, so it is not a choice this package offers.

func (*Client) ChannelMembers

func (c *Client) ChannelMembers(ctx context.Context, team TeamID, channel ChannelID) iter.Seq2[ConversationMember, error]

ChannelMembers iterates a channel's members directly, following pagination, for the same reason and with the same truncation hazard as Client.ChatMembers.

Needs ListChannels.

func (*Client) ChannelMessages

func (c *Client) ChannelMessages(ctx context.Context, team TeamID, channel ChannelID, query ChannelMessageQuery) iter.Seq2[ChatMessage, error]

ChannelMessages iterates a channel's messages, following pagination.

This returns ROOT messages only, at any page depth -- however far a caller pages, no reply ever appears in this collection, whether or not ChannelMessageQuery.WithReplies was set. A transcript built from this alone shows every conversation opener and none of the conversation. Read a thread with Client.Replies and Client.MoreReplies, or expand the first page of each thread inline with WithReplies.

Needs ReadChannelMessages.

func (*Client) Channels

func (c *Client) Channels(ctx context.Context, team TeamID, query ChannelQuery) iter.Seq2[Channel, error]

Channels iterates a team's channels.

Needs ListChannels.

func (*Client) ChatMembers

func (c *Client) ChatMembers(ctx context.Context, chat ChatID) iter.Seq2[ConversationMember, error]

ChatMembers iterates a chat's members directly, following pagination.

This exists because $expand=members on Client.GetChat or Client.Chats truncates at 25 members with no continuation link -- see ChatQuery.WithMembers. A chat reporting exactly 25 members from an expansion almost certainly has more; this endpoint is the only way to read them all. Member ids returned here are opaque strings scoped to the chat; do not parse them.

Needs ListChats.

func (*Client) Chats

func (c *Client) Chats(ctx context.Context, query ChatQuery) iter.Seq2[Chat, error]

Chats iterates the signed-in user's chats, following pagination.

Needs ListChats.

func (*Client) GetChannel

func (c *Client) GetChannel(ctx context.Context, team TeamID, channel ChannelID) (Channel, *msgraph.Response, error)

GetChannel fetches a single channel.

Needs ListChannels.

func (*Client) GetChat

func (c *Client) GetChat(ctx context.Context, chat ChatID, query ChatQuery) (Chat, *msgraph.Response, error)

GetChat fetches a single chat.

Needs ListChats.

func (*Client) GetMessage

func (c *Client) GetMessage(ctx context.Context, chat ChatID, message MessageID) (ChatMessage, *msgraph.Response, error)

GetMessage fetches a single chat message.

There is no query parameter, unlike Client.GetChat or Client.Messages: $select and $expand are unsupported on a message resource just as they are on the listing, so a query argument here would only let a caller build a request the service answers with 400.

Needs ReadChatMessages.

func (*Client) GetTeam

func (c *Client) GetTeam(ctx context.Context, team TeamID) (Team, *msgraph.Response, error)

GetTeam fetches a single team, populating every property -- including Visibility, InternalID, and WebURL, which a value from Client.JoinedTeams leaves null.

Needs ListTeams.

func (*Client) Graph

func (c *Client) Graph() *msgraph.Client

Graph returns the underlying Graph client for calls this package does not model.

func (*Client) JoinedTeams

func (c *Client) JoinedTeams(ctx context.Context) iter.Seq2[Team, error]

JoinedTeams iterates the teams the signed-in user belongs to.

GET /me/joinedTeams accepts no OData query options at all -- no $select, $filter, $orderby, or $expand -- which is why this takes no query parameter, unlike every other listing in this package.

The listing also populates only id, displayName, description, isArchived, and tenantId. Every other property, including Visibility, InternalID, and WebURL, comes back null. Reading Visibility off a value from this iterator sees the empty string and, if compared against TeamVisibilityPublic or read as "falsy", concludes the team is public -- a different claim from "this property was never loaded." Call Client.GetTeam on the team's ID to read the rest.

Needs ListTeams.

func (*Client) ListChats

func (c *Client) ListChats(ctx context.Context, query ChatQuery) (msgraph.Page[Chat], *msgraph.Response, error)

ListChats returns one page of chats plus the msgraph.Response metadata. Use Client.Chats to page automatically.

Needs ListChats.

func (*Client) ListJoinedTeams

func (c *Client) ListJoinedTeams(ctx context.Context) (msgraph.Page[Team], *msgraph.Response, error)

ListJoinedTeams returns one page of joined teams plus the msgraph.Response metadata. Use Client.JoinedTeams to page automatically. See Client.JoinedTeams for which properties are actually populated.

Needs ListTeams.

func (*Client) ListMessages

func (c *Client) ListMessages(ctx context.Context, chat ChatID, query MessageQuery) (msgraph.Page[ChatMessage], *msgraph.Response, error)

ListMessages returns one page of a chat's messages plus the msgraph.Response metadata. Use Client.Messages to page automatically.

Needs ReadChatMessages.

func (*Client) Messages

func (c *Client) Messages(ctx context.Context, chat ChatID, query MessageQuery) iter.Seq2[ChatMessage, error]

Messages iterates a chat's messages, following pagination.

Needs ReadChatMessages.

func (*Client) MoreReplies

func (c *Client) MoreReplies(ctx context.Context, message ChatMessage) iter.Seq2[ChatMessage, error]

MoreReplies continues a truncated, expanded replies list.

ChatMessage.RepliesNextLink is a second pagination nested inside the message collection's own @odata.nextLink: the outer link pages root messages and says nothing about a reply thread that did not fit in the expansion. Nothing in Client.ChannelMessages's own iteration follows it, so a caller reading only the outer link truncates every long thread silently, mid-argument, with no error and no gap it can detect.

This follows the link directly with Client.Graph, which is safe because msgraph-go pins requests to the configured Graph host regardless of what an absolute link names. A message with no RepliesNextLink yields nothing, not an error: there is nothing more to fetch.

Needs ReadChannelMessages.

func (*Client) MyPresence

func (c *Client) MyPresence(ctx context.Context) (Presence, *msgraph.Response, error)

MyPresence reads the signed-in user's own presence.

Needs ReadOwnPresence.

func (*Client) Replies

func (c *Client) Replies(ctx context.Context, team TeamID, channel ChannelID, message MessageID, query MessageQuery) iter.Seq2[ChatMessage, error]

Replies iterates the replies to a channel message, following pagination.

The reply thread is one level deep: a reply's own id cannot be passed here to read further replies, because there are none -- see Client.ReplyToChannelMessage.

Needs ReadChannelMessages.

func (*Client) ReplyToChannelMessage

func (c *Client) ReplyToChannelMessage(ctx context.Context, team TeamID, channel ChannelID, message MessageID, draft Draft) (ChatMessage, *msgraph.Response, error)

ReplyToChannelMessage posts a reply to a root channel message.

The thread is one level deep. Posting to a reply's own id as though it were a root message fails, because a reply has no replies collection of its own -- see Client.Replies.

Needs SendChannelMessage.

func (*Client) SearchMessages

func (c *Client) SearchMessages(ctx context.Context, query SearchQuery) iter.Seq2[SearchHit, error]

SearchMessages runs a full-text message search, following the service's offset-based continuation until moreResultsAvailable is false.

This is the one collection in this package with no @odata.nextLink: the endpoint pages a search by advancing From by the page size instead, and nothing in Client.Graph or the foundation's paginate helper models that. A run is bounded by the client's WithMaxPages the same way every other listing here is, so a service that never stops reporting more results available cannot make this loop forever.

Needs SearchMessages.

func (*Client) SearchPage

func (c *Client) SearchPage(ctx context.Context, query SearchQuery) (hits []SearchHit, more bool, resp *msgraph.Response, err error)

SearchPage runs one page of a message search.

With Chat.Read alone -- and no channel-message permission -- this returns chat hits and no channel hits, with no error and nothing in the response distinguishing that from a search that genuinely found nothing in channels. An answer that looks complete is not the same as one that searched everywhere it could have.

Needs SearchMessages.

func (*Client) Self

func (c *Client) Self() UserID

Self returns the signed-in user's ID configured with WithSelf, or the zero UserID if none was set.

func (*Client) SendChannelMessage

func (c *Client) SendChannelMessage(ctx context.Context, team TeamID, channel ChannelID, draft Draft) (ChatMessage, *msgraph.Response, error)

SendChannelMessage posts a new root message to a channel and returns the created copy.

Needs SendChannelMessage.

func (*Client) SendMessage

func (c *Client) SendMessage(ctx context.Context, chat ChatID, draft Draft) (ChatMessage, *msgraph.Response, error)

SendMessage posts a new message to a chat and returns the created copy.

Needs SendChatMessage.

func (*Client) SetReaction

func (c *Client) SetReaction(ctx context.Context, chat ChatID, message MessageID, reaction ReactionType) (*msgraph.Response, error)

SetReaction reacts to a message.

Reacting again with the same type is idempotent. Reacting with a different type does not replace the first: the message ends up carrying both reactions from the same person, which surprises anyone expecting a radio button where the service implements a set. Call Client.UnsetReaction with the old type first to swap one reaction for another.

Needs ChangeChat.

func (*Client) SoftDeleteMessage

func (c *Client) SoftDeleteMessage(ctx context.Context, chat ChatID, message MessageID) (*msgraph.Response, error)

SoftDeleteMessage deletes a message.

The message is not removed. It keeps its id, keeps appearing in every listing that already reached it, and reports ChatMessage.Deleted true with its body emptied by the service. A caller that wants a soft-deleted message gone from a rendered transcript has to filter it out itself; the listing will not do that for them.

Needs ChangeChat.

func (*Client) UndoSoftDeleteMessage

func (c *Client) UndoSoftDeleteMessage(ctx context.Context, chat ChatID, message MessageID) (*msgraph.Response, error)

UndoSoftDeleteMessage restores a message soft-deleted with Client.SoftDeleteMessage. The body the service emptied on delete is not restored; only the deletedDateTime marker is cleared.

Needs ChangeChat.

func (*Client) UnsetReaction

func (c *Client) UnsetReaction(ctx context.Context, chat ChatID, message MessageID, reaction ReactionType) (*msgraph.Response, error)

UnsetReaction removes a reaction of the given type. Removing a reaction that was never set is not an error.

Needs ChangeChat.

func (*Client) UserPresence

func (c *Client) UserPresence(ctx context.Context, user UserID) (Presence, *msgraph.Response, error)

UserPresence reads another user's presence.

Needs ReadPresence.

type ConversationMember

type ConversationMember struct {
	// ID identifies this membership record, not the user. It is an opaque
	// string scoped to the chat or channel; do not parse it. See
	// [ConversationMember.UserID] for the user's own identifier.
	ID          string       `json:"id,omitempty"`
	DisplayName string       `json:"displayName,omitempty"`
	UserID      UserID       `json:"userId,omitempty"`
	Email       string       `json:"email,omitempty"`
	Roles       []MemberRole `json:"roles,omitempty"`
	// VisibleHistoryStartDateTime is set when a member was added to an
	// existing chat and can see history only from that point forward. A nil
	// value means the member sees the whole history, not that this property
	// was not loaded -- member listings always populate it.
	VisibleHistoryStartDateTime *time.Time `json:"visibleHistoryStartDateTime,omitempty"`
}

ConversationMember is one member of a chat or channel.

type Draft

type Draft struct {
	Body        *ItemBody           `json:"body"`
	Subject     string              `json:"subject,omitempty"`
	Importance  Importance          `json:"importance,omitempty"`
	Mentions    []Mention           `json:"mentions,omitempty"`
	Attachments []MessageAttachment `json:"attachments,omitempty"`
}

Draft is the set of properties Graph accepts when creating a chat or channel message.

It is a separate type from ChatMessage because the send endpoint accepts only a handful of properties and silently discards the rest: posting a round-tripped ChatMessage sends an id, an etag, and timestamps that Graph ignores, which reads as "it worked" right up until a caller relies on one of those fields meaning something in the response.

func (Draft) IsEmpty

func (d Draft) IsEmpty() bool

IsEmpty reports whether sending the draft would post nothing: no body text, no subject, no importance, no mentions, and no attachments.

type ETag

type ETag string

ETag is a resource version tag, used to make a write conditional.

func (ETag) String

func (e ETag) String() string

type ErrMissingCapability

type ErrMissingCapability struct {
	// Capability is what was needed.
	Capability Capability
	// Accepts lists the permissions that would satisfy it.
	Accepts []string
	// AdminOnly is true when none of Accepts can be granted by the
	// signed-in user; see [NeedsAdminConsent].
	AdminOnly bool
}

ErrMissingCapability reports a permission the caller does not hold. It is returned before the request is sent, so nothing is attempted that the service would only refuse.

func (*ErrMissingCapability) Error

func (e *ErrMissingCapability) Error() string

type EventDetail

type EventDetail struct {
	Type      EventDetailType `json:"@odata.type,omitempty"`
	Initiator *IdentitySet    `json:"initiator,omitempty"`
	Raw       json.RawMessage `json:"-"`
}

EventDetail is a system event message's event payload.

It is a discriminator plus a raw copy of the JSON rather than a Go interface with one implementation per variant. Graph has added roughly forty of these without a version bump, and an interface registry makes an unrecognized variant decode to nil -- which renders a message as a blank line where somebody left the team, was added to a call, or had a message pinned, with no indication anything was lost. Keeping the raw bytes means an unknown variant still round-trips and still answers EventDetail.Decode for whatever shape a caller already knows to expect from it.

func (*EventDetail) CallDuration

func (d *EventDetail) CallDuration() (time.Duration, bool)

CallDuration returns how long the call lasted from a callEnded event, reporting false for any other event type and for a duration this package cannot parse. A zero-length call and an unreadable one are different facts, and collapsing them both into a zero time.Duration would hide which one happened.

func (*EventDetail) Decode

func (d *EventDetail) Decode(into any) error

Decode unmarshals the raw event payload into into, giving access to a variant-specific field this type does not model directly.

func (*EventDetail) Known

func (d *EventDetail) Known() bool

Known reports whether Type is one of the variants this package recognizes. It is false for a nil receiver and for any @odata.type Graph has added since this package was last updated -- which EventDetail.Decode still answers, using Raw.

func (EventDetail) MarshalJSON

func (d EventDetail) MarshalJSON() ([]byte, error)

MarshalJSON returns Raw when it is populated, so a value decoded from the service round-trips byte for byte including whatever variant-specific fields this type does not model. A value built by hand, with no Raw, falls back to marshaling the typed fields.

func (*EventDetail) MembersAdded

func (d *EventDetail) MembersAdded() ([]Identity, bool)

MembersAdded returns the members named by a membersAdded or membersJoined event, reporting false for any other event type.

func (*EventDetail) MembersRemoved

func (d *EventDetail) MembersRemoved() ([]Identity, bool)

MembersRemoved returns the members named by a membersDeleted or membersLeft event, reporting false for any other event type.

func (*EventDetail) NewTopic

func (d *EventDetail) NewTopic() (string, bool)

NewTopic returns the chat's new name from a chatRenamed event, reporting false for any other event type.

func (*EventDetail) Summary

func (d *EventDetail) Summary() (EventSummary, bool)

Summary builds an EventSummary for a known event type, reporting false for a nil receiver or an event type EventDetail.Known does not recognize.

func (*EventDetail) UnmarshalJSON

func (d *EventDetail) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the typed fields and keeps a copy of the raw bytes for EventDetail.Decode and for round-tripping an unrecognized variant.

The copy is load-bearing: encoding/json reuses its scratch buffer across calls to a custom UnmarshalJSON, so data is only valid for the duration of this call, and keeping a reference to it instead of a copy would corrupt Raw the next time anything on the same decoder runs.

type EventDetailType

type EventDetailType string

EventDetailType is the @odata.type of a system event message's eventDetail, naming which of the roughly forty variants Graph has added over the years it is.

const (
	EventDetailMembersAdded      EventDetailType = "#microsoft.graph.membersAddedEventMessageDetail"
	EventDetailMembersDeleted    EventDetailType = "#microsoft.graph.membersDeletedEventMessageDetail"
	EventDetailMembersJoined     EventDetailType = "#microsoft.graph.membersJoinedEventMessageDetail"
	EventDetailMembersLeft       EventDetailType = "#microsoft.graph.membersLeftEventMessageDetail"
	EventDetailChatRenamed       EventDetailType = "#microsoft.graph.chatRenamedEventMessageDetail"
	EventDetailChannelAdded      EventDetailType = "#microsoft.graph.channelAddedEventMessageDetail"
	EventDetailChannelRenamed    EventDetailType = "#microsoft.graph.channelRenamedEventMessageDetail"
	EventDetailCallStarted       EventDetailType = "#microsoft.graph.callStartedEventMessageDetail"
	EventDetailCallEnded         EventDetailType = "#microsoft.graph.callEndedEventMessageDetail"
	EventDetailMeetingStarted    EventDetailType = "#microsoft.graph.meetingStartedEventMessageDetail"
	EventDetailMeetingEnded      EventDetailType = "#microsoft.graph.meetingEndedEventMessageDetail"
	EventDetailTeamsAppInstalled EventDetailType = "#microsoft.graph.teamsAppInstalledEventMessageDetail"
	EventDetailMessagePinned     EventDetailType = "#microsoft.graph.messagePinnedEventMessageDetail"
	EventDetailMessageUnpinned   EventDetailType = "#microsoft.graph.messageUnpinnedEventMessageDetail"
)

type EventSummary

type EventSummary struct {
	Event    EventDetailType
	Actor    Identity
	Subjects []Identity
	Topic    string
	Duration time.Duration
}

EventSummary is a structured summary of a system event, built from whichever of EventDetail's typed accessors apply to its Event type.

This is a struct rather than an assembled sentence because wording, tense, and language are display decisions this package does not make on a caller's behalf.

type HostedContentID

type HostedContentID string

HostedContentID identifies an inline image or file referenced from a message body.

func (HostedContentID) String

func (i HostedContentID) String() string

type HostedContentRef

type HostedContentRef struct {
	ID  HostedContentID
	URL string
	Alt string
}

HostedContentRef is an inline image ChatMessage.InlineImages found in a message body, referencing the chat or channel's hostedContents collection rather than carrying the image bytes itself.

type Identity

type Identity struct {
	ID          string       `json:"id,omitempty"`
	DisplayName string       `json:"displayName,omitempty"`
	Type        IdentityType `json:"userIdentityType,omitempty"`
}

Identity is a single Graph identity resource: a person, application, or device with an id and a display name.

type IdentityKind

type IdentityKind string

IdentityKind names which slot of an IdentitySet was populated.

const (
	IdentityKindUser        IdentityKind = "user"
	IdentityKindApplication IdentityKind = "application"
	IdentityKindDevice      IdentityKind = "device"
)

type IdentitySet

type IdentitySet struct {
	User        *Identity `json:"user,omitempty"`
	Application *Identity `json:"application,omitempty"`
	Device      *Identity `json:"device,omitempty"`
}

IdentitySet is Graph's identitySet resource: at most one of User, Application, or Device is populated, naming whatever sent the message. On a system event message every field is nil, because nothing sent it.

func (*IdentitySet) Actor

func (s *IdentitySet) Actor() (Identity, IdentityKind, bool)

Actor returns whichever identity is populated and reports which kind it was. It is nil-receiver-safe: a caller holding a *IdentitySet obtained from ChatMessage.From does not have to check for nil before calling this, only before doing anything with the returned bool.

func (*IdentitySet) Name

func (s *IdentitySet) Name() string

Name returns the populated identity's display name, or the empty string when the set is nil or empty. It is nil-receiver-safe for the same reason IdentitySet.Actor is.

type IdentityType

type IdentityType string

IdentityType is Graph's teamworkUserIdentityType enum, present on identities that come from presence and meeting-participant endpoints. A chat message's sender does not carry one; the zero value there means "not applicable", not "aadUser".

const (
	IdentityTypeAADUser                  IdentityType = "aadUser"
	IdentityTypeOnPremiseAADUser         IdentityType = "onPremiseAadUser"
	IdentityTypeAnonymousGuest           IdentityType = "anonymousGuest"
	IdentityTypeFederatedUser            IdentityType = "federatedUser"
	IdentityTypePersonalMicrosoftAccount IdentityType = "personalMicrosoftAccount"
	IdentityTypeSkypeUser                IdentityType = "skypeUser"
	IdentityTypePhoneUser                IdentityType = "phoneUser"
	IdentityTypeUnknownFutureValue       IdentityType = "unknownFutureValue"
)

type Importance

type Importance string

Importance is a chat message's importance level.

const (
	ImportanceNormal Importance = "normal"
	ImportanceHigh   Importance = "high"
	ImportanceUrgent Importance = "urgent"
)

type ItemBody

type ItemBody struct {
	ContentType BodyType `json:"contentType,omitempty"`
	Content     string   `json:"content,omitempty"`
}

ItemBody is a message or draft body.

func HTMLBody

func HTMLBody(content string) *ItemBody

HTMLBody builds an HTML body.

func TextBody

func TextBody(content string) *ItemBody

TextBody builds a plain-text body.

func (*ItemBody) Text

func (b *ItemBody) Text() string

Text returns the body's content, unescaped of HTML entities when the content type is HTML but with markup left untouched otherwise -- this is the raw body, not a rendering of it. It is nil-safe and does no mention resolution, since an ItemBody on its own carries no mentions list to resolve against; see ChatMessage.PlainText for that.

type MemberRole

type MemberRole string

MemberRole is a conversation member's role. It is a plain string rather than a closed enum for the same reason ReactionType is: the set is not exhaustively documented and an unrecognized value should still round-trip rather than collapse to nothing.

const (
	// MemberRoleOwner marks a member who can manage the chat or channel.
	MemberRoleOwner MemberRole = "owner"
	// MemberRoleGuest marks a member signed in from outside the tenant. The
	// absence of a role -- an empty Roles slice -- means an ordinary member,
	// not "role not loaded": member listings always populate Roles.
	MemberRoleGuest MemberRole = "guest"
)

type Mention

type Mention struct {
	ID          int32                 `json:"id"`
	MentionText string                `json:"mentionText,omitempty"`
	Mentioned   *MentionedIdentitySet `json:"mentioned,omitempty"`
}

Mention is one entry in a chat message's mentions list, matched against an <at id="N"> tag in the body by Mention.ID.

type MentionedIdentitySet

type MentionedIdentitySet struct {
	User         *Identity `json:"user,omitempty"`
	Application  *Identity `json:"application,omitempty"`
	Device       *Identity `json:"device,omitempty"`
	Conversation *Identity `json:"conversation,omitempty"`
	Tag          *Identity `json:"tag,omitempty"`
}

MentionedIdentitySet is Graph's chatMessageMentionedIdentitySet: wider than IdentitySet because a mention can also name the conversation itself or a tag, neither of which is ever the sender of a message.

type MessageAttachment

type MessageAttachment struct {
	ID           string `json:"id,omitempty"`
	ContentType  string `json:"contentType,omitempty"`
	ContentURL   string `json:"contentUrl,omitempty"`
	Content      string `json:"content,omitempty"`
	Name         string `json:"name,omitempty"`
	ThumbnailURL string `json:"thumbnailUrl,omitempty"`
}

MessageAttachment is a chat or channel message's attachment metadata.

type MessageID

type MessageID string

MessageID identifies a chat or channel message.

It is epoch milliseconds, assigned by Graph when the message was created, and it is unique only within the chat, channel, or reply thread it was posted to -- the same numeric ID can name two entirely different messages in two different chats. That is why nothing in this package accepts a MessageID on its own: every operation also names the chat, channel, or thread the message belongs to.

func (MessageID) String

func (i MessageID) String() string

func (MessageID) Time

func (i MessageID) Time() (time.Time, bool)

Time parses a MessageID back into the instant Graph assigned it, reporting false rather than the zero time when the ID is not a decimal number. ID "0" and an ID that is not numeric at all are different facts, and collapsing both into time.Time{} would make a malformed ID indistinguishable from a message posted at the Unix epoch.

type MessageOrder

type MessageOrder string

MessageOrder names the property a message listing is sorted and windowed by. See MessageQuery for why this is one field rather than a separate filter property and sort property.

const (
	// ByCreated orders by createdDateTime. It is MessageQuery's zero value.
	ByCreated MessageOrder = "createdDateTime"
	// ByLastModified orders by lastModifiedDateTime, which also moves when a
	// message is reacted to, edited, or soft-deleted -- so a listing ordered
	// by it surfaces recently-touched messages, not only recently-sent ones.
	ByLastModified MessageOrder = "lastModifiedDateTime"
)

func Newest

func Newest() MessageOrder

Newest orders a message listing by creation time, newest first. This is also MessageQuery's zero value for Order, so passing it is never required -- it exists for a call site that wants to say the ordering out loud rather than lean on an implicit default.

type MessagePreview

type MessagePreview struct {
	ID              MessageID    `json:"id,omitempty"`
	CreatedDateTime time.Time    `json:"createdDateTime,omitzero"`
	From            *IdentitySet `json:"from,omitempty"`
	Body            *ItemBody    `json:"body,omitempty"`
	MessageType     MessageType  `json:"messageType,omitempty"`
	IsDeleted       bool         `json:"isDeleted,omitempty"`
	EventDetail     *EventDetail `json:"eventDetail,omitempty"`
}

MessagePreview is the abbreviated message Graph attaches to a chat when ChatQuery.WithLastMessagePreview or ChatQuery.ByRecentMessage is set.

It is a separate type from ChatMessage on purpose. The service populates only the handful of properties below; reusing ChatMessage here would present forty zero-valued fields -- Mentions, Reactions, Attachments, and the rest -- as though the message genuinely had none of them, rather than as "not part of a preview."

type MessageQuery

type MessageQuery struct {
	// Order is the property the listing is filtered and sorted on. The zero
	// value is [ByCreated].
	Order MessageOrder
	// Since is an inclusive lower bound on Order. The zero time omits it.
	Since time.Time
	// Before is an exclusive upper bound on Order. The zero time omits it.
	Before time.Time
	// Top asks for a page size, clamped to [MaxMessagePageSize]. Zero asks
	// for the service's own default rather than the maximum.
	Top int
	// Custom carries query parameters this package does not model.
	Custom url.Values
}

MessageQuery selects and windows a chat or channel message listing.

Why Order is one field, not a filter property and a sort property

A Teams message listing is indexed on exactly one property at a time, and the service does not tell you when a request asks for two: a $filter on createdDateTime combined with $orderby on lastModifiedDateTime is not refused. It is answered 200 OK, with the filter silently discarded and the whole collection returned instead. A caller asking for the last day of a chat gets a year of it back, and a UI that trusts the response renders a year of messages as though they were all from today, with nothing in the response shape to suggest anything went wrong.

Splitting Order from Since/Before -- as two independent fields a caller could set inconsistently -- would make that mistake something this type permits and even encourages, since the zero value of an unset sort field looks like "no preference" rather than "different from the filter." Folding them into a single Order field removes the inconsistent state from the type entirely: MessageQuery.Params always renders $filter and $orderby against the same property, because there is only one property to name. See FieldLastMessagePreviewCreated and ChatQuery for the same hazard one level up, on the chat listing that orders by a message inside it.

Why there is no Select, Expand, Where, or Descending field

A chat or channel message listing refuses $select and $expand outright with 400 Bad Request -- unlike the filter mismatch above, this one really is an error, which is itself worth knowing, since the two look identical from the caller's side until one of them silently succeeds. (The one exception, expanding replies on a channel listing, is ChannelMessageQuery.WithReplies.) And the service only ever serves descending order; an ascending $orderby is refused the same way. Fields that a request would only be refused for existing are not offered here at all, so the type cannot express the requests that never work.

func (MessageQuery) Params

func (q MessageQuery) Params() msgraph.Params

Params renders the query as Graph query parameters, always filtering and ordering on the same property -- see the type doc for why that coupling is the entire reason this type exists -- and always ordering descending, since ascending is refused.

func (MessageQuery) Validate

func (q MessageQuery) Validate() error

Validate reports ErrInvertedWindow when Since is after Before. Call it before sending the query; every operation in this file does, so this is mainly useful to a caller building a MessageQuery to hand to Client.ListMessages or Client.Messages and wanting to fail before the round trip rather than after it.

type MessageType

type MessageType string

MessageType is Graph's evolvable chatMessageType enum. See doc.go for what it costs a caller who does not send msgraph.PreferIncludeUnknownEnumMembers: every system event message decodes to MessageTypeUnknownFutureValue instead of MessageTypeSystemEventMessage.

const (
	MessageTypeMessage            MessageType = "message"
	MessageTypeChatEvent          MessageType = "chatEvent"
	MessageTypeTyping             MessageType = "typing"
	MessageTypeSystemEventMessage MessageType = "systemEventMessage"
	MessageTypeUnknownFutureValue MessageType = "unknownFutureValue"
)

type OnlineMeetingInfo

type OnlineMeetingInfo struct {
	Organizer  *IdentitySet `json:"organizer,omitempty"`
	JoinWebURL string       `json:"joinWebUrl,omitempty"`
}

OnlineMeetingInfo is the minimal join information Graph attaches to a meeting chat.

type Option

type Option func(*Client) error

Option configures a Client.

func WithMaxPages

func WithMaxPages(pages int) Option

WithMaxPages bounds how many pages a single listing will follow, replacing msgraph.DefaultMaxPages.

The bound exists because refusing to repeat a link is not enough. A service that mints a fresh nextLink every time is followed forever, and the set of links already seen grows by one URL per page while it happens, so the run consumes requests and memory without end. A page count is the only thing that stops it.

func WithPageCapNotify

func WithPageCapNotify(notify func(requested, served int)) Option

WithPageCapNotify registers a callback told when a request was served fewer items than it asked for because an endpoint capped it -- Teams listings do this silently, with no error and no indication in the response that anything was capped. notify is called with the number requested and the number actually served.

func WithPrefer

func WithPrefer(directives ...msgraph.PreferDirective) Option

WithPrefer adds arbitrary Prefer directives to every request.

func WithSelf

func WithSelf(user UserID) Option

WithSelf records the signed-in user's ID, so ChatMessage.Mine and future call sites that need to distinguish "I sent this" from "someone else sent this" do not each have to be told separately.

type OutOfOfficeSettings

type OutOfOfficeSettings struct {
	IsOutOfOffice bool   `json:"isOutOfOffice,omitempty"`
	Message       string `json:"message,omitempty"`
}

OutOfOfficeSettings is a user's out-of-office state, as reported by presence.

type Presence

type Presence struct {
	ID            UserID                 `json:"id,omitempty"`
	Availability  Availability           `json:"availability,omitempty"`
	Activity      Activity               `json:"activity,omitempty"`
	OutOfOffice   *OutOfOfficeSettings   `json:"outOfOfficeSettings,omitempty"`
	StatusMessage *PresenceStatusMessage `json:"statusMessage,omitempty"`
}

Presence is a user's Teams presence.

type PresenceExpiry

type PresenceExpiry struct {
	DateTime string `json:"dateTime,omitempty"`
	TimeZone string `json:"timeZone,omitempty"`
}

PresenceExpiry is Graph's dateTimeTimeZone shape, used by a presence status message's expiry. TimeZone is whatever zone name the service sent, which -- as elsewhere in Graph -- is not guaranteed to be an IANA name.

type PresenceStatusMessage

type PresenceStatusMessage struct {
	Message        *ItemBody       `json:"message,omitempty"`
	ExpiryDateTime *PresenceExpiry `json:"expiryDateTime,omitempty"`
}

PresenceStatusMessage is a user-set status message shown alongside presence.

type Reaction

type Reaction struct {
	ReactionType    ReactionType `json:"reactionType,omitempty"`
	CreatedDateTime time.Time    `json:"createdDateTime,omitzero"`
	// User is Graph's chatMessageReactionIdentitySet, which happens to share
	// IdentitySet's shape: the wire nests the reacting person one level
	// deeper than it looks, under this object's own "user" key, rather than
	// holding the identity directly. IdentitySet already models a "user" key
	// holding an Identity, so reusing it here decodes the extra nesting for
	// free -- the reacting person is User.User, reached the same way as any
	// other IdentitySet, via [IdentitySet.Actor] or [IdentitySet.Name].
	User *IdentitySet `json:"user,omitempty"`
}

Reaction is one reaction on a chat message.

type ReactionType

type ReactionType string

ReactionType names a message reaction. It is a plain string rather than a closed enum: a tenant can enable custom emoji reactions beyond the six built in ones, and a reaction using one arrives as whatever string the tenant named it. Treating this as an exhaustive switch would silently drop every custom reaction a message received.

const (
	ReactionLike      ReactionType = "like"
	ReactionHeart     ReactionType = "heart"
	ReactionLaugh     ReactionType = "laugh"
	ReactionSurprised ReactionType = "surprised"
	ReactionSad       ReactionType = "sad"
	ReactionAngry     ReactionType = "angry"
)

type SearchHit

type SearchHit struct {
	Rank    int32       `json:"rank"`
	Summary string      `json:"summary,omitempty"`
	Message ChatMessage `json:"resource"`
}

SearchHit is one match from a message search.

type SearchQuery

type SearchQuery struct {
	// Term is the text to search for.
	Term string
	// From is the zero-based offset of the first hit to return.
	From int
	// Size is the page size, clamped to [MaxSearchPageSize]. Zero, like a
	// negative value, is treated as [MaxSearchPageSize] rather than as "ask
	// the service for its own default": unlike an OData $top, this number
	// also has to be added to From between pages, and a page size this
	// package never learned cannot be added correctly.
	Size int
	// Fields restricts which properties of each hit's resource the service
	// returns.
	Fields []string
}

SearchQuery is a full-text message search.

There is no sort field: the endpoint refuses sortProperties for a chatMessage search outright, answering 400. Message hits always come back in relevance order, and a caller wanting them chronological has to sort the hits itself after fetching them.

type Team

type Team struct {
	ID          TeamID `json:"id,omitempty"`
	DisplayName string `json:"displayName,omitempty"`
	Description string `json:"description,omitempty"`
	// InternalID is the messaging thread id backing the team's General
	// channel conversation, not the team's own identifier. Passing it where
	// a TeamID is expected -- or a TeamID where this is expected -- 404s;
	// the two are different opaque strings that happen to share a shape.
	InternalID string         `json:"internalId,omitempty"`
	WebURL     string         `json:"webUrl,omitempty"`
	Visibility TeamVisibility `json:"visibility,omitempty"`
	IsArchived bool           `json:"isArchived,omitempty"`
	TenantID   string         `json:"tenantId,omitempty"`
}

Team is a Microsoft Teams team.

A field being its zero value on a value returned from Client.JoinedTeams does not mean the property is genuinely empty; see Client.JoinedTeams.

type TeamID

type TeamID string

TeamID identifies a team.

func (TeamID) String

func (i TeamID) String() string

type TeamVisibility

type TeamVisibility string

TeamVisibility says who can find and join a team without an invitation.

const (
	TeamVisibilityPrivate          TeamVisibility = "private"
	TeamVisibilityPublic           TeamVisibility = "public"
	TeamVisibilityHiddenMembership TeamVisibility = "hiddenMembership"
)

type UserID

type UserID string

UserID identifies a user, typically an Azure AD object ID.

func (UserID) String

func (i UserID) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL