commit
b7a1ff139e
6 changed files with 308 additions and 32 deletions
14
README.md
14
README.md
|
@ -1,3 +1,17 @@
|
|||
# ssb_ap_bridge
|
||||
|
||||
A Javscript module to implement ActivityPub compatibility to [ssb](https://github.com/ssbc/ssb-db)
|
||||
|
||||
Feel free to open a PR or issue!
|
||||
|
||||
## What currently works:
|
||||
|
||||
- Webfinger detection of known ssb users
|
||||
- logging AP messages in ssb
|
||||
|
||||
## What still needs work:
|
||||
|
||||
- Converting messages between the services
|
||||
- encryption of converted messages
|
||||
|
||||
|
||||
|
|
4
config.json
Normal file
4
config.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"DOMAIN": "",
|
||||
"PORT": 3000
|
||||
}
|
29
index.js
Normal file
29
index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const config = require('./config');
|
||||
const server = require('./server');
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const bodyParser = require('body-parser');
|
||||
const http = require('http');
|
||||
|
||||
var options = { //todo: maybe be more specific here
|
||||
type: "application/*"
|
||||
};
|
||||
|
||||
app.use(bodyParser.raw(options));
|
||||
app.use(bodyParser.urlencoded({extended: true}));
|
||||
|
||||
app.set('domain', config.DOMAIN);
|
||||
app.set('port', process.env.PORT || config.PORT || 3000);
|
||||
app.set('port-https', process.env.PORT_HTTPS || 8443);
|
||||
|
||||
app.get('/', (req, res) => res.send('Hello World!'));
|
||||
app.get('/u/:name', server.get_user);
|
||||
app.get('/.well-known/webfinger', server.get_webfinger);
|
||||
app.get('/inbox', (req, res) => res.send('Here lies the inbox'));
|
||||
app.get('/users', server.get_users);
|
||||
|
||||
app.post('/inbox', server.post_inbox);
|
||||
|
||||
http.createServer(app).listen(app.get('port'), function () {
|
||||
console.log('Express server listening on port ' + app.get('port'));
|
||||
});
|
|
@ -2,9 +2,13 @@
|
|||
"name": "scuttlebutt_to_ap",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"ssb-client": "latest",
|
||||
"express": "latest",
|
||||
"http": "latest",
|
||||
"node-fetch": "latest",
|
||||
"pull-stream": "latest",
|
||||
"ssb-client": "latest",
|
||||
"ssb-friends": "latest",
|
||||
"stream-to-pull-stream": "latest",
|
||||
"node-fetch": "latest"
|
||||
"body-parser": "latest"
|
||||
}
|
||||
}
|
||||
|
|
151
server.js
Normal file
151
server.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
const config = require('./config');
|
||||
const ssb_bridge = require('./ssb_bridge');
|
||||
|
||||
DOMAIN = config.DOMAIN;
|
||||
|
||||
let chars_to_encode = "_!*'();:@&=+$,/?#[]"; // basically anything covered by %-encoding plus underscore
|
||||
|
||||
function decode_webfinger_name(name) {
|
||||
name = name + "=.ed25519";
|
||||
for (let i in chars_to_encode) {
|
||||
name = name.replace("_" + chars_to_encode[i].charCodeAt(0).toString(16) + "_", chars_to_encode[i]);
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
function encode_webfinger_name(name) {
|
||||
for (let i in chars_to_encode) {
|
||||
name = name.replace(chars_to_encode[i], "_" + chars_to_encode[i].charCodeAt(0).toString(16) + "_");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function get_webfinger(req, res) {
|
||||
let resource = req.query.resource;
|
||||
if (!resource || !resource.includes('acct:')) {
|
||||
return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.');
|
||||
} else {
|
||||
let name = resource.replace('acct:', '');
|
||||
name = name.substr(0, name.indexOf('@'));
|
||||
let encoded_name = encodeURIComponent(name);
|
||||
|
||||
let p = ssb_bridge.check_if_in_friends(decode_webfinger_name(name));
|
||||
|
||||
p.then((result) => {
|
||||
if (result) {
|
||||
res.json(
|
||||
{
|
||||
'subject': `acct:${encoded_name}@${DOMAIN}`,
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `https://${DOMAIN}/u/${encoded_name}`
|
||||
}
|
||||
],
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return res.status(404).send(`No record found for ${encoded_name}.`);
|
||||
}
|
||||
}).catch((err) => {
|
||||
return res.status(500).send(`An error occured: ${err}.`);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function get_user(req, res) {
|
||||
let name = req.params.name;
|
||||
if (!name) {
|
||||
return res.status(400).send('Bad request.');
|
||||
} else {
|
||||
|
||||
let p = ssb_bridge.check_if_in_friends(decode_webfinger_name(name));
|
||||
|
||||
p.then((result) => {
|
||||
if (result) {
|
||||
|
||||
let uname_p = ssb_bridge.get_username(decode_webfinger_name(name));
|
||||
let uname = '';
|
||||
|
||||
uname_p.then((uname_res) => {
|
||||
uname = uname_res;
|
||||
});
|
||||
|
||||
if (uname === '') {
|
||||
uname = name;
|
||||
}
|
||||
|
||||
res.json(
|
||||
{
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1'
|
||||
],
|
||||
|
||||
id: `https://${DOMAIN}/u/${name}`,
|
||||
type: 'Person',
|
||||
preferredUsername: uname,
|
||||
name: uname,
|
||||
// inbox: `https://${DOMAIN}/u/${name}/inbox`,
|
||||
inbox: `https://${DOMAIN}/inbox`,
|
||||
|
||||
publicKey: {
|
||||
id: `https://${DOMAIN}/u/${name}#main-key`,
|
||||
owner: `https://${DOMAIN}/u/${name}`,
|
||||
publicKeyPem: '-----BEGIN PUBLIC KEY-----\n' +
|
||||
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsT4MEtffaV0uFr9jgSnx\n' +
|
||||
'kb1+MMd1/jGYBugJ4jkHc9rvQRJLA2C8O2LbrdNb/00TRHh0pkR7AJW7DWMhIF/P\n' +
|
||||
'WmsHcdouAUhovAVO+4yrRK5fMA96JP6k2YwqJK+yjK4SMm9iwvcdBlrkZif0KWvA\n' +
|
||||
'Qf4eU24n64NSEdVu48cgZwMvQeYKaAtf2LIhXYOE4pA16C05z3BAar+9m2e1yZMG\n' +
|
||||
'+JzhoywmpqlrB+XK55wjAIhvwVGgOMtUg5FbHU5sH7wZv7H945t40x7HjNCBxU6d\n' +
|
||||
'yrF7Bl6nMg+ifT5a6SzPSJ0f3g99AyfMVL5fnhSodjpsnjohfIsx9Vzd4oO1JhDx\n' +
|
||||
'SwIDAQAB\n' +
|
||||
'-----END PUBLIC KEY-----'
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return res.status(404).send(`No record found for ${name}.`);
|
||||
}
|
||||
}).catch((err) => {
|
||||
return res.status(500).send(`An error occured: ${err}.`);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function get_users(req, res) {
|
||||
let p = ssb_bridge.get_friends();
|
||||
|
||||
p.then((result) => {
|
||||
if (result) {
|
||||
let out = {};
|
||||
for (let i in result) {
|
||||
out[i] = encode_webfinger_name(result[i].substr(1).replace("=.ed25519", ""));
|
||||
}
|
||||
res.json(out);
|
||||
} else {
|
||||
return res.status(404).send(`No record found for ${name}.`);
|
||||
}
|
||||
}).catch((err) => {
|
||||
return res.status(500).send(`An error occured: ${err}.`);
|
||||
})
|
||||
}
|
||||
|
||||
function post_inbox(req, res) {
|
||||
console.log("Saved activity to ssb log.");
|
||||
try {
|
||||
let in_activity = JSON.parse(req.body.toString());
|
||||
ssb_bridge.save(in_activity);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return res.status(200).send('ayy\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get_user,
|
||||
get_webfinger,
|
||||
get_users,
|
||||
post_inbox,
|
||||
};
|
|
@ -99,16 +99,16 @@ async function get_last_by_activity_id(id) {
|
|||
if (err) reject(err);
|
||||
for (let i in array) {
|
||||
if (array[i].value.content.type.length > 3 &&
|
||||
array[i].value.content.type.substr(0,3) === 'ap-' &&
|
||||
array[i].value.content.type.substr(0, 3) === 'ap-' &&
|
||||
allowed_message_types.indexOf(array[i].value.content.type.substr(3)) >= 0) {
|
||||
if (array[i].value.content.id === id) {
|
||||
last = array[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (last){
|
||||
if (last) {
|
||||
resolve(last);
|
||||
}else {
|
||||
} else {
|
||||
reject('no activity found');
|
||||
}
|
||||
|
||||
|
@ -199,7 +199,7 @@ function add_ssb_message(type, id, actor, summary, object, origin = null, target
|
|||
|
||||
}
|
||||
|
||||
async function get_json_from_blob(blob_id){
|
||||
async function get_json_from_blob(blob_id) {
|
||||
|
||||
/*
|
||||
* Gets the Object with the specified id from the
|
||||
|
@ -225,7 +225,7 @@ async function get_json_from_blob(blob_id){
|
|||
return await out;
|
||||
}
|
||||
|
||||
async function restore_ssb_message(id){
|
||||
async function restore_ssb_message(id) {
|
||||
|
||||
/*
|
||||
* Gets the Message with the specified id from the
|
||||
|
@ -246,12 +246,12 @@ async function restore_ssb_message(id){
|
|||
msg.type = msg.type.substr(3);
|
||||
|
||||
delete msg.actor.otherData;
|
||||
for (let key in actor_data){
|
||||
for (let key in actor_data) {
|
||||
msg.actor[key] = actor_data[key];
|
||||
}
|
||||
|
||||
delete msg.object.otherData;
|
||||
for (let key in object_data){
|
||||
for (let key in object_data) {
|
||||
msg.object[key] = object_data[key];
|
||||
}
|
||||
|
||||
|
@ -266,9 +266,84 @@ async function restore_ssb_message(id){
|
|||
return await out;
|
||||
}
|
||||
|
||||
exports.bridge = {
|
||||
save : (message) => {
|
||||
if (message.@context === "https://www.w3.org/ns/activitystreams") {
|
||||
async function check_if_in_friends(name) {
|
||||
let result = false;
|
||||
let out = new Promise((resolve, reject) => {
|
||||
ssbClient((err, sbot) => {
|
||||
if (err) reject(err);
|
||||
|
||||
pull(
|
||||
sbot.friends.createFriendStream(),
|
||||
pull.collect((err, array) => {
|
||||
array.forEach(function (actor) {
|
||||
let short_actor = (actor.substr(1));
|
||||
if (short_actor === name) {
|
||||
result = true;
|
||||
}
|
||||
});
|
||||
sbot.close();
|
||||
resolve(result);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
return await out;
|
||||
}
|
||||
|
||||
async function get_friends() {
|
||||
let out = new Promise((resolve, reject) => {
|
||||
ssbClient((err, sbot) => {
|
||||
if (err) reject(err);
|
||||
|
||||
pull(
|
||||
sbot.friends.createFriendStream(),
|
||||
pull.collect((err, array) => {
|
||||
sbot.close();
|
||||
resolve(array);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
return await out;
|
||||
}
|
||||
|
||||
async function get_username(name) {
|
||||
|
||||
let last = null;
|
||||
|
||||
let out = new Promise((resolve, reject) => {
|
||||
ssbClient((err, sbot) => {
|
||||
pull(
|
||||
sbot.createLogStream(),
|
||||
pull.collect(function (err, array) {
|
||||
sbot.close();
|
||||
if (err) reject(err);
|
||||
console.log(array);
|
||||
console.log(name);
|
||||
for (let i in array) {
|
||||
if (array[i].value.content.type === 'about' &&
|
||||
array[i].value.author.substr(1) === name &&
|
||||
array[i].value.content.hasOwnProperty('name')) {
|
||||
last = array[i].value.content.name;
|
||||
}
|
||||
}
|
||||
if (last) {
|
||||
resolve(last);
|
||||
} else {
|
||||
resolve('');
|
||||
}
|
||||
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return await out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
save: (message) => {
|
||||
if (message["@context"] === "https://www.w3.org/ns/activitystreams") {
|
||||
|
||||
add_ssb_message(
|
||||
message.type,
|
||||
|
@ -284,9 +359,8 @@ exports.bridge = {
|
|||
throw ("Invalid message context.");
|
||||
}
|
||||
},
|
||||
restore: (id) => {
|
||||
let p = restore_ssb_message(id);
|
||||
|
||||
return p;
|
||||
}
|
||||
restore: restore_ssb_message,
|
||||
check_if_in_friends,
|
||||
get_friends,
|
||||
get_username
|
||||
};
|
Loading…
Reference in a new issue