Skip to content

Commit

Permalink
Forum channels (#1246)
Browse files Browse the repository at this point in the history
* feat: forums

Initial implementation of forum channels REST API

* fix: linter

* feat: cosmetic changes

Added periods in the end of all documentation comments

* fix(ChannelFlagRequireTag): incorrect value

Fix incorrect value of ChannelFlagRequireTag.

* refactor(DefaultReaction): rename to ForumDefaultReaction

Rename DefaultReaction to ForumDefaultReaction.

* fix(Channel): use ForumDefaultReaction

Use ForumDefaultReaction instead of DefaultReaction in DefaultReactionEmoji field.

* fix: gofmt

* feat: cosmetic changes

* Change "GUILD_FORUM" to "forum" in comment to ForumTag
* Fix spacing for documentation of embeds parameter in ForumThreadStartEmbeds

* docs(ForumThreadStartComplex): align parameters

Align documentation for parameters of ForumThreadStartComplex.
  • Loading branch information
FedorLap2006 committed Sep 29, 2022
1 parent fea3d77 commit e570648
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 22 deletions.
94 changes: 94 additions & 0 deletions restapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -2496,6 +2496,100 @@ func (s *Session) ThreadStart(channelID, name string, typ ChannelType, archiveDu
})
}

// ForumThreadStartComplex starts a new thread (creates a post) in a forum channel.
// channelID : Channel to create thread in.
// threadData : Parameters of the thread.
// messageData : Parameters of the starting message.
func (s *Session) ForumThreadStartComplex(channelID string, threadData *ThreadStart, messageData *MessageSend) (th *Channel, err error) {
endpoint := EndpointChannelThreads(channelID)

// TODO: Remove this when compatibility is not required.
if messageData.Embed != nil {
if messageData.Embeds == nil {
messageData.Embeds = []*MessageEmbed{messageData.Embed}
} else {
err = fmt.Errorf("cannot specify both Embed and Embeds")
return
}
}

for _, embed := range messageData.Embeds {
if embed.Type == "" {
embed.Type = "rich"
}
}

// TODO: Remove this when compatibility is not required.
files := messageData.Files
if messageData.File != nil {
if files == nil {
files = []*File{messageData.File}
} else {
err = fmt.Errorf("cannot specify both File and Files")
return
}
}

data := struct {
*ThreadStart
Message *MessageSend `json:"message"`
}{ThreadStart: threadData, Message: messageData}

var response []byte
if len(files) > 0 {
contentType, body, encodeErr := MultipartBodyWithJSON(data, files)
if encodeErr != nil {
return th, encodeErr
}

response, err = s.request("POST", endpoint, contentType, body, endpoint, 0)
} else {
response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint)
}
if err != nil {
return
}

err = unmarshal(response, &th)
return
}

// ForumThreadStart starts a new thread (post) in a forum channel.
// channelID : Channel to create thread in.
// name : Name of the thread.
// archiveDuration : Auto archive duration.
// content : Content of the starting message.
func (s *Session) ForumThreadStart(channelID, name string, archiveDuration int, content string) (th *Channel, err error) {
return s.ForumThreadStartComplex(channelID, &ThreadStart{
Name: name,
AutoArchiveDuration: archiveDuration,
}, &MessageSend{Content: content})
}

// ForumThreadStartEmbed starts a new thread (post) in a forum channel.
// channelID : Channel to create thread in.
// name : Name of the thread.
// archiveDuration : Auto archive duration.
// embed : Embed data of the starting message.
func (s *Session) ForumThreadStartEmbed(channelID, name string, archiveDuration int, embed *MessageEmbed) (th *Channel, err error) {
return s.ForumThreadStartComplex(channelID, &ThreadStart{
Name: name,
AutoArchiveDuration: archiveDuration,
}, &MessageSend{Embeds: []*MessageEmbed{embed}})
}

// ForumThreadStartEmbeds starts a new thread (post) in a forum channel.
// channelID : Channel to create thread in.
// name : Name of the thread.
// archiveDuration : Auto archive duration.
// embeds : Embeds data of the starting message.
func (s *Session) ForumThreadStartEmbeds(channelID, name string, archiveDuration int, embeds []*MessageEmbed) (th *Channel, err error) {
return s.ForumThreadStartComplex(channelID, &ThreadStart{
Name: name,
AutoArchiveDuration: archiveDuration,
}, &MessageSend{Embeds: embeds})
}

// ThreadJoin adds current user to a thread
func (s *Session) ThreadJoin(id string) error {
endpoint := EndpointThreadMember(id, "@me")
Expand Down
104 changes: 82 additions & 22 deletions structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,20 @@ const (
ChannelTypeGuildPublicThread ChannelType = 11
ChannelTypeGuildPrivateThread ChannelType = 12
ChannelTypeGuildStageVoice ChannelType = 13
ChannelTypeGuildForum ChannelType = 15
)

// ChannelFlags represent flags of a channel/thread.
type ChannelFlags int

// Block containing known ChannelFlags values.
const (
// ChannelFlagPinned indicates whether the thread is pinned in the forum channel.
// NOTE: forum threads only.
ChannelFlagPinned ChannelFlags = 1 << 1
// ChannelFlagRequireTag indicates whether a tag is required to be specified when creating a thread.
// NOTE: forum channels only.
ChannelFlagRequireTag ChannelFlags = 1 << 4
)

// A Channel holds all data related to an individual Discord channel.
Expand Down Expand Up @@ -332,6 +346,18 @@ type Channel struct {

// All thread members. State channels only.
Members []*ThreadMember `json:"-"`

// Channel flags.
Flags ChannelFlags `json:"flags"`

// The set of tags that can be used in a forum channel.
AvailableTags []ForumTag `json:"available_tags"`

// The IDs of the set of tags that have been applied to a thread in a forum channel.
AppliedTags []string `json:"applied_tags"`

// Emoji to use as the default reaction to a forum post.
DefaultReactionEmoji ForumDefaultReaction `json:"default_reaction_emoji"`
}

// Mention returns a string which mentions the channel
Expand All @@ -355,13 +381,22 @@ type ChannelEdit struct {
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
ParentID string `json:"parent_id,omitempty"`
RateLimitPerUser *int `json:"rate_limit_per_user,omitempty"`
Flags *ChannelFlags `json:"flags,omitempty"`

// NOTE: threads only

Archived *bool `json:"archived,omitempty"`
AutoArchiveDuration int `json:"auto_archive_duration,omitempty"`
Locked *bool `json:"locked,omitempty"`
Invitable *bool `json:"invitable,omitempty"`

// NOTE: forum channels only

AvailableTags *[]ForumTag `json:"available_tags,omitempty"`
DefaultReactionEmoji *ForumDefaultReaction `json:"default_reaction_emoji,omitempty"`

// NOTE: forum threads only
AppliedTags *[]string `json:"applied_tags,omitempty"`
}

// A ChannelFollow holds data returned after following a news channel
Expand Down Expand Up @@ -395,6 +430,9 @@ type ThreadStart struct {
Type ChannelType `json:"type,omitempty"`
Invitable bool `json:"invitable"`
RateLimitPerUser int `json:"rate_limit_per_user,omitempty"`

// NOTE: forum threads only
AppliedTags []string `json:"applied_tags,omitempty"`
}

// ThreadMetadata contains a number of thread-specific channel fields that are not needed by other channel types.
Expand Down Expand Up @@ -438,6 +476,24 @@ type AddedThreadMember struct {
Presence *Presence `json:"presence"`
}

// ForumDefaultReaction specifies emoji to use as the default reaction to a forum post.
// NOTE: Exactly one of EmojiID and EmojiName must be set.
type ForumDefaultReaction struct {
// The id of a guild's custom emoji.
EmojiID string `json:"emoji_id,omitempty"`
// The unicode character of the emoji.
EmojiName string `json:"emoji_name,omitempty"`
}

// ForumTag represents a tag that is able to be applied to a thread in a forum channel.
type ForumTag struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Moderated bool `json:"moderated"`
EmojiID string `json:"emoji_id,omitempty"`
EmojiName string `json:"emoji_name,omitempty"`
}

// Emoji struct holds data related to Emoji's
type Emoji struct {
ID string `json:"id"`
Expand Down Expand Up @@ -2074,6 +2130,7 @@ const (
ErrCodeUnknownGuildWelcomeScreen = 10069
ErrCodeUnknownGuildScheduledEvent = 10070
ErrCodeUnknownGuildScheduledEventUser = 10071
ErrUnknownTag = 10087

ErrCodeBotsCannotUseEndpoint = 20001
ErrCodeOnlyBotsCanUseEndpoint = 20002
Expand All @@ -2087,28 +2144,30 @@ const (
ErrCodeStageTopicContainsNotAllowedWordsForPublicStages = 20031
ErrCodeGuildPremiumSubscriptionLevelTooLow = 20035

ErrCodeMaximumGuildsReached = 30001
ErrCodeMaximumPinsReached = 30003
ErrCodeMaximumNumberOfRecipientsReached = 30004
ErrCodeMaximumGuildRolesReached = 30005
ErrCodeMaximumNumberOfWebhooksReached = 30007
ErrCodeMaximumNumberOfEmojisReached = 30008
ErrCodeTooManyReactions = 30010
ErrCodeMaximumNumberOfGuildChannelsReached = 30013
ErrCodeMaximumNumberOfAttachmentsInAMessageReached = 30015
ErrCodeMaximumNumberOfInvitesReached = 30016
ErrCodeMaximumNumberOfAnimatedEmojisReached = 30018
ErrCodeMaximumNumberOfServerMembersReached = 30019
ErrCodeMaximumNumberOfGuildDiscoverySubcategoriesReached = 30030
ErrCodeGuildAlreadyHasATemplate = 30031
ErrCodeMaximumNumberOfThreadParticipantsReached = 30033
ErrCodeMaximumNumberOfBansForNonGuildMembersHaveBeenExceeded = 30035
ErrCodeMaximumNumberOfBansFetchesHasBeenReached = 30037
ErrCodeMaximumNumberOfUncompletedGuildScheduledEventsReached = 30038
ErrCodeMaximumNumberOfStickersReached = 30039
ErrCodeMaximumNumberOfPruneRequestsHasBeenReached = 30040
ErrCodeMaximumNumberOfGuildWidgetSettingsUpdatesHasBeenReached = 30042
ErrCodeMaximumNumberOfEditsToMessagesOlderThanOneHourReached = 30046
ErrCodeMaximumGuildsReached = 30001
ErrCodeMaximumPinsReached = 30003
ErrCodeMaximumNumberOfRecipientsReached = 30004
ErrCodeMaximumGuildRolesReached = 30005
ErrCodeMaximumNumberOfWebhooksReached = 30007
ErrCodeMaximumNumberOfEmojisReached = 30008
ErrCodeTooManyReactions = 30010
ErrCodeMaximumNumberOfGuildChannelsReached = 30013
ErrCodeMaximumNumberOfAttachmentsInAMessageReached = 30015
ErrCodeMaximumNumberOfInvitesReached = 30016
ErrCodeMaximumNumberOfAnimatedEmojisReached = 30018
ErrCodeMaximumNumberOfServerMembersReached = 30019
ErrCodeMaximumNumberOfGuildDiscoverySubcategoriesReached = 30030
ErrCodeGuildAlreadyHasATemplate = 30031
ErrCodeMaximumNumberOfThreadParticipantsReached = 30033
ErrCodeMaximumNumberOfBansForNonGuildMembersHaveBeenExceeded = 30035
ErrCodeMaximumNumberOfBansFetchesHasBeenReached = 30037
ErrCodeMaximumNumberOfUncompletedGuildScheduledEventsReached = 30038
ErrCodeMaximumNumberOfStickersReached = 30039
ErrCodeMaximumNumberOfPruneRequestsHasBeenReached = 30040
ErrCodeMaximumNumberOfGuildWidgetSettingsUpdatesHasBeenReached = 30042
ErrCodeMaximumNumberOfEditsToMessagesOlderThanOneHourReached = 30046
ErrCodeMaximumNumberOfPinnedThreadsInForumChannelHasBeenReached = 30047
ErrCodeMaximumNumberOfTagsInForumChannelHasBeenReached = 30048

ErrCodeUnauthorized = 40001
ErrCodeActionRequiredVerifiedAccount = 40002
Expand All @@ -2121,6 +2180,7 @@ const (
ErrCodeMessageAlreadyCrossposted = 40033
ErrCodeAnApplicationWithThatNameAlreadyExists = 40041
ErrCodeInteractionHasAlreadyBeenAcknowledged = 40060
ErrCodeTagNamesMustBeUnique = 40061

ErrCodeMissingAccess = 50001
ErrCodeInvalidAccountType = 50002
Expand Down

0 comments on commit e570648

Please sign in to comment.