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.
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.
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.
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!