NewMessage
The type you receive in every message handler. A thin wrapper around the raw TL Message that adds a Client reference plus the shortcut methods you actually want — Reply, Edit, React, Download, Ask, ForwardTo, and everything else.
What NewMessage wraps
*NewMessage holds the raw *telegram.MessageObj the server sent plus a back-reference to the client that produced it. Every accessor on the wrapper pulls from the underlying message; every write method routes back through the client and returns the fresh *NewMessage for the resulting server-side object.
Identity accessors
m.ID // int32 message id inside its chat m.ChatID() // int64 signed chat id (Bot API convention) m.ChannelID() // int64 bare channel/supergroup id m.SenderID() // int64 sender user id m.Date() // int32 unix timestamp m.GetPeer() // (id, accessHash) pair for the chat m.GetChat() // (*ChatObj, error) — basic groups only m.GetChannel() // (*Channel, error) — channels/supergroups only m.GetSender() // (*UserObj, error)
m.ID // int32 message id inside its chat
m.ChatID() // int64 signed chat id (Bot API convention)
m.ChannelID() // int64 bare channel/supergroup id
m.SenderID() // int64 sender user id
m.Date() // int32 unix timestamp
m.GetPeer() // (id, accessHash) pair for the chat
m.GetChat() // (*ChatObj, error) — basic groups only
m.GetChannel() // (*Channel, error) — channels/supergroups only
m.GetSender() // (*UserObj, error)Chat kind and state
The Is* predicates cover both the shape of the chat (private, group, channel) and the state of the message (edited, forwarded, service, empty). Use them in filters rather than switching on Peer type when you can.
m.ChatType() // "user" / "group" / "supergroup" / "channel" m.IsPrivate() // 1:1 DM m.IsGroup() // basic group or supergroup m.IsChannel() // broadcast channel m.IsChannelPost() // sent by the channel itself, not a linked group m.IsAnonymous() // posted as an admin anonymously m.IsService() // service message (joins, promotions, etc.) m.IsOutgoing() // sent by the current session m.IsForward() // has a forward header m.IsReply() // is a reply to another message m.IsMedia() // carries any media attachment m.IsCommand() // starts with / or ! m.IsEmpty() // a MessageEmpty placeholder
m.ChatType() // "user" / "group" / "supergroup" / "channel"
m.IsPrivate() // 1:1 DM
m.IsGroup() // basic group or supergroup
m.IsChannel() // broadcast channel
m.IsChannelPost() // sent by the channel itself, not a linked group
m.IsAnonymous() // posted as an admin anonymously
m.IsService() // service message (joins, promotions, etc.)
m.IsOutgoing() // sent by the current session
m.IsForward() // has a forward header
m.IsReply() // is a reply to another message
m.IsMedia() // carries any media attachment
m.IsCommand() // starts with / or !
m.IsEmpty() // a MessageEmpty placeholderBody and content
m.Text() // plain text with entities stripped
m.RawText() // raw text with formatting characters intact
m.RawText(true) // rendered as Markdown
m.MessageText() // the on-wire Message field (no processing)
m.SetText("...") // mutate before you forward or edit
m.ReplyID() // id of the message this replies to (or 0)
m.ReplyToMsgID() // alias, same value
m.ReplySenderID() // id of the sender of the replied-to message
m.TopicID() // (id, isTopic) — forum topic id
m.ReplyMarkup() // *ReplyMarkup on the message, if any
m.Link() // https://t.me/... deep link to this exact messagem.Text() // plain text with entities stripped
m.RawText() // raw text with formatting characters intact
m.RawText(true) // rendered as Markdown
m.MessageText() // the on-wire Message field (no processing)
m.SetText("...") // mutate before you forward or edit
m.ReplyID() // id of the message this replies to (or 0)
m.ReplyToMsgID() // alias, same value
m.ReplySenderID() // id of the sender of the replied-to message
m.TopicID() // (id, isTopic) — forum topic id
m.ReplyMarkup() // *ReplyMarkup on the message, if any
m.Link() // https://t.me/... deep link to this exact messageTextgives you plain text with entities dropped — the usual thing. RawText keeps the original characters intact; passing true re-serialises the entities as Markdown for editing round-trips.
Media typed helpers
For every media kind the raw union has, *NewMessage ships a helper that returns the concrete Go type or nil. Handy when you only care about one kind and would rather skip the type-switch:
// media() returns a MessageMedia interface — type-assert to reach
// concrete fields. See the peers page for the full pattern.
switch v := m.Media().(type) {
case *telegram.MessageMediaPhoto: // ...
case *telegram.MessageMediaDocument: // ...
}
// or use the typed helpers, each nil when the media does not match:
m.Photo() // *PhotoObj
m.Document() // *DocumentObj
m.Video() // *DocumentObj with video attributes
m.Audio() // *DocumentObj with audio attributes
m.Voice() // *DocumentObj marked as voice
m.Animation() // *DocumentObj marked as animated
m.Sticker() // *DocumentObj marked as sticker
m.Geo() // *GeoPointObj
m.Venue() // *MessageMediaVenue
m.Contact() // *MessageMediaContact
m.Poll() // *MessageMediaPoll
m.Invoice() // *MessageMediaInvoice
m.WebPage() // *WebPageObj
m.Game() // *MessageMediaGame
m.MediaType() // "photo" / "video" / "document" / ... or ""// media() returns a MessageMedia interface — type-assert to reach
// concrete fields. See the peers page for the full pattern.
switch v := m.Media().(type) {
case *telegram.MessageMediaPhoto: // ...
case *telegram.MessageMediaDocument: // ...
}
// or use the typed helpers, each nil when the media does not match:
m.Photo() // *PhotoObj
m.Document() // *DocumentObj
m.Video() // *DocumentObj with video attributes
m.Audio() // *DocumentObj with audio attributes
m.Voice() // *DocumentObj marked as voice
m.Animation() // *DocumentObj marked as animated
m.Sticker() // *DocumentObj marked as sticker
m.Geo() // *GeoPointObj
m.Venue() // *MessageMediaVenue
m.Contact() // *MessageMediaContact
m.Poll() // *MessageMediaPoll
m.Invoice() // *MessageMediaInvoice
m.WebPage() // *WebPageObj
m.Game() // *MessageMediaGame
m.MediaType() // "photo" / "video" / "document" / ... or ""Command parsing
Any message that starts with / or ! is treated as a command. The helpers strip the leading character, drop the optional @botnamesuffix, and give you the parts:
client.On("/order", func(m *telegram.NewMessage) error {
if m.GetCommand() != "order" {
return nil
}
args := m.Args() // everything after the command as a string
parts := m.ArgsList() // split by whitespace
fmt.Println(args, parts)
return nil
})client.On("/order", func(m *telegram.NewMessage) error {
if m.GetCommand() != "order" {
return nil
}
args := m.Args() // everything after the command as a string
parts := m.ArgsList() // split by whitespace
fmt.Println(args, parts)
return nil
})Reply, Respond, Edit
// Reply — sends a message with ReplyID set to this one
m.Reply("got it")
// Respond — same chat, no reply threading
m.Respond("posted")
// Edit — edits *this* message (only if you sent it)
m.Edit("updated")
// ReplyWithoutError — returns just *NewMessage, error is swallowed
// (useful in chains where you don't care)
m.ReplyWithoutError("ok")
// Every variant takes optional *SendOptions:
m.Reply("**bold**", &telegram.SendOptions{ParseMode: "Markdown"})// Reply — sends a message with ReplyID set to this one
m.Reply("got it")
// Respond — same chat, no reply threading
m.Respond("posted")
// Edit — edits *this* message (only if you sent it)
m.Edit("updated")
// ReplyWithoutError — returns just *NewMessage, error is swallowed
// (useful in chains where you don't care)
m.ReplyWithoutError("ok")
// Every variant takes optional *SendOptions:
m.Reply("**bold**", &telegram.SendOptions{ParseMode: "Markdown"})The distinction is which threading the sent message gets: Reply attaches a reply pointer to this message, Respond sends into the same chat without threading, Edit edits this exact message. All three take the same *SendOptions as SendMessage.
Rich variants
Each of the reply / respond / edit calls has a *Rich counterpart that takes a pre-built *RichBuilder:
msg := telegram.NewRichMessage().
Heading("Ship report").
Paragraph("Everything went out at 14:32 UTC.")
m.ReplyRich(msg)
m.RespondRich(msg)
m.EditRich(msg)msg := telegram.NewRichMessage().
Heading("Ship report").
Paragraph("Everything went out at 14:32 UTC.")
m.ReplyRich(msg)
m.RespondRich(msg)
m.EditRich(msg)Media, album, dice, action
m.ReplyMedia("./cover.jpg")
m.ReplyMedia("./cover.jpg", &telegram.MediaOptions{Caption: "look"})
m.RespondMedia(inputPhoto)
m.ReplyAlbum([]any{"./a.jpg", "./b.jpg", "./c.jpg"})
m.RespondAlbum([]any{doc1, doc2})
m.SendDice("🎲") // roll a die in this chat
m.SendAction(&telegram.SendMessageTypingAction{}) // "typing…" indicator
m.GetMediaGroup() // if this is one of a grouped album, fetch the othersm.ReplyMedia("./cover.jpg")
m.ReplyMedia("./cover.jpg", &telegram.MediaOptions{Caption: "look"})
m.RespondMedia(inputPhoto)
m.ReplyAlbum([]any{"./a.jpg", "./b.jpg", "./c.jpg"})
m.RespondAlbum([]any{doc1, doc2})
m.SendDice("🎲") // roll a die in this chat
m.SendAction(&telegram.SendMessageTypingAction{}) // "typing…" indicator
m.GetMediaGroup() // if this is one of a grouped album, fetch the othersDelete, react, forward
m.Delete()
m.React("🔥")
m.React("👍", "❤️", "🎉") // premium — up to three at once
m.ForwardTo(otherChatID)
m.ForwardTo(otherChatID, &telegram.ForwardOptions{
HideAuthor: true,
HideCaption: true,
})
m.MarkRead()m.Delete()
m.React("🔥")
m.React("👍", "❤️", "🎉") // premium — up to three at once
m.ForwardTo(otherChatID)
m.ForwardTo(otherChatID, &telegram.ForwardOptions{
HideAuthor: true,
HideCaption: true,
})
m.MarkRead()MarkReadis worth knowing: for userbots it moves the "read up to" pointer, which suppresses further notifications the server would otherwise push.
Download
Shortcut for downloading any media attached to this message. Skips the media-type lookup you would otherwise do to build the input file location manually:
path, err := m.Download()
path, err = m.Download(&telegram.DownloadOptions{
FileName: "./saved/media.bin",
Threads: 8,
})path, err := m.Download()
path, err = m.Download(&telegram.DownloadOptions{
FileName: "./saved/media.bin",
Threads: 8,
})Conversations and prompts
// see the Conversations page for the full flow
conv, _ := m.Conv(60) // start a conversation in this chat
prompt, answer, _ := m.Ask("Your name?")
click, _ := m.WaitClick(30) // wait for an inline-keyboard click// see the Conversations page for the full flow
conv, _ := m.Conv(60) // start a conversation in this chat
prompt, answer, _ := m.Ask("Your name?")
click, _ := m.WaitClick(30) // wait for an inline-keyboard clickFull details on the Conversations page. The big three are m.Ask (one-shot Q&A), m.WaitClick (wait for an inline-keyboard tap), and m.Conv (open a proper multi-turn conversation).
Other utilities
m.GetReplyMessage() // fetch the message this replies to m.GetDiscussionMessages() // fetch the linked-group discussion thread m.GetSenderChat() // *Channel when a channel posted as itself m.Mention(userID, "Alice") // "@Alice" formatted mention string m.Mention(userID, "Alice", true) // markdown form m.Fact() // FactCheck notes if the message has any m.Marshal() // JSON serialisation for logs m.Unmarshal(data) // inverse
m.GetReplyMessage() // fetch the message this replies to
m.GetDiscussionMessages() // fetch the linked-group discussion thread
m.GetSenderChat() // *Channel when a channel posted as itself
m.Mention(userID, "Alice") // "@Alice" formatted mention string
m.Mention(userID, "Alice", true) // markdown form
m.Fact() // FactCheck notes if the message has any
m.Marshal() // JSON serialisation for logs
m.Unmarshal(data) // inverse