How to work with attachments in comments? Media vs Attachments nightmare

The problem I am trying to solve is given a known attachment, understand which comments contain that attachment. I thought this would be easy but here we are…

Attachment created event data when a user adds an attachment to a comment:

{
  eventType: 'avi:jira:created:attachment',
  attachment: {
    id: '10050',
    issueId: '10000',
    fileName: 'sample.txt',
    createDate: '2023-11-01 15:36:43.5',
    size: '2938068',
    mimeType: 'binary/octet-stream',
    author: { accountId: '712020:8b663a9b-2990-4aa5-93f5-2210ebfe882b' }
  },
  selfGenerated: false,
  context: {
    cloudId: 'e31953d8-0a96-435a-ac70-ec72898f7431',
    moduleKey: 'hello-world'
  },
  contextToken: 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImZvcmdlL2NvbnRleHQtdG9rZW4vYzFiM2I0OWItYTlhYi00ZWQxLTkzM2MtOGI2YmJkYTE4MWQxIn0.eyJjb250ZXh0Ijp7ImNsb3VkSWQiOiJlMzE5NTNkOC0wYTk2LTQzNWEtYWM3MC1lYzcyODk4Zjc0MzEiLCJtb2R1bGVLZXkiOiJmb3JnZWZpbGVzY2FubmVyLWhlbGxvLXdvcmxkIn0sImFjY291bnRJZCI6IjcxMjAyMDo5YTYwN2E5NC1mYWNhLTQ1ODYtYWRjZi1mMjZmNTBkMTM1ZTUiLCJleHRlbnNpb25JZCI6ImFyaTpjbG91ZDplY29zeXN0ZW06OmV4dGVuc2lvbi9hYTkzOTg4MS1jZTE0LTQxMWQtOWIzYi0yNDIwNmJkYjZkNDQvZTEyNjc0YWItMTAyNS00YzlmLWFjYmEtY2Y2Yjg3ZWZhNTJmL3N0YXRpYy9mb3JnZWZpbGVzY2FubmVyLWhlbGxvLXdvcmxkIiwiY29udGV4dElkcyI6WyJhcmk6Y2xvdWQ6amlyYTplMzE5NTNkOC0wYTk2LTQzNWEtYWM3MC1lYzcyODk4Zjc0MzE6aXNzdWUvMTAwMDAiLCJhcmk6Y2xvdWQ6amlyYTo6c2l0ZS9lMzE5NTNkOC0wYTk2LTQzNWEtYWM3MC1lYzcyODk4Zjc0MzEiXSwiYXBwSWQiOiJhYTkzOTg4MS1jZTE0LTQxMWQtOWIzYi0yNDIwNmJkYjZkNDQiLCJhcHBWZXJzaW9uIjoiMy4xLjAiLCJleHRlbnNpb25UeXBlIjoiY29yZTp0cmlnZ2VyIiwidW5saWNlbnNlZCI6ZmFsc2UsImlzcyI6ImZvcmdlL2NvbnRleHQtdG9rZW4iLCJhdWQiOiJmb3JnZSIsImlhdCI6MTY5ODg1MzAwNCwibmJmIjoxNjk4ODUzMDA0LCJleHAiOjE2OTg4NTM5MDQsImp0aSI6ImQzMTIxODRjNzVkZGM3MmYzNjg1ZmJkMWYzMDU0MzQxYmJmMzRjMWYifQ.Cihc_Cp4l26lisycszwDaClh6hOiUhfa8-GmQOuFFy6ZgL2Fjmxw8yuGCQH7aWCXAe_vN25PK2UdFgNbY0TX3fqfR2c5aGtT_EIBQCESvpkV4CuyEV3TB6PpdoxzB3gEMwQeYwqCg96Nm9DvNcPZ_3o1KsUJXE7_KYghvCozGOMxmulvWLivo57aw4LCqCkA1Am5ljybNNge2Auke7RwJ06u10VUMuoiUIOP6BVk2jkmkE1ru60HGn1XNwb5SuRfMmtOVYYKM07RA71_2eq5V6dpzhD7iqgUH1t92W3g5h7L0225uJKNqci1IsE304m3mneyo3h5ddnliI7HIgqMHA'
}

That same attachment when I pull the comment via /rest/api/3/comment/list:

      "body": {
        "version": 1,
        "type": "doc",
        "content": [
          {
            "type": "mediaGroup",
            "content": [
              {
                "type": "media",
                "attrs": {
                  "id": "de020006-11ee-4c15-bb4e-b3ca56f1019a",
                  "type": "file",
                  "collection": ""
                }
              }
            ]
          },
          {
            "type": "paragraph",
            "content": []
          }
        ]
      },

How is it possible to map these to one another? One uses a filename? The other uses a media id?

Media nodes seem to not have any concept of filenames? https://developer.atlassian.com/cloud/jira/platform/apis/document/nodes/media/

Getting attachment metadata doesn’t return a media id https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-attachment-id-get

1 Like

Replying to myself since new users can’t post 3 links in a post:

There doesn’t seem to be any access to the media API as a whole? Fetching media from Atlassian Document Format - #12 by mchzawalski

Hey @BPB ,

There has to be a connection between the attachment ID you get in the Forge event and the media ID somewhere in the darkness of the underlying Atlassian infrastructure.

This relation is used by the endpoint: /rest/api/3/attachment/content/{id} – it returns the actual (content of the) attachment.

As luck would have it I know that this endpoint answers with a status code 3xx which forwards to the media server.

To illustrate what I mean I have dug out a publicly available attachment from an Atlassian Jira:

https://ecosystem.atlassian.net/rest/api/3/attachment/content/217145

Let’s make a HEAD request to this URL (HEAD returns the headers that a GET would return).

> HEAD /rest/api/3/attachment/content/217145 HTTP/2
> Host: ecosystem.atlassian.net
> cookie: atlassian.xsrf.token=8_redacted
> accept: */*

* TLSv1.2 (IN), TLS header, Supplemental data (23):

< HTTP/2 303 
< date: Wed, 01 Nov 2023 17:34:15 GMT
< content-type: application/json;charset=UTF-8
< server: AtlassianEdge
< timing-allow-origin: *
< x-arequestid: c8b4502dfa6c00146bb553adf468284e
< cache-control: no-cache, no-store, no-transform
< location: https://api.media.atlassian.com/file/ff4a5337-ab17-4572-ac66-04e3d869d92a/binary?token=ey_redacted&client=0a67ae6f-ef7d-4e7e-9d37-ff56929fa074&dl=true&name=image-20231027-152009.png
< server-timing: filter-workcontext;dur=68, filter-frontend-router;dur=54, mcache-client;dur=6, filter-request-papi;dur=68, sql;dur=5
< vary: Accept-Encoding
< x-content-type-options: nosniff
< x-xss-protection: 1; mode=block
[redacted]

The interesting part is the Location header (so the URL Atlassian redirects us too). It is the actual download URL. It contains an ID that looks like the media ID you are looking for. I verified this with the example attachment from above. :+1:

So, this is the path I would pursue.

Next challenge: how to do that in your code? The problem you’ll probably face is that the Forge backend doesn’t give you a real implementation of the Node.js fetch method. But I fear that could be needed to properly implement a “convertAttachmentIdToMediaId” function in Javascript.

A Javascript implementation should probably look like this:

const mediaId = (await fetch("https://ecosystem.atlassian.net/rest/api/3/attachment/content/217145")).url

I’m not sure about the Forge runtime (this could be a good chance to use the new Node runtime which should give you a real fetch) but at least in the browser this works:

Well, after that you’ll have to parse the URL and extract the id from the path. You can use the URL constructor for that. If you came to this point, this shouldn’t be a problem :smiley:

Hope this helps, happy hacking!

Cheers
Julian

3 Likes

Ok so here is the code for posterity. This is using the new node runtime.

function extractUUID(url) {
    const regex = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/;
    const match = url.match(regex);
    return match ? match[0] : null;
}

//issueIdorKey is the issue on which I am operating, sanitizedContent in this case is the file I am attaching, and fileName is the original file name I am working with
async function createAttachment(issueIdOrKey, sanitizedContent, fileName) {
    try {
        var n = fileName.lastIndexOf(".");
        var newFileName = fileName.substring(0, n) + "-cleaned" + fileName.substring(n);
        
        const form = new FormData();

        // Convert sanitizedContent to a Buffer if it's a string or an object
        const fileBuffer = Buffer.from(JSON.stringify(sanitizedContent));
        form.append('file', fileBuffer, { filename: newFileName });
        console.log('uploading file to jira');
        const response = await api.asApp().requestJira(route`/rest/api/3/issue/${issueIdOrKey}/attachments`, {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'X-Atlassian-Token': 'no-check',  // This header is required for file uploads
                ...form.getHeaders()
            },
            body: form
        });

        console.log(`Response: ${response.status} ${response.statusText}`);

        const responseJson = await response.json();
        //console.log(JSON.stringify(responseJson[0].id));

        const requestUrl = `/rest/api/3/attachment/content/${responseJson[0].id}`;

        const attachmentResponse = await api.asApp().requestJira(route`${requestUrl}`, {
            headers: {
                'Accept': 'application/json'
            }
        });
        
        console.log(`New Attachment Response: ${attachmentResponse.status} ${attachmentResponse.statusText} ${attachmentResponse.url}`);   

        const mediaURL = await attachmentResponse.url;
        const newMediaId = extractUUID(mediaURL);

        return {
            status: response.status === 200,
            id: newMediaId
        };
        
    } catch (error) {
        console.error('Error creating attachment:', error);
    }
}

Full app code is available OSS for review at GitHub - AbregaInc/securely

4 Likes