Riding the Mammoth

Seitdem die Social Media Plattform Twitter ins Trudeln geraten und ihre Zukunft ungewisser denn je ist, gewinnen alternative Angebote immer mehr an Bedeutung. Bekannteste Alternative zu Twitter ist das dezentrale und quelloffene Mastodon.

Mastodon ist wie Twitter ein Kurzmitteilung-Service, der aber im Gegensatz zur kommerziellen Plattform einige gravierende Unterschiede aufweist.

Der Mastodon Dienst ist dezentral aufgebaut. Es gibt also nicht einen einzigen Anbietern, sondern jeder kann einen eigenen Mastodon Server betreiben und ihn mit anderen Mastodon Servern verknüpfen. Das ist keine besonders innovative Sache, denn Telefonnetze, E-Mail Dienste und das gesamte WWW arbeitet mit solchen dezentralen Netzen.

Dies Mastodon Server kommunizieren mit anderen Servern über das ActivityPub Protokoll des W3C Consortiums. Das ActivityPub Protocol ist ein standardisiertes Format für dezentrale Social Media Dienste und wird nicht nur von Mastodon genutzt. Auch andere Dienste wie PeerTube (YouTube Alternative), PIXELFED (Instagram Alternative), BookWyrm (GoodRead Alternative) nutzen dieses Protokoll. Damit ist es möglich zwischen diesen recht unterschiedlichen Diensten Daten auszutauschen. Alle diese Dienste gemeinsam werden auch als Fediverse (Federation und Universe) bezeichnet.

Das Fediverse

Für Software Entwickler ist das ganze Ferdiverse eine spannende Sache, denn hier können neue Dienste ersonnen, Erweiterungen für bestehende Dienste, Bibliotheken und Bots implementiert werden. Ein recht bekannter Bot auf Mastodon ist beispielsweise der Mastodon Users (bitcoinhackers.org/@mastodonus). Er liefert in regelmäßigen Abständen Informationen über die aktuelle Größe des Mastodon Dienstes.

Wachstumsdiagramme vom Mastodon Users

Bots sind als solche in ihrem Nutzer Profil gekennzeichnet und bieten Service Informationen jeglicher Art an. Bevor ein eigener Bot entwickelt werden kann, sollten aber erst einmal einige Grundlagen erarbeitet werden. Für Java Entwickler enttäuschend, gibt es keine wirklich gute Bibliothek für Mastodon oder ActivityPub. Erfreulicherweise ist das Arbeiten mit der Mastodon API überaus einfach.

Um sich mit einem Mastodon Server zu unterhalten muss ein Java Client sich mit OAuth2 authentifizieren. Dafür muss einmalig der eigene Client bei einer Instanz angemeldet werden. Dafür existiert der API Endpunkt /api/v1/apps. Das Ergebnis enthält eine Client-ID und ein Client-Secret, die für alle weiteren API Aufrufe benötigt werden.

In diesem Blog Beitrag soll erst einmal nur die eigenen Nutzer Informationen aus Mastodon ausgelesen werden. Dafür wird ein Aufruf auf den Endpunkt /api/v1/accounts/verify_credentials benötigt. Dieser liefert für einen korrekt angemeldeten Benutzer die eigenen Account Informationen.

@Component
@AllArgsConstructor
@Slf4j
public class MastodonClient {

  private final RestTemplate restTemplate;

  public AccountDto verifyAccountCredentials() {
    return restTemplate.getForObject("/api/v1/accounts/verify_credentials", AccountDto.class);
  }
}

Die Spring Boot Komponente MastodonClient verwendet ein RestTemplate um auf den Endpunkt zuzugreifen. Als Ergebnis gibt es eine Instanz vom Typ AccountDto mit allen interessanten Informationen.

Die RestTemplate Instanz für den MastodonClient ist vorkonfiguriert und stammt aus der folgenden MammouthConfig.

@Configuration
@AllArgsConstructor
public class MammouthConfig {

  private final MammothProperties applicationProperties;

  @Bean
  public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder, RestTemplate authenticate) {
    return restTemplateBuilder.rootUri(applicationProperties.getRootUri().toString())
        .additionalInterceptors(new MammothClientHttpRequestInterceptor(authenticate, applicationProperties))
        .defaultHeader("User-Agent", "Mammut 0.0.1").build();
  }

  @Bean
  public RestTemplate authenticate(RestTemplateBuilder restTemplateBuilder) {
    return restTemplateBuilder.rootUri(applicationProperties.getRootUri().toString())
        .defaultHeader("User-Agent", "Mammut 0.0.1").build();
  }
}

Die Methode restTemplate erzeugt ein RestTemplate für die API Anfragen. Dafür verwendet sie den RestTemplateBuilder und ein weiteres RestTemplate, das von der Methode authenticate erzeugt wird. Beide nutzen die Mastodon Server Adresse, die über die applications.properties bereitgestellt wird.

Damit auf die Mastodon API zugegriffen werden kann, benötigt der Client ein Token. Dieses Token liefert der Mastodon Server über den Endpunkt /oauth/token. Dafür werden die Client-ID und Client-Secret der eigenen Anwendung benötigt. Möchte sich die Anwendung für einen Benutzer authentifizieren, dann benötigt sie zusätzlich einen speziellen Autorisierung-Code. Diesen Autorisierung-Code gibt es über einen speziellen Webseiten Aufruf.

https://beispiel-server/oauth/authorize?client_id=DEINE_CLIENT_ID&scope=read+write+follow+push&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code

Nach erfolgter Bestätigung durch den Benutzer, kann der Autorisierung-Code aus der Seite kopiert werden.

Autorisierung-Code des Benutzers

Um die Lösung einfach zu halten, wird der Autorisierung-Code als Kommandozeilen Parameter an die Anwendung übergeben.

Die Autorisierung am Mastodon Server wird während des API Aufrufes durchgeführt. Dafür wurde der eigene MammothClientHttpRequestInterceptor für das RestTemplate konfiguriert. Vor dem eigentlichen REST Call wird geprüft, ob der Anwender schon autorisiert wurde. Fehlt die Autorisierung, dann wird diese über das andere RestTemplate eingeholt.

@Slf4j
public class MammothClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

  @Override
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    MammothAuthentication authentication = getAuthentication();
    if (!authentication.isAuthenticated()) {
      authentication = fetchAuthentication(authentication);
    }
    if (authentication.getDetails() instanceof TokenDto tokenDto) {
      request.getHeaders().setBearerAuth(tokenDto.getAccessToken());
    }
    return execution.execute(request, body);
  }

  private MammothAuthentication getAuthentication() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication instanceof MammothAuthentication mammothAuthentication) {
      return mammothAuthentication;
    }
    return new MammothAuthentication(new TokenDto(), null, false);
  }

  private MammothAuthentication fetchAuthentication(MammothAuthentication currentAuthentication) {
    String credentials = currentAuthentication.getCredentials();
    String grantType = credentials == null ? "client_credentials" : "authorization_code";
    TokenRequest clientCredentials = new TokenRequest(applicationProperties.getClientId(),
        applicationProperties.getClientSecret(), "urn:ietf:wg:oauth:2.0:oob", grantType, credentials);
    TokenDto tokenDto = authenticate.postForObject("/oauth/token", clientCredentials, TokenDto.class);
    MammothAuthentication authentication = new MammothAuthentication(tokenDto, credentials, true);
    SecurityContextHolder.getContext().setAuthentication(authentication);
    return authentication;
  }
}

In der intercept Methode wird zuerst geprüft, ob der Benutzer authentisiert ist und ggf. eine neue MammothAuthentication Instanz angefordert. Danach wird der Authorization Header im REST Request mit dem gespeicherten Bearer-Token befüllt und dann die Verarbeitung des Request fortgeführt.

In der fetch Methode wird der fehlende Bearer-Token über den /oauth/token Endpunkt geholt. enthält die MammothAuthentication Instanz einen Autorisierung-Code als Credential, dann wird ein Autorisierung für den entsprechenden Benutzer durchgeführt, ansonsten nur eine Autorisierung für den Client. Wurde ein korrekte Token vom Server geliefert, dann wird diese in eine neue authentisierte MammothAuthentication eingefügt und im SecurityContextHolder gespeichert.

Beim nächsten API Zugriff findet dann die getAuthentication Methode das authentisierte MammothAuthentication und kann weitere API Aufrufe ohne erneute Autorisierung durchführen.

Unsere erste Mammouth Anwendung sieht nun folgendermaßen aus.

@SpringBootApplication
@EnableConfigurationProperties(ApplicationProperties.class)
@Slf4j
public class MammutApplication implements ApplicationRunner {
  // ...

  @Autowired
  private MastodonClient mastodonClient;

  @Autowired
  private MammothProperties properties;

  @Override
  public void run(ApplicationArguments args) {
    SecurityContextHolder.getContext().setAuthentication(new MammothAuthentication(new TokenDto(), getCode(args), false));
    AccountDto account = mastodonClient.verifyAccountCredentials();
    log.info("account: {}", account);
  }

  private String getCode(ApplicationArguments args) {
    return Optional.ofNullable(args.getOptionValues("code")).filter(Predicate.not(List::isEmpty)).map(l -> l.get(0)).orElse(null);
  }
}

In der run Methode wird eine neue unauthorisierte MammothAuthentication Instanz mit dem Autorisierung-Code von der Kommandozeile erzeugt und im SecurityContext gespeichert. Danach wird die verifyAccountCredentials Methode aufgerufen und die (DSGVO konform gereinigte) Account Informationen ausgegeben.

account: AccountDto(id=xxxx, username=xxxx, acct=xxxx, displayName=xxxx, 
    note=<p>Software Entwickler aus Bielefeld mit Interesse an Agilität, Java und Spring Boot.</p>, 
    avatar=https://xxxxxxxx/system/accounts/avatars/xxxx.jpg, avatarStatic=https://nrw.social/system/accounts/avatars/xxxx.jpg, 
    header=https://nrw.social/system/accounts/headers/xxxx.png, headerStatic=https://nrw.social/system/accounts/headers/xxxx.png, 
    locked=false, discoverable=true, createdAt=20xx-xx-xxT00:00:00Z, lastStatusAt=20xx-xx-xx, 
    statusesCount=xxxx, followersCount=xxxx, followingCount=xxxx)

Der nächste Beitrag zum Thema Mastodon wird sich mit der Implementierung eines eigenen Bots beschäftigen. Bis dahin – Tröööt!

Schreibe einen Kommentar