-
Notifications
You must be signed in to change notification settings - Fork 623
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
1,728 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<artifactId>gephi-plugin-parent</artifactId> | ||
<groupId>org.gephi</groupId> | ||
<version>0.10.0</version> | ||
</parent> | ||
|
||
<groupId>fr.totetmatt</groupId> | ||
<artifactId>bluesky-gephi</artifactId> | ||
<version>0.1.0</version> | ||
<packaging>nbm</packaging> | ||
|
||
<name>Bluesky Gephi</name> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>com.fasterxml.jackson.core</groupId> | ||
<artifactId>jackson-databind</artifactId> | ||
<version>2.13.4.1</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.netbeans.api</groupId> | ||
<artifactId>org-openide-awt</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>${project.parent.groupId}</groupId> | ||
<artifactId>visualization-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>${project.parent.groupId}</groupId> | ||
<artifactId>datalab-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.netbeans.api</groupId> | ||
<artifactId>org-openide-windows</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.netbeans.api</groupId> | ||
<artifactId>org-netbeans-modules-settings</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.netbeans.api</groupId> | ||
<artifactId>org-openide-util-lookup</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.netbeans.api</groupId> | ||
<artifactId>org-openide-util</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.gephi</groupId> | ||
<artifactId>graph-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.gephi</groupId> | ||
<artifactId>project-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.gephi</groupId> | ||
<artifactId>desktop-project</artifactId> | ||
|
||
</dependency> | ||
<dependency> | ||
<groupId>org.gephi</groupId> | ||
<artifactId>utils-longtask</artifactId> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.apache.netbeans.utilities</groupId> | ||
<artifactId>nbm-maven-plugin</artifactId> | ||
<configuration> | ||
<verifyRuntime>skip</verifyRuntime> | ||
<licenseName>Apache 2.0</licenseName> | ||
<author>totetmatt</author> | ||
<authorEmail>[email protected]</authorEmail> | ||
<authorUrl>https://totetmatt.fr</authorUrl> | ||
<sourceCodeUrl>https://github.com/totetmatt/gephi-plugins.git</sourceCodeUrl> | ||
<publicPackages> | ||
<!-- Insert public packages --> | ||
</publicPackages> | ||
</configuration> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
|
||
<!-- Snapshot Repositories (only needed if developing against a SNAPSHOT version) --> | ||
<repositories> | ||
<repository> | ||
<id>oss-sonatype</id> | ||
<name>oss-sonatype</name> | ||
<url>https://oss.sonatype.org/content/repositories/snapshots/</url> | ||
<snapshots> | ||
<enabled>true</enabled> | ||
</snapshots> | ||
</repository> | ||
</repositories> | ||
</project> | ||
|
||
|
215 changes: 215 additions & 0 deletions
215
modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
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.common.Identity; | ||
import java.awt.Color; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Set; | ||
import java.util.logging.Logger; | ||
import java.util.prefs.Preferences; | ||
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.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 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"); | ||
final private GraphModel graphModel; | ||
|
||
public BlueskyGephi() { | ||
initProjectAndWorkspace(); | ||
graphModel = Lookup.getDefault().lookup(GraphController.class).getGraphModel(); | ||
} | ||
|
||
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); | ||
} | ||
|
||
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.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, boolean isFollowsActive, boolean isFollowersActive, boolean isDeepSearch) { | ||
// To avoid locking Gephi UI | ||
Thread t = new Thread() { | ||
private ProgressTicket progressTicket; | ||
Set<String> foaf = new HashSet<>(); | ||
|
||
private void process(String actor, boolean isDeepSearch) { | ||
if (isFollowsActive) { | ||
List<AppBskyGraphGetFollows> responses = client.appBskyGraphGetFollows(actor); | ||
|
||
graphModel.getGraph().writeLock(); | ||
for (var response : responses) { | ||
Identity subject = response.getSubject(); | ||
Node source = createNode(subject); | ||
for (var follow : response.getFollows()) { | ||
if (isDeepSearch) { | ||
foaf.add(follow.getDid()); | ||
} | ||
Node target = createNode(follow); | ||
createEdge(source, target); | ||
} | ||
|
||
} | ||
graphModel.getGraph().writeUnlock(); | ||
} | ||
|
||
if (isFollowersActive) { | ||
List<AppBskyGraphGetFollowers> responses = client.appBskyGraphGetFollowers(actor); | ||
|
||
graphModel.getGraph().writeLock(); | ||
for (var response : responses) { | ||
Identity subject = response.getSubject(); | ||
Node target = createNode(subject); | ||
for (var follower : response.getFollowers()) { | ||
if (isDeepSearch) { | ||
foaf.add(follower.getDid()); | ||
} | ||
Node source = createNode(follower); | ||
createEdge(source, target); | ||
} | ||
} | ||
graphModel.getGraph().writeUnlock(); | ||
} | ||
} | ||
|
||
@Override | ||
public void run() { | ||
this.setName("Bluesky Gephi Fetching Data for " + actor); | ||
progressTicket = Lookup.getDefault() | ||
.lookup(ProgressTicketProvider.class) | ||
.createTicket(this.getName(), () -> { | ||
interrupt(); | ||
Progress.finish(progressTicket); | ||
return true; | ||
}); | ||
|
||
Progress.start(progressTicket); | ||
Progress.switchToIndeterminate(progressTicket); | ||
|
||
process(actor, isDeepSearch); | ||
if (isDeepSearch) { | ||
for (var foafActor : foaf) { | ||
process(foafActor, false); | ||
} | ||
} | ||
Progress.finish(progressTicket); | ||
} | ||
}; | ||
t.start(); | ||
|
||
} | ||
|
||
public void fetchFollowerFollowsFromActors(List<String> actors, boolean isFollowsActive, boolean isFollowersActive, boolean isBlocksActive) { | ||
actors.stream().forEach(actor -> fetchFollowerFollowsFromActor(actor, isFollowsActive, isFollowersActive, getIsDeepSearch())); | ||
} | ||
|
||
public void fetchFollowerFollowsFromActors(List<String> actors) { | ||
actors.stream().forEach(actor -> fetchFollowerFollowsFromActor(actor, getIsFollowsActive(), getIsFollowersActive(), getIsDeepSearch())); | ||
} | ||
} |
Oops, something went wrong.