initial commit

This commit is contained in:
2025-09-19 18:48:08 +02:00
commit 60dfe90152
7 changed files with 626 additions and 0 deletions
@@ -0,0 +1,57 @@
package de.jalin.fedipage;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URISyntaxException;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class FediServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private VelocityEngine velocityEngine;
@Override
public void init() throws ServletException {
velocityEngine = new VelocityEngine();
velocityEngine.setProperty("resource.loaders", "class");
velocityEngine.setProperty("resource.loader.class.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
velocityEngine.init();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
final JSONLoader loader = new JSONLoader("mastodon.social", "Mastodon");
final VelocityContext context = new VelocityContext();
context.put("profile", loader.loadProfile());
context.put("pinned", loader.loadPinned());
context.put("timeline", loader.loadTimeline());
final Template template = velocityEngine.getTemplate("/templates/profil.vm");
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter writer = response.getWriter()) {
template.merge(context, writer);
}
} catch (ResourceNotFoundException e) {
throw new ServletException("Template nicht gefunden", e);
} catch (ParseErrorException e) {
throw new ServletException("Fehler beim Parsen des Templates", e);
} catch (URISyntaxException e) {
throw new ServletException("Instanz URL ist falsch", e);
}
}
}
@@ -0,0 +1,99 @@
package de.jalin.fedipage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONObject;
public class JSONLoader implements Serializable {
private static final long serialVersionUID = 7255602422151204643L;
private final String instance;
private final String account;
private int id;
public JSONLoader(String instance, String account) {
this.instance = instance;
this.account = account;
this.id = -1;
}
public JSONObject loadProfile() throws IOException, URISyntaxException {
final URI uri = new URI("https://" + instance + "/api/v1/accounts/lookup?acct=" + account);
final URL url = uri.toURL();
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
final StringBuilder content = new StringBuilder();
while ((line = reader.readLine()) != null) {
content.append(line);
}
final JSONObject data = new JSONObject(content.toString());
id = data.getInt("id");
return data;
}
public JSONArray loadPinned() throws IOException, URISyntaxException {
final JSONArray pinnedStatusesArray = loadStatuses("pinned=true");
// final ArrayList<Status> pinnedStatuses = new ArrayList<>();
return pinnedStatusesArray;
}
public JSONArray loadTimeline() throws IOException, URISyntaxException {
final JSONArray lastStatusesArray = loadStatuses("limit=20");
// for (int idx=0; idx< lastStatusesArray.length(); idx++) {
// final Object object = lastStatusesArray.get(idx);
// JSONObject json = (JSONObject) object;
// final Status status = new Status();
// JSONObject account = json.getJSONObject("account");
// if (json.get("reblog") != null) {
// status.setBoostedBy(account.getString("display_name"));
// json = json.getJSONObject("reblog");
// }
// }
// final ArrayList<Status> latestStatuses = new ArrayList<>();
return lastStatusesArray;
}
private JSONArray loadStatuses(final String filter) throws IOException, URISyntaxException {
if (id < 0) {
loadProfile();
}
final URI uri = new URI("https://" + instance + "/api/v1/accounts/" + id + "/statuses?" + filter);
final URL url = uri.toURL();
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
final StringBuilder content = new StringBuilder();
while ((line = reader.readLine()) != null) {
content.append(line);
}
return new JSONArray(content.toString());
}
public static void main(String[] args) {
// final JSONLoader loader = new JSONLoader(args[0], args[1]);
final JSONLoader loader = new JSONLoader("krefeld.life", "darestiet");
try {
final JSONArray pinned = loader.loadTimeline();
for (Object obj : pinned) {
@SuppressWarnings("unchecked")
final Map<String, ?> json = (Map<String, ?>) obj;
System.out.println(json.toString());
}
} catch (IOException | URISyntaxException e) {
}
}
}
+380
View File
@@ -0,0 +1,380 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$profile.display_name - $profile.username</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a1a;
color: #ffffff;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
background: #282c37;
min-height: 100vh;
}
.header {
position: relative;
height: 200px;
background: linear-gradient(45deg, #6364ff, #563acc);
overflow: hidden;
}
.banner {
width: 100%;
height: 100%;
object-fit: cover;
}
.profile-section {
position: relative;
padding: 0 20px 20px;
background: #282c37;
}
.profile-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
border: 4px solid #282c37;
margin-top: -40px;
background: #fff;
}
.profile-info {
margin-top: 15px;
}
.display-name {
font-size: 20px;
font-weight: bold;
margin-bottom: 5px;
}
.username {
color: #9baec8;
margin-bottom: 10px;
}
.username a {
color: #9baec8;
text-decoration: none;
}
.bio {
margin-bottom: 15px;
line-height: 1.5;
}
.stats {
display: flex;
gap: 20px;
font-size: 14px;
color: #9baec8;
}
.controls {
padding: 20px;
background: #313543;
border-bottom: 1px solid #393f4f;
}
.checkbox-group {
display: flex;
gap: 20px;
align-items: center;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: #9baec8;
}
.checkbox-group input[type="checkbox"] {
accent-color: #6364ff;
}
.posts {
background: #282c37;
}
.post {
border-bottom: 1px solid #393f4f;
padding: 15px 20px;
}
.post-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.post-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.post-user {
flex: 1;
}
.post-display-name {
font-weight: bold;
font-size: 14px;
}
.post-username {
color: #9baec8;
font-size: 13px;
}
.post-username a {
color: #9baec8;
font-size: 13px;
text-decoration: none;
}
.post-date {
color: #9baec8;
font-size: 13px;
}
.post-content {
margin-bottom: 10px;
}
.post-content p {
margin-bottom: 10px;
}
.post-content a {
color: #6364ff;
text-decoration: none;
}
.post-content a:hover {
text-decoration: underline;
}
.hashtag {
color: #00d4aa;
}
.mention {
color: #6364ff;
}
.post-media {
margin-top: 10px;
}
.media-grid {
display: grid;
gap: 4px;
border-radius: 8px;
overflow: hidden;
}
.media-grid.single {
grid-template-columns: 1fr;
}
.media-grid.double {
grid-template-columns: 1fr 1fr;
}
.media-grid.multiple {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.media-item {
position: relative;
background: #000;
}
.media-item img {
width: 100%;
height: 200px;
object-fit: cover;
cursor: pointer;
}
.post-stats {
display: flex;
gap: 15px;
margin-top: 10px;
color: #9baec8;
font-size: 13px;
}
.boost-indicator {
color: #00d4aa;
font-size: 13px;
margin-bottom: 10px;
}
.loading {
text-align: center;
padding: 40px;
color: #9baec8;
}
.error {
text-align: center;
padding: 40px;
color: #ff5050;
}
.refresh-btn {
background: #6364ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
}
.refresh-btn:hover {
background: #5a5bef;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img id="banner" class="banner" alt="Banner" src="$profile.header">
</div>
<div class="profile-section">
<img id="avatar" class="profile-avatar" alt="Avatar" src="$profile.avatar">
<div class="profile-info">
<div id="displayName" class="display-name">$profile.display_name</div>
<div id="username" class="username"><a href="$profile.url">@$profile.username</a></div>
<div id="bio" class="bio">$profile.note</div>
<div class="stats">
<span id="followingCount">$profile.following_count Folgt</span>
<span id="followersCount">$profile.followers_count Follower</span>
<span id="statusesCount">$profile.statuses_count Beiträge</span>
</div>
</div>
</div>
<!--
<div class="controls">
<div class="checkbox-group">
<label>
<input type="checkbox" id="includeBoosts">
Geteilte Inhalte anzeigen
</label>
<label>
<input type="checkbox" id="includeReplies">
Antworten anzeigen
</label>
<button class="refresh-btn" onclick="loadPosts()">Aktualisieren</button>
</div>
</div>
-->
<!--
<div id="loading" class="loading">Lade Daten...</div>
<div id="error" class="error" style="display: none;"></div>
-->
<div id="posts" class="posts">
#foreach( $post in $pinned )
<div class="post">
<div class="boost-indicator">📌 Angeheftet</div>
<div class="post-header">
<img class="post-avatar" src="$post.account.avatar" alt="Avatar">
<div class="post-user">
<div class="post-display-name">$post.account.display_name</div>
<div class="post-username"><a href="$post.account.url">@$post.account.acct</a></div>
</div>
</div>
<div class="post-content">$post.content</div>
#if( $post.media_attachments.length > 0 )
<div class="post-media">
<div class="media-grid single">
<div class="media-item">
<img src="$post.media_attachments[0].preview_url" alt="$post.media_attachments[0].description" onclick="window.open('$post.media_attachments[0].url', '_blank')">
</div>
</div>
</div>
#end
<div class="post-stats">
<span>🔄 $post.reblogs_count</span>
<span>⭐ $post.favourites_count</span>
<span>💬 $post.replies_count</span>
</div>
</div>
#end
#foreach( $post in $timeline )
<div class="post">
#if( $post.reblog.account )
<div class="boost-indicator">🔄 $post.account.display_name hat geteilt</div>
<div class="post-header">
<img class="post-avatar" src="$post.reblog.account.avatar" alt="Avatar">
<div class="post-user">
<div class="post-display-name">$post.reblog.account.display_name</div>
<div class="post-username"><a href="$post.reblog.account.url">@$post.reblog.account.acct</a></div>
</div>
</div>
<div class="post-content">$post.reblog.content</div>
#if( $post.reblog.media_attachments.length > 0 )
<div class="post-media">
<div class="media-grid single">
<div class="media-item">
<img src="$post.reblog.media_attachments[0].preview_url" alt="$post.reblog.media_attachments[0].description" onclick="window.open('$post.reblog.media_attachments[0].url', '_blank')">
</div>
</div>
</div>
#end
#else
<div class="post-header">
<img class="post-avatar" src="$post.account.avatar" alt="Avatar">
<div class="post-user">
<div class="post-display-name">$post.account.display_name</div>
<div class="post-username"><a href="$post.account.url">@$post.account.acct</a></div>
</div>
</div>
<div class="post-content">$post.content</div>
#if( $post.media_attachments.length > 0 )
<div class="post-media">
<div class="media-grid single">
<div class="media-item">
<img src="$post.media_attachments[0].preview_url" alt="$post.media_attachments[0].description" onclick="window.open('$post.media_attachments[0].url', '_blank')">
</div>
</div>
</div>
#end
<div class="post-stats">
<span>🔄 $post.reblogs_count</span>
<span>⭐ $post.favourites_count</span>
<span>💬 $post.replies_count</span>
</div>
#end
</div>
#end
</div>
</div>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Fediverse Homepage</display-name>
<servlet>
<servlet-name>FediServlet</servlet-name>
<servlet-class>de.jalin.fedipage.FediServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FediServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>/</welcome-file>
</welcome-file-list>
</web-app>