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");
|
console.log("Loading nodejs");
|
||||||
|
|
||||||
const express = require("express");
|
require("./lib/ssb");
|
||||||
const app = express();
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
const posts = [
|
require("./lib/express");
|
||||||
{"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}`)
|
|
||||||
);
|
|
||||||
|
|
|
@ -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": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bindings-noderify-nodejs-mobile": "10.3.0",
|
"bindings-noderify-nodejs-mobile": "10.3.0",
|
||||||
|
"debug": "^4.1.1",
|
||||||
"express": "^4.17.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": {
|
"devDependencies": {
|
||||||
"noderify": "4.3.0"
|
"noderify": "4.3.0"
|
||||||
|
|
|
@ -20,11 +20,6 @@ function onFailure() {
|
||||||
# remove unused packages such as sodium-browserify etc
|
# remove unused packages such as sodium-browserify etc
|
||||||
# leveldown: newer versions of leveldown are intentionally ignoring
|
# leveldown: newer versions of leveldown are intentionally ignoring
|
||||||
# nodejs-mobile support, so we run an older version
|
# 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
|
# 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
|
# bufferutil: because we want nodejs-mobile to load its native bindings
|
||||||
# supports-color: optional dependency within package `debug`
|
# supports-color: optional dependency within package `debug`
|
||||||
|
@ -34,20 +29,9 @@ $(npm bin)/noderify \
|
||||||
--replace.bindings=bindings-noderify-nodejs-mobile \
|
--replace.bindings=bindings-noderify-nodejs-mobile \
|
||||||
--replace.chloride=sodium-chloride-native-nodejs-mobile \
|
--replace.chloride=sodium-chloride-native-nodejs-mobile \
|
||||||
--replace.leveldown=leveldown-nodejs-mobile \
|
--replace.leveldown=leveldown-nodejs-mobile \
|
||||||
--replace.utp-native=utp-native-nodejs-mobile \
|
|
||||||
--replace.node-extend=xtend \
|
--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=bl \
|
||||||
--filter=bufferutil \
|
--filter=bufferutil \
|
||||||
--filter=supports-color \
|
--filter=supports-color \
|
||||||
--filter=utf-8-validate \
|
--filter=utf-8-validate \
|
||||||
--filter=bip39/src/wordlists/chinese_simplified.json \
|
index.js > out/index.js;
|
||||||
--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;
|
|
|
@ -30,3 +30,5 @@ find ./node_modules \
|
||||||
-name "electron-napi.node" \
|
-name "electron-napi.node" \
|
||||||
\) \
|
\) \
|
||||||
-print0 | xargs -0 rm -rf # delete everything in the list
|
-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 () {
|
(async function () {
|
||||||
if (process.env.NODE_ENV == "production") {
|
await runAndReport(
|
||||||
await runAndReport(
|
"Remove unused files meant for Android or Electron",
|
||||||
"Install backend node modules",
|
exec("./tools/backend/remove-unused-files.sh")
|
||||||
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(
|
await runAndReport(
|
||||||
"Bundle and minify backend JS into one file",
|
"Bundle and minify backend JS into one file",
|
|
@ -17,8 +17,10 @@
|
||||||
902D04812458AAAA007CFE56 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 902D047F2458AAAA007CFE56 /* LaunchScreen.storyboard */; };
|
902D04812458AAAA007CFE56 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 902D047F2458AAAA007CFE56 /* LaunchScreen.storyboard */; };
|
||||||
902D048C2458AAAA007CFE56 /* feedlessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902D048B2458AAAA007CFE56 /* feedlessTests.swift */; };
|
902D048C2458AAAA007CFE56 /* feedlessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902D048B2458AAAA007CFE56 /* feedlessTests.swift */; };
|
||||||
902D04972458AAAA007CFE56 /* feedlessUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902D04962458AAAA007CFE56 /* feedlessUITests.swift */; };
|
902D04972458AAAA007CFE56 /* feedlessUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902D04962458AAAA007CFE56 /* feedlessUITests.swift */; };
|
||||||
90B81B90245ECB25005C5D31 /* NodeMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90B81B8F245ECB25005C5D31 /* NodeMobile.framework */; };
|
9053055B245F7C23006C054D /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9053055A245F7C23006C054D /* Utils.swift */; };
|
||||||
90B81B91245ECB25005C5D31 /* NodeMobile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 90B81B8F245ECB25005C5D31 /* NodeMobile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
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 */; };
|
90B81B96245ECC00005C5D31 /* NodeRunner.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90B81B95245ECC00005C5D31 /* NodeRunner.mm */; };
|
||||||
90B81B98245F1699005C5D31 /* backend in Resources */ = {isa = PBXBuildFile; fileRef = 90B81B97245F1698005C5D31 /* backend */; };
|
90B81B98245F1699005C5D31 /* backend in Resources */ = {isa = PBXBuildFile; fileRef = 90B81B97245F1698005C5D31 /* backend */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
@ -41,13 +43,13 @@
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
90B81B92245ECB25005C5D31 /* Embed Frameworks */ = {
|
90A1A5332460D24600D00245 /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolderSpec = 10;
|
dstSubfolderSpec = 10;
|
||||||
files = (
|
files = (
|
||||||
90B81B91245ECB25005C5D31 /* NodeMobile.framework in Embed Frameworks */,
|
90A1A5322460D24600D00245 /* NodeMobile.framework in Embed Frameworks */,
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -71,7 +73,9 @@
|
||||||
902D04922458AAAA007CFE56 /* feedlessUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = feedlessUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
90B81B95245ECC00005C5D31 /* NodeRunner.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = NodeRunner.mm; sourceTree = "<group>"; };
|
||||||
|
@ -83,7 +87,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
90B81B90245ECB25005C5D31 /* NodeMobile.framework in Frameworks */,
|
90A1A5312460D24600D00245 /* NodeMobile.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -149,6 +153,8 @@
|
||||||
90B81B93245ECBBB005C5D31 /* NodeRunner.h */,
|
90B81B93245ECBBB005C5D31 /* NodeRunner.h */,
|
||||||
90B81B95245ECC00005C5D31 /* NodeRunner.mm */,
|
90B81B95245ECC00005C5D31 /* NodeRunner.mm */,
|
||||||
90B81B94245ECBFF005C5D31 /* feedless-Bridging-Header.h */,
|
90B81B94245ECBFF005C5D31 /* feedless-Bridging-Header.h */,
|
||||||
|
9053055A245F7C23006C054D /* Utils.swift */,
|
||||||
|
9053055E245FE975006C054D /* Types.swift */,
|
||||||
);
|
);
|
||||||
path = feedless;
|
path = feedless;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -182,7 +188,7 @@
|
||||||
90B81B8E245ECB25005C5D31 /* Frameworks */ = {
|
90B81B8E245ECB25005C5D31 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
90B81B8F245ECB25005C5D31 /* NodeMobile.framework */,
|
90A1A52D2460D16500D00245 /* NodeMobile.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -197,7 +203,7 @@
|
||||||
902D046D2458AAA7007CFE56 /* Sources */,
|
902D046D2458AAA7007CFE56 /* Sources */,
|
||||||
902D046E2458AAA7007CFE56 /* Frameworks */,
|
902D046E2458AAA7007CFE56 /* Frameworks */,
|
||||||
902D046F2458AAA7007CFE56 /* Resources */,
|
902D046F2458AAA7007CFE56 /* Resources */,
|
||||||
90B81B92245ECB25005C5D31 /* Embed Frameworks */,
|
90A1A5332460D24600D00245 /* Embed Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
@ -324,8 +330,10 @@
|
||||||
902D04752458AAA7007CFE56 /* AppDelegate.swift in Sources */,
|
902D04752458AAA7007CFE56 /* AppDelegate.swift in Sources */,
|
||||||
90B81B96245ECC00005C5D31 /* NodeRunner.mm in Sources */,
|
90B81B96245ECC00005C5D31 /* NodeRunner.mm in Sources */,
|
||||||
900BE29A2458B05800C77595 /* Login.swift in Sources */,
|
900BE29A2458B05800C77595 /* Login.swift in Sources */,
|
||||||
|
9053055B245F7C23006C054D /* Utils.swift in Sources */,
|
||||||
902D04772458AAA7007CFE56 /* SceneDelegate.swift in Sources */,
|
902D04772458AAA7007CFE56 /* SceneDelegate.swift in Sources */,
|
||||||
900BE2972458AEEC00C77595 /* Index.swift in Sources */,
|
900BE2972458AEEC00C77595 /* Index.swift in Sources */,
|
||||||
|
9053055F245FE975006C054D /* Types.swift in Sources */,
|
||||||
902D04792458AAA7007CFE56 /* Wall.swift in Sources */,
|
902D04792458AAA7007CFE56 /* Wall.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -495,6 +503,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"feedless/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"feedless/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 8FY84BDRUF;
|
DEVELOPMENT_TEAM = 8FY84BDRUF;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -522,6 +531,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"feedless/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"feedless/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 8FY84BDRUF;
|
DEVELOPMENT_TEAM = 8FY84BDRUF;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -535,6 +545,7 @@
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.rogeriochaves.feedless;
|
PRODUCT_BUNDLE_IDENTIFIER = com.rogeriochaves.feedless;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "feedless/feedless-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "feedless/feedless-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
|
|
Binary file not shown.
|
@ -14,8 +14,8 @@
|
||||||
filePath = "feedless/screens/Wall.swift"
|
filePath = "feedless/screens/Wall.swift"
|
||||||
startingColumnNumber = "9223372036854775807"
|
startingColumnNumber = "9223372036854775807"
|
||||||
endingColumnNumber = "9223372036854775807"
|
endingColumnNumber = "9223372036854775807"
|
||||||
startingLineNumber = "33"
|
startingLineNumber = "19"
|
||||||
endingLineNumber = "33"
|
endingLineNumber = "19"
|
||||||
landmarkName = "init()"
|
landmarkName = "init()"
|
||||||
landmarkType = "7">
|
landmarkType = "7">
|
||||||
</BreakpointContent>
|
</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.
|
// 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).
|
// 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 {
|
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.
|
// Create the SwiftUI view that provides the window contents.
|
||||||
let contentView = Index()
|
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
|
import SwiftUI
|
||||||
|
|
||||||
struct Index: View {
|
class Context: ObservableObject {
|
||||||
var body: some View {
|
@Published var responding:Bool = false
|
||||||
NavigationView {
|
@Published var loggedIn:Bool = false
|
||||||
NavigationLink(destination: Login()) {
|
@Published var profile:Profile? = nil
|
||||||
Text("Login")
|
|
||||||
|
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
|
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 {
|
class FetchPosts: ObservableObject {
|
||||||
// 1.
|
@Published var posts = [Entry<AuthorContent<Post>>]()
|
||||||
@Published var posts = [Entry<AuthorContent<Post>>]()
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let url = URL(string: "http://127.0.0.1:3000/posts")!
|
let url = URL(string: "http://127.0.0.1:3000/posts")!
|
||||||
// 2.
|
|
||||||
URLSession.shared.dataTask(with: url) {(data, response, error) in
|
URLSession.shared.dataTask(with: url) {(data, response, error) in
|
||||||
do {
|
do {
|
||||||
if let todoData = data {
|
if let todoData = data {
|
||||||
// 3.
|
|
||||||
let decodedData = try JSONDecoder().decode([Entry<AuthorContent<Post>>].self, from: todoData)
|
let decodedData = try JSONDecoder().decode([Entry<AuthorContent<Post>>].self, from: todoData)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.posts = decodedData
|
self.posts = decodedData
|
||||||
|
@ -55,7 +39,6 @@ struct Wall: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $selection){
|
TabView(selection: $selection){
|
||||||
VStack {
|
VStack {
|
||||||
// 2.
|
|
||||||
List(fetch.posts, id: \.key) { post in
|
List(fetch.posts, id: \.key) { post in
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(post.value.content.text)
|
Text(post.value.content.text)
|
||||||
|
@ -64,7 +47,8 @@ struct Wall: View {
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
VStack {
|
VStack {
|
||||||
Text("🙂")
|
Image(uiImage: "🙂".image()!).renderingMode(.original)
|
||||||
|
Text("Profile")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tag(0)
|
.tag(0)
|
||||||
|
@ -72,11 +56,12 @@ struct Wall: View {
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
VStack {
|
VStack {
|
||||||
Text("👨👧👦")
|
Image(uiImage: "👨👧👦".image()!).renderingMode(.original)
|
||||||
|
Text("Friends")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tag(1)
|
.tag(1)
|
||||||
}
|
}.accentColor(Color.purple)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,5 +1,4 @@
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const leftpad = require("left-pad"); // I don't believe I'm depending on this
|
|
||||||
const pull = require("pull-stream");
|
const pull = require("pull-stream");
|
||||||
const split = require("split-buffer");
|
const split = require("split-buffer");
|
||||||
const metrics = require("./metrics");
|
const metrics = require("./metrics");
|
||||||
|
|
Loading…
Reference in New Issue