Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Threads #922

Closed
wants to merge 25 commits into from
Closed

Threads #922

wants to merge 25 commits into from

Conversation

phenpessoa
Copy link
Contributor

@phenpessoa phenpessoa commented May 3, 2021

I've started the work on threads.
This PR also adds missing message types/flags.
Any feedback/help is greatly appreciated. Mainly what is missing are the endpoints and the RESTAPI funcs.

-- EDIT
Everything should be done now

@phenpessoa
Copy link
Contributor Author

I think that's everything.
Waiting on feedbacks

@phenpessoa
Copy link
Contributor Author

Updated the code to be in line with these changes on the threads docs: discord/discord-api-docs@663dc59

@phenpessoa
Copy link
Contributor Author

Breaking change!

Webhooks now have an optional ThreadID field. Docs: discord/discord-api-docs@cd0cae8

Because of the way WebhookExecute is currently written, this is a breaking change.
Updated the code to be in line with discord.

Lemme know if there should be another/better approach to this.

@belak
Copy link

belak commented Jul 27, 2021

It looks like this is being pushed out publicly now - what would it take to get this merged?

@phenpessoa
Copy link
Contributor Author

There were a few changes to the discord API since I last updated this PR (nothing major, though). This is why this is still a draft.
I also need to merge the new changes from the upstream.

I've been busy lately, that's why I haven't finished this. But I will finish and undraft this PR later this week.

@connorkuehl connorkuehl mentioned this pull request Jul 28, 2021
2 tasks
@phenpessoa phenpessoa marked this pull request as ready for review August 2, 2021 22:18
@phenpessoa
Copy link
Contributor Author

I think this is ready for review now. @CarsonHoffman

}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just lurking around since I'm waiting for this feature in the upstream and decided to give the PR a look.

Should not the method take into account AddedMembers field too? The documentation of the method says it updates the member object of the thread but it only takes care of the case when the user was removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right! Will push an update. Let me know what you think.

restapi.go Outdated
Comment on lines 2756 to 2763
// ThreadEdit edits the given thread
// threadID : The ID of a Thread
// name : The new name to assign the thread.
func (s *Session) ThreadEdit(channelID string, name string) (*Channel, error) {
return s.ThreadEditComplex(channelID, &ThreadEditData{
Name: name,
})
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that ThreadEditComplex clears out all properties not specified, I'm not sure that having this function as is here makes a ton of sense. It seems to me like it would quite quickly lead to unexpected behavior and not really have much use.

structs.go Outdated
type ThreadMember struct {
ID string `json:"id,omitempty"`
UserID string `json:"user_id,omitempty"`
JoinTimeStamp Timestamp `json:"join_timestamp"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably stick to JoinTimestamp to match ArchiveTimestamp.

structs.go Outdated
Comment on lines 371 to 372
// Defaults to `PRIVATE_THREAD` in order to match the behavior
// when thread documentation was first published.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that this comment is really relevant here given that we don't specify omitempty; there's no way for the "default" behavior on the API side to take effect. Perhaps having omitempty might make sense?

restapi.go Outdated
Comment on lines 2653 to 2663
// ActiveThreads returns all active threads in the channel
// GET /channels/{channel.id}/threads/active
func (s *Session) ActiveThreads(channelID string) (st *ThreadsResponseBody, err error) {
body, err := s.RequestWithBucketID("GET", EndpointChannelActiveThreads(channelID), nil, EndpointThreadMembers(""))
if err != nil {
return
}

err = unmarshal(body, &st)
return
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this endpoint is already deprecated. We might just want to move to the guild active threads endpoint to begin with; that one also has a more reasonable response structure, such that we could return a slice of threads and a slice of members rather than a custom struct specifically for one function.

Edit: it looks like we already have that one in this PR! I'd just remove this and ThreadsResponseBody, to be honest.

Comment on lines +310 to +311
// Voice region ID for the voice channel, automatic when set to null
RTCRegion *string `json:"rtc_region"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this field is replacing Region, so we should probably remove that one while we're at it.

restapi.go Outdated
// ListActiveThreads returns all active threads in the guild,
// including public and private threads.
// Threads are ordered by their `id`, in descending order.
func (s *Session) ListActiveThreads(guildID string) (st *ListActiveThreadsResponseBody, err error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just return the threads and members as two return values, rather than having ListActiveThreadsResponseBody exported just for this function.

@phenpessoa
Copy link
Contributor Author

@CarsonHoffman I think that is everything you requested.
Let me know if I missed anything or if I did something wrongly please.

structs.go Outdated

// Constants for the Video Quality Modes of a channel
const (
VideoQualityModeAuto VideoQualityMode = iota + 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is small, but please avoid using iota for constants defined by Discord. We've run into many instances in the past where changes on Discord's end have gone awry doing this, especially when they add gaps between values (which is not uncommon). You'll notice that none of the other blocks of constants that you changed in this PR use iota, either.

restapi.go Outdated
Comment on lines 2137 to 2139
if threadID != "" {
v.Set("thread_id", threadID)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to add the thread ID in, as it's not part of the body; it's a query parameter.

webhook.go Outdated
@@ -35,6 +36,7 @@ type WebhookParams struct {
Components []MessageComponent `json:"components"`
Embeds []*MessageEmbed `json:"embeds,omitempty"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
ThreadID string `json:"thread_id,omitempty"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the thread ID isn't part of the body, we should just have `json:"-"` here.

state.go Outdated
return err
}

if len(t.ChannelIDs) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs note that channel_ids could be omitted, in which case all channels for the guild are being synced. Should we really be doing nothing and returning ErrStateNotFound if this is the case?

state.go Outdated
defer s.Unlock()

for _, parentChannelID := range t.ChannelIDs {
for _, stateChannel := range s.channelMap {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems really nasty—it's O(nm), where n is the list of all channels the bot has access to and m is the number of channels being synced. e.g. if the bot can see 2000 guilds, each has 20 channels, and 10 channels were synced (these numbers seem somewhat reasonable for a larger bot), we just looped four hundred thousand times. Perhaps we could limit the search to only the indicated guild's channels? We're already looping through g.Channels below, and we can delete from channelMap without looping through it (since the keys match).

@phenpessoa
Copy link
Contributor Author

Updated it @CarsonHoffman
Let me know what you think!

state.go Outdated
Comment on lines 631 to 653
OUTER:
for _, c := range g.Channels {
if !c.IsThread() || c.ThreadMetadata.Archived {
g.Channels[index] = c
index++
continue OUTER
}

if len(t.ChannelIDs) == 0 {
delete(s.channelMap, c.ID)
continue OUTER
}

for _, parentChannelID := range t.ChannelIDs {
if c.ParentID == parentChannelID {
delete(s.channelMap, c.ID)
continue OUTER
}
}

g.Channels[index] = c
index++
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A labeled loop with three different (and similar) continue points seems really difficult to reason about to me. I would suggest inverting the loop—first loop over the thread sync event's channels, and then do the guild channels in the inner loop, since you'll then be starting the guild channels loop anew each time and won't need to worry about mutating in place.

@phenpessoa
Copy link
Contributor Author

Updated @CarsonHoffman

state.go Outdated
}
}

return ErrStateNotFound
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me like this function always returns ErrStateNotFound if it makes it past the initial s == nil check. I'd probably invert the if statement to if !ok to return early and keep the happy path left (in addition to above in ThreadMemberUpdate).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

restapi.go Outdated
Comment on lines 2644 to 2654
// ThreadMembers lists the members of a thread
// GET /channels/{channel.id}/threads-members
func (s *Session) ThreadMembers(channelID string) (st []*ThreadMember, err error) {
body, err := s.RequestWithBucketID("GET", EndpointThreadMembers(channelID), nil, EndpointThreadMembers(""))
if err != nil {
return
}

err = unmarshal(body, &st)
return
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like this function works at all:

panic: HTTP 404 Not Found, {"message": "404: Not Found", "code": 0}

It seems like it's calling to /thread-members/ rather than /thread-members. The fact that EndpointThreadMembers doesn't take in the user ID as a parameter seems like a bit of a smell. Having two separate endpoint functions and taking in the user ID as a parameter in one (and consequently updating JoinThread, AddUserToThread, etc. might be a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

@phenpessoa phenpessoa closed this Aug 20, 2021
@phenpessoa phenpessoa reopened this Aug 20, 2021
Copy link
Collaborator

@CarsonHoffman CarsonHoffman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that things are nearing ready to go here! I noticed that we seem to be limiting many of the thread API calls too strictly by not taking into account the channel ID as a major parameter, but otherwise things are looking great.

//
// POST /channels/{channel.id}/messages/{message.id}/threads
func (s *Session) StartThreadWithMessage(channelID, messageID string, data *ThreadCreateData) (st *Channel, err error) {
body, err := s.RequestWithBucketID("POST", EndpointChannelMessageThreads(channelID, messageID), data, EndpointChannelMessageThreads("", ""))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the following passage from the rate limits documentation:

Additionally, rate limits take into account major parameters in the URL. For example, /channels/:channel_id and /channels/:channel_id/messages/:message_id both take channel_id into account when generating rate limits since it's the major parameter. Currently, the only major parameters are channel_id, guild_id, and webhook_id + webhook_token.

It seems to me like the bucket ID should include the channel ID here, since each different channel will be in a different bucket. I think that this also applies to StartThreadWithoutMessage, JoinThread, AddUserToThread, LeaveThread, RemoveUserFromThread, ThreadMembers, PublicArchivedThreads, PrivateArchivedThreads, and JoinedPrivateArchivedThreads (ThreadEditComplex and ListActiveThreads already do this).

Copy link
Collaborator

@CarsonHoffman CarsonHoffman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had another chance to look more closely at the code here, and I am really concerned about what I see. It appears that much of the gateway handling code that was introduced in this commit is copied from #982's implementation (it was committed on July 29th there and August 2nd here). I listed three examples in this review; it seems like you attempted to obfuscate this by removing comments and changing != to >, but the code retains almost the exact same structure, the same logic, and the same bugs as the implementation in #982. Are you claiming that the work in this PR is your own? If you were looking to credit the author of #982, the time was when you added that code, not after (and I'm still not sure that this would be an acceptable route given another active PR, but at least it's not direct stealing). I hope that I'm somehow misinterpreting the situation here, but taking code from others' active PRs and trying to represent it as your own is in no way acceptable behavior.

state.go Outdated
Comment on lines 582 to 604
// ThreadMembersUpdate updates the member object of the thread.
// If the user was removed, the ThreadMember object in the state
// will be set to nil.
func (s *State) ThreadMembersUpdate(t *ThreadMembersUpdate) error {
if s == nil {
return ErrNilState
}

s.Lock()
defer s.Unlock()

thread, ok := s.channelMap[t.ID]
if ok {
for _, memberID := range t.RemovedMembersIDs {
if memberID == s.User.ID {
thread.Member = nil
break
}
}
}

return ErrStateNotFound
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state.go Outdated
Comment on lines 563 to 580
// ThreadMemberUpdate updates the ThreadMember object for the user,
// in a specific thread.
func (s *State) ThreadMemberUpdate(t *ThreadMember) error {
if s == nil {
return ErrNilState
}

channel, ok := s.channelMap[t.ID]
if ok {
s.Lock()
defer s.Unlock()

channel.Member = t
return nil
}

return ErrStateNotFound
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state.go Outdated
Comment on lines 606 to 653
// ThreadListSync syncs new threads and threads member that
// the bot gained access to.
// https://discord.com/developers/docs/topics/gateway#thread-list-sync
func (s *State) ThreadListSync(t *ThreadListSync) error {
if s == nil {
return ErrNilState
}

g, err := s.Guild(t.GuildID)
if err != nil {
return err
}

if len(t.ChannelIDs) > 0 {
s.Lock()
defer s.Unlock()

for _, parentChannelID := range t.ChannelIDs {
for _, stateChannel := range s.channelMap {
if stateChannel.IsThread() && stateChannel.ParentID == parentChannelID && !stateChannel.ThreadMetadata.Archived {
for i, c := range g.Channels {
if c.ID == stateChannel.ID {
g.Channels = append(g.Channels[:i], g.Channels[i+1:]...)
break
}
}
delete(s.channelMap, stateChannel.ID)
}
}
}

for _, channel := range t.Threads {
s.channelMap[channel.ID] = channel
g.Channels = append(g.Channels, channel)
}

for _, member := range t.Members {
channel, ok := s.channelMap[member.ID]
if ok {
channel.Member = member
}
}

return nil
}

return ErrStateNotFound
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be copied from https://github.com/bwmarrin/discordgo/pull/982/files/ecd5d0d76e298de23c533d7e4f1f8c026930fb07#diff-320959764bfa2277e0bdd4eaa741f02891d123c42150d4ccb76de0bdbf6e591aR605-R659, and retains the same very unusual structure of the nested loops (it's so non-straightforward that I cannot imagine two people coming to this independently), the same misunderstanding of what the case of len(t.ChannelIDs) == 0 means, and the same return of ErrStateNotFound when that error really doesn't make a ton of sense in this context.

@phenpessoa
Copy link
Contributor Author

I had another chance to look more closely at the code here, and I am really concerned about what I see. It appears that much of the gateway handling code that was introduced in this commit is copied from #982's implementation (it was committed on July 29th there and August 2nd here). I listed three examples in this review; it seems like you attempted to obfuscate this by removing comments and changing != to >, but the code retains almost the exact same structure, the same logic, and the same bugs as the implementation in #982. Are you claiming that the work in this PR is your own? If you were looking to credit the author of #982, the time was when you added that code, not after (and I'm still not sure that this would be an acceptable route given another active PR, but at least it's not direct stealing). I hope that I'm somehow misinterpreting the situation here, but taking code from others' active PRs and trying to represent it as your own is in no way acceptable behavior.

It is definitely not a copied work. This PR was opened way before that other one. I did take a look at that, to see if I missed something. And I missed the state work and the new webhook type.
Still it doesn't mean I copied anything from that. We had similar approaches.
Yes, I misinterpreted the threadlistsync handler because of how that PR handled it. Should have referred to the docs instead of that PR to work on it.

I also should have mentioned that I added these changes because I saw that PR. I apologize for that, it was my bad.
But code is not a copy. And it is definitely not stealing.

There are bugs there that are not on this PR. And vice versa.

A lot of code there is extremely similar to code here too.
Even some comments there are the same as mine.

@CarsonHoffman
Copy link
Collaborator

I'm not sure that "we had similar approaches" really does a good job of explaining the situation here. Other than changing one-line map-access-in-if-statements to two-line ones (which consistently changed in all three of them), removing comments and using the IsThread method, the three functions are essentially exactly the same character-for-character (ThreadMembersUpdate and ThreadMemberUpdate are). These are not methods where there's one defined pattern of doing things in the library, like a REST API method might have, or property changes directly corresponding to Discord's objects; there are choices to be made in how things are done. They retain the same bugs in all cases (including a function always returning the wrong thing). They have the same nested loop structure with exactly the same variable names; stateChannel.ParentID == parentChannelID in line 625 means that this whole nested loop is essentially just the same as s.channelMap[parentChannelID], but with more unnecessary looping. Roundabout ways of doing things like this don't coincidentally match, especially considering the other two methods.

This PR was opened way before that other one.

Sure. I'm talking specifically about the state management code, which was added after that one's, as you acknowledge.

There are bugs there that are not on this PR.

Not in the state management code.

And vice versa.

Sure, but I'm only referring to the state management code.

Even some comments there are the same as mine.

I fail to see anything significant that isn't either a natural way of describing things or from Discord's documentation.

I unfortunately see no path forward here. I cannot in good conscience accept code that began its journey as copied code, even if it is no longer directly in the final PR. It would be crediting you with someone else's work. I'd recommend really trying to reflect on this and the impact it might have on others if you're planning on contributing to other open-source projects in the future.

@phenpessoa phenpessoa deleted the threads branch August 21, 2021 20:59
vys534 added a commit to vys534/discordgo that referenced this pull request Aug 24, 2021
@vys534 vys534 mentioned this pull request Aug 24, 2021
cheesycod added a commit to selectlist-archive/discordgo that referenced this pull request Dec 9, 2021
@FedorLap2006 FedorLap2006 mentioned this pull request Dec 18, 2021
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants