Finally got a setup working with native bindings for chrolide, sodium and whatever else for require('ssb-keys') to work
This commit is contained in:
parent
7ac7db8621
commit
5c9521090a
|
@ -0,0 +1,11 @@
|
|||
In order to build this project you'll need installed
|
||||
|
||||
XCode
|
||||
NodeJS
|
||||
Android NDK (because of some native bindings)
|
||||
|
||||
To compile the js to be able to run from xcode, execute:
|
||||
|
||||
```
|
||||
npm run build:watch
|
||||
```
|
|
@ -1,18 +1,5 @@
|
|||
console.log("Loading nodejs");
|
||||
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
require("./lib/ssb");
|
||||
|
||||
const posts = [
|
||||
{"key":"%PvT5scAQqPNiVaoYUoz5Omdx3+ds6lLEKp79Kwm02Kc=.sha256","value":{"previous":"%IU4a9V1ieToeUE2SXoqQXH0DMI0/alvxoEkGFjhoZeY=.sha256","sequence":389,"author":"@mfY4X9Gob0w2oVfFv+CpX56PfL0GZ2RNQkc51SJlMvc=.ed25519","timestamp":1588479011745,"hash":"sha256","content":{"type":"post","root":"%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256","branch":"%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256","reply":{"%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256":"@EaYYQo5nAQRabB9nxAn5i2uiIZ665b90Qk2U/WHNVE8=.ed25519"},"channel":null,"recps":null,"text":"This is very cool. I sometimes get mantis pods to eat pests in the garden or on our fruit trees. \n\nIt's good that you noticed that they hatched - supposedly they'll start eating each other if you leave them unattended for too long. Then I think the last one standing is the boss you have to fight to level up.\n\nIt's always so weird and cool to see them so small and in such huge numbers.","mentions":[]},"signature":"y5ixxWK/Z7R+8q7FbgImgQWKQJ+HZXqyOi9HXNPr2m8BvOHXV2zFPt/scz7Eq+1Sn1eCi7WFYK2pL+2Xk4wmCw==.sig.ed25519"},"timestamp":1588479014165,"rts":1588479011745},
|
||||
{"key":"%bpmnlkq5tf5GLhV4gt8z8rZ8gsvIE55+KpRZomWug6o=.sha256","value":{"previous":"%KlYtnEt6tnVzCdjVxkye/Yy+P2Tuu7/pORBjxqvpa4M=.sha256","sequence":17384,"author":"@+oaWWDs8g73EZFUMfW37R/ULtFEjwKN/DczvdYihjbU=.ed25519","timestamp":1588466897752,"hash":"sha256","content":{"type":"post","text":"[@Powersource](@Vz6v3xKpzViiTM/GAe+hKkACZSqrErQQZgv4iqQxEn8=.ed25519)\r\n\r\nIt depends on the implementation, but I'd expect that mentions won't work unless the client specifically supports your message type.","mentions":[{"link":"@Vz6v3xKpzViiTM/GAe+hKkACZSqrErQQZgv4iqQxEn8=.ed25519","name":"Powersource"}],"root":"%jK2xn0GE975NzHfAridPvdraqDx3dM60i9UVL7JRSiE=.sha256","branch":["%1O8ZJGxOnhZ624m1nMYM57xLv3LqPDCF/q9DedaPlRc=.sha256","%nrrnKl8YJQYHWmyEjTJevOJdb7/3wcNLKoLG+z2S00c=.sha256"]},"signature":"winljHJAxDLAvRa0uc0nYQvtDh3czkHCVvzKQ+eMH+tV07EGY16z947JZ2X+djctkI6baYpaWkpezXGXc87nAg==.sig.ed25519"},"timestamp":1588466900188,"rts":1588466897752}
|
||||
]
|
||||
|
||||
app.get("/posts", (req, res) => {
|
||||
res.json(posts);
|
||||
});
|
||||
|
||||
const expressServer = app.listen(port, () =>
|
||||
console.log(`Example app listening at http://localhost:${port}`)
|
||||
);
|
||||
require("./lib/express");
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
const express = require("express");
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const posts = [
|
||||
{
|
||||
key: "%PvT5scAQqPNiVaoYUoz5Omdx3+ds6lLEKp79Kwm02Kc=.sha256",
|
||||
value: {
|
||||
previous: "%IU4a9V1ieToeUE2SXoqQXH0DMI0/alvxoEkGFjhoZeY=.sha256",
|
||||
sequence: 389,
|
||||
author: "@mfY4X9Gob0w2oVfFv+CpX56PfL0GZ2RNQkc51SJlMvc=.ed25519",
|
||||
timestamp: 1588479011745,
|
||||
hash: "sha256",
|
||||
content: {
|
||||
type: "post",
|
||||
root: "%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256",
|
||||
branch: "%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256",
|
||||
reply: {
|
||||
"%RRIlEQi1Mo75X5pKdJ5HOnxRU+4n2bclwIDqiLpCWf0=.sha256":
|
||||
"@EaYYQo5nAQRabB9nxAn5i2uiIZ665b90Qk2U/WHNVE8=.ed25519",
|
||||
},
|
||||
channel: null,
|
||||
recps: null,
|
||||
text:
|
||||
"This is very cool. I sometimes get mantis pods to eat pests in the garden or on our fruit trees. \n\nIt's good that you noticed that they hatched - supposedly they'll start eating each other if you leave them unattended for too long. Then I think the last one standing is the boss you have to fight to level up.\n\nIt's always so weird and cool to see them so small and in such huge numbers.",
|
||||
mentions: [],
|
||||
},
|
||||
signature:
|
||||
"y5ixxWK/Z7R+8q7FbgImgQWKQJ+HZXqyOi9HXNPr2m8BvOHXV2zFPt/scz7Eq+1Sn1eCi7WFYK2pL+2Xk4wmCw==.sig.ed25519",
|
||||
},
|
||||
timestamp: 1588479014165,
|
||||
rts: 1588479011745,
|
||||
},
|
||||
{
|
||||
key: "%bpmnlkq5tf5GLhV4gt8z8rZ8gsvIE55+KpRZomWug6o=.sha256",
|
||||
value: {
|
||||
previous: "%KlYtnEt6tnVzCdjVxkye/Yy+P2Tuu7/pORBjxqvpa4M=.sha256",
|
||||
sequence: 17384,
|
||||
author: "@+oaWWDs8g73EZFUMfW37R/ULtFEjwKN/DczvdYihjbU=.ed25519",
|
||||
timestamp: 1588466897752,
|
||||
hash: "sha256",
|
||||
content: {
|
||||
type: "post",
|
||||
text:
|
||||
"[@Powersource](@Vz6v3xKpzViiTM/GAe+hKkACZSqrErQQZgv4iqQxEn8=.ed25519)\r\n\r\nIt depends on the implementation, but I'd expect that mentions won't work unless the client specifically supports your message type.",
|
||||
mentions: [
|
||||
{
|
||||
link: "@Vz6v3xKpzViiTM/GAe+hKkACZSqrErQQZgv4iqQxEn8=.ed25519",
|
||||
name: "Powersource",
|
||||
},
|
||||
],
|
||||
root: "%jK2xn0GE975NzHfAridPvdraqDx3dM60i9UVL7JRSiE=.sha256",
|
||||
branch: [
|
||||
"%1O8ZJGxOnhZ624m1nMYM57xLv3LqPDCF/q9DedaPlRc=.sha256",
|
||||
"%nrrnKl8YJQYHWmyEjTJevOJdb7/3wcNLKoLG+z2S00c=.sha256",
|
||||
],
|
||||
},
|
||||
signature:
|
||||
"winljHJAxDLAvRa0uc0nYQvtDh3czkHCVvzKQ+eMH+tV07EGY16z947JZ2X+djctkI6baYpaWkpezXGXc87nAg==.sig.ed25519",
|
||||
},
|
||||
timestamp: 1588466900188,
|
||||
rts: 1588466897752,
|
||||
},
|
||||
];
|
||||
|
||||
app.get("/user", (req, res) => {
|
||||
res.json({
|
||||
profile: {
|
||||
id: "@PvT5scAQqPNiVaoYUoz5Omdx3",
|
||||
name: "Jose",
|
||||
image: "http://pudim.com.br/pudim.jpg",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/posts", (req, res) => {
|
||||
res.json(posts);
|
||||
});
|
||||
|
||||
app.listen(port, () =>
|
||||
console.log(`Example app listening at http://localhost:${port}`)
|
||||
);
|
|
@ -0,0 +1,145 @@
|
|||
// Monkeypatched to add getGraph
|
||||
|
||||
"use strict";
|
||||
var LayeredGraph = require("layered-graph");
|
||||
var pull = require("pull-stream");
|
||||
var isFeed = require("ssb-ref").isFeed;
|
||||
// friends plugin
|
||||
// methods to analyze the social graph
|
||||
// maintains a 'follow' and 'flag' graph
|
||||
|
||||
exports.name = "friends";
|
||||
exports.version = "1.0.0";
|
||||
exports.manifest = {
|
||||
hopStream: "source",
|
||||
onEdge: "sync",
|
||||
isFollowing: "async",
|
||||
isBlocking: "async",
|
||||
getGraph: "async",
|
||||
hops: "async",
|
||||
help: "sync",
|
||||
// createLayer: 'sync', // not exposed over RPC as returns a function
|
||||
get: "async", // legacy
|
||||
createFriendStream: "source", // legacy
|
||||
stream: "source", // legacy
|
||||
};
|
||||
|
||||
//mdm.manifest(apidoc)
|
||||
|
||||
exports.init = function (sbot, config) {
|
||||
var max =
|
||||
(config.friends && config.friends.hops) ||
|
||||
(config.replicate && config.replicate.hops) ||
|
||||
3;
|
||||
var layered = LayeredGraph({ max: max, start: sbot.id });
|
||||
|
||||
function getGraph(cb) {
|
||||
layered.onReady(function () {
|
||||
var g = layered.getGraph();
|
||||
cb(null, g);
|
||||
});
|
||||
}
|
||||
|
||||
function isFollowing(opts, cb) {
|
||||
layered.onReady(function () {
|
||||
var g = layered.getGraph();
|
||||
cb(null, g[opts.source] ? g[opts.source][opts.dest] >= 0 : false);
|
||||
});
|
||||
}
|
||||
|
||||
function isBlocking(opts, cb) {
|
||||
layered.onReady(function () {
|
||||
var g = layered.getGraph();
|
||||
cb(null, Math.round(g[opts.source] && g[opts.source][opts.dest]) == -1);
|
||||
});
|
||||
}
|
||||
|
||||
//opinion: do not authorize peers blocked by this node.
|
||||
sbot.auth.hook(function (fn, args) {
|
||||
var self = this;
|
||||
isBlocking({ source: sbot.id, dest: args[0] }, function (err, blocked) {
|
||||
if (blocked) args[1](new Error("client is blocked"));
|
||||
else fn.apply(self, args);
|
||||
});
|
||||
});
|
||||
|
||||
if (!sbot.replicate)
|
||||
throw new Error("ssb-friends expects a replicate plugin to be available");
|
||||
|
||||
// opinion: replicate with everyone within max hops (max passed to layered above ^)
|
||||
pull(
|
||||
layered.hopStream({ live: true, old: true }),
|
||||
pull.drain(function (data) {
|
||||
if (data.sync) return;
|
||||
for (var k in data) {
|
||||
sbot.replicate.request(k, data[k] >= 0);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
require("ssb-friends/contacts")(sbot, layered.createLayer, config);
|
||||
|
||||
var legacy = require("ssb-friends/legacy")(layered);
|
||||
|
||||
//opinion: pass the blocks to replicate.block
|
||||
setImmediate(function () {
|
||||
var block =
|
||||
(sbot.replicate && sbot.replicate.block) || (sbot.ebt && sbot.ebt.block);
|
||||
if (block) {
|
||||
function handleBlockUnlock(from, to, value) {
|
||||
if (value === false) block(from, to, true);
|
||||
else block(from, to, false);
|
||||
}
|
||||
pull(
|
||||
legacy.stream({ live: true }),
|
||||
pull.drain(function (contacts) {
|
||||
if (!contacts) return;
|
||||
|
||||
if (isFeed(contacts.from) && isFeed(contacts.to)) {
|
||||
// live data
|
||||
handleBlockUnlock(contacts.from, contacts.to, contacts.value);
|
||||
} else {
|
||||
// initial data
|
||||
for (var from in contacts) {
|
||||
var relations = contacts[from];
|
||||
for (var to in relations)
|
||||
handleBlockUnlock(from, to, relations[to]);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hopStream: layered.hopStream,
|
||||
onEdge: layered.onEdge,
|
||||
isFollowing: isFollowing,
|
||||
isBlocking: isBlocking,
|
||||
getGraph: getGraph,
|
||||
|
||||
// expose createLayer, so that other plugins may express relationships
|
||||
createLayer: layered.createLayer,
|
||||
|
||||
// legacy, debugging
|
||||
hops: function (opts, cb) {
|
||||
layered.onReady(function () {
|
||||
if (isFunction(opts)) (cb = opts), (opts = {});
|
||||
cb(null, layered.getHops(opts));
|
||||
});
|
||||
},
|
||||
help: function () {
|
||||
return require("ssb-friends/help");
|
||||
},
|
||||
// legacy
|
||||
get: legacy.get,
|
||||
createFriendStream: legacy.createFriendStream,
|
||||
stream: legacy.stream,
|
||||
};
|
||||
};
|
||||
|
||||
// helpers
|
||||
|
||||
function isFunction(f) {
|
||||
return "function" === typeof f;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// Differently from ssb-identities, this plugin only keeps keys in memory, as we don't want to save them on the server
|
||||
|
||||
const ssbKeys = require("ssb-keys");
|
||||
const { create } = require("ssb-validate");
|
||||
|
||||
exports.name = "identities";
|
||||
exports.version = "1.0.0";
|
||||
exports.manifest = {
|
||||
create: "sync",
|
||||
addUnboxer: "sync",
|
||||
publishAs: "async",
|
||||
createNewKey: "sync",
|
||||
};
|
||||
|
||||
let unboxersAdded = [];
|
||||
let locks = {};
|
||||
|
||||
const toTarget = (t) => (typeof t == "object" ? t && t.link : t);
|
||||
|
||||
const addUnboxer = (ssb) => (key) => {
|
||||
if (unboxersAdded.includes(key.id)) return;
|
||||
|
||||
ssb.addUnboxer({
|
||||
key: (content) => {
|
||||
const unboxKey = ssbKeys.unboxKey(content, key);
|
||||
if (unboxKey) return unboxKey;
|
||||
},
|
||||
value: (content, key) => {
|
||||
return ssbKeys.unboxBody(content, key);
|
||||
},
|
||||
});
|
||||
unboxersAdded.push(key.id);
|
||||
};
|
||||
|
||||
const publishAs = (ssb, config) => ({ key, private, content }, cb) => {
|
||||
const id = key.id;
|
||||
if (locks[id]) throw new Error("already writing");
|
||||
|
||||
const recps = [].concat(content.recps).map(toTarget);
|
||||
|
||||
if (content.recps && !private) {
|
||||
return new Error("recps set, but private not set");
|
||||
} else if (!content.recps && private) {
|
||||
return new Error("private set, but content.recps not set");
|
||||
} else if (!!content.recps && private) {
|
||||
if (!Array.isArray(content.recps) || !~recps.indexOf(id))
|
||||
return new Error(
|
||||
"content.recps must be an array containing publisher id:" +
|
||||
id +
|
||||
" was:" +
|
||||
JSON.stringify(recps) +
|
||||
" indexOf:" +
|
||||
recps.indexOf(id)
|
||||
);
|
||||
content = ssbKeys.box(content, recps);
|
||||
}
|
||||
|
||||
locks[id] = true;
|
||||
ssb.getLatest(id, (_err, data) => {
|
||||
const state = data
|
||||
? {
|
||||
id: data.key,
|
||||
sequence: data.value.sequence,
|
||||
timestamp: data.value.timestamp,
|
||||
queue: [],
|
||||
}
|
||||
: { id: null, sequence: null, timestamp: null, queue: [] };
|
||||
|
||||
ssb.add(
|
||||
create(state, key, config.caps && config.caps.sign, content, Date.now()),
|
||||
(err, a, b) => {
|
||||
delete locks[id];
|
||||
cb(err, a, b);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
exports.init = function (ssb, config) {
|
||||
return {
|
||||
addUnboxer: addUnboxer(ssb),
|
||||
publishAs: publishAs(ssb, config),
|
||||
createNewKey: ssbKeys.generate,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,696 @@
|
|||
const pull = require("pull-stream");
|
||||
const cat = require("pull-cat");
|
||||
const debugPosts = require("debug")("queries:posts"),
|
||||
debugMessages = require("debug")("queries:messages"),
|
||||
debugFriends = require("debug")("queries:friends"),
|
||||
debugFriendshipStatus = require("debug")("queries:friendship_status"),
|
||||
debugSearch = require("debug")("queries:search"),
|
||||
debugProfile = require("debug")("queries:profile"),
|
||||
debugCommunities = require("debug")("queries:communities"),
|
||||
debugCommunityMembers = require("debug")("queries:communityMembers"),
|
||||
debugCommunityPosts = require("debug")("queries:communityPosts"),
|
||||
debugCommunityIsMember = require("debug")("queries:communityIsMember"),
|
||||
debugCommunityProfileCommunities = require("debug")(
|
||||
"queries:communityProfileCommunities"
|
||||
);
|
||||
const paramap = require("pull-paramap");
|
||||
const { promisePull, mapValues } = require("./utils");
|
||||
const ssb = require("./ssb-client");
|
||||
|
||||
const latestOwnerValue = ({ key, dest }) => {
|
||||
return promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
author: dest,
|
||||
content: { type: "about", about: dest },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
pull.filter((msg) => {
|
||||
return (
|
||||
msg.value.content &&
|
||||
key in msg.value.content &&
|
||||
!(msg.value.content[key] && msg.value.content[key].remove)
|
||||
);
|
||||
}),
|
||||
pull.take(1)
|
||||
).then(([entry]) => {
|
||||
if (entry) {
|
||||
return entry.value.content[key];
|
||||
}
|
||||
return ssb.client().about.latestValue({ key, dest });
|
||||
});
|
||||
};
|
||||
|
||||
const mapProfiles = (data, callback) =>
|
||||
getProfile(data.value.author)
|
||||
.then((author) => {
|
||||
data.value.authorProfile = author;
|
||||
callback(null, data);
|
||||
})
|
||||
.catch((err) => callback(err, null));
|
||||
|
||||
const getPosts = async (profile) => {
|
||||
debugPosts("Fetching");
|
||||
|
||||
const posts = await promisePull(
|
||||
// @ts-ignore
|
||||
cat([
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
private: { $not: true },
|
||||
content: {
|
||||
root: profile.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
limit: 100,
|
||||
}),
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
author: profile.id,
|
||||
private: { $not: true },
|
||||
content: {
|
||||
type: "post",
|
||||
root: { $not: true },
|
||||
channel: { $not: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
limit: 100,
|
||||
}),
|
||||
]),
|
||||
pull.filter((msg) => msg.value.content.type == "post"),
|
||||
paramap(mapProfiles)
|
||||
);
|
||||
|
||||
debugPosts("Done");
|
||||
|
||||
return mapValues(posts);
|
||||
};
|
||||
|
||||
const getSecretMessages = async (profile) => {
|
||||
debugMessages("Fetching");
|
||||
const messagesPromise = promisePull(
|
||||
// @ts-ignore
|
||||
cat([
|
||||
ssb.client().private.read({
|
||||
reverse: true,
|
||||
limit: 100,
|
||||
}),
|
||||
]),
|
||||
pull.filter(
|
||||
(msg) =>
|
||||
msg.value.content.type == "post" &&
|
||||
msg.value.content.recps &&
|
||||
msg.value.content.recps.includes(profile.id)
|
||||
)
|
||||
);
|
||||
|
||||
const deletedPromise = promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
author: profile.id,
|
||||
content: {
|
||||
type: "delete",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
).then(Object.values);
|
||||
|
||||
const [messages, deleted] = await Promise.all([
|
||||
messagesPromise,
|
||||
deletedPromise,
|
||||
]);
|
||||
|
||||
const deletedIds = deleted.map((x) => x.value.content.dest);
|
||||
|
||||
const messagesByAuthor = {};
|
||||
for (const message of messages) {
|
||||
if (message.value.author == profile.id) {
|
||||
for (const recp of message.value.content.recps) {
|
||||
if (recp == profile.id) continue;
|
||||
if (!messagesByAuthor[recp]) {
|
||||
messagesByAuthor[recp] = {
|
||||
author: recp,
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const author = message.value.author;
|
||||
if (!messagesByAuthor[author]) {
|
||||
messagesByAuthor[author] = {
|
||||
author: message.value.author,
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
if (!deletedIds.includes(message.key))
|
||||
messagesByAuthor[author].messages.push(message);
|
||||
}
|
||||
|
||||
const profilesList = await Promise.all(
|
||||
Object.keys(messagesByAuthor).map((id) => getProfile(id))
|
||||
);
|
||||
const profilesHash = profilesList.reduce((hash, profile) => {
|
||||
hash[profile.id] = profile;
|
||||
return hash;
|
||||
}, {});
|
||||
|
||||
const chatList = Object.values(messagesByAuthor).map((m) => {
|
||||
m.authorProfile = profilesHash[m.author];
|
||||
return m;
|
||||
});
|
||||
|
||||
debugMessages("Done");
|
||||
return chatList;
|
||||
};
|
||||
|
||||
const search = async (search) => {
|
||||
debugSearch("Fetching");
|
||||
|
||||
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
|
||||
const normalizedSearch = search
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "");
|
||||
const safelyEscapedSearch = normalizedSearch.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
"\\$&"
|
||||
);
|
||||
const loosenSpacesSearch = safelyEscapedSearch.replace(" ", ".*");
|
||||
const searchRegex = new RegExp(`.*${loosenSpacesSearch}.*`, "i");
|
||||
|
||||
const peoplePromise = promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
content: {
|
||||
type: "about",
|
||||
name: { $is: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
pull.filter((msg) => {
|
||||
if (!msg.value.content) return;
|
||||
|
||||
const normalizedName = msg.value.content.name
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "");
|
||||
return searchRegex.exec(normalizedName);
|
||||
}),
|
||||
paramap(mapProfiles)
|
||||
);
|
||||
|
||||
const communitiesPostsPromise = promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
private: { $not: true },
|
||||
content: {
|
||||
type: "post",
|
||||
channel: { $truthy: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
limit: 3000,
|
||||
})
|
||||
);
|
||||
|
||||
const [people, communitiesPosts] = await Promise.all([
|
||||
peoplePromise,
|
||||
communitiesPostsPromise,
|
||||
]);
|
||||
|
||||
const communities = Array.from(
|
||||
new Set(communitiesPosts.map((p) => p.value.content.channel))
|
||||
).filter((name) => {
|
||||
const normalizedName = name
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "");
|
||||
return searchRegex.exec(normalizedName);
|
||||
});
|
||||
|
||||
debugSearch("Done");
|
||||
return { people: Object.values(mapValues(people)), communities };
|
||||
};
|
||||
|
||||
const getFriends = async (profile) => {
|
||||
debugFriends("Fetching");
|
||||
|
||||
let graph = await ssb.client().friends.getGraph();
|
||||
|
||||
let connections = {};
|
||||
for (let key in graph) {
|
||||
let isFollowing = graph[profile.id] && graph[profile.id][key] > 0;
|
||||
let isFollowingBack = graph[key] && graph[key][profile.id] > 0;
|
||||
if (isFollowing && isFollowingBack) {
|
||||
connections[key] = "friends";
|
||||
} else if (isFollowing && !isFollowingBack) {
|
||||
connections[key] = "requestsSent";
|
||||
} else if (!isFollowing && isFollowingBack) {
|
||||
if (!graph[profile.id] || graph[profile.id][key] === undefined)
|
||||
connections[key] = "requestsReceived";
|
||||
}
|
||||
}
|
||||
|
||||
const profilesList = await Promise.all(
|
||||
Object.keys(connections).map((id) => getProfile(id))
|
||||
);
|
||||
const profilesHash = profilesList.reduce((hash, profile) => {
|
||||
hash[profile.id] = profile;
|
||||
return hash;
|
||||
}, {});
|
||||
|
||||
let result = {
|
||||
friends: [],
|
||||
requestsSent: [],
|
||||
requestsReceived: [],
|
||||
};
|
||||
for (let key in connections) {
|
||||
result[connections[key]].push(profilesHash[key]);
|
||||
}
|
||||
|
||||
debugFriends("Done");
|
||||
return result;
|
||||
};
|
||||
|
||||
const getFriendshipStatus = async (source, dest) => {
|
||||
debugFriendshipStatus("Fetching");
|
||||
|
||||
let requestRejectionsPromise = promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
author: source,
|
||||
content: {
|
||||
type: "contact",
|
||||
following: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
limit: 100,
|
||||
})
|
||||
).then(mapValues);
|
||||
|
||||
const [isFollowing, isFollowingBack, requestRejections] = await Promise.all([
|
||||
ssb.client().friends.isFollowing({ source: source, dest: dest }),
|
||||
ssb.client().friends.isFollowing({ source: dest, dest: source }),
|
||||
requestRejectionsPromise.then((x) => x.map((y) => y.content.contact)),
|
||||
]);
|
||||
|
||||
let status = "no_relation";
|
||||
if (isFollowing && isFollowingBack) {
|
||||
status = "friends";
|
||||
} else if (isFollowing && !isFollowingBack) {
|
||||
status = "request_sent";
|
||||
} else if (!isFollowing && isFollowingBack) {
|
||||
if (requestRejections.includes(dest)) {
|
||||
status = "request_rejected";
|
||||
} else {
|
||||
status = "request_received";
|
||||
}
|
||||
}
|
||||
debugFriendshipStatus("Done");
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
const getAllEntries = (query) => {
|
||||
let queries = [];
|
||||
if (query.author) {
|
||||
queries.push({ $filter: { value: { author: query.author } } });
|
||||
}
|
||||
if (query.type) {
|
||||
queries.push({ $filter: { value: { content: { type: query.type } } } });
|
||||
}
|
||||
const queryOpts = queries.length > 0 ? { query: queries } : {};
|
||||
|
||||
return promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
limit: 1000,
|
||||
...queryOpts,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
let profileCache = {};
|
||||
const getProfile = async (id) => {
|
||||
if (profileCache[id]) return profileCache[id];
|
||||
|
||||
let getKey = (key) => latestOwnerValue({ key, dest: id });
|
||||
|
||||
let [name, image, description] = await Promise.all([
|
||||
getKey("name"),
|
||||
getKey("image"),
|
||||
getKey("description"),
|
||||
]).catch((err) => {
|
||||
console.error("Could not retrieve profile for", id, err);
|
||||
});
|
||||
|
||||
let profile = { id, name, image, description };
|
||||
profileCache[id] = profile;
|
||||
|
||||
return profile;
|
||||
};
|
||||
|
||||
const progress = (callback) => {
|
||||
pull(
|
||||
ssb.client().replicate.changes(),
|
||||
pull.drain(callback, (err) => {
|
||||
console.error("Progress drain error", err);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const autofollow = async (id) => {
|
||||
const isFollowing = await ssb.client().friends.isFollowing({
|
||||
source: ssb.client().id,
|
||||
dest: id,
|
||||
});
|
||||
|
||||
if (!isFollowing) {
|
||||
await ssb.client().publish({
|
||||
type: "contact",
|
||||
contact: id,
|
||||
following: true,
|
||||
autofollow: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getCommunities = async () => {
|
||||
debugCommunities("Fetching");
|
||||
|
||||
const communitiesPosts = await promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
private: { $not: true },
|
||||
content: {
|
||||
type: "post",
|
||||
channel: { $truthy: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
limit: 1000,
|
||||
})
|
||||
);
|
||||
|
||||
const communities = Array.from(
|
||||
new Set(communitiesPosts.map((p) => p.value.content.channel))
|
||||
);
|
||||
|
||||
debugCommunities("Done");
|
||||
|
||||
return communities;
|
||||
};
|
||||
|
||||
const isMember = async (id, channel) => {
|
||||
debugCommunityIsMember("Fetching");
|
||||
const [lastSubscription] = await promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
limit: 1,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
author: id,
|
||||
content: {
|
||||
type: "channel",
|
||||
channel: channel,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
debugCommunityIsMember("Done");
|
||||
|
||||
return lastSubscription && lastSubscription.value.content.subscribed;
|
||||
};
|
||||
|
||||
const getCommunityMembers = async (name) => {
|
||||
debugCommunityMembers("Fetching");
|
||||
|
||||
const communityMembers = await promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
content: {
|
||||
type: "channel",
|
||||
channel: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
limit: 100,
|
||||
}),
|
||||
paramap(mapProfiles)
|
||||
);
|
||||
const dedupMembers = {};
|
||||
for (const member of communityMembers) {
|
||||
const author = member.value.author;
|
||||
if (dedupMembers[author]) continue;
|
||||
dedupMembers[author] = member;
|
||||
}
|
||||
const onlySubscribedMembers = Object.values(dedupMembers).filter(
|
||||
(x) => x.value.content.subscribed
|
||||
);
|
||||
const memberProfiles = onlySubscribedMembers.map(
|
||||
(x) => x.value.authorProfile
|
||||
);
|
||||
|
||||
debugCommunityMembers("Done");
|
||||
|
||||
return memberProfiles;
|
||||
};
|
||||
|
||||
const getProfileCommunities = async (id) => {
|
||||
debugCommunityProfileCommunities("Fetching");
|
||||
const subscriptions = await promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
author: id,
|
||||
content: {
|
||||
type: "channel",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
const dedupSubscriptions = {};
|
||||
for (const subscription of subscriptions) {
|
||||
const channel = subscription.value.content.channel;
|
||||
if (dedupSubscriptions[channel]) continue;
|
||||
dedupSubscriptions[channel] = subscription;
|
||||
}
|
||||
const onlyActiveSubscriptions = Object.values(dedupSubscriptions).filter(
|
||||
(x) => x.value.content.subscribed
|
||||
);
|
||||
const channelNames = onlyActiveSubscriptions.map(
|
||||
(x) => x.value.content.channel
|
||||
);
|
||||
debugCommunityProfileCommunities("Done");
|
||||
|
||||
return channelNames;
|
||||
};
|
||||
|
||||
const getPostWithReplies = async (channel, key) => {
|
||||
debugCommunityPosts("Fetching");
|
||||
|
||||
const postWithReplies = await promisePull(
|
||||
// @ts-ignore
|
||||
cat([
|
||||
ssb.client().query.read({
|
||||
reverse: false,
|
||||
limit: 1,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
key: key,
|
||||
value: {
|
||||
content: {
|
||||
type: "post",
|
||||
channel: channel,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
ssb.client().query.read({
|
||||
reverse: false,
|
||||
limit: 50,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
content: {
|
||||
root: key,
|
||||
channel: channel,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
ssb.client().query.read({
|
||||
reverse: false,
|
||||
limit: 50,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
content: {
|
||||
reply: { $prefix: [key] },
|
||||
channel: channel,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]),
|
||||
paramap(mapProfiles)
|
||||
);
|
||||
|
||||
debugCommunityPosts("Done");
|
||||
return postWithReplies;
|
||||
};
|
||||
|
||||
const getCommunityPosts = async (name) => {
|
||||
debugCommunityPosts("Fetching");
|
||||
|
||||
const communityPosts = await promisePull(
|
||||
ssb.client().query.read({
|
||||
reverse: true,
|
||||
query: [
|
||||
{
|
||||
$filter: {
|
||||
value: {
|
||||
content: {
|
||||
type: "post",
|
||||
channel: name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
limit: 1000,
|
||||
}),
|
||||
paramap(mapProfiles)
|
||||
);
|
||||
let communityPostsByKey = {};
|
||||
let replies = [];
|
||||
|
||||
let rootKey = (post) => {
|
||||
let replyKey =
|
||||
post.value.content.reply && Object.keys(post.value.content.reply)[0];
|
||||
return replyKey || post.value.content.root;
|
||||
};
|
||||
|
||||
for (let post of communityPosts) {
|
||||
if (rootKey(post)) {
|
||||
replies.push(post);
|
||||
} else {
|
||||
post.value.replies = [];
|
||||
communityPostsByKey[post.key] = post;
|
||||
}
|
||||
}
|
||||
for (let reply of replies) {
|
||||
let root = communityPostsByKey[rootKey(reply)];
|
||||
if (root) root.value.replies.push(reply);
|
||||
}
|
||||
|
||||
debugCommunityPosts("Done");
|
||||
|
||||
return Object.values(communityPostsByKey);
|
||||
};
|
||||
|
||||
if (!global.clearProfileInterval) {
|
||||
global.clearProfileInterval = setInterval(() => {
|
||||
debugProfile("Clearing profile cache");
|
||||
profileCache = {};
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mapProfiles,
|
||||
getPosts,
|
||||
search,
|
||||
getFriends,
|
||||
getAllEntries,
|
||||
getProfile,
|
||||
getSecretMessages,
|
||||
profileCache,
|
||||
getFriendshipStatus,
|
||||
getCommunities,
|
||||
getCommunityMembers,
|
||||
getCommunityPosts,
|
||||
getPostWithReplies,
|
||||
progress,
|
||||
autofollow,
|
||||
isMember,
|
||||
getProfileCommunities,
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
const Client = require("ssb-client");
|
||||
const ssbKeys = require("ssb-keys");
|
||||
const ssbConfig = require("./ssb-config");
|
||||
const queries = require("./queries");
|
||||
const debug = require("debug")("express");
|
||||
const { ssbFolder } = require("./utils");
|
||||
const fetch = require("node-fetch").default;
|
||||
|
||||
let ssbClient;
|
||||
let syncing = false;
|
||||
|
||||
const mode = process.env.MODE || "standalone";
|
||||
const ssbSecret = ssbKeys.loadOrCreateSync(`${ssbFolder()}/secret`);
|
||||
|
||||
const connectClient = (ssbSecret) => {
|
||||
Client(ssbSecret, ssbConfig, async (err, server) => {
|
||||
if (err) throw err;
|
||||
|
||||
ssbClient = server;
|
||||
|
||||
queries.progress(({ rate, feeds, incompleteFeeds, progress, total }) => {
|
||||
if (incompleteFeeds > 0) {
|
||||
if (!syncing) debug("syncing");
|
||||
syncing = true;
|
||||
} else {
|
||||
syncing = false;
|
||||
}
|
||||
});
|
||||
console.log("SSB Client ready");
|
||||
|
||||
if (mode == "standalone") addFirstPub();
|
||||
});
|
||||
};
|
||||
|
||||
const addFirstPub = async () => {
|
||||
const peers = await ssbClient.gossip.peers();
|
||||
if (peers.length == 0) {
|
||||
console.log("No pubs found, adding pub.feedless.social as a first pub");
|
||||
try {
|
||||
const response = await fetch("https://feedless.social/pub_invite");
|
||||
const { invite } = await response.json();
|
||||
await ssbClient.invite.accept(invite);
|
||||
} catch (e) {
|
||||
console.error("Could add feedless pub", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.client = () => ssbClient;
|
||||
module.exports.isSyncing = () => syncing;
|
||||
module.exports.reconnectWith = connectClient;
|
||||
|
||||
connectClient(ssbSecret);
|
|
@ -0,0 +1,20 @@
|
|||
const configInject = require("ssb-config/inject");
|
||||
|
||||
module.exports = configInject(process.env.CONFIG_FOLDER || "ssb", {
|
||||
connections: {
|
||||
incoming: {
|
||||
net: [
|
||||
{
|
||||
scope: "public",
|
||||
host: "0.0.0.0",
|
||||
external: "pub.feedless.social",
|
||||
transform: "shs",
|
||||
port: process.env.SSB_PORT || 8008,
|
||||
},
|
||||
],
|
||||
},
|
||||
outgoing: {
|
||||
net: [{ transform: "shs" }],
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
try {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { writeKey, ssbFolder } = require("./utils");
|
||||
const SecretStack = require("secret-stack");
|
||||
const mkdirp = require("mkdirp");
|
||||
// const ssbKeys = require("ssb-keys");
|
||||
|
||||
console.log("ssbFolder", ssbFolder());
|
||||
|
||||
const folderExists = fs.existsSync(ssbFolder());
|
||||
if (!folderExists) mkdirp.sync(ssbFolder());
|
||||
|
||||
const keysPath = path.join(ssbFolder(), "/secret");
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
}
|
||||
// const keys = ssbKeys.loadOrCreateSync(keysPath);
|
||||
|
||||
// // Need to use secret-stack directly instead of ssb-server here otherwise is not compatible with patchwork .ssb folder
|
||||
// const Server = require("secret-stack")();
|
||||
// .use(require("ssb-db"))
|
||||
// .use(require("ssb-master"));
|
||||
// .use(require("ssb-gossip"))
|
||||
// .use(require("ssb-replicate"))
|
||||
// .use(require("ssb-backlinks"))
|
||||
// .use(require("ssb-about"))
|
||||
// .use(require("ssb-contacts"))
|
||||
// .use(require("ssb-invite"))
|
||||
// .use(require("./monkeypatch/ssb-friends"))
|
||||
// .use(require("ssb-query"))
|
||||
// .use(require("ssb-device-address"))
|
||||
// .use(require("./plugins/memory-identities"))
|
||||
// .use(require("ssb-blobs"))
|
||||
// .use(require("ssb-private"));
|
||||
|
||||
// const config = require("./ssb-config");
|
||||
// const server = Server(config);
|
||||
// console.log("SSB server started at", config.port);
|
||||
|
||||
// // save an updated list of methods this server has made public
|
||||
// // in a location that ssb-client will know to check
|
||||
// const manifest = server.getManifest();
|
||||
// fs.writeFileSync(
|
||||
// path.join(config.path, "manifest.json"), // ~/.ssb/manifest.json
|
||||
// JSON.stringify(manifest)
|
||||
// );
|
||||
|
||||
// // SSB server automatically creates a secret key, but we want the user flow where they choose to create a key or use an existing one
|
||||
// const mode = process.env.MODE || "standalone";
|
||||
// if (mode == "standalone" && !secretExists) {
|
||||
// fs.writeFileSync(`${ssbFolder()}/logged-out`, "");
|
||||
// }
|
|
@ -0,0 +1,79 @@
|
|||
const fs = require("fs");
|
||||
const pull = require("pull-stream");
|
||||
|
||||
module.exports.asyncRouter = (app) => {
|
||||
const debug = require("debug")("router");
|
||||
|
||||
let wrapper = (method, path, opts, fn) => async (req, res, next) => {
|
||||
if (typeof opts == "function") fn = opts;
|
||||
if (!opts.public && !req.context.profile) {
|
||||
if (method == "POST") {
|
||||
res.status(401);
|
||||
return res.send("You are not logged in");
|
||||
}
|
||||
return res.redirect("/");
|
||||
}
|
||||
|
||||
req.context.path = path;
|
||||
try {
|
||||
debug(`${method} ${path}`);
|
||||
await fn(req, res);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
return {
|
||||
get: (path, fn, opts) => {
|
||||
app.get(path, wrapper("GET", path, fn, opts));
|
||||
},
|
||||
post: (path, fn, opts) => {
|
||||
debug(`POST ${path}`);
|
||||
app.post(path, wrapper("POST", path, fn, opts));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ssbFolder = () => {
|
||||
let homeFolder =
|
||||
process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
return `${process.argv[2] || homeFolder + "/.ssb"}`;
|
||||
};
|
||||
module.exports.ssbFolder = ssbFolder;
|
||||
|
||||
module.exports.writeKey = (key, path) => {
|
||||
let secretPath = `${ssbFolder()}${path}`;
|
||||
|
||||
// Same options ssb-keys use
|
||||
try {
|
||||
fs.mkdirSync(ssbFolder(), { recursive: true });
|
||||
} catch (e) {}
|
||||
fs.writeFileSync(secretPath, key, { mode: 0x100, flag: "wx" });
|
||||
};
|
||||
|
||||
// From ssb-keys
|
||||
module.exports.reconstructKeys = (keyfile) => {
|
||||
var privateKey = keyfile
|
||||
.replace(/\s*\#[^\n]*/g, "")
|
||||
.split("\n")
|
||||
.filter((x) => x)
|
||||
.join("");
|
||||
|
||||
var keys = JSON.parse(privateKey);
|
||||
const hasSigil = (x) => /^(@|%|&)/.test(x);
|
||||
|
||||
if (!hasSigil(keys.id)) keys.id = "@" + keys.public;
|
||||
return keys;
|
||||
};
|
||||
|
||||
module.exports.promisePull = (...streams) =>
|
||||
new Promise((resolve, reject) => {
|
||||
pull(
|
||||
...streams,
|
||||
pull.collect((err, msgs) => {
|
||||
if (err) return reject(err);
|
||||
return resolve(msgs);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
module.exports.mapValues = (x) => x.map((y) => y.value);
|
File diff suppressed because it is too large
Load Diff
|
@ -4,14 +4,41 @@
|
|||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "./tools/build-backend.js"
|
||||
"start": "SSB_DIR=~/.ssb node index.js",
|
||||
"build": "./tools/build-backend.js",
|
||||
"build:watch": "watch ./tools/build-backend.js lib/"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bindings-noderify-nodejs-mobile": "10.3.0",
|
||||
"debug": "^4.1.1",
|
||||
"express": "^4.17.1",
|
||||
"ora": "^4.0.4"
|
||||
"layered-graph": "^1.1.3",
|
||||
"leveldown-nodejs-mobile": "5.1.1-3",
|
||||
"ora": "^4.0.4",
|
||||
"pull-cat": "^1.1.11",
|
||||
"pull-paramap": "^1.2.2",
|
||||
"pull-stream": "^3.6.14",
|
||||
"secret-stack": "6.3.1",
|
||||
"sodium-chloride-native-nodejs-mobile": "1.1.0",
|
||||
"ssb-about": "^2.0.1",
|
||||
"ssb-backlinks": "^1.0.0",
|
||||
"ssb-blobs": "^1.2.2",
|
||||
"ssb-config": "^3.4.4",
|
||||
"ssb-contacts": "0.0.2",
|
||||
"ssb-db": "^19.4.0",
|
||||
"ssb-device-address": "^1.1.6",
|
||||
"ssb-friends": "^4.1.4",
|
||||
"ssb-gossip": "^1.1.1",
|
||||
"ssb-invite": "^2.1.4",
|
||||
"ssb-keys": "7.2.0",
|
||||
"ssb-master": "^1.0.3",
|
||||
"ssb-private": "^0.2.3",
|
||||
"ssb-query": "^2.4.3",
|
||||
"ssb-ref": "^2.13.9",
|
||||
"ssb-replicate": "^1.3.2",
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"noderify": "4.3.0"
|
||||
|
|
|
@ -20,11 +20,6 @@ function onFailure() {
|
|||
# remove unused packages such as sodium-browserify etc
|
||||
# leveldown: newer versions of leveldown are intentionally ignoring
|
||||
# nodejs-mobile support, so we run an older version
|
||||
# utp-native: we want to compile for nodejs-mobile instead of using prebuilds
|
||||
# node-extend: can't remember why we need to replace it, build seemed to fail
|
||||
# non-private-ip: we use a "better" fork of this package
|
||||
# multiserver net plugin: we're fixing a corner case bug with error recovery
|
||||
# rn-bridge: this is not an npm package, it's just a nodejs-mobile shortcut
|
||||
# bl: we didn't use it, and bl@0.8.x has security vulnerabilities
|
||||
# bufferutil: because we want nodejs-mobile to load its native bindings
|
||||
# supports-color: optional dependency within package `debug`
|
||||
|
@ -34,20 +29,9 @@ $(npm bin)/noderify \
|
|||
--replace.bindings=bindings-noderify-nodejs-mobile \
|
||||
--replace.chloride=sodium-chloride-native-nodejs-mobile \
|
||||
--replace.leveldown=leveldown-nodejs-mobile \
|
||||
--replace.utp-native=utp-native-nodejs-mobile \
|
||||
--replace.node-extend=xtend \
|
||||
--replace.non-private-ip=non-private-ip-android \
|
||||
--replace.multiserver/plugins/net=staltz-multiserver/plugins/net \
|
||||
--filter=rn-bridge \
|
||||
--filter=bl \
|
||||
--filter=bufferutil \
|
||||
--filter=supports-color \
|
||||
--filter=utf-8-validate \
|
||||
--filter=bip39/src/wordlists/chinese_simplified.json \
|
||||
--filter=bip39/src/wordlists/chinese_traditional.json \
|
||||
--filter=bip39/src/wordlists/french.json \
|
||||
--filter=bip39/src/wordlists/italian.json \
|
||||
--filter=bip39/src/wordlists/japanese.json \
|
||||
--filter=bip39/src/wordlists/korean.json \
|
||||
--filter=bip39/src/wordlists/spanish.json \
|
||||
backend/index.js > out/index.js;
|
||||
index.js > out/index.js;
|
|
@ -30,3 +30,5 @@ find ./node_modules \
|
|||
-name "electron-napi.node" \
|
||||
\) \
|
||||
-print0 | xargs -0 rm -rf # delete everything in the list
|
||||
# Android builds
|
||||
rm -rf ./node_modules/sodium-native-nodejs-mobile/libsodium/android-*
|
|
@ -36,17 +36,10 @@ async function runAndReport(label, task) {
|
|||
}
|
||||
|
||||
(async function () {
|
||||
if (process.env.NODE_ENV == "production") {
|
||||
await runAndReport(
|
||||
"Install backend node modules",
|
||||
exec("npm install --no-optional")
|
||||
);
|
||||
|
||||
await runAndReport(
|
||||
"Remove unused files meant for macOS or Windows or Electron",
|
||||
exec("./tools/backend/remove-unused-files.sh")
|
||||
);
|
||||
}
|
||||
await runAndReport(
|
||||
"Remove unused files meant for Android or Electron",
|
||||
exec("./tools/backend/remove-unused-files.sh")
|
||||
);
|
||||
|
||||
await runAndReport(
|
||||
"Bundle and minify backend JS into one file",
|
|
@ -17,8 +17,10 @@
|
|||
902D04812458AAAA007CFE56 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 902D047F2458AAAA007CFE56 /* LaunchScreen.storyboard */; };
|
||||
902D048C2458AAAA007CFE56 /* feedlessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902D048B2458AAAA007CFE56 /* feedlessTests.swift */; };
|
||||
902D04972458AAAA007CFE56 /* feedlessUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902D04962458AAAA007CFE56 /* feedlessUITests.swift */; };
|
||||
90B81B90245ECB25005C5D31 /* NodeMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90B81B8F245ECB25005C5D31 /* NodeMobile.framework */; };
|
||||
90B81B91245ECB25005C5D31 /* NodeMobile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 90B81B8F245ECB25005C5D31 /* NodeMobile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
9053055B245F7C23006C054D /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9053055A245F7C23006C054D /* Utils.swift */; };
|
||||
9053055F245FE975006C054D /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9053055E245FE975006C054D /* Types.swift */; };
|
||||
90A1A5312460D24600D00245 /* NodeMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90A1A52D2460D16500D00245 /* NodeMobile.framework */; };
|
||||
90A1A5322460D24600D00245 /* NodeMobile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 90A1A52D2460D16500D00245 /* NodeMobile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
90B81B96245ECC00005C5D31 /* NodeRunner.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90B81B95245ECC00005C5D31 /* NodeRunner.mm */; };
|
||||
90B81B98245F1699005C5D31 /* backend in Resources */ = {isa = PBXBuildFile; fileRef = 90B81B97245F1698005C5D31 /* backend */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
@ -41,13 +43,13 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
90B81B92245ECB25005C5D31 /* Embed Frameworks */ = {
|
||||
90A1A5332460D24600D00245 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
90B81B91245ECB25005C5D31 /* NodeMobile.framework in Embed Frameworks */,
|
||||
90A1A5322460D24600D00245 /* NodeMobile.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -71,7 +73,9 @@
|
|||
902D04922458AAAA007CFE56 /* feedlessUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = feedlessUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
902D04962458AAAA007CFE56 /* feedlessUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = feedlessUITests.swift; sourceTree = "<group>"; };
|
||||
902D04982458AAAA007CFE56 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
90B81B8F245ECB25005C5D31 /* NodeMobile.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NodeMobile.framework; path = libnode/NodeMobile.framework; sourceTree = "<group>"; };
|
||||
9053055A245F7C23006C054D /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
|
||||
9053055E245FE975006C054D /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = "<group>"; };
|
||||
90A1A52D2460D16500D00245 /* NodeMobile.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NodeMobile.framework; path = libnode/NodeMobile.framework; sourceTree = "<group>"; };
|
||||
90B81B93245ECBBB005C5D31 /* NodeRunner.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NodeRunner.h; sourceTree = "<group>"; };
|
||||
90B81B94245ECBFF005C5D31 /* feedless-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "feedless-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
90B81B95245ECC00005C5D31 /* NodeRunner.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = NodeRunner.mm; sourceTree = "<group>"; };
|
||||
|
@ -83,7 +87,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
90B81B90245ECB25005C5D31 /* NodeMobile.framework in Frameworks */,
|
||||
90A1A5312460D24600D00245 /* NodeMobile.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -149,6 +153,8 @@
|
|||
90B81B93245ECBBB005C5D31 /* NodeRunner.h */,
|
||||
90B81B95245ECC00005C5D31 /* NodeRunner.mm */,
|
||||
90B81B94245ECBFF005C5D31 /* feedless-Bridging-Header.h */,
|
||||
9053055A245F7C23006C054D /* Utils.swift */,
|
||||
9053055E245FE975006C054D /* Types.swift */,
|
||||
);
|
||||
path = feedless;
|
||||
sourceTree = "<group>";
|
||||
|
@ -182,7 +188,7 @@
|
|||
90B81B8E245ECB25005C5D31 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90B81B8F245ECB25005C5D31 /* NodeMobile.framework */,
|
||||
90A1A52D2460D16500D00245 /* NodeMobile.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -197,7 +203,7 @@
|
|||
902D046D2458AAA7007CFE56 /* Sources */,
|
||||
902D046E2458AAA7007CFE56 /* Frameworks */,
|
||||
902D046F2458AAA7007CFE56 /* Resources */,
|
||||
90B81B92245ECB25005C5D31 /* Embed Frameworks */,
|
||||
90A1A5332460D24600D00245 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -324,8 +330,10 @@
|
|||
902D04752458AAA7007CFE56 /* AppDelegate.swift in Sources */,
|
||||
90B81B96245ECC00005C5D31 /* NodeRunner.mm in Sources */,
|
||||
900BE29A2458B05800C77595 /* Login.swift in Sources */,
|
||||
9053055B245F7C23006C054D /* Utils.swift in Sources */,
|
||||
902D04772458AAA7007CFE56 /* SceneDelegate.swift in Sources */,
|
||||
900BE2972458AEEC00C77595 /* Index.swift in Sources */,
|
||||
9053055F245FE975006C054D /* Types.swift in Sources */,
|
||||
902D04792458AAA7007CFE56 /* Wall.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -495,6 +503,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"feedless/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 8FY84BDRUF;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -522,6 +531,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"feedless/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 8FY84BDRUF;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -535,6 +545,7 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = com.rogeriochaves.feedless;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "feedless/feedless-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
|
Binary file not shown.
|
@ -14,8 +14,8 @@
|
|||
filePath = "feedless/screens/Wall.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "33"
|
||||
endingLineNumber = "33"
|
||||
startingLineNumber = "19"
|
||||
endingLineNumber = "19"
|
||||
landmarkName = "init()"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
//
|
||||
// Queries.swift
|
||||
// feedless
|
||||
//
|
||||
// Created by Rogerio Chaves on 04/05/20.
|
||||
// Copyright © 2020 Rogerio Chaves. All rights reserved.
|
||||
//
|
|
@ -21,22 +21,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
|
||||
let jsFile = Bundle.main.path(forResource: "backend/index.js", ofType: nil)!
|
||||
let jsFile = Bundle.main.path(forResource: "backend/out/index.js", ofType: nil)!
|
||||
|
||||
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
|
||||
print("documentsPath", documentsPath)
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
NodeRunner.startEngine(withArguments: ["node", jsFile])
|
||||
NodeRunner.startEngine(withArguments: ["node", jsFile, documentsPath])
|
||||
}
|
||||
|
||||
//
|
||||
// do {
|
||||
// context.evaluateScript(try String(contentsOf: url), withSourceURL: url)
|
||||
// } catch _ {
|
||||
// fatalError("could not evaluate index.js")
|
||||
// }
|
||||
//
|
||||
// let main = context.objectForKeyedSubscript("main")
|
||||
// print("aaaaaaaaaaaaaaaaaaaa", main?.call(withArguments: []))
|
||||
|
||||
// Create the SwiftUI view that provides the window contents.
|
||||
let contentView = Index()
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Types.swift
|
||||
// feedless
|
||||
//
|
||||
// Created by Rogerio Chaves on 04/05/20.
|
||||
// Copyright © 2020 Rogerio Chaves. All rights reserved.
|
||||
//
|
||||
|
||||
struct Post: Codable {
|
||||
public var text: String
|
||||
}
|
||||
|
||||
struct AuthorContent<T: Codable>: Codable {
|
||||
public var author: String
|
||||
public var content: T
|
||||
}
|
||||
|
||||
struct Entry<T: Codable>: Codable {
|
||||
public var key: String
|
||||
public var value: T
|
||||
}
|
||||
|
||||
struct Profile: Codable {
|
||||
public var id: String
|
||||
public var name: String
|
||||
public var image: String
|
||||
}
|
||||
|
||||
struct User : Codable {
|
||||
public var profile: Profile?
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Utils.swift
|
||||
// feedless
|
||||
//
|
||||
// Created by Rogerio Chaves on 04/05/20.
|
||||
// Copyright © 2020 Rogerio Chaves. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension String {
|
||||
func image() -> UIImage? {
|
||||
let nsString = (self as NSString)
|
||||
let font = UIFont.systemFont(ofSize: 16) // you can change your font size here
|
||||
let stringAttributes = [NSAttributedString.Key.font: font]
|
||||
let imageSize = nsString.size(withAttributes: stringAttributes)
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0) // begin image context
|
||||
UIColor.clear.set() // clear background
|
||||
UIRectFill(CGRect(origin: CGPoint(), size: imageSize)) // set rect size
|
||||
nsString.draw(at: CGPoint.zero, withAttributes: stringAttributes) // draw text within rect
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext() // create image from context
|
||||
UIGraphicsEndImageContext() // end image context
|
||||
|
||||
return image ?? UIImage()
|
||||
}
|
||||
}
|
|
@ -8,13 +8,60 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct Index: View {
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
NavigationLink(destination: Login()) {
|
||||
Text("Login")
|
||||
class Context: ObservableObject {
|
||||
@Published var responding:Bool = false
|
||||
@Published var loggedIn:Bool = false
|
||||
@Published var profile:Profile? = nil
|
||||
|
||||
func fetch() {
|
||||
let url = URL(string: "http://127.0.0.1:3000/user")!
|
||||
|
||||
URLSession.shared.dataTask(with: url) {(data, response, error) in
|
||||
if let todoData = data {
|
||||
DispatchQueue.main.async {
|
||||
self.responding = true
|
||||
self.loggedIn = true
|
||||
}
|
||||
|
||||
do {
|
||||
let decodedData = try JSONDecoder().decode(User.self, from: todoData)
|
||||
DispatchQueue.main.async {
|
||||
self.profile = decodedData.profile
|
||||
self.loggedIn = true
|
||||
}
|
||||
} catch {
|
||||
print("Error loading user")
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
struct Index: View {
|
||||
@ObservedObject var context = Context()
|
||||
@State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if (context.responding) {
|
||||
if (context.loggedIn) {
|
||||
Wall()
|
||||
} else {
|
||||
NavigationView {
|
||||
NavigationLink(destination: Login()) {
|
||||
Text("Login")
|
||||
}
|
||||
.navigationBarTitle(Text("Index"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Waiting for server")
|
||||
}
|
||||
}
|
||||
.onReceive(self.timer) { (_) in
|
||||
if (!self.context.responding) {
|
||||
self.context.fetch()
|
||||
}
|
||||
.navigationBarTitle(Text("Index"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,31 +8,15 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct Post: Codable {
|
||||
public var text: String
|
||||
}
|
||||
|
||||
struct AuthorContent<T: Codable>: Codable {
|
||||
public var author: String
|
||||
public var content: T
|
||||
}
|
||||
|
||||
struct Entry<T: Codable>: Codable {
|
||||
public var key: String
|
||||
public var value: T
|
||||
}
|
||||
|
||||
class FetchPosts: ObservableObject {
|
||||
// 1.
|
||||
@Published var posts = [Entry<AuthorContent<Post>>]()
|
||||
@Published var posts = [Entry<AuthorContent<Post>>]()
|
||||
|
||||
init() {
|
||||
let url = URL(string: "http://127.0.0.1:3000/posts")!
|
||||
// 2.
|
||||
|
||||
URLSession.shared.dataTask(with: url) {(data, response, error) in
|
||||
do {
|
||||
if let todoData = data {
|
||||
// 3.
|
||||
let decodedData = try JSONDecoder().decode([Entry<AuthorContent<Post>>].self, from: todoData)
|
||||
DispatchQueue.main.async {
|
||||
self.posts = decodedData
|
||||
|
@ -55,7 +39,6 @@ struct Wall: View {
|
|||
var body: some View {
|
||||
TabView(selection: $selection){
|
||||
VStack {
|
||||
// 2.
|
||||
List(fetch.posts, id: \.key) { post in
|
||||
VStack(alignment: .leading) {
|
||||
Text(post.value.content.text)
|
||||
|
@ -64,7 +47,8 @@ struct Wall: View {
|
|||
}
|
||||
.tabItem {
|
||||
VStack {
|
||||
Text("🙂")
|
||||
Image(uiImage: "🙂".image()!).renderingMode(.original)
|
||||
Text("Profile")
|
||||
}
|
||||
}
|
||||
.tag(0)
|
||||
|
@ -72,11 +56,12 @@ struct Wall: View {
|
|||
.font(.title)
|
||||
.tabItem {
|
||||
VStack {
|
||||
Text("👨👧👦")
|
||||
Image(uiImage: "👨👧👦".image()!).renderingMode(.original)
|
||||
Text("Friends")
|
||||
}
|
||||
}
|
||||
.tag(1)
|
||||
}
|
||||
}.accentColor(Color.purple)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,5 +1,4 @@
|
|||
const fs = require("fs");
|
||||
const leftpad = require("left-pad"); // I don't believe I'm depending on this
|
||||
const pull = require("pull-stream");
|
||||
const split = require("split-buffer");
|
||||
const metrics = require("./metrics");
|
||||
|
|
Loading…
Reference in New Issue