API post comments with attached files

Is it possible to post comments with attached files with the api to speckle.xyz?

1 Like

It is!

The broad steps are to upload a file to the blob storage API, and then when creating a comment and adding it to a stream or commit, you include any fileIds to that payload.

This is a two-stepper using our REST API with GraphQL or can be achieved with SDK helper functions.

What environment would you like to do this? Python, Javascript, C# or something else?

1 Like

Okey great, is there any more information on the REST and GraphQL apis than here REST API | Speckle Docs ?

We’re using javascript or C#.

There isn’t, that’s on us. :cry: (

I will write examples here, and we’ll work on it!

Oki, do you have an example on how to post a comment to at commit?

I’m working something up for you right now.

In the meantime, I have an example to post thousands of comments over here: Gotta catch-em-all

That example is using python but should be reasonably easy to parse.

1 Like

Hey @Sandra, this took a while longer than I had hoped. Nevertheless, this is an example of attaching files to new comments.

I include below C# code to achieve this. I could post javascript afterwards though it has a few gotchas to work around that I mention later.

The first step is to set the server (host_url) and a personal access token (token), if we are not relying on a local installation using Speckle Manager to handle accounts.

string host_url = "HOST_SERVER";
string token = "ACCESS_TOKEN";

With those in place, we can create a SpeckleApiClient object, which will be used to interact with the server.

using Speckle.Core.Api;
using Speckle.Core.Credentials;

Account account = new Account();
account.serverInfo = new ServerInfo(){ url=host_url };
account.token = token;

var stream_id = "bb8470f5b6";
var commit_id = "34247c2a44";

var client = new Client(account);

In this example, I will attach a local file to my script and read it into a Stream and, from there, into a Blob object.

using System.IO;

var filePath = "./assets/blue.png";

byte[] blob;
using (var stream = new FileStream(filePath, FileMode.Open))
{
    using (var memoryStream = new MemoryStream())
    {
        stream.CopyTo(memoryStream);
        blob = memoryStream.ToArray();
    }
}

With that in place and an authenticated client, we can add that File Blob to the Stream with an effective POST request.

I’m posting this as C# code as it isn’t encumbered by web sandboxing. Suppose you are using javascript to do this to the speckle.xyz server, or any server on a different host to your javascript app, you will have to use a CORS proxy.

A POST request with mode no-cors will add the file to the server but will not allow you to receive the return object.

Also fine would be to use Postman or similar to upload the file.

The server endpoint is in the form of host_url/api/stream/{stream_id}/blob, and the request body is a FormData object with a file key and the File Blob as the value.

using System.Net.Http;
using System.Net.Http.Headers;

var fileContent = new ByteArrayContent(File.ReadAllBytes(filePath));
fileContent.Headers.ContentType = 
    MediaTypeHeaderValue.Parse("application/octet-stream");

var formData = new MultipartFormDataContent();
formData.Add(fileContent, "file", Path.GetFileName(filePath));

System.Net.Http.HttpResponseMessage response;

using (var http_client = new HttpClient())
{
    http_client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Bearer", token);
    response = await http_client.PostAsync(
        $"{host_url}/api/stream/{stream_id}/blob", formData);
}

The server will return a JSON object with the file resource information, which you can then use to add the file to a comment.

The response object is in the form:

{
    "uploadResults": [
        {
            "blobId": "9ec.....",
            "fileName": "blue.png",
            "uploadStatus": 1,
            "fileSize": 4760,
            "formKey": "file"
        }
    ]
}

Speckle includes a namespaced version of Newtonsoft.Json, so you can use that to parse the response, but it isn’t critical.

using Speckle.Newtonsoft.Json;

string blobId = null;

if (response.IsSuccessStatusCode)
{
    var responseContent = await response.Content.ReadAsStringAsync();
    dynamic fileUploadResponse = JsonConvert.DeserializeObject(responseContent);
    blobId = fileUploadResponse.uploadResults[0].blobId;
}

With that key blobId, you can add the file to a comment.

We have a few other posts here on the forum that discuss the GraphQL for comments, but the minimal viable query that is effective is:

using GraphQL;
using GraphQL.Client.Http;

var query = @"mutation CommentCreateInput(
    $streamId: String!,
    $blobIds: [String!]!,
    $commitId: String!
  ) { 

  commentCreate(
    input: {
      streamId: $streamId, 
      resources: [
        {
          resourceType: commit
          resourceId: $commitId, 
        }
      ], 
      blobIds: $blobIds
      screenshot: null
      text: {
         type: ""doc"", 
         content: [{type: ""paragraph"", content: [
            {type: ""text"", text: ""My Comment""}]}]}
      },
      data: { location: { x: 10, y: 7.775978194702619, z: 6.737419726085228 } }
    }
  )
}";

var variables = new
{
  streamId = stream_id,
  commitId = commit_id,
  blobIds = new[] { blobId }
};

var request = new GraphQLRequest { Query = query, Variables = variables };    

With the query and variable substitutions in place, we can use the Speckle GQLClient to send the query.

using System.Threading;
using System.Threading.Tasks;

await client.GQLClient
  .SendMutationAsync<LimitedUserData>(request, CancellationToken.None)
  .ConfigureAwait(false);

And that’s it!!

We are missing a few functions with the commenting API that would make replicating the functions available in the Speckle Frontend at speckle.xyz, but this is the gist of it.

Currently missing features:

  • The ability to add a screenshot of the comment-defined viewport
  • The ability to add a selection of objects to the comment

I think the answer is yes; you can add files to the comments. Some parts of this workflow have been in flux for a while, but we hear the need to do better with the documentation!!!

4 Likes

I have wrapped the above up into a Polyglot Jupyter Notebook

speckle-comment-attachments.ipynb (12.8 KB)

The only additional code is to get the Environment Variables where I store the Access Token.

3 Likes

I will add that comment text is internally stored in a ProseMirror document structure, so if anyone’s wondering about how to properly format text for the commentCreate mutation you can find more information here:
ProseMirror Guide
ProseMirror Reference manual

Or it might be easier to just see the TypeScript type for this structure: tiptap/types.ts at acca921184e13d7d1ac0eb63fc3dc3b5bcc3f58d · ueberdosis/tiptap · GitHub

Jonathon’s example shows an example of this structure being sent to the mutation - the structure represents a paragraph with a basic text node without any formatting.

3 Likes

Thanks! I’m testing with Postman but getting “Cannot POST /api/streams/{stream_id}/blob” ?

Do you have a variable setup to exchange with stream_id?

yes, you mean I’m replacing {stream_id} with my stream id?

What size file are you trying to send @Sandra ?

32 kB, what is the limit?

more than that!!! ~10MB

Checklist:

Headers:

Authorisation: `Bearer PERSONAL_ACCESS_TOKEN`
Content-Type: multipart/form-data; boundary= (postman should calculate this for you)
Content-Size: (postman should calculate this for you)

Body:

POST https://{{host}}/api/stream/{{stream_id}}/blob

where host is e.g. speckle.xyz and stream_id is a stream you are an owner / collaborator

Sorry! Did not realize there was an s too muchin /streamS/, now it works :+1:

Is it possible to mention someone in the comment? Something more than adding a @ is needed?

1 Like

:man_facepalming: apologies i also totally missed that in my/your example

I’ve actually never tried - this is good to have if we don’t. I’ll get back to you if you don’t manage to test it yourself.

I’m getting links and styling to work, but not mentions?

@Sandra, I have been looking into this for you.

As @fabians expanded on my earlier example, we use a wrapper around ProseMirror to generate comment content. Part of that wrapper is a mentions extension.

A mention node’s schema is an id and a label.

So a theoretical example may be:

...
text: {
         type: ""doc"", 
         content: [{type: ""paragraph"", content: [
            {type: ""text"", text: ""I think ""},
            {type: ""mention"", attrs: {
               id: ""__USER_ID__"", 
               label: ""__USER_DISPLAYNAME__""
            }},
            {type: ""text"", text: "" should take a look at this.""},
         ]}]}
      },
...

If you are generating these mentions yourself, you will need to handle the correct id to be added carefully. The label should ideally match what would have been generated in the UI, but it isn’t critical.

My understanding of the parsing of new comments is that they will be parsed for mentions and the server will handle the actions associated with that regardless of whether they are being made programmatically or by user interface submission.


I will add this to a working example, which should confirm the actions, but this snippet should give you some direction.

Thanks, when putting id and label in attrs it works :+1:

text: {
    type: "doc"
    content: [
        {
            type: "paragraph", 
            content: [
                {
                    type: "text"
                    text: "I think "
                }
                {
                    type: "mention"
                    attrs: {
                        id: "__USER_ID__"
                        label: "__USER_DISPLAYNAME__"
                    }

                }
                {
                    type: "text"
                    text: " should take a look at this."
                }
            ]
        }
        
    ]
}

Also seems like the mention is handled the same way when posted programatically as when posten by UI submission.

1 Like