Publicar en Linkedin con Groovy
Tags: linkedin, groovy
Published: 2023-03-14 06:33:34

---
title: Publicar en Linkedin con Groovy
published: true
date: 2023-02-14 00:00:00 UTC
tags: linkedin,groovy
canonical_url: https://jorge.aguilera.soy/blog/2023/groovy-linkedin.html
---

<dl>
<dt>INFO</dt>
<dd>
<p>Si no has leído el articulo anterior sobre publicar en Linkedin usando Gmail,
te animo a que lo hagas para tener más contexto</p>
</dd>
</dl>

Linkedin, la plataforma del postureo, tiene un API que puedes usar, entre otras cosas, para publicar textos, artículos, imágenes, etc de tal forma que puedes usar herramientas externas o incluso hacerte tu propia integración. En este post vamos a investigar cómo podemos publicar en esta red usando un script en Groovy que podemos planificar

En concreto vamos a ver cómo publicar diariamente la efeméride del CalendarioCientificoEscolar (del cual ya he hablado en alguna otra entrada del blog)

## Requisitos

- Cuenta en Linkedin

- Groovy (y Java) instalados

- Un token de Linkedin (ver el post "Publicar en Linkedin desde Gmail" para saber cómo obtenerlo)

## Idea

El CalendarioCientificoEscolar es un proyecto open source que mantiene en una serie de ficheros CSV una efemeride por día, con textos en diferentes idiomas y una imagen para cada una. La idea es parsear este fichero, buscar la efemeride del día asi como la imagen asociada y publicarlo en Linkedin usando un script un Groovy

<dl>
<dt>WARNING</dt>
<dd>
<p>En el blog ya he comentado en alguna entrada sobre este proyecto por si quieres conocer más de él</p>
</dd>
</dl>

## Script

Este es el script a ejecutar. A continuación comentaré las partes más importantes

```
@Grab('io.github.http-builder-ng:http-builder-ng-core:1.0.4')
@Grab(group='javax.mail', module='mail', version='1.4.7')

import static groovyx.net.http.MultipartContent.multipart
import static groovyx.net.http.HttpBuilder.configure
import static groovyx.net.http.ContentTypes.JSON
import groovyx.net.http.CoreEncoders
import groovyx.net.http.*
import static java.util.Calendar.*

year = args.length > 0 ? args[0] as int : new Date()[YEAR]
month = args.length > 1 ? args[1] as int : new Date()[MONTH]+1
day = args.length > 2 ? args[2] as int : new Date()[DAY_OF_MONTH]

println "Processing $year/$month/$day"

author=System.getenv("LINKEDIN_USER")
accessToken=System.getenv("LINKEDIN_TOKEN")

if( !author || !accessToken ){
    println "Necesito la configuracion de telegram"
    return
}

http = configure{
    request.uri = "https://api.linkedin.com"
    request.contentType = JSON[0]
    request.headers['Authorization'] = "Bearer $accessToken"
    request.headers['X-Restli-Protocol-Version'] = '2.0.0'
}

html = ""
img = ""

['es':'🇪🇸',
 'en':'🏴󠁧󠁢󠁥󠁮󠁧󠁿',
 'fra':'☫',
 'arab':'🇸🇦',
 'pt':'🇵🇹',
 'gal':'🐙',
 'astu':'🐮',
 'eus':'🪨',
 'cat':'🌊',
 'arag':'⛰️',
// 'epo':'🌍',
 ].each{ kv ->
    String lang = kv.key
    String emoji = kv.value
    String[]found

    if( new File("source/csv/${year}/${lang}.tsv").exists() ){
        new File("source/csv/${year}/${lang}.tsv").withReader{ reader ->
            reader.readLine()
            String line
            while( (line=reader.readLine()) != null){
                def fields = line.split('\t')
                if( fields.length != 5)
                    continue
                if( fields[0] as int == day && fields[1] as int == month && fields[2] as int == year){
                    found = fields
                    break
                }
            }
        }
    }

    if(!found){
        println "not found $year/$month/$day"
        return
    }

    String title= found[4].split('\\.').first()
	String body= found[4].split('\\.').drop(1).join(' ')

    if( !img ){
        img = "https://calendario-cientifico-escolar.gitlab.io/_/images/${year}/${found[3]}.png"
    }

    html +="""
$emoji $title

$body

"""
}

html += """
Fuente: Calendario Cientifico Escolar, https://t.me/CalendarioCientifico
"""

json = http.post{
    request.uri.path = "/v2/assets"
    request.uri.query = ["action":"registerUpload"]
    request.body = [
        "registerUploadRequest": [
            "recipes": [
                "urn:li:digitalmediaRecipe:feedshare-image"
            ],
            "owner": "urn:li:person:"+author,
            "serviceRelationships": [
                [
                    "relationshipType": "OWNER",
                    "identifier": "urn:li:userGeneratedContent"
                ]
            ],
            "supportedUploadMechanism":[
                "SYNCHRONOUS_UPLOAD"
            ]
        ]
    ]
}
uploadUrl = json.value.uploadMechanism["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"].uploadUrl

someFile = File.createTempFile("img","png")
someFile.bytes = img.toURL().bytes

configure{
    request.uri = uploadUrl
    request.contentType = 'image/png'
    request.headers['Authorization'] = "Bearer $accessToken"
    request.headers['X-Restli-Protocol-Version'] = '2.0.0'
    request.headers['Accept']='*/*'
    request.body = someFile
    request.encoder('image/png'){ ChainedHttpConfig config, ToServer req->
        req.toServer(new ByteArrayInputStream(someFile.bytes))
    }
}.put()

post = [
    "author": "urn:li:person:"+author,
    "lifecycleState":'PUBLISHED',
    "visibility": [
        "com.linkedin.ugc.MemberNetworkVisibility":'PUBLIC'
    ],
    "specificContent":[
        "com.linkedin.ugc.ShareContent":[
            "shareCommentary":[
                "text": html
            ],
            "shareMediaCategory" : "IMAGE",
            "media":[
                [status:'READY',media: json.value.asset]
            ]
        ]
    ]
]

http.post{
    request.uri.path = "/v2/ugcPosts"
    request.body = post
}
```

## Calendario

Como he comentado el calendario se compone de varios ficheros cada uno con una efemeride en un idioma. Lo que hacemos es crear un mapa con los idiomas disponibles e ir concatenando en una cadena los diferentes textos. Cada idioma será un párrafo con un emoji al inicio:

```
['es':'🇪🇸',
 'en':'🏴󠁧󠁢󠁥󠁮󠁧󠁿',
 'fra':'☫',
 'arab':'🇸🇦',
 'pt':'🇵🇹',
 'gal':'🐙',
 'astu':'🐮',
 'eus':'🪨',
 'cat':'🌊',
 'arag':'⛰️',
// 'epo':'🌍',
 ].each{ kv ->
    String lang = kv.key
    String emoji = kv.value
    String[]found

    if( new File("source/csv/${year}/${lang}.tsv").exists() ){
        new File("source/csv/${year}/${lang}.tsv").withReader{ reader ->
...
    html +="""
$emoji $title

$body

"""
}
```

Básicamente una vez ejecutado el bucle tenemos una cadena con el texto que queremos publicar.

## Librerías

Las dos primeras lineas del script nos sirven para indicar las dependencias que tiene el mismo. En este caso uso un proyecto `http-ng-builder` que me encanta aunque ya está abandonado (creo) y la librería standard de javax mail para poder subir la imagen.

## Linkedin

Este proyecto nos proporciona un DSL muy intuitivo para interactuar con servicios remotos.

- Preparamos un objeto `http` con el token de acceso:

```
http = configure{
    request.uri = "https://api.linkedin.com"
    request.contentType = JSON[0]
    request.headers['Authorization'] = "Bearer $accessToken"
    request.headers['X-Restli-Protocol-Version'] = '2.0.0'
}
```

- Le decimos a Linkedin que queremos subir una imagen:

```
json = http.post{
    request.uri.path = "/v2/assets"
    request.uri.query = ["action":"registerUpload"]
    request.body = [
        "registerUploadRequest": [
            "recipes": [
                "urn:li:digitalmediaRecipe:feedshare-image"
            ],
            "owner": "urn:li:person:"+author,
            "serviceRelationships": [
                [
                    "relationshipType": "OWNER",
                    "identifier": "urn:li:userGeneratedContent"
                ]
            ],
            "supportedUploadMechanism":[
                "SYNCHRONOUS_UPLOAD"
            ]
        ]
    ]
}
uploadUrl = json.value.uploadMechanism["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"].uploadUrl
```

- Linkedin nos proporciona una URL (uploadURL) donde subir nuestra imagen mediante el método `put`:

```
configure{
    request.uri = uploadUrl
    request.contentType = 'image/png'
    request.headers['Authorization'] = "Bearer $accessToken"
    request.headers['X-Restli-Protocol-Version'] = '2.0.0'
    request.headers['Accept']='*/*'
    request.body = someFile
    request.encoder('image/png'){ ChainedHttpConfig config, ToServer req->
        req.toServer(new ByteArrayInputStream(someFile.bytes))
    }
}.put()
```

- Publicamos el artículo indicando como atributo el ID de la imagen a adjuntar

```
post = [
    "author": "urn:li:person:"+author,
    "lifecycleState":'PUBLISHED',
    "visibility": [
        "com.linkedin.ugc.MemberNetworkVisibility":'PUBLIC'
    ],
    "specificContent":[
        "com.linkedin.ugc.ShareContent":[
            "shareCommentary":[
                "text": html
            ],
            "shareMediaCategory" : "IMAGE",
            "media":[
                [status:'READY',media: json.value.asset]
            ]
        ]
    ]
]

http.post{
    request.uri.path = "/v2/ugcPosts"
    request.body = post
}
```

## Conclusion

Obviamente este script es muy específico para el caso del Calendario pero es muy fácil extraer las partes necesarias para poder crear uno que se adapte a nuestras necesidades y poder publicar en Linkedin de forma desatendida