diff --git a/modules/BlueskyGephi/README.md b/modules/BlueskyGephi/README.md
new file mode 100644
index 000000000..bb4757f7b
--- /dev/null
+++ b/modules/BlueskyGephi/README.md
@@ -0,0 +1,39 @@
+# Bluesky Gephi
+
+This plugin allow you to visualize and explore the network of users in bluesky via the atprotocol.
+
+# Quick start
+- Get a bluesky account
+- Generate a password https://bsky.app/settings/app-passwords
+- Install the plugin in Gephi
+- Open Gephi
+- Put handle and password information
+- Search for yourself
+- Graph of your connection should appears.
+
+# Docs
+
+Keep in mind current atproto access point from bluesky is quite permissive and might change in the future.
+
+## Fetch from user
+You can fetch network from user from multiple way :
+- Put one or multiple (separated by line return) handles or dids inside the plugin textarea and click on "Go!"
+- You can right click on a node and select contextual menu item related to the plugin
+ - **Bluesky Fetch default data** , will fetch network based on the current configuration on the plugin panel
+ - **Fetch followers only data**, will fetch only the followers of the node
+ - **Fetch follows only data**, will fetch only the follows of the node
+
+## Deep Search
+By activating **Fetch also n+1**, the plugin will fetch the selected handles network **and also** the network of the handles found.
+
+/!\ Keep in mind that this can be very long as some users has a long list of followers or follows. /!\
+
+## Crawl Limit
+To have the list of the followers and follows of a user, the atproto api is build in a way that the application need to loop over multiple
+"pages". It means that for hub user, that have a high number of followers and follows (10k, 100k,1M) it might take an important amount of time
+to retrive information for this kind of user.
+
+Therefore, there is a possibility to limit this by only retriving a fraction of the followers and follows in order to speedup the exploration.
+
+It's ok to do that if analysing theses hub isn't your main goal, as if theses hub are highly connected, they will automatically appears on the relationship
+of other users.
\ No newline at end of file
diff --git a/modules/BlueskyGephi/pom.xml b/modules/BlueskyGephi/pom.xml
new file mode 100644
index 000000000..18da75772
--- /dev/null
+++ b/modules/BlueskyGephi/pom.xml
@@ -0,0 +1,103 @@
+
+
+ 4.0.0
+
+ gephi-plugin-parent
+ org.gephi
+ 0.10.0
+
+
+ fr.totetmatt
+ bluesky-gephi
+ 0.1.0
+ nbm
+
+ Bluesky Gephi
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.13.4.1
+
+
+ org.netbeans.api
+ org-openide-awt
+
+
+ ${project.parent.groupId}
+ visualization-api
+
+
+ ${project.parent.groupId}
+ datalab-api
+
+
+ org.netbeans.api
+ org-openide-windows
+
+
+ org.netbeans.api
+ org-netbeans-modules-settings
+
+
+ org.netbeans.api
+ org-openide-util-lookup
+
+
+ org.netbeans.api
+ org-openide-util
+
+
+ org.gephi
+ graph-api
+
+
+ org.gephi
+ project-api
+
+
+ org.gephi
+ desktop-project
+
+
+
+ org.gephi
+ utils-longtask
+
+
+
+
+
+
+ org.apache.netbeans.utilities
+ nbm-maven-plugin
+
+ skip
+ Apache 2.0
+ totetmatt
+ matthieu.totet@gmail.com
+ https://totetmatt.fr
+ https://github.com/totetmatt/gephi-plugins.git
+
+
+
+
+
+
+
+
+
+
+
+ oss-sonatype
+ oss-sonatype
+ https://oss.sonatype.org/content/repositories/snapshots/
+
+ true
+
+
+
+
+
+
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephi.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephi.java
new file mode 100644
index 000000000..7efadd9f4
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephi.java
@@ -0,0 +1,295 @@
+package fr.totetmatt.blueskygephi;
+
+import fr.totetmatt.blueskygephi.atproto.AtClient;
+import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetFollowers;
+import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetFollows;
+import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetList;
+import fr.totetmatt.blueskygephi.atproto.response.common.Identity;
+import java.awt.Color;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.prefs.Preferences;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.gephi.graph.api.Edge;
+import org.gephi.graph.api.GraphController;
+import org.gephi.graph.api.GraphModel;
+import org.gephi.graph.api.Node;
+import org.gephi.project.api.Project;
+import org.gephi.project.api.ProjectController;
+import org.gephi.utils.progress.Progress;
+import org.gephi.utils.progress.ProgressTicket;
+import org.gephi.utils.progress.ProgressTicketProvider;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.NbPreferences;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author totetmatt
+ */
+@ServiceProvider(service = BlueskyGephi.class)
+public class BlueskyGephi {
+
+ protected static final Logger logger = Logger.getLogger(BlueskyGephi.class.getName());
+ private final static String NBPREF_BSKY_HANDLE = "bsky.handle";
+ private final static String NBPREF_BSKY_PASSWORD = "bsky.password";
+ private final static String NBPREF_QUERY = "query";
+ private final static String NBPREF_QUERY_ISFOLLOWERSACTIVE = "query.isFollowersActive";
+ private final static String NBPREF_QUERY_ISFOLLOWSACTIVE = "query.isFollowsActive";
+ private final static String NBPREF_QUERY_ISDEEPSEARCH = "query.isDeepSearch";
+ private final static String NBPREF_QUERY_ISLIMITCRAWLACTIVE = "query.isLimitCrawlActive";
+ private final static String NBPREF_QUERY_LIMITCRAWL = "query.limitCrawl";
+
+ private final Preferences nbPref = NbPreferences.forModule(BlueskyGephi.class);
+ // If ATProto get released and decentralized, this will change to adapt to other instances
+ final private AtClient client = new AtClient("bsky.social");
+ private GraphModel graphModel;
+
+ public BlueskyGephi() {
+ initProjectAndWorkspace();
+
+ }
+
+ private void initProjectAndWorkspace() {
+ ProjectController projectController = Lookup.getDefault().lookup(ProjectController.class);
+ Project currentProject = projectController.getCurrentProject();
+ if (currentProject == null) {
+ projectController.newProject();
+ }
+ }
+
+ public boolean connect(String handle, String password) {
+ nbPref.put(NBPREF_BSKY_HANDLE, handle);
+ nbPref.put(NBPREF_BSKY_PASSWORD, password);
+
+ return client.comAtprotoServerCreateSession(handle, password);
+
+ }
+
+ public String getHandle() {
+ return nbPref.get(NBPREF_BSKY_HANDLE, "");
+ }
+
+ public String getPassword() {
+ return nbPref.get(NBPREF_BSKY_PASSWORD, "");
+ }
+
+ public void setQuery(String query) {
+ nbPref.put(NBPREF_QUERY, query);
+ }
+
+ public String getQuery() {
+ return nbPref.get(NBPREF_QUERY, "");
+ }
+
+ public void setIsFollowersActive(boolean isFollowersActive) {
+ nbPref.putBoolean(NBPREF_QUERY_ISFOLLOWERSACTIVE, isFollowersActive);
+ }
+
+ public boolean getIsFollowersActive() {
+ return nbPref.getBoolean(NBPREF_QUERY_ISFOLLOWERSACTIVE, true);
+ }
+
+ public void setIsFollowsActive(boolean isFollowsActive) {
+ nbPref.putBoolean(NBPREF_QUERY_ISFOLLOWSACTIVE, isFollowsActive);
+ }
+
+ public boolean getIsFollowsActive() {
+ return nbPref.getBoolean(NBPREF_QUERY_ISFOLLOWSACTIVE, true);
+ }
+
+ public void setIsDeepSearch(boolean setIsDeepSearch) {
+ nbPref.putBoolean(NBPREF_QUERY_ISDEEPSEARCH, setIsDeepSearch);
+ }
+
+ public boolean getIsDeepSearch() {
+ return nbPref.getBoolean(NBPREF_QUERY_ISDEEPSEARCH, true);
+ }
+
+ public void setIsLimitCrawlActive(boolean isLimitCrawlActive) {
+ nbPref.putBoolean(NBPREF_QUERY_ISLIMITCRAWLACTIVE, isLimitCrawlActive);
+ }
+
+ public boolean getIsLimitCrawlActive() {
+ return nbPref.getBoolean(NBPREF_QUERY_ISLIMITCRAWLACTIVE, true);
+ }
+
+ public void setLimitCrawl(int limitCrawl) {
+ nbPref.putInt(NBPREF_QUERY_LIMITCRAWL, limitCrawl);
+ }
+
+ public int getLimitCrawl() {
+ return nbPref.getInt(NBPREF_QUERY_LIMITCRAWL, 50);
+ }
+
+ private Node createNode(Identity i) {
+
+ Node node = graphModel.getGraph().getNode(i.getDid());
+ if (node == null) {
+ node = graphModel.factory().newNode(i.getDid());
+ node.setLabel(i.getHandle());
+ node.setAttribute("Description", i.getDescription());
+ node.setSize(10);
+ node.setColor(Color.GRAY);
+ node.setX((float) ((0.01 + Math.random()) * 1000) - 500);
+ node.setY((float) ((0.01 + Math.random()) * 1000) - 500);
+ graphModel.getGraph().addNode(node);
+ }
+
+ return node;
+ }
+
+ private Edge createEdge(Node source, Node target) {
+
+ Edge edge = graphModel.getGraph().getEdge(source, target);
+ if (edge == null) {
+ edge = graphModel.factory().newEdge(source, target, true);
+ edge.setWeight(1.0);
+ edge.setColor(Color.GRAY);
+ graphModel.getGraph().addEdge(edge);
+ }
+
+ return edge;
+ }
+
+ private void fetchFollowerFollowsFromActor(String actor, List listInit, boolean isFollowsActive, boolean isFollowersActive, boolean isDeepSearch) {
+ // To avoid locking Gephi UI
+ Thread t = new Thread() {
+ private ProgressTicket progressTicket;
+ Set foaf = new HashSet<>();
+
+ private void process(String actor, boolean isDeepSearch, Optional limitCrawl) {
+
+ try {
+ if (isFollowsActive) {
+ List responses = client.appBskyGraphGetFollows(actor, limitCrawl);
+
+ for (var response : responses) {
+ graphModel.getGraph().writeLock();
+ Identity subject = response.getSubject();
+ Node source = createNode(subject);
+ source.setColor(Color.GREEN);
+ for (var follow : response.getFollows()) {
+ if (isDeepSearch) {
+ foaf.add(follow.getDid());
+ }
+ Node target = createNode(follow);
+ createEdge(source, target);
+ }
+ graphModel.getGraph().writeUnlock();
+
+ }
+
+ }
+
+ if (isFollowersActive) {
+ List responses = client.appBskyGraphGetFollowers(actor, limitCrawl);
+
+ for (var response : responses) {
+ graphModel.getGraph().writeLock();
+ Identity subject = response.getSubject();
+ Node target = createNode(subject);
+ target.setColor(Color.GREEN);
+ for (var follower : response.getFollowers()) {
+ if (isDeepSearch) {
+ foaf.add(follower.getDid());
+ }
+ Node source = createNode(follower);
+ createEdge(source, target);
+ }
+ graphModel.getGraph().writeUnlock();
+ }
+
+ }
+ } catch (Exception e) {
+ Exceptions.printStackTrace(e);
+ } finally {
+ }
+ }
+
+ @Override
+ public void run() {
+
+ if (actor != null) {
+ this.setName("[Bsky] fetching" + actor);
+ } else {
+ this.setName("[Bsky] fetching List");
+ }
+ progressTicket = Lookup.getDefault()
+ .lookup(ProgressTicketProvider.class)
+ .createTicket(this.getName(), () -> {
+ interrupt();
+ Progress.finish(progressTicket);
+ return true;
+ });
+ Progress.start(progressTicket);
+ Progress.switchToIndeterminate(progressTicket);
+
+ if (listInit != null) {
+ this.foaf.addAll(listInit);
+ }
+ if (actor != null) {
+ process(actor, isDeepSearch, Optional.empty());
+ }
+ if (listInit != null || isDeepSearch) {
+ Progress.switchToDeterminate(progressTicket, foaf.size());
+ for (var foafActor : foaf) {
+ Progress.setDisplayName(progressTicket, "[Bsky] fetching " + actor + " n+1 > " + foafActor);
+ if (getIsLimitCrawlActive()) {
+ process(foafActor, false, Optional.of(getLimitCrawl()));
+ } else {
+ process(foafActor, false, Optional.empty());
+ }
+ Progress.progress(progressTicket);
+
+ }
+ }
+ Progress.finish(progressTicket);
+ }
+ };
+ t.start();
+
+ }
+
+ private Stream manageList(String listId) {
+ List list = client.appBskyGraphGetList(listId);
+ return list.stream().flatMap(x -> x.getItems().stream().map(y -> y.getSubject().getDid()));
+
+ }
+
+ private void initGraphTable() {
+ // Create necessary model for the graph entities
+ if (!graphModel.getNodeTable().hasColumn("Description")) {
+ graphModel.getNodeTable().addColumn("Description", String.class);
+ }
+ }
+
+ public void fetchFollowerFollowsFromActors(List actors, boolean isFollowsActive, boolean isFollowersActive, boolean isBlocksActive) {
+ graphModel = Lookup.getDefault().lookup(GraphController.class).getGraphModel();
+ initGraphTable();
+ actors.stream().forEach(actor -> fetchFollowerFollowsFromActor(actor, null, isFollowsActive, isFollowersActive, getIsDeepSearch()));
+ }
+
+ public void fetchFollowerFollowsFromActors(List actors) {
+ graphModel = Lookup.getDefault().lookup(GraphController.class).getGraphModel();
+ initGraphTable();
+ actors
+ .stream()
+ .filter(x -> !x.contains("app.bsky.graph.list"))
+ .forEach(actor -> fetchFollowerFollowsFromActor(actor, null, getIsFollowsActive(), getIsFollowersActive(), getIsDeepSearch()));
+
+ List listActor = actors
+ .stream()
+ .filter(x -> x.contains("app.bsky.graph.list"))
+ .flatMap(this::manageList)
+ .collect(Collectors.toList());
+
+ fetchFollowerFollowsFromActor(null, listActor, getIsFollowsActive(), getIsFollowersActive(), getIsDeepSearch());
+
+ }
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.form b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.form
new file mode 100644
index 000000000..42402520a
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.form
@@ -0,0 +1,278 @@
+
+
+
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.java
new file mode 100644
index 000000000..0cb5f9bfd
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.java
@@ -0,0 +1,311 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/GUIForms/JPanel.java to edit this template
+ */
+package fr.totetmatt.blueskygephi;
+
+import java.awt.Color;
+import java.awt.Desktop;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.windows.TopComponent;
+
+/**
+ *
+ * @author totetmatt
+ */
+
+@TopComponent.Description(preferredID = "BlueskyGephiMainPanel",
+ iconBase = "fr/totetmatt/gephi/twitter/twitterlogo.png"
+)
+
+
+@ActionReference(path = "Menu/Window", position = 334)
+@TopComponent.OpenActionRegistration(displayName = "Bluesky Gephi",
+ preferredID = "MainTwitterStreamerWindow")
+@ActionID(category = "Window", id = "fr.totetmatt.blueskygephi.BlueskyGephiMainPanel")
+@TopComponent.Registration(mode = "layoutmode", openAtStartup = true, position=2)
+public class BlueskyGephiMainPanel extends TopComponent {
+ protected static final Logger consoleLogger = Logger.getLogger(BlueskyGephiMainPanel.class.getName());
+ private final BlueskyGephi blueskyGephi;
+ /**
+ * Creates new form BlueskyGephiMainPanel
+ */
+ public BlueskyGephiMainPanel() {
+ initComponents();
+ blueskyGephi = Lookup.getDefault().lookup(BlueskyGephi.class);
+
+ credentialsHandleField.setText(blueskyGephi.getHandle());
+ credentialsPasswordField.setText(blueskyGephi.getPassword());
+ handleSearchTextArea.setText(blueskyGephi.getQuery());
+ isFollowersActivated.setSelected(blueskyGephi.getIsFollowersActive());
+ isFollowsActivated.setSelected(blueskyGephi.getIsFollowsActive());
+ isDeepSearch.setSelected(blueskyGephi.getIsDeepSearch());
+ limitCrawlCheckbox.setSelected(blueskyGephi.getIsLimitCrawlActive());
+ limitCrawlSpinner.setValue(blueskyGephi.getLimitCrawl()*100);
+ limitCrawlSpinner.setEnabled(limitCrawlCheckbox.isSelected());
+ }
+
+ /**
+ * This method is called from within the constructor to initialize the form.
+ * WARNING: Do NOT modify this code. The content of this method is always
+ * regenerated by the Form Editor.
+ */
+ @SuppressWarnings("unchecked")
+ // //GEN-BEGIN:initComponents
+ private void initComponents() {
+
+ credentialsPanel = new javax.swing.JPanel();
+ credentialsHandleLabel = new javax.swing.JLabel();
+ credentialsHandleField = new javax.swing.JTextField();
+ credentialsPasswordLabel = new javax.swing.JLabel();
+ credentialsPasswordField = new javax.swing.JPasswordField();
+ credentialsConnectButton = new javax.swing.JButton();
+ jScrollPane1 = new javax.swing.JScrollPane();
+ handleSearchTextArea = new javax.swing.JTextArea();
+ fetchLabel = new javax.swing.JLabel();
+ isFollowersActivated = new javax.swing.JCheckBox();
+ isFollowsActivated = new javax.swing.JCheckBox();
+ runFetchButton = new javax.swing.JButton();
+ isDeepSearch = new javax.swing.JCheckBox();
+ limitCrawlCheckbox = new javax.swing.JCheckBox();
+ limitCrawlSpinner = new javax.swing.JSpinner();
+
+ setToolTipText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.toolTipText")); // NOI18N
+ setName("Bluesky Gephi"); // NOI18N
+
+ credentialsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsPanel.border.title"))); // NOI18N
+ credentialsPanel.setToolTipText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.Credentials.toolTipText")); // NOI18N
+ credentialsPanel.setName("Credentials"); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(credentialsHandleLabel, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsHandleLabel.text")); // NOI18N
+
+ credentialsHandleField.setText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsHandleField.text")); // NOI18N
+
+ credentialsPasswordLabel.setForeground(new java.awt.Color(0, 51, 255));
+ org.openide.awt.Mnemonics.setLocalizedText(credentialsPasswordLabel, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsPasswordLabel.text")); // NOI18N
+ credentialsPasswordLabel.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR));
+ credentialsPasswordLabel.addMouseListener(new java.awt.event.MouseAdapter() {
+ public void mouseClicked(java.awt.event.MouseEvent evt) {
+ credentialsPasswordLabelMouseClicked(evt);
+ }
+ });
+
+ credentialsPasswordField.setText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsPasswordField.text")); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(credentialsConnectButton, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsConnectButton.text")); // NOI18N
+ credentialsConnectButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ credentialsConnectButtonActionPerformed(evt);
+ }
+ });
+
+ javax.swing.GroupLayout credentialsPanelLayout = new javax.swing.GroupLayout(credentialsPanel);
+ credentialsPanel.setLayout(credentialsPanelLayout);
+ credentialsPanelLayout.setHorizontalGroup(
+ credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(credentialsPanelLayout.createSequentialGroup()
+ .addContainerGap()
+ .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(credentialsConnectButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
+ .addGroup(credentialsPanelLayout.createSequentialGroup()
+ .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(credentialsPasswordLabel)
+ .addComponent(credentialsHandleLabel))
+ .addGap(18, 18, 18)
+ .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(credentialsHandleField)
+ .addComponent(credentialsPasswordField))))
+ .addContainerGap())
+ );
+ credentialsPanelLayout.setVerticalGroup(
+ credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(credentialsPanelLayout.createSequentialGroup()
+ .addContainerGap()
+ .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(credentialsHandleLabel)
+ .addComponent(credentialsHandleField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(credentialsPasswordLabel)
+ .addComponent(credentialsPasswordField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(credentialsConnectButton)
+ .addContainerGap(15, Short.MAX_VALUE))
+ );
+
+ handleSearchTextArea.setColumns(20);
+ handleSearchTextArea.setRows(5);
+ jScrollPane1.setViewportView(handleSearchTextArea);
+
+ org.openide.awt.Mnemonics.setLocalizedText(fetchLabel, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.fetchLabel.text")); // NOI18N
+
+ isFollowersActivated.setSelected(true);
+ org.openide.awt.Mnemonics.setLocalizedText(isFollowersActivated, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.isFollowersActivated.text")); // NOI18N
+ isFollowersActivated.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ isFollowersActivatedActionPerformed(evt);
+ }
+ });
+
+ isFollowsActivated.setSelected(true);
+ org.openide.awt.Mnemonics.setLocalizedText(isFollowsActivated, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.isFollowsActivated.text")); // NOI18N
+ isFollowsActivated.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ isFollowsActivatedActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(runFetchButton, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.runFetchButton.text")); // NOI18N
+ runFetchButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ runFetchButtonActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(isDeepSearch, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.isDeepSearch.text")); // NOI18N
+ isDeepSearch.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ isDeepSearchActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(limitCrawlCheckbox, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.limitCrawlCheckbox.text")); // NOI18N
+ limitCrawlCheckbox.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ limitCrawlCheckboxActionPerformed(evt);
+ }
+ });
+
+ limitCrawlSpinner.setToolTipText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.limitCrawlSpinner.toolTipText")); // NOI18N
+ limitCrawlSpinner.addPropertyChangeListener(new java.beans.PropertyChangeListener() {
+ public void propertyChange(java.beans.PropertyChangeEvent evt) {
+ limitCrawlSpinnerPropertyChange(evt);
+ }
+ });
+
+ javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+ this.setLayout(layout);
+ layout.setHorizontalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addContainerGap()
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addComponent(fetchLabel)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addComponent(isFollowersActivated)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(isFollowsActivated))
+ .addComponent(isDeepSearch))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
+ .addComponent(limitCrawlSpinner)
+ .addComponent(limitCrawlCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
+ .addComponent(runFetchButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 353, Short.MAX_VALUE)
+ .addComponent(credentialsPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+ .addGap(12, 12, 12))
+ );
+ layout.setVerticalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addContainerGap()
+ .addComponent(credentialsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(fetchLabel)
+ .addComponent(isFollowersActivated)
+ .addComponent(isFollowsActivated)
+ .addComponent(limitCrawlCheckbox))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(isDeepSearch)
+ .addComponent(limitCrawlSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(runFetchButton)
+ .addContainerGap(51, Short.MAX_VALUE))
+ );
+
+ limitCrawlSpinner.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.limitCrawlSpinner.AccessibleContext.accessibleDescription")); // NOI18N
+ }// //GEN-END:initComponents
+
+ private void isFollowersActivatedActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_isFollowersActivatedActionPerformed
+ blueskyGephi.setIsFollowersActive(isFollowersActivated.isSelected());
+ }//GEN-LAST:event_isFollowersActivatedActionPerformed
+
+ private void credentialsConnectButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_credentialsConnectButtonActionPerformed
+ if(blueskyGephi.connect(credentialsHandleField.getText(), String.valueOf(credentialsPasswordField.getPassword()))){
+ credentialsConnectButton.setBackground(Color.GREEN);
+ } else {
+ credentialsConnectButton.setBackground(Color.RED);
+ }
+ }//GEN-LAST:event_credentialsConnectButtonActionPerformed
+
+ private void runFetchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_runFetchButtonActionPerformed
+ blueskyGephi.setQuery(handleSearchTextArea.getText());
+ List actors = Arrays.asList(handleSearchTextArea.getText().split("\\n"))
+ .stream()
+ .map(x -> x.trim())
+ .collect(Collectors.toList());
+ blueskyGephi.fetchFollowerFollowsFromActors(actors);
+
+ }//GEN-LAST:event_runFetchButtonActionPerformed
+
+ private void isFollowsActivatedActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_isFollowsActivatedActionPerformed
+ blueskyGephi.setIsFollowsActive(isFollowsActivated.isSelected());
+ }//GEN-LAST:event_isFollowsActivatedActionPerformed
+
+ private void credentialsPasswordLabelMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_credentialsPasswordLabelMouseClicked
+ try {
+ Desktop.getDesktop().browse(URI.create("https://bsky.app/settings/app-passwords")); // TODO add your handling code here:
+ } catch (IOException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ }//GEN-LAST:event_credentialsPasswordLabelMouseClicked
+
+ private void isDeepSearchActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_isDeepSearchActionPerformed
+ blueskyGephi.setIsDeepSearch(isDeepSearch.isSelected());
+ }//GEN-LAST:event_isDeepSearchActionPerformed
+
+ private void limitCrawlCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_limitCrawlCheckboxActionPerformed
+ blueskyGephi.setIsLimitCrawlActive(limitCrawlCheckbox.isSelected());
+ limitCrawlSpinner.setEnabled(limitCrawlCheckbox.isSelected());
+
+ }//GEN-LAST:event_limitCrawlCheckboxActionPerformed
+
+ private void limitCrawlSpinnerPropertyChange(java.beans.PropertyChangeEvent evt) {//GEN-FIRST:event_limitCrawlSpinnerPropertyChange
+ blueskyGephi.setLimitCrawl(Math.max(1,((int)limitCrawlSpinner.getValue())/100));
+ }//GEN-LAST:event_limitCrawlSpinnerPropertyChange
+
+
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JButton credentialsConnectButton;
+ private javax.swing.JTextField credentialsHandleField;
+ private javax.swing.JLabel credentialsHandleLabel;
+ private javax.swing.JPanel credentialsPanel;
+ private javax.swing.JPasswordField credentialsPasswordField;
+ private javax.swing.JLabel credentialsPasswordLabel;
+ private javax.swing.JLabel fetchLabel;
+ private javax.swing.JTextArea handleSearchTextArea;
+ private javax.swing.JCheckBox isDeepSearch;
+ private javax.swing.JCheckBox isFollowersActivated;
+ private javax.swing.JCheckBox isFollowsActivated;
+ private javax.swing.JScrollPane jScrollPane1;
+ private javax.swing.JCheckBox limitCrawlCheckbox;
+ private javax.swing.JSpinner limitCrawlSpinner;
+ private javax.swing.JButton runFetchButton;
+ // End of variables declaration//GEN-END:variables
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtClient.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtClient.java
new file mode 100644
index 000000000..4281f6743
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtClient.java
@@ -0,0 +1,162 @@
+package fr.totetmatt.blueskygephi.atproto;
+
+/**
+ *
+ * @author totetmatt
+ */
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import fr.totetmatt.blueskygephi.atproto.response.AppBskyActorGetProfile;
+import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetFollowers;
+import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetFollows;
+import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetList;
+import fr.totetmatt.blueskygephi.atproto.response.ComAtprotoServerCreateSession;
+import java.io.IOException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.openide.util.Exceptions;
+
+public class AtClient {
+
+ private final ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ private final AtContext context;
+
+ private final HttpClient client = HttpClient.newHttpClient();
+ private ComAtprotoServerCreateSession session = null;
+ExecutorService executorService = Executors.newFixedThreadPool(2);
+ public AtClient(String host) {
+ context = new AtContext(host);
+ }
+
+ public boolean comAtprotoServerCreateSession(String identifier, String password) {
+ try {
+ HttpRequest request = HttpRequest.newBuilder(context.getURIForLexicon("com.atproto.server.createSession"))
+ .POST(HttpRequest.BodyPublishers.ofString("{\"identifier\":\"" + identifier + "\",\"password\":\"" + password + "\"}"))
+ .header("Content-Type", "application/json")
+ .build();
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ this.session = objectMapper.readValue(response.body(), ComAtprotoServerCreateSession.class);
+ return true;
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private HttpRequest getRequest(String xrpcMethod, HashMap params) {
+ return HttpRequest
+ .newBuilder(context.getURIForLexicon(xrpcMethod, params))
+ .GET()
+ .header("Authorization", "Bearer " + session.getAccessJwt())
+ .build();
+ }
+
+ // Yeah, it should be generalized, and async, but it works right now so it's ok.
+ public List appBskyGraphGetFollowers(String actor, Optional limitCrawl) {
+ List pagedResponse = new ArrayList<>();
+ try {
+ var params = new HashMap();
+ params.put("actor", actor);
+ params.put("limit", "100");
+ int currentCrawlLoop=0;
+ while (limitCrawl.isEmpty() ||currentCrawlLoop < limitCrawl.get()) {
+ var request = getRequest("app.bsky.graph.getFollowers", params);
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ AppBskyGraphGetFollowers objectResponse = objectMapper.readValue(response.body(), AppBskyGraphGetFollowers.class);
+ pagedResponse.add(objectResponse);
+ if (objectResponse.getCursor() == null) {
+ break;
+ }
+ params.put("cursor", objectResponse.getCursor());
+ currentCrawlLoop++;
+ }
+ return pagedResponse;
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public List appBskyGraphGetFollows(String actor, Optional limitCrawl) {
+ List pagedResponse = new ArrayList<>();
+ try {
+ var params = new HashMap();
+ params.put("actor", actor);
+ params.put("limit", "100");
+ int currentCrawlLoop=0;
+ while (limitCrawl.isEmpty() || currentCrawlLoop < limitCrawl.get()) {
+ var request = getRequest("app.bsky.graph.getFollows", params);
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ var objectResponse = objectMapper.readValue(response.body(), AppBskyGraphGetFollows.class);
+ pagedResponse.add(objectResponse);
+ if (objectResponse.getCursor() == null) {
+ break;
+ }
+ params.put("cursor", objectResponse.getCursor());
+ currentCrawlLoop++;
+ }
+ return pagedResponse;
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public AppBskyActorGetProfile appBskyActorGetProfile(String actor) {
+ try {
+ var params = new HashMap();
+ params.put("actor", actor);
+ var request = getRequest("app.bsky.actor.getProfile", params);
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ System.out.println(response.body());
+ return objectMapper.readValue(response.body(), AppBskyActorGetProfile.class);
+ } catch (IOException | InterruptedException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ return null;
+ }
+
+ public List appBskyGraphGetList(String list) {
+ List lists = new ArrayList<>();
+ try {
+ var params = new HashMap();
+ params.put("list", list);
+ params.put("limit", "100");
+ while (true) {
+ var request = getRequest("app.bsky.graph.getList", params);
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ var objectResponse = objectMapper.readValue(response.body(), AppBskyGraphGetList.class);
+ lists.add(objectResponse);
+ System.out.println(response.body());
+ if (objectResponse.getCursor() == null) {
+ break;
+ }
+
+ params.put("cursor", objectResponse.getCursor());
+ }
+ } catch (IOException | InterruptedException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ return lists;
+ }
+
+ public AppBskyActorGetProfile appBskyActorGetProfiles(String actors) {
+ try {
+ var request = HttpRequest
+ .newBuilder(context.getURIForLexicon("app.bsky.actor.getProfiles", actors))
+ .GET()
+ .header("Authorization", "Bearer " + session.getAccessJwt())
+ .build();
+ var response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ return objectMapper.readValue(response.body(), AppBskyActorGetProfile.class);
+ } catch (IOException | InterruptedException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ return null;
+ }
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtContext.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtContext.java
new file mode 100644
index 000000000..42c70824a
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtContext.java
@@ -0,0 +1,44 @@
+package fr.totetmatt.blueskygephi.atproto;
+
+/**
+ *
+ * @author totetmatt
+ */
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+
+public class AtContext {
+
+ private final String host;
+
+ public AtContext(String host) {
+ this.host = host;
+ }
+
+ private String formatUrl(String host, String lexicon) {
+ return "https://" + host + "/xrpc/" + lexicon;
+ }
+
+ public URI getURIForLexicon(String lexicon) {
+
+ return URI.create(formatUrl(host, lexicon));
+ }
+
+ public URI getURIForLexicon(String lexicon, HashMap parameters) {
+
+ String url_parameters = parameters.entrySet().stream().map(x
+ -> URLEncoder.encode(x.getKey(), StandardCharsets.UTF_8) + "=" + URLEncoder.encode(x.getValue(), StandardCharsets.UTF_8)
+ ).collect(Collectors.joining("&"));
+
+ return URI.create(formatUrl(host, lexicon) + "?" + url_parameters);
+ }
+
+ public URI getURIForLexicon(String lexicon, String parameters) {
+
+ return URI.create(formatUrl(host, lexicon) + "?" + parameters);
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyActorGetProfile.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyActorGetProfile.java
new file mode 100644
index 000000000..791b02e1d
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyActorGetProfile.java
@@ -0,0 +1,28 @@
+package fr.totetmatt.blueskygephi.atproto.response;
+
+/**
+ *
+ * @author totetmatt
+ */
+public class AppBskyActorGetProfile {
+
+ private String did;
+ private String handle;
+
+ public String getDid() {
+ return did;
+ }
+
+ public void setDid(String did) {
+ this.did = did;
+ }
+
+ public String getHandle() {
+ return handle;
+ }
+
+ public void setHandle(String handle) {
+ this.handle = handle;
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollowers.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollowers.java
new file mode 100644
index 000000000..75e095381
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollowers.java
@@ -0,0 +1,40 @@
+package fr.totetmatt.blueskygephi.atproto.response;
+
+/**
+ *
+ * @author totetmatt
+ */
+import fr.totetmatt.blueskygephi.atproto.response.common.Identity;
+import java.util.List;
+
+public class AppBskyGraphGetFollowers {
+
+ private Identity subject;
+
+ public Identity getSubject() {
+ return subject;
+ }
+
+ public void setSubject(Identity subject) {
+ this.subject = subject;
+ }
+
+ public List getFollowers() {
+ return followers;
+ }
+
+ public void setFollowers(List followers) {
+ this.followers = followers;
+ }
+
+ public String getCursor() {
+ return cursor;
+ }
+
+ public void setCursor(String cursor) {
+ this.cursor = cursor;
+ }
+
+ private List followers;
+ private String cursor;
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollows.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollows.java
new file mode 100644
index 000000000..950fba017
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollows.java
@@ -0,0 +1,40 @@
+package fr.totetmatt.blueskygephi.atproto.response;
+
+/**
+ *
+ * @author totetmatt
+ */
+import fr.totetmatt.blueskygephi.atproto.response.common.Identity;
+import java.util.List;
+
+public class AppBskyGraphGetFollows {
+
+ private Identity subject;
+
+ public Identity getSubject() {
+ return subject;
+ }
+
+ public void setSubject(Identity subject) {
+ this.subject = subject;
+ }
+
+ public List getFollows() {
+ return follows;
+ }
+
+ public void setFollows(List follows) {
+ this.follows = follows;
+ }
+
+ public String getCursor() {
+ return cursor;
+ }
+
+ public void setCursor(String cursor) {
+ this.cursor = cursor;
+ }
+
+ private List follows;
+ private String cursor;
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetList.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetList.java
new file mode 100644
index 000000000..7702d8291
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetList.java
@@ -0,0 +1,38 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package fr.totetmatt.blueskygephi.atproto.response;
+
+import fr.totetmatt.blueskygephi.atproto.response.common.Subject;
+import java.util.List;
+
+/**
+ *
+ * @author totetmatt
+ */
+public class AppBskyGraphGetList {
+ private List items;
+ private String cursor;
+
+
+ public List getItems() {
+ return items;
+ }
+
+ public void setItems(List items) {
+ this.items = items;
+ }
+
+ public String getCursor() {
+ return cursor;
+ }
+
+ public void setCursor(String cursor) {
+ this.cursor = cursor;
+ }
+
+
+
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/ComAtprotoServerCreateSession.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/ComAtprotoServerCreateSession.java
new file mode 100644
index 000000000..181032a56
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/ComAtprotoServerCreateSession.java
@@ -0,0 +1,65 @@
+package fr.totetmatt.blueskygephi.atproto.response;
+
+/**
+ *
+ * @author totetmatt
+ */
+public class ComAtprotoServerCreateSession {
+
+ private String did;
+ private String handle;
+ private String email;
+ private String accessJwt;
+ private String refreshJwt;
+
+ public String getDid() {
+ return did;
+ }
+
+ public void setDid(String did) {
+ this.did = did;
+ }
+
+ public String getHandle() {
+ return handle;
+ }
+
+ public void setHandle(String handle) {
+ this.handle = handle;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getAccessJwt() {
+ return accessJwt;
+ }
+
+ public void setAccessJwt(String accessJwt) {
+ this.accessJwt = accessJwt;
+ }
+
+ public String getRefreshJwt() {
+ return refreshJwt;
+ }
+
+ public void setRefreshJwt(String refreshJwt) {
+ this.refreshJwt = refreshJwt;
+ }
+
+ @Override
+ public String toString() {
+ return "GetSessionResponse{"
+ + "did='" + did + '\''
+ + ", handle='" + handle + '\''
+ + ", email='" + email + '\''
+ + ", accessJwt='" + accessJwt + '\''
+ + ", refreshJwt='" + refreshJwt + '\''
+ + '}';
+ }
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Identity.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Identity.java
new file mode 100644
index 000000000..dfa3237b9
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Identity.java
@@ -0,0 +1,66 @@
+package fr.totetmatt.blueskygephi.atproto.response.common;
+
+/**
+ *
+ * @author totetmatt
+ */
+public class Identity {
+
+ private String did;
+ private String handle;
+ private String description;
+
+ private String avatar;
+ private String indexedAt;
+
+ public String getDid() {
+ return did;
+ }
+
+ public void setDid(String did) {
+ this.did = did;
+ }
+
+ public String getHandle() {
+ return handle;
+ }
+
+ public void setHandle(String handle) {
+ this.handle = handle;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getAvatar() {
+ return avatar;
+ }
+
+ public void setAvatar(String avatar) {
+ this.avatar = avatar;
+ }
+
+ public String getIndexedAt() {
+ return indexedAt;
+ }
+
+ public void setIndexedAt(String indexedAt) {
+ this.indexedAt = indexedAt;
+ }
+
+ @Override
+ public String toString() {
+ return "Identity{"
+ + "did='" + did + '\''
+ + ", handle='" + handle + '\''
+ + ", description='" + description + '\''
+ + ", avatar='" + avatar + '\''
+ + ", indexedAt='" + indexedAt + '\''
+ + '}';
+ }
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Subject.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Subject.java
new file mode 100644
index 000000000..e0297632e
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Subject.java
@@ -0,0 +1,22 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package fr.totetmatt.blueskygephi.atproto.response.common;
+
+/**
+ *
+ * @author totetmatt
+ */
+public class Subject {
+ private Identity subject;
+
+ public Identity getSubject() {
+ return subject;
+ }
+
+ public void setSubject(Identity subject) {
+ this.subject = subject;
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulator.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulator.java
new file mode 100644
index 000000000..50c810343
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulator.java
@@ -0,0 +1,95 @@
+package fr.totetmatt.blueskygephi.graphmanipulator;
+
+import fr.totetmatt.blueskygephi.BlueskyGephi;
+import java.awt.event.KeyEvent;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.swing.Icon;
+import org.gephi.datalab.spi.ContextMenuItemManipulator;
+import org.gephi.datalab.spi.ManipulatorUI;
+import org.gephi.datalab.spi.nodes.NodesManipulator;
+import org.gephi.graph.api.Graph;
+import org.gephi.graph.api.Node;
+import org.gephi.visualization.spi.GraphContextMenuItem;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author totetmatt
+ */
+@ServiceProvider(service = GraphContextMenuItem.class)
+public class BlueskyGephiDefaultGraphManipulator implements NodesManipulator, GraphContextMenuItem {
+
+ private Node[] nodes;
+
+ @Override
+ public void setup(Node[] nodes, Node node) {
+ this.nodes = nodes;
+ }
+
+ @Override
+ public ContextMenuItemManipulator[] getSubItems() {
+ return null;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public Integer getMnemonicKey() {
+ return KeyEvent.VK_W;
+ }
+
+ @Override
+ public void execute() {
+ List actors = Stream.of(nodes).map(x -> (String) x.getId()).collect(Collectors.toList());
+ Lookup.getDefault()
+ .lookup(BlueskyGephi.class)
+ .fetchFollowerFollowsFromActors(actors);
+ }
+
+ @Override
+ public String getName() {
+ return "Bluesky Fetch default data";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Fetch configured network from the selected nodes";
+ }
+
+ @Override
+ public boolean canExecute() {
+ return true;
+ }
+
+ @Override
+ public ManipulatorUI getUI() {
+ return null;
+ }
+
+ @Override
+ public int getType() {
+ return 200;
+ }
+
+ @Override
+ public int getPosition() {
+ return 200;
+ }
+
+ @Override
+ public Icon getIcon() {
+ return null;
+ }
+
+ @Override
+ public void setup(Graph graph, Node[] nodes) {
+ this.nodes = nodes;
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulatorBuilder.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulatorBuilder.java
new file mode 100644
index 000000000..6ac931f3b
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulatorBuilder.java
@@ -0,0 +1,19 @@
+package fr.totetmatt.blueskygephi.graphmanipulator;
+
+import org.gephi.datalab.spi.nodes.NodesManipulator;
+import org.gephi.datalab.spi.nodes.NodesManipulatorBuilder;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author totetmatt
+ */
+@ServiceProvider(service = NodesManipulatorBuilder.class)
+public class BlueskyGephiDefaultGraphManipulatorBuilder implements NodesManipulatorBuilder {
+
+ @Override
+ public NodesManipulator getNodesManipulator() {
+ return new BlueskyGephiDefaultGraphManipulator();
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowersGraphManipulator.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowersGraphManipulator.java
new file mode 100644
index 000000000..f4626484b
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowersGraphManipulator.java
@@ -0,0 +1,93 @@
+package fr.totetmatt.blueskygephi.graphmanipulator;
+
+import fr.totetmatt.blueskygephi.BlueskyGephi;
+import java.awt.event.KeyEvent;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.swing.Icon;
+import org.gephi.datalab.spi.ContextMenuItemManipulator;
+import org.gephi.datalab.spi.ManipulatorUI;
+import org.gephi.datalab.spi.nodes.NodesManipulator;
+import org.gephi.graph.api.Graph;
+import org.gephi.graph.api.Node;
+import org.gephi.visualization.spi.GraphContextMenuItem;
+import org.openide.util.Lookup;
+
+/**
+ *
+ * @author totetmatt
+ */
+public class BlueskyGephiFollowersGraphManipulator implements NodesManipulator, GraphContextMenuItem {
+
+ private Node[] nodes;
+
+ @Override
+ public void setup(Node[] nodes, Node node) {
+ this.nodes = nodes;
+ }
+
+ @Override
+ public ContextMenuItemManipulator[] getSubItems() {
+ return null;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public Integer getMnemonicKey() {
+ return KeyEvent.VK_C;
+ }
+
+ @Override
+ public void execute() {
+ List actors = Stream.of(nodes).map(x -> (String) x.getId()).collect(Collectors.toList());
+ Lookup.getDefault()
+ .lookup(BlueskyGephi.class)
+ .fetchFollowerFollowsFromActors(actors, false, true, false);
+ }
+
+ @Override
+ public String getName() {
+ return "Fetch followers only data";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Fetch only followers network from the selected nodes";
+ }
+
+ @Override
+ public boolean canExecute() {
+ return true;
+ }
+
+ @Override
+ public ManipulatorUI getUI() {
+ return null;
+ }
+
+ @Override
+ public int getType() {
+ return 200;
+ }
+
+ @Override
+ public int getPosition() {
+ return 200;
+ }
+
+ @Override
+ public Icon getIcon() {
+ return null;
+ }
+
+ @Override
+ public void setup(Graph graph, Node[] nodes) {
+ this.nodes = nodes;
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowsGraphManipulator.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowsGraphManipulator.java
new file mode 100644
index 000000000..5fda7c5a9
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowsGraphManipulator.java
@@ -0,0 +1,93 @@
+package fr.totetmatt.blueskygephi.graphmanipulator;
+
+import fr.totetmatt.blueskygephi.BlueskyGephi;
+import java.awt.event.KeyEvent;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.swing.Icon;
+import org.gephi.datalab.spi.ContextMenuItemManipulator;
+import org.gephi.datalab.spi.ManipulatorUI;
+import org.gephi.datalab.spi.nodes.NodesManipulator;
+import org.gephi.graph.api.Graph;
+import org.gephi.graph.api.Node;
+import org.gephi.visualization.spi.GraphContextMenuItem;
+import org.openide.util.Lookup;
+
+/**
+ *
+ * @author totetmatt
+ */
+public class BlueskyGephiFollowsGraphManipulator implements NodesManipulator, GraphContextMenuItem {
+
+ private Node[] nodes;
+
+ @Override
+ public void setup(Node[] nodes, Node node) {
+ this.nodes = nodes;
+ }
+
+ @Override
+ public ContextMenuItemManipulator[] getSubItems() {
+ return null;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public Integer getMnemonicKey() {
+ return KeyEvent.VK_X;
+ }
+
+ @Override
+ public void execute() {
+ List actors = Stream.of(nodes).map(x -> (String) x.getId()).collect(Collectors.toList());
+ Lookup.getDefault()
+ .lookup(BlueskyGephi.class)
+ .fetchFollowerFollowsFromActors(actors, true, false, false);
+ }
+
+ @Override
+ public String getName() {
+ return "Fetch follows only data";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Fetch only follows network from the selected nodes";
+ }
+
+ @Override
+ public boolean canExecute() {
+ return true;
+ }
+
+ @Override
+ public ManipulatorUI getUI() {
+ return null;
+ }
+
+ @Override
+ public int getType() {
+ return 200;
+ }
+
+ @Override
+ public int getPosition() {
+ return 200;
+ }
+
+ @Override
+ public Icon getIcon() {
+ return null;
+ }
+
+ @Override
+ public void setup(Graph graph, Node[] nodes) {
+ this.nodes = nodes;
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulator.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulator.java
new file mode 100644
index 000000000..ca50ecb82
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulator.java
@@ -0,0 +1,88 @@
+package fr.totetmatt.blueskygephi.graphmanipulator;
+
+import java.awt.event.KeyEvent;
+import javax.swing.Icon;
+import org.gephi.datalab.spi.ContextMenuItemManipulator;
+import org.gephi.datalab.spi.ManipulatorUI;
+import org.gephi.datalab.spi.nodes.NodesManipulator;
+import org.gephi.graph.api.Graph;
+import org.gephi.graph.api.Node;
+import org.gephi.visualization.spi.GraphContextMenuItem;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author totetmatt
+ */
+@ServiceProvider(service = GraphContextMenuItem.class)
+public class BlueskyGephiMainGraphManipulator implements NodesManipulator, GraphContextMenuItem {
+
+ @Override
+ public void setup(Node[] nodes, Node node) {
+
+ }
+
+ @Override
+ public ContextMenuItemManipulator[] getSubItems() {
+ return new ContextMenuItemManipulator[]{
+ new BlueskyGephiFollowsGraphManipulator(),
+ new BlueskyGephiFollowersGraphManipulator()
+ };
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public Integer getMnemonicKey() {
+ return KeyEvent.VK_W;
+ }
+
+ @Override
+ public void execute() {
+
+ }
+
+ @Override
+ public String getName() {
+ return "Bluesky";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Fetch network from the selected nodes";
+ }
+
+ @Override
+ public boolean canExecute() {
+ return true;
+ }
+
+ @Override
+ public ManipulatorUI getUI() {
+ return null;
+ }
+
+ @Override
+ public int getType() {
+ return 200;
+ }
+
+ @Override
+ public int getPosition() {
+ return 200;
+ }
+
+ @Override
+ public Icon getIcon() {
+ return null;
+ }
+
+ @Override
+ public void setup(Graph graph, Node[] nodes) {
+
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulatorBuilder.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulatorBuilder.java
new file mode 100644
index 000000000..04fd713f1
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulatorBuilder.java
@@ -0,0 +1,19 @@
+package fr.totetmatt.blueskygephi.graphmanipulator;
+
+import org.gephi.datalab.spi.nodes.NodesManipulator;
+import org.gephi.datalab.spi.nodes.NodesManipulatorBuilder;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author totetmatt
+ */
+@ServiceProvider(service = NodesManipulatorBuilder.class)
+public class BlueskyGephiMainGraphManipulatorBuilder implements NodesManipulatorBuilder {
+
+ @Override
+ public NodesManipulator getNodesManipulator() {
+ return new BlueskyGephiMainGraphManipulator();
+ }
+
+}
diff --git a/modules/BlueskyGephi/src/main/nbm/manifest.mf b/modules/BlueskyGephi/src/main/nbm/manifest.mf
new file mode 100644
index 000000000..873ac14d0
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/nbm/manifest.mf
@@ -0,0 +1,5 @@
+Manifest-Version: 1.0
+OpenIDE-Module-Name: Bluesky Gephi
+OpenIDE-Module-Short-Description: Import network from Bluesky / AtProtocol
+OpenIDE-Module-Long-Description: Import network from Bluesky / AtProtocol
+OpenIDE-Module-Display-Category: Import
diff --git a/modules/BlueskyGephi/src/main/resources/fr/totetmatt/blueskygephi/Bundle.properties b/modules/BlueskyGephi/src/main/resources/fr/totetmatt/blueskygephi/Bundle.properties
new file mode 100644
index 000000000..30123d975
--- /dev/null
+++ b/modules/BlueskyGephi/src/main/resources/fr/totetmatt/blueskygephi/Bundle.properties
@@ -0,0 +1,16 @@
+BlueskyGephiMainPanel.toolTipText=Bluesky Gephi
+BlueskyGephiMainPanel.Credentials.toolTipText=Credentials
+BlueskyGephiMainPanel.credentialsPanel.border.title=Credentials
+BlueskyGephiMainPanel.credentialsHandleLabel.text=Handle
+BlueskyGephiMainPanel.credentialsHandleField.text=
+BlueskyGephiMainPanel.credentialsConnectButton.text=Connect
+BlueskyGephiMainPanel.runFetchButton.text=Go !
+BlueskyGephiMainPanel.isFollowsActivated.text=Follows
+BlueskyGephiMainPanel.isFollowersActivated.text=Followers
+BlueskyGephiMainPanel.fetchLabel.text=Fetch
+BlueskyGephiMainPanel.credentialsPasswordField.text=
+BlueskyGephiMainPanel.credentialsPasswordLabel.text=Password
+BlueskyGephiMainPanel.isDeepSearch.text=Fetch also n+1
+BlueskyGephiMainPanel.limitCrawlCheckbox.text=Crawl Limit
+BlueskyGephiMainPanel.limitCrawlSpinner.AccessibleContext.accessibleDescription=
+BlueskyGephiMainPanel.limitCrawlSpinner.toolTipText=Nb Follower / Following to fetch max on a n+1 crawl
diff --git a/pom.xml b/pom.xml
index a5a924c08..1ca1569b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -79,6 +79,7 @@
modules/OrderedLayout
modules/OpenSeadragonPlugin
modules/WordCloudPlugin
+ modules/BlueskyGephi