Ghost, seine API und ich

Im Rahmen eines kleinen, neuen Projektes nutze ich eine gesonderte Ghost Instanz, um Bilder, die ich mit meiner Nikon gemacht habe (bzw. machen werde), aller Welt zu zeigen :-). Nachdem die relative stupide - immer wiederkehrende - Arbeit des Bilder umwandelns, Einbetten in die jquery basierte Gallerie etc. auf Dauer eher nervt, machte ich mich dran mir die Ghost API mal genauer anzusehen, allerdings wird mit Aussnahme der Client / User Authentication kaum auf den privaten Teil eingegangen, der für das Erzeugen und Modifizieren von Content aber nötig ist.

Ein wenig Stöbern im WWW aber hauptsächlich einiges an Reverse Engineering hat aber den gewünschen Erfolgt gebracht. Daher hier ein kurzer Überblick was man machen muss, um mittels API Blog-Post zu erzeugen.

Im ersten Schritt muss man die client_id bzw. das client_secret direkt aus der DB ermitteln

select secret from clients where slug='ghost-admin';

Das ermittelte secret muss dann in der Tabelle client_trusted_domains eintragen, quasi auf die Whitelist setzen

insert into client_trusted_domains(id, client_id, trusted_domain) 
values ('<ID>','<SECRET>','<IP ADRESSE>' );

der Wert den man als Wert für die Spalte id eintragen muss, generiert man in dem man im ghost Applikationsverzeichnis eine node shell startet

# node
> require('bson-objectid')()
ObjectID(5a7c4485904d0946371da09e)

Einen gepflegten Neustart von Ghost später, haben wir serverseitig soweit alles vorbereitet, so dass wir uns an die Client Seite machen.

Ein vorhandenes ghost-client pip, lies sich in meiner Umgebung nicht sauber installieren, deswegen hies es "Back to the roots" und ich nahm mir das requests Modul zur Hilfe.

Wie in der offiziellen Doku beschrieben ist, benötigt man das Bearer Token, um sich erfolgreich am Blog zu authentifizieren

 res = requests.post(url + '/ghost/api/v0.1/authentication/token', data={
          'username': user_name,
          'password': user_password,
          'grant_type': 'password',
          'client_id': client_id,
          'client_secret': client_secret,
      })
 
      return json.loads(res.content)['access_token']

Den eigentlichen Post erstellt man dann mittels

 post_content = post_content.replace('"', '\\"').replace('\n', '\\n')
   pd = dict(author="1",
             featured=False,
             feature_image=post_image,
             meta_description=None,
             meta_title=post_title,
             mobiledoc='{"version":"0.3.1","markups":[],"atoms":[],"cards":
             [["card-markdown",{"cardName":"card-markdown","markdown":"' +
             post_content +'"}]],"sections":[[10,0]]}',
             page=False,
             published_by=None,
             slug=post_title,
             status="published",
             title=post_title,
             published_at=created_time)

die interesanntesten Attribute sind an dieser Stelle

AttributBeschreibung
feature_imageFeature Image des Blog Posts, sprich das Post Logo
mobiledocder JSON Wert des Attributes mobiledoc enthält unter anderem den tatsächlichen Post-Markdown
slugKontext unter dem der Blog-Post referenziert werden kann, ich halte den immer parallel zu dem eigentlichen Post-Titel
titleTitel des Blog-Eintrages
statuspublished, um sicher zustellen, dass der Eintrag gleich öffentlich zugänglich ist

Ein simples Beispiel habe ich mal uf Github als Gist hinterlegt

das zugehörige yaml config File sieht wie folgt aus

log:
    url: https://yourblog.de
    username: blogadmin@yourblog.de
    password: 12234_sdfs-§$23-wDM;a
    client_id: ghost-admin
    client_secret: 87Rdbd2br5d4

Ruft man den Snippet mit Werten für den Blog-Titel und den Content ala

ghost_api_demo.py  "Blogpost Test" "<h1>Test via API</h1> <br/>A simple <b>test</b> using the <i>Ghost</i> api<br/>"

auf, wird der entsprechende Post erzeugt