GetStream / stream-chat-android

:speech_balloon: Android Chat SDK ➜ Stream Chat API. UI component libraries for chat apps. Kotlin & Jetpack Compose messaging SDK for Android chat
https://getstream.io/chat/sdk/android/
Other
1.47k stars 274 forks source link

New channel creation issue #422

Closed elevenetc closed 4 years ago

elevenetc commented 4 years ago

I'm receiving this issue with the latest client 3.3.0. If I create the channel like so (kotlin):

val channel = client.channel(ModelType.channel_messaging, hashMapOf<String, Any>(), listOf(user, userToChatWith))

it leads to this error:

GetOrCreateChannel failed with error: "When using member based IDs specify at least 2 members"

I tried with setting the channelId explicitly however that leads to an unreadable channel for the second user to join:

val channelId = listOf(user, userToChatWith).sorted().joinToString("-")
val channel = client.channel(ModelType.channel_messaging, channelId, hashMapOf<String, Any>("members" to listOf(user, userToChatWith)))

which leads to this error for the second user (works fine for the user starting the channel): GetOrCreateChannel failed with error: "User 'chuck' with role user is not allowed to access Resource ReadChannel on channel type messaging when I try to .watch the channel.

Both users are being created as a part of a log in process via a nodejs backend:

const client = new StreamChat(apiKey, apiSecret);

const user = Object.assign({}, data, {
  id: req.user.sender,
  role: 'user',
  image: `https://robohash.org/${req.user.sender}`,
});
const token = client.createToken(user.id);
await client.updateUsers([user]);

What am I doing wrong?

Originally posted by @psylinse in https://github.com/GetStream/stream-chat-android/issues/184#issuecomment-587122673

elevenetc commented 4 years ago

https://github.com/GetStream/stream-chat-android/issues/184#issuecomment-587134325

elevenetc commented 4 years ago

@psylinse This code doesn't really create a channel:

val channel = client.channel(ModelType.channel_messaging, hashMapOf<String, Any>(), listOf(user, userToChatWith))

It creates new in memory instance which doesn't do any request:

public Channel channel(String type, HashMap<String, Object> extraData, List<String> members) {
  return new Channel(this, type, extraData, members);
}

Do you call client.queryChannel to create new channel? This code snippet works and it creates channel with two members:

List<String> list = new ArrayList<>();
list.add("user-id-a");
list.add("user-id-b");
Client client = StreamChat.getInstance(getApplication());
Channel newChannel = client.channel(ModelType.channel_messaging, new HashMap<String, Object>(), list);
client.queryChannel(newChannel, new ChannelQueryRequest(), new QueryChannelCallback() {
    @Override
    public void onSuccess(ChannelState response) {
    }

    @Override
    public void onError(String errMsg, int errCode) {
    }
});

Could you try it?

jetaggart commented 4 years ago

Everything works if I change to using the channel.query or client.queryChannel. The source of my issue was I was using .watch first, which is what was trying to create the channel. It appears the semantics of .watch are different or it's straight up dropping the members field. Can you try switching your code to using .watch instead of .queryChannel and see what happens?

See: https://github.com/GetStream/stream-chat-android/issues/184#issuecomment-587134325

FWIW, if I we're to guess without digging in too deep, I think there's some weird mix of type erasure + serialization that's the root cause here. Since .watch takes a ChannelWatchRequest but ultimately gets mapped to a ChannelQueryRequest in retrofit, I wonder if it's somehow losing information when serializing. Just speculation, but might be a useful place to start.

jetaggart commented 4 years ago

Also, for reference, this is the working code: https://github.com/nparsons08/stream-flutter/blob/2-messaging/mobile/android/app/src/main/kotlin/io/getstream/flutter_the_stream/MainActivity.kt#L135-L179

The only difference when it was failing was the use of .watch instead of .query

elevenetc commented 4 years ago

@psylinse I tried your code and it still works with watch. This code:

Client client = StreamChat.getInstance(getActivity());

String name = "new-20";
List<String> members = new ArrayList<>();
HashMap<String, Object> extra = new HashMap<>();

members.add("stream-eugene");
members.add("stream-eugene-2");

extra.put("members", members);
extra.put("name", name);

Channel newChannel = client.channel(ModelType.channel_messaging, name, extra);

newChannel.watch(new ChannelWatchRequest().withMessages(25), new QueryWatchCallback() {
    @Override
    public void onSuccess(ChannelState response) {
    }

    @Override
    public void onError(String errMsg, int errCode) {
    }
});

Is visible in OkHttp logcat logs:

--> POST https://chat-us-east-staging.stream-io-api.com/channels/messaging/new-20/query?api_key=d2q3juekvgsf&user_id=stream-eugene&client_id=d65b8877-dc66-44b1-b700-61a141e6310a
Content-Type: application/json; charset=UTF-8
Content-Length: 139
{"data":{"name":"new-20","members":["stream-eugene","stream-eugene-2"]},"messages":{"limit":25},"presence":false,"state":true,"watch":true}
--> END POST (139-byte body)

Which is finished successfully with 201.

elevenetc commented 4 years ago

@psylinse I think something is missed around users which you try to use with watch request:

val channelId = listOf(user, userToChatWith).sorted().joinToString("-")
val channel = client.channel(ModelType.channel_messaging, channelId, hashMapOf<String, Any>("members" to listOf(user, userToChatWith)))

Your error says about chuck user (which is current one, you called setUser(chuck)). It means that user chuck is not a member of the channel, so he can't watch it. So whats inside this list listOf(user, userToChatWith)? Is it listOf("chuck", "another-user")?

jetaggart commented 4 years ago

Can you try joining the channel with the other user? If I created the channel with watch with an explicit channelId, it was created fine for the first user but there are read issues for the second user:

GetOrCreateChannel failed with error: "User 'chuck' with role user is not allowed to access Resource ReadChannel on channel type messaging

My code is just a list of strings: listOf("creating-user", "chuck").

jetaggart commented 4 years ago

Also, try creating the channel without any channelId information:

val channel = client.channel(ModelType.channel_messaging, hashMapOf<String, Any>(), listOf(user, userToChatWith))

You should see it fail to create the channel entirely upon .watch

elevenetc commented 4 years ago

Can you try joining the channel with the other user? If I created the channel with watch with an explicit channelId, it was created fine for the first user but there are read issues for the second user:

How exactly you're adding a new member to the channel?

Also, try creating the channel without any channelId information:

val channel = client.channel(ModelType.channel_messaging, hashMapOf<String, Any>(), listOf(user, userToChatWith))

You should see it fail to create the channel entirely upon .watch

Tried it and everything works as expected. Backend creates channel id if it is not defined in request.

The same error says about the same thing: chuck is not a member of the channel, that's why he is not allowed to watch the channel

We're walking around the same problem and seems without logcat logs I can't really help you. I have feeling that still something wrong with users. If you record all logs into file and post it here I can try to find the problem.

jetaggart commented 4 years ago

I've created two scenarios to show the two ways .watch breaks. I'm using the full code and giving full logs in case there's some nuance I'm missing somewhere. When checking out the logs, the interesting stuff happens at the bottom.

Scenario 1: nico chats with sara, explicit channel id

In this case, we're only changing channel.query to channel.watch with an explicit channel id. This one fails for Sara, who joins the chat second. Nico is able to create/watch the channel. Sara hits a permission error.

Notice the channel is created with no members, so only Nico has permission to join.

Scenario 2: hugo tries to chat with anyone, no explicit channel id

Here hugo can't start a chat with anyone. In this case, we're only changing channel.query to channel.watch without an explicit channel id. He's trying to create channels without explicit channel id's and hits a permission error right away:

Notice it's failing on members being empty again. What am I missing? It seems like channel.query w/ a .withWatch should be equivalent to channel.watch. It feels like there's some serialization issue where .watch is causing the POST to be void of member information.

I've confirmed the code with .query works. Here's a scenario with henry chatting with john:

elevenetc commented 4 years ago

Reviewed logs and everything looks correct. Just to have the same understanding:

Scenario 1:

  1. nico queries channel with id nico-sara. Response says that there is no members.
  2. sara tries to watch channel nico-sara
  3. backend checks members list and can't find sara there, so returns error: not allowed to watch

To fix Scenario 1

To fix Scenario 2

Here I can't see where you're adding members.

Here is snippet which uses withWatch with members and no channel id:

HashMap<String, Object> extraData = new HashMap<>();
extraData.put("name", channelName + "(no id with watch)");

List<String> members = new ArrayList<>();

members.add("stream-eugene"); //current user
members.add("stream-eugene-2");

extraData.put("members", members);

Channel channel = new Channel(client, ModelType.channel_messaging, extraData, members);

ChannelQueryRequest request = new ChannelQueryRequest().withMessages(10).withWatch();

channel.query(request, new QueryChannelCallback() {
    @Override
    public void onSuccess(ChannelState response) {

    }

    @Override
    public void onError(String errMsg, int errCode) {
    }
});

And in logs it has members and watch:true:

 OkHttp  D  --> POST https://chat-us-east-staging.stream-io-api.com/channels/messaging/query?api_key=d2q3juekvgsf&user_id=stream-eugene&client_id=f5e67ef4-25d6-42f8-bd49-99a5d15521e6
         D  Content-Type: application/json; charset=UTF-8
         D  Content-Length: 160
         D  {"data":{"name":"hello-002(no id with watch)","members":["stream-eugene","stream-eugene-2"]},"messages":{"limit":10},"presence":false,"state":true,"watch":true}
         D  --> END POST (160-byte body)
 OkHttp  D  <-- 201 Created https://chat-us-east-staging.stream-io-api.com/channels/messaging/query?api_key=d2q3juekvgsf&user_id=stream-eugene&client_id=f5e67ef4-25d6-42f8-bd49-99a5d15521e6 (692ms)
         D  Server: fasthttp
         D  Date: Thu, 05 Mar 2020 13:37:18 GMT
         D  Content-Type: application/json;charset=utf-8
         D  Content-Length: 3328
         D  X-Ratelimit-Limit: 10000
         D  Access-Control-Max-Age: 86400
         D  Access-Control-Allow-Headers: x-requested-with, content-type, accept, origin, authorization, x-csrftoken, x-stream-client, stream-auth-type
         D  Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
         D  X-Ratelimit-Remaining: 9999
         D  X-Ratelimit-Reset: 1583415480
         D  Access-Control-Allow-Origin: *
         D  Cache-Control: no-cache
         D  {"channel":{"id":"!members-uCKt4UDFUY2_IQGW6Fx_VSUaAuBSdIKye7vYJ6AjowQ","type":"messaging","cid":"messaging:!members-uCKt4UDFUY2_IQGW6Fx_VSUaAuBSdIKye7vYJ6AjowQ","last_message_at":"2020-03-05T13:35:58.876192Z","created_at":"2020-02-18T14:07:00.957059
            Z","updated_at":"2020-02-18T14:07:00.95706Z","created_by":{"id":"stream-eugene","role":"user","created_at":"2020-01-20T14:42:19.78607Z","updated_at":"2020-03-05T13:35:06.380982Z","last_active":"2020-03-05T13:35:06.376615Z","banned":false,"online":tru
            e,"extraData":{},"image":"https://bit.ly/31EzdNK","name":"stream-eugene","totalUnreadCount":0,"unreadChannels":0},"frozen":false,"member_count":2,"config":{"created_at":"2020-03-05T12:52:42.205963741Z","updated_at":"2020-03-05T12:52:42.2059639Z","nam
            e":"messaging","typing_events":true,"read_events":true,"connect_events":true,"search":true,"reactions":true,"replies":true,"mutes":true,"uploads":true,"url_enrichment":true,"message_retention":"infinite","max_message_length":5000,"automod":"disabled"
            ,"automod_behavior":"flag","commands":[{"name":"giphy","description":"Post a random gif to the channel","args":"[text]","set":"fun_set"}]}},"messages":[{"id":"stream-eugene-638f1bc4-29f6-46f1-b0c9-4748fb528264","text":"Q","html":"\u003cp\u003eQ\u003c
            /p\u003e\n","type":"regular","user":{"id":"stream-eugene","role":"user","created_at":"2020-01-20T14:42:19.78607Z","updated_at":"2020-03-05T13:35:06.380982Z","last_active":"2020-03-05T13:35:06.376615Z","banned":false,"online":true,"extraData":{},"imag
            e":"https://bit.ly/31EzdNK","name":"stream-eugene","totalUnreadCount":0,"unreadChannels":0},"attachments":[],"latest_reactions":[],"own_reactions":[],"reaction_counts":{},"reaction_scores":{},"reply_count":0,"created_at":"2020-03-05T13:35:58.876192Z"
            ,"updated_at":"2020-03-05T13:35:58.876193Z","mentioned_users":[]}],"watcher_count":1,"read":[{"user":{"id":"stream-eugene-2","role":"user","created_at":"2020-01-23T13:12:52.01943Z","updated_at":"2020-03-04T16:44:41.493571Z","last_active":"2020-03-04T
            16:44:41.490551Z","banned":false,"online":false,"image":"https://bit.ly/2BAm5OT","name":"stream-eugene-2"},"last_read":"2020-02-18T14:07:00.972643328Z"},{"user":{"id":"stream-eugene","role":"user","created_at":"2020-01-20T14:42:19.78607Z","updated_at
            ":"2020-03-05T13:35:06.380982Z","last_active":"2020-03-05T13:35:06.376615Z","banned":false,"online":true,"extraData":{},"image":"https://bit.ly/31EzdNK","name":"stream-eugene","totalUnreadCount":0,"unreadChannels":0},"last_read":"2020-02-18T14:07:00.
            97229952Z"}],"members":[{"user":{"id":"stream-eugene","role":"user","created_at":"2020-01-20T14:42:19.78607Z","updated_at":"2020-03-05T13:35:06.380982Z","last_active":"2020-03-05T13:35:06.376615Z","banned":false,"online":true,"name":"stream-eugene","
            totalUnreadCount":0,"unreadChannels":0,"extraData":{},"image":"https://bit.ly/31EzdNK"},"role":"owner","created_at":"2020-02-18T14:07:00.963203Z","updated_at":"2020-02-18T14:07:00.963203Z"},{"user":{"id":"stream-eugene-2","role":"user","created_at":"
            2020-01-23T13:12:52.01943Z","updated_at":"2020-03-04T16:44:41.493571Z","last_active":"2020-03-04T16:44:41.490551Z","banned":false,"online":false,"image":"https://bit.ly/2BAm5OT","name":"stream-eugene-2"},"role":"member","created_at":"2020-02-18T14:07
            :00.963203Z","updated_at":"2020-02-18T14:07:00.963203Z"}],"duration":"6.20ms"}
jetaggart commented 4 years ago

Hmm, I don't think that's correct still. The line you linked is for a different channel set up (livestream). The method we're looking at is .setupPrivateChannel in my source. Here are the relevant lines from that same commit. Sorry for that confusion.

In Scenario 1, calling .update is not required if I use .query. In Scenario 2, I'm doing exactly what you describe and it does not work. Here's some more info:

Scenario 1 I shouldn't have to call .update. What is wrong with this line in the scenario 1 branch? Nico is creating the channel with a .watch and member information should be in the request because of that line. In the logs, it is not making it into the POST. You're saying to call .update after, but I don't need to do this if I use .query instead of .watch initially as nico. The request is generated with member data with .query but is absent for .watch. Just for clarity, here is the relevant code cut out of my source:

val client = StreamChat.getInstance(application)
val channel = client.channel(ModelType.channel_messaging, channelId, hashMapOf<String, Any>("members" to listOf(user, userToChatWith)))
channel.watch(ChannelWatchRequest().withMessages(25), object : QueryWatchCallback {
  override fun onSuccess(response: ChannelState) {
    // do stuff
  }

  override fun onError(errMsg: String, errCode: Int) {
     // error
   }
})

Scenario 2 I am adding members: see this line in the scenario 2 branch. Once again, if I call .query it works fine, only .watch fails. If I add a channel id we end up at Scenario 1. Here is the relevant code sliced out:

val client = StreamChat.getInstance(application)
val channel = client.channel(ModelType.channel_messaging, hashMapOf<String, Any>(), listOf(user, userToChatWith))
channel.watch(ChannelWatchRequest().withMessages(25), object : QueryWatchCallback {
  override fun onSuccess(response: ChannelState) {
    eventSink.success(ObjectMapper().writeValueAsString(response.messages))
  }

  override fun onError(errMsg: String, errCode: Int) {
    eventSink.error(errCode.toString(), errMsg, null)
  }
})

I really think there's a semantics bug between .watch and .query.

jetaggart commented 4 years ago

BTW, the code should be easily runnable with the associated blog post in each branch if you want to repro it:

tbarbugli commented 4 years ago

This problem is going to be completely resolved on the upcoming v4 release, I am going to close this issue. You can already try the beta version of v4 or otherwise subscribe to this issue #554 to get notified when v4 is released as stable.