ActivityPub Social Manager, and many updates to Search functionality
* Manage Following/Followers (WIP, bugs, some missing functionality) * Search for Actors (WIP, some bugs)
This commit is contained in:
parent
2aec375bee
commit
e8c42a9b2e
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -484,16 +484,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activityPubFollowingManager: {
|
||||||
|
config: {
|
||||||
|
selectedActorInfoFormat: "|00|15{preferredUsername}\n|02{name}\n{summary}"
|
||||||
|
statusIndicatorEnabled: "|00|10√"
|
||||||
|
staticIndicatorDisabled: "|00|12X"
|
||||||
|
}
|
||||||
|
0: {
|
||||||
|
mci: {
|
||||||
|
VM1: {
|
||||||
|
height: 15
|
||||||
|
width: 30
|
||||||
|
itemFormat: "|00|03{subject}|00 {statusIndicator}"
|
||||||
|
focusItemFormat: "|00|19|15{subject}|00 {statusIndicator}"
|
||||||
|
}
|
||||||
|
MT2: {
|
||||||
|
height: 15
|
||||||
|
width: 32
|
||||||
|
}
|
||||||
|
TM3: {
|
||||||
|
focusTextStyle: first upper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// :TODO: move this to the right area, rename, etc.
|
// :TODO: move this to the right area, rename, etc.
|
||||||
mainMenuActivityPubActorSearch: {
|
mainMenuActivityPubActorSearch: {
|
||||||
|
config: {
|
||||||
|
followingIndicator: "|00|14> |10Following|08!|14 <"
|
||||||
|
notFollowingIndicator: "|00|14> |02Not Following|14 <"
|
||||||
|
viewInfoFormat10: "{actorFollowingIndicator}"
|
||||||
|
}
|
||||||
0: {
|
0: {
|
||||||
mci: {
|
mci: {
|
||||||
TL1: {
|
TL1: {
|
||||||
width: 70
|
width: 70
|
||||||
}
|
|
||||||
BT2: {
|
|
||||||
width: 20
|
|
||||||
focusTextStyle: upper
|
|
||||||
submit: true
|
submit: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -524,14 +550,9 @@
|
||||||
height: 3
|
height: 3
|
||||||
mode: preview
|
mode: preview
|
||||||
}
|
}
|
||||||
BT8: {
|
TL10: {
|
||||||
focusTextStyle: upper
|
width: 24
|
||||||
submit: true
|
|
||||||
}
|
|
||||||
BT9: {
|
|
||||||
text: back
|
|
||||||
focusTextStyle: upper
|
|
||||||
submit: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ const { queryWebFinger } = require('../webfinger');
|
||||||
const EnigAssert = require('../enigma_assert');
|
const EnigAssert = require('../enigma_assert');
|
||||||
const ActivityPubSettings = require('./settings');
|
const ActivityPubSettings = require('./settings');
|
||||||
const ActivityPubObject = require('./object');
|
const ActivityPubObject = require('./object');
|
||||||
const { ActivityStreamMediaType } = require('./const');
|
const { ActivityStreamMediaType, Collections } = require('./const');
|
||||||
const apDb = require('../database').dbs.activitypub;
|
const apDb = require('../database').dbs.activitypub;
|
||||||
const Config = require('../config').get;
|
const Config = require('../config').get;
|
||||||
|
|
||||||
|
@ -73,7 +73,12 @@ module.exports = class Actor extends ActivityPubObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
static get WellKnownLinkTypes() {
|
static get WellKnownLinkTypes() {
|
||||||
return ['inbox', 'outbox', 'following', 'followers'];
|
return [
|
||||||
|
Collections.Inbox,
|
||||||
|
Collections.Outbox,
|
||||||
|
Collections.Following,
|
||||||
|
Collections.Followers,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromLocalUser(user, webServer, cb) {
|
static fromLocalUser(user, webServer, cb) {
|
||||||
|
|
|
@ -3,11 +3,15 @@ const { Errors } = require('../enig_error');
|
||||||
const Actor = require('../activitypub/actor');
|
const Actor = require('../activitypub/actor');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const { htmlToMessageBody } = require('./util');
|
const { htmlToMessageBody } = require('./util');
|
||||||
|
const { Collections } = require('./const');
|
||||||
const Collection = require('./collection');
|
const Collection = require('./collection');
|
||||||
|
const EnigAssert = require('../enigma_assert');
|
||||||
|
const { sendFollowRequest, sendUnfollowRequest } = require('./follow_util');
|
||||||
|
const { getServer } = require('../listening_server');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const { get, truncate, isEmpty } = require('lodash');
|
const { get, isEmpty, isObject, cloneDeep } = require('lodash');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name: 'ActivityPub Actor Search',
|
name: 'ActivityPub Actor Search',
|
||||||
|
@ -22,8 +26,7 @@ const FormIds = {
|
||||||
|
|
||||||
const MciViewIds = {
|
const MciViewIds = {
|
||||||
main: {
|
main: {
|
||||||
searchUrl: 1,
|
searchQuery: 1,
|
||||||
searchButton: 2,
|
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
userName: 1,
|
userName: 1,
|
||||||
|
@ -33,8 +36,8 @@ const MciViewIds = {
|
||||||
numberFollowers: 5,
|
numberFollowers: 5,
|
||||||
numberFollowing: 6,
|
numberFollowing: 6,
|
||||||
summary: 7,
|
summary: 7,
|
||||||
followButton: 8,
|
|
||||||
cancelButton: 9,
|
customRangeStart: 10,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,27 +50,34 @@ exports.getModule = class ActivityPubActorSearch extends MenuModule {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
submit: (formData, extraArgs, cb) => {
|
search: (formData, extraArgs, cb) => {
|
||||||
switch (formData.submitId) {
|
return this._search(formData.value, cb);
|
||||||
case MciViewIds.main.searchUrl: {
|
},
|
||||||
return this._search(formData.value, cb);
|
toggleFollowKeyPressed: (formData, extraArgs, cb) => {
|
||||||
}
|
return this._toggleFollowStatus(err => {
|
||||||
case MciViewIds.main.searchButton: {
|
if (err) {
|
||||||
return this._search(formData.value, cb);
|
this.client.log.error(
|
||||||
}
|
{ error: err.message },
|
||||||
|
'Failed to toggle follow status'
|
||||||
default:
|
|
||||||
cb(
|
|
||||||
Errors.UnexpectedState(
|
|
||||||
`Unexpected submitId: ${formData.submitId}`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return cb(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
backKeyPressed: (formData, extraArgs, cb) => {
|
||||||
|
return this._displayMainPage(true, cb);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initSequence() {
|
initSequence() {
|
||||||
|
this.webServer = getServer('codes.l33t.enigma.web.server');
|
||||||
|
if (!this.webServer) {
|
||||||
|
this.client.log('Could not get Web server');
|
||||||
|
return this.prevMenu();
|
||||||
|
}
|
||||||
|
this.webServer = this.webServer.instance;
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
callback => {
|
callback => {
|
||||||
|
@ -84,7 +94,7 @@ exports.getModule = class ActivityPubActorSearch extends MenuModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
_search(values, cb) {
|
_search(values, cb) {
|
||||||
const searchString = values['searchUrl'].trim();
|
const searchString = values.searchQuery.trim();
|
||||||
//TODO: Handle empty searchString
|
//TODO: Handle empty searchString
|
||||||
Actor.fromId(searchString, (err, remoteActor) => {
|
Actor.fromId(searchString, (err, remoteActor) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -95,11 +105,15 @@ exports.getModule = class ActivityPubActorSearch extends MenuModule {
|
||||||
// TODO: Add error to page for failure to find actor
|
// TODO: Add error to page for failure to find actor
|
||||||
return this._displayMainPage(true, cb);
|
return this._displayMainPage(true, cb);
|
||||||
}
|
}
|
||||||
return this._displayListScreen(remoteActor, cb);
|
|
||||||
|
this.selectedActorInfo = remoteActor;
|
||||||
|
return this._displayViewPage(cb);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_displayListScreen(remoteActor, cb) {
|
_displayViewPage(cb) {
|
||||||
|
EnigAssert(isObject(this.selectedActorInfo), 'No Actor selected!');
|
||||||
|
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
callback => {
|
callback => {
|
||||||
|
@ -122,59 +136,65 @@ exports.getModule = class ActivityPubActorSearch extends MenuModule {
|
||||||
callback => {
|
callback => {
|
||||||
return this.validateMCIByViewIds(
|
return this.validateMCIByViewIds(
|
||||||
'view',
|
'view',
|
||||||
Object.values(MciViewIds.view),
|
Object.values(MciViewIds.view).filter(
|
||||||
|
id => id !== MciViewIds.view.customRangeStart
|
||||||
|
),
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
callback => {
|
||||||
|
this._updateCollectionItemCount(Collections.Following, () => {
|
||||||
|
return this._updateCollectionItemCount(
|
||||||
|
Collections.Followers,
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
callback => {
|
callback => {
|
||||||
const v = id => this.getView('view', id);
|
const v = id => this.getView('view', id);
|
||||||
|
|
||||||
const nameView = v(MciViewIds.view.userName);
|
const nameView = v(MciViewIds.view.userName);
|
||||||
nameView.setText(
|
nameView.setText(this.selectedActorInfo.preferredUsername);
|
||||||
truncate(remoteActor.preferredUsername, {
|
|
||||||
length: nameView.getWidth(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const fullNameView = v(MciViewIds.view.fullName);
|
const fullNameView = v(MciViewIds.view.fullName);
|
||||||
fullNameView.setText(
|
fullNameView.setText(this.selectedActorInfo.name);
|
||||||
truncate(remoteActor.name, { length: fullNameView.getWidth() })
|
|
||||||
);
|
|
||||||
|
|
||||||
const datePublishedView = v(MciViewIds.view.datePublished);
|
const datePublishedView = v(MciViewIds.view.datePublished);
|
||||||
if (isEmpty(remoteActor.published)) {
|
if (isEmpty(this.selectedActorInfo.published)) {
|
||||||
datePublishedView.setText('Not available.');
|
datePublishedView.setText('Not available.');
|
||||||
} else {
|
} else {
|
||||||
const publishedDate = moment(remoteActor.published);
|
const publishedDate = moment(this.selectedActorInfo.published);
|
||||||
datePublishedView.setText(
|
datePublishedView.setText(
|
||||||
publishedDate.format(this.getDateFormat())
|
publishedDate.format(this.getDateFormat())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const manualFollowersView = v(MciViewIds.view.manualFollowers);
|
const manualFollowersView = v(MciViewIds.view.manualFollowers);
|
||||||
manualFollowersView.setText(remoteActor.manuallyApprovesFollowers);
|
manualFollowersView.setText(
|
||||||
|
this.selectedActorInfo.manuallyApprovesFollowers
|
||||||
|
);
|
||||||
|
|
||||||
const followerCountView = v(MciViewIds.view.numberFollowers);
|
const followerCountView = v(MciViewIds.view.numberFollowers);
|
||||||
this._updateViewWithCollectionItemCount(
|
followerCountView.setText(
|
||||||
remoteActor.followers,
|
this.selectedActorInfo._followersCount > -1
|
||||||
followerCountView
|
? this.selectedActorInfo._followersCount
|
||||||
|
: '--'
|
||||||
);
|
);
|
||||||
|
|
||||||
const followingCountView = v(MciViewIds.view.numberFollowing);
|
const followingCountView = v(MciViewIds.view.numberFollowing);
|
||||||
this._updateViewWithCollectionItemCount(
|
followingCountView.setText(
|
||||||
remoteActor.following,
|
this.selectedActorInfo._followingCount > -1
|
||||||
followingCountView
|
? this.selectedActorInfo._followingCount
|
||||||
|
: '--'
|
||||||
);
|
);
|
||||||
|
|
||||||
const summaryView = v(MciViewIds.view.summary);
|
const summaryView = v(MciViewIds.view.summary);
|
||||||
summaryView.setText(htmlToMessageBody(remoteActor.summary));
|
summaryView.setText(
|
||||||
|
htmlToMessageBody(this.selectedActorInfo.summary)
|
||||||
|
);
|
||||||
summaryView.redraw();
|
summaryView.redraw();
|
||||||
|
|
||||||
const followButtonView = v(MciViewIds.view.followButton);
|
return this._setFollowStatus(callback);
|
||||||
// TODO: FIXME: Real status
|
|
||||||
followButtonView.setText('follow');
|
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
err => {
|
err => {
|
||||||
|
@ -183,6 +203,102 @@ exports.getModule = class ActivityPubActorSearch extends MenuModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setFollowStatus(cb) {
|
||||||
|
Collection.ownedObjectByNameAndId(
|
||||||
|
Collections.Following,
|
||||||
|
this.client.user,
|
||||||
|
this.selectedActorInfo.id,
|
||||||
|
(err, followingActorEntry) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedActorInfo._isFollowing = followingActorEntry ? true : false;
|
||||||
|
this.selectedActorInfo._followingIndicator =
|
||||||
|
this._getFollowingIndicator();
|
||||||
|
|
||||||
|
this.updateCustomViewTextsWithFilter(
|
||||||
|
'view',
|
||||||
|
MciViewIds.view.customRangeStart,
|
||||||
|
this._getCustomInfoFormatObject()
|
||||||
|
);
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_toggleFollowStatus(cb) {
|
||||||
|
// catch early key presses
|
||||||
|
if (!this.selectedActorInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedActorInfo._isFollowing = !this.selectedActorInfo._isFollowing;
|
||||||
|
this.selectedActorInfo._followingIndicator = this._getFollowingIndicator();
|
||||||
|
|
||||||
|
const finish = e => {
|
||||||
|
this.updateCustomViewTextsWithFilter(
|
||||||
|
'view',
|
||||||
|
MciViewIds.view.customRangeStart,
|
||||||
|
this._getCustomInfoFormatObject()
|
||||||
|
);
|
||||||
|
|
||||||
|
return cb(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const actor = this._getSelectedActor(); // actor info -> actor
|
||||||
|
return this.selectedActorInfo._isFollowing
|
||||||
|
? sendFollowRequest(this.client.user, actor, this.webServer, finish)
|
||||||
|
: sendUnfollowRequest(this.client.user, actor, this.webServer, finish);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSelectedActor() {
|
||||||
|
const actor = cloneDeep(this.selectedActorInfo);
|
||||||
|
|
||||||
|
// nuke our added properties
|
||||||
|
delete actor._isFollowing;
|
||||||
|
delete actor._followingIndicator;
|
||||||
|
delete actor._followingCount;
|
||||||
|
delete actor._followersCount;
|
||||||
|
|
||||||
|
return actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getFollowingIndicator() {
|
||||||
|
return this.selectedActorInfo._isFollowing
|
||||||
|
? this.config.followingIndicator || 'Following'
|
||||||
|
: this.config.notFollowingIndicator || 'Not following';
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCustomInfoFormatObject() {
|
||||||
|
const formatObj = {
|
||||||
|
followingCount: this.selectedActorInfo._followingCount,
|
||||||
|
followerCount: this.selectedActorInfo._followersCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
const v = f => {
|
||||||
|
return this.selectedActorInfo[f] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(formatObj, {
|
||||||
|
actorId: v('id'),
|
||||||
|
actorSubject: v('subject'),
|
||||||
|
actorType: v('type'),
|
||||||
|
actorName: v('name'),
|
||||||
|
actorSummary: v('summary'),
|
||||||
|
actorPreferredUsername: v('preferredUsername'),
|
||||||
|
actorUrl: v('url'),
|
||||||
|
actorImage: v('image'),
|
||||||
|
actorIcon: v('icon'),
|
||||||
|
actorFollowing: this.selectedActorInfo._isFollowing,
|
||||||
|
actorFollowingIndicator: v('_followingIndicator'),
|
||||||
|
text: v('name'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatObj;
|
||||||
|
}
|
||||||
|
|
||||||
_displayMainPage(clearScreen, cb) {
|
_displayMainPage(clearScreen, cb) {
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
|
@ -208,17 +324,20 @@ exports.getModule = class ActivityPubActorSearch extends MenuModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateViewWithCollectionItemCount(collectionUrl, view) {
|
_updateCollectionItemCount(collectionName, cb) {
|
||||||
|
const collectionUrl = this.selectedActorInfo[collectionName];
|
||||||
this._retrieveCountFromCollectionUrl(collectionUrl, (err, count) => {
|
this._retrieveCountFromCollectionUrl(collectionUrl, (err, count) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.client.log.warn(
|
this.client.log.warn(
|
||||||
{ err: err },
|
{ err: err },
|
||||||
`Unable to get Collection count for ${collectionUrl}`
|
`Unable to get Collection count for ${collectionUrl}`
|
||||||
);
|
);
|
||||||
view.setText('--');
|
this.selectedActorInfo[`_${collectionName}Count`] = -1;
|
||||||
} else {
|
} else {
|
||||||
view.setText(count);
|
this.selectedActorInfo[`_${collectionName}Count`] = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +348,7 @@ exports.getModule = class ActivityPubActorSearch extends MenuModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection.getRemoteCollectionStats(collectionUrl, (err, stats) => {
|
Collection.getRemoteCollectionStats(collectionUrl, (err, stats) => {
|
||||||
return cb(err, stats.totalItems);
|
return cb(err, err ? null : stats.totalItems);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ const { Errors } = require('../enig_error.js');
|
||||||
const {
|
const {
|
||||||
PublicCollectionId: APPublicCollectionId,
|
PublicCollectionId: APPublicCollectionId,
|
||||||
ActivityStreamMediaType,
|
ActivityStreamMediaType,
|
||||||
|
Collections,
|
||||||
} = require('./const');
|
} = require('./const');
|
||||||
const UserProps = require('../user_property');
|
const UserProps = require('../user_property');
|
||||||
const { getJson } = require('../http_util');
|
const { getJson } = require('../http_util');
|
||||||
|
@ -55,7 +56,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
|
|
||||||
static followers(collectionId, page, cb) {
|
static followers(collectionId, page, cb) {
|
||||||
return Collection.publicOrderedById(
|
return Collection.publicOrderedById(
|
||||||
'followers',
|
Collections.Followers,
|
||||||
collectionId,
|
collectionId,
|
||||||
page,
|
page,
|
||||||
e => e.id,
|
e => e.id,
|
||||||
|
@ -65,7 +66,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
|
|
||||||
static following(collectionId, page, cb) {
|
static following(collectionId, page, cb) {
|
||||||
return Collection.publicOrderedById(
|
return Collection.publicOrderedById(
|
||||||
'following',
|
Collections.Following,
|
||||||
collectionId,
|
collectionId,
|
||||||
page,
|
page,
|
||||||
e => e.id,
|
e => e.id,
|
||||||
|
@ -74,13 +75,19 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
static outbox(collectionId, page, cb) {
|
static outbox(collectionId, page, cb) {
|
||||||
return Collection.publicOrderedById('outbox', collectionId, page, null, cb);
|
return Collection.publicOrderedById(
|
||||||
|
Collections.Outbox,
|
||||||
|
collectionId,
|
||||||
|
page,
|
||||||
|
null,
|
||||||
|
cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addFollower(owningUser, followingActor, webServer, ignoreDupes, cb) {
|
static addFollower(owningUser, followingActor, webServer, ignoreDupes, cb) {
|
||||||
const collectionId = Endpoints.followers(webServer, owningUser);
|
const collectionId = Endpoints.followers(webServer, owningUser);
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'followers',
|
Collections.Followers,
|
||||||
owningUser,
|
owningUser,
|
||||||
collectionId,
|
collectionId,
|
||||||
followingActor.id, // Actor following owningUser
|
followingActor.id, // Actor following owningUser
|
||||||
|
@ -95,7 +102,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
const collectionId =
|
const collectionId =
|
||||||
Endpoints.makeUserUrl(webServer, owningUser) + 'follow-requests';
|
Endpoints.makeUserUrl(webServer, owningUser) + 'follow-requests';
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'follow-requests',
|
Collections.FollowRequests,
|
||||||
owningUser,
|
owningUser,
|
||||||
collectionId,
|
collectionId,
|
||||||
requestingActor.id, // Actor requesting to follow owningUser
|
requestingActor.id, // Actor requesting to follow owningUser
|
||||||
|
@ -109,7 +116,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
static addFollowing(owningUser, followingActor, webServer, ignoreDupes, cb) {
|
static addFollowing(owningUser, followingActor, webServer, ignoreDupes, cb) {
|
||||||
const collectionId = Endpoints.following(webServer, owningUser);
|
const collectionId = Endpoints.following(webServer, owningUser);
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'following',
|
Collections.Following,
|
||||||
owningUser,
|
owningUser,
|
||||||
collectionId,
|
collectionId,
|
||||||
followingActor.id, // Actor owningUser is following
|
followingActor.id, // Actor owningUser is following
|
||||||
|
@ -123,7 +130,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
static addOutboxItem(owningUser, outboxItem, isPrivate, webServer, ignoreDupes, cb) {
|
static addOutboxItem(owningUser, outboxItem, isPrivate, webServer, ignoreDupes, cb) {
|
||||||
const collectionId = Endpoints.outbox(webServer, owningUser);
|
const collectionId = Endpoints.outbox(webServer, owningUser);
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'outbox',
|
Collections.Outbox,
|
||||||
owningUser,
|
owningUser,
|
||||||
collectionId,
|
collectionId,
|
||||||
outboxItem.id,
|
outboxItem.id,
|
||||||
|
@ -137,7 +144,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
static addInboxItem(inboxItem, owningUser, webServer, ignoreDupes, cb) {
|
static addInboxItem(inboxItem, owningUser, webServer, ignoreDupes, cb) {
|
||||||
const collectionId = Endpoints.inbox(webServer, owningUser);
|
const collectionId = Endpoints.inbox(webServer, owningUser);
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'inbox',
|
Collections.Inbox,
|
||||||
owningUser,
|
owningUser,
|
||||||
collectionId,
|
collectionId,
|
||||||
inboxItem.id,
|
inboxItem.id,
|
||||||
|
@ -150,7 +157,7 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
|
|
||||||
static addSharedInboxItem(inboxItem, ignoreDupes, cb) {
|
static addSharedInboxItem(inboxItem, ignoreDupes, cb) {
|
||||||
return Collection.addToCollection(
|
return Collection.addToCollection(
|
||||||
'sharedInbox',
|
Collections.SharedInbox,
|
||||||
null, // N/A
|
null, // N/A
|
||||||
Collection.PublicCollectionId,
|
Collection.PublicCollectionId,
|
||||||
inboxItem.id,
|
inboxItem.id,
|
||||||
|
@ -161,13 +168,79 @@ module.exports = class Collection extends ActivityPubObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static objectById(objectId, cb) {
|
// Get Object(s) by ID; There may be multiples as they may be
|
||||||
|
// e.g. Actors belonging to multiple followers collections.
|
||||||
|
// This method also returns information about the objects
|
||||||
|
// and any items that can't be parsed
|
||||||
|
static objectsById(objectId, cb) {
|
||||||
|
apDb.all(
|
||||||
|
`SELECT name, timestamp, owner_actor_id, object_json, is_private
|
||||||
|
FROM collection
|
||||||
|
WHERE object_id = ?;`,
|
||||||
|
[objectId],
|
||||||
|
(err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = (rows || []).map(r => {
|
||||||
|
const info = {
|
||||||
|
info: this._rowToObjectInfo(r),
|
||||||
|
object: ActivityPubObject.fromJsonString(r.object_json),
|
||||||
|
};
|
||||||
|
if (!info.object) {
|
||||||
|
info.raw = r.object_json;
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
});
|
||||||
|
|
||||||
|
return cb(null, results);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ownedObjectByNameAndId(collectionName, owningUser, objectId, cb) {
|
||||||
|
const actorId = owningUser.getProperty(UserProps.ActivityPubActorId);
|
||||||
|
if (!actorId) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingProperty(
|
||||||
|
`User "${owningUser.username}" is missing property '${UserProps.ActivityPubActorId}'`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
apDb.get(
|
||||||
|
`SELECT name, timestamp, owner_actor_id, object_json, is_private
|
||||||
|
FROM collection
|
||||||
|
WHERE name = ? AND owner_actor_id = ? AND object_id = ?
|
||||||
|
LIMIT 1;`,
|
||||||
|
[collectionName, actorId, objectId],
|
||||||
|
(err, row) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return cb(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = ActivityPubObject.fromJsonString(row.object_json);
|
||||||
|
if (!obj) {
|
||||||
|
return cb(Errors.Invalid('Failed to parse Object JSON'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null, obj, Collection._rowToObjectInfo(row));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static objectByNameAndId(collectionName, objectId, cb) {
|
||||||
apDb.get(
|
apDb.get(
|
||||||
`SELECT name, timestamp, owner_actor_id, object_json, is_private
|
`SELECT name, timestamp, owner_actor_id, object_json, is_private
|
||||||
FROM collection
|
FROM collection
|
||||||
WHERE name = ? AND object_id = ?
|
WHERE name = ? AND object_id = ?
|
||||||
LIMIT 1;`,
|
LIMIT 1;`,
|
||||||
[objectId],
|
[collectionName, objectId],
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
|
|
@ -31,3 +31,13 @@ exports.HttpSignatureSignHeaders = [
|
||||||
'digest',
|
'digest',
|
||||||
'content-type',
|
'content-type',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const Collections = {
|
||||||
|
Following: 'following',
|
||||||
|
Followers: 'followers',
|
||||||
|
FollowRequests: 'followRequests',
|
||||||
|
Outbox: 'outbox',
|
||||||
|
Inbox: 'inbox',
|
||||||
|
SharedInbox: 'sharedInbox',
|
||||||
|
};
|
||||||
|
exports.Collections = Collections;
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
const { Collections, WellKnownActivity } = require('./const');
|
||||||
|
const ActivityPubObject = require('./object');
|
||||||
|
const UserProps = require('../user_property');
|
||||||
|
const { Errors } = require('../enig_error');
|
||||||
|
const Collection = require('./collection');
|
||||||
|
|
||||||
|
exports.sendFollowRequest = sendFollowRequest;
|
||||||
|
exports.sendUnfollowRequest = sendUnfollowRequest;
|
||||||
|
|
||||||
|
function sendFollowRequest(fromUser, toActor, webServer, cb) {
|
||||||
|
const fromActorId = fromUser.getProperty(UserProps.ActivityPubActorId);
|
||||||
|
if (!fromActorId) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingProperty(
|
||||||
|
`User missing "${UserProps.ActivityPubActorId}" property`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We always add to the following collection;
|
||||||
|
// We expect an async follow up request to our server of
|
||||||
|
// Accept or Reject but it's not guaranteed
|
||||||
|
Collection.addFollowing(fromUser, toActor, webServer, true, err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const followRequest = new ActivityPubObject({
|
||||||
|
id: ActivityPubObject.makeObjectId(webServer, 'follow'),
|
||||||
|
type: WellKnownActivity.Follow,
|
||||||
|
actor: fromActorId,
|
||||||
|
object: toActor.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return followRequest.sendTo(toActor.inbox, fromUser, webServer, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendUnfollowRequest(fromUser, toActor, webServer, cb) {
|
||||||
|
const fromActorId = fromUser.getProperty(UserProps.ActivityPubActorId);
|
||||||
|
if (!fromActorId) {
|
||||||
|
return cb(
|
||||||
|
Errors.MissingProperty(
|
||||||
|
`User missing "${UserProps.ActivityPubActorId}" property`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always remove from the local collection, notify the remote server
|
||||||
|
Collection.removeOwnedById(Collections.Following, fromUser, toActor.inbox, err => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const undoRequest = new ActivityPubObject({
|
||||||
|
id: ActivityPubObject.makeObjectId(webServer, 'undo'),
|
||||||
|
type: WellKnownActivity.Undo,
|
||||||
|
actor: fromActorId,
|
||||||
|
object: toActor.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return undoRequest.sendTo(toActor.inbox, fromUser, webServer, cb);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,372 @@
|
||||||
|
const { MenuModule } = require('../menu_module');
|
||||||
|
const Collection = require('./collection');
|
||||||
|
const { getServer } = require('../listening_server');
|
||||||
|
const Endpoints = require('./endpoint');
|
||||||
|
const Actor = require('./actor');
|
||||||
|
const stringFormat = require('../string_format');
|
||||||
|
const { pipeToAnsi } = require('../color_codes');
|
||||||
|
const MultiLineEditTextView =
|
||||||
|
require('../multi_line_edit_text_view').MultiLineEditTextView;
|
||||||
|
const { sendFollowRequest, sendUnfollowRequest } = require('./follow_util');
|
||||||
|
const { Collections } = require('./const');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const async = require('async');
|
||||||
|
const { get, cloneDeep } = require('lodash');
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name: 'ActivityPub Social Manager',
|
||||||
|
desc: 'Manages ActivityPub Actors the current user is following or being followed by.',
|
||||||
|
author: 'NuSkooler',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormIds = {
|
||||||
|
main: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MciViewIds = {
|
||||||
|
main: {
|
||||||
|
actorList: 1,
|
||||||
|
selectedActorInfo: 2,
|
||||||
|
navMenu: 3,
|
||||||
|
|
||||||
|
customRangeStart: 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getModule = class ActivityPubFollowingManager extends MenuModule {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this.setConfigWithExtraArgs(options);
|
||||||
|
|
||||||
|
this.followingActors = [];
|
||||||
|
this.followerActors = [];
|
||||||
|
this.currentCollection = Collections.Following;
|
||||||
|
|
||||||
|
this.menuMethods = {
|
||||||
|
spaceKeyPressed: (formData, extraArgs, cb) => {
|
||||||
|
return this._toggleSelectedActorStatus(cb);
|
||||||
|
},
|
||||||
|
listKeyPressed: (formData, extraArgs, cb) => {
|
||||||
|
const actorListView = this.getView('main', MciViewIds.main.actorList);
|
||||||
|
if (actorListView) {
|
||||||
|
const keyName = get(formData, 'key.name');
|
||||||
|
switch (keyName) {
|
||||||
|
case 'down arrow':
|
||||||
|
actorListView.focusNext();
|
||||||
|
break;
|
||||||
|
case 'up arrow':
|
||||||
|
actorListView.focusPrevious();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cb(null);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
initSequence() {
|
||||||
|
this.webServer = getServer('codes.l33t.enigma.web.server');
|
||||||
|
if (!this.webServer) {
|
||||||
|
this.client.log('Could not get Web server');
|
||||||
|
return this.prevMenu();
|
||||||
|
}
|
||||||
|
this.webServer = this.webServer.instance;
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
callback => {
|
||||||
|
return this.beforeArt(callback);
|
||||||
|
},
|
||||||
|
callback => {
|
||||||
|
return this._displayMainPage(callback);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
this.finishedLoading();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_displayMainPage(cb) {
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
callback => {
|
||||||
|
return this.displayArtAndPrepViewController(
|
||||||
|
'main',
|
||||||
|
FormIds.main,
|
||||||
|
{ clearScreen: true },
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
},
|
||||||
|
callback => {
|
||||||
|
return this.validateMCIByViewIds(
|
||||||
|
'main',
|
||||||
|
Object.values(MciViewIds.main).filter(
|
||||||
|
id => id !== MciViewIds.main.customRangeStart
|
||||||
|
),
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
},
|
||||||
|
callback => {
|
||||||
|
this._fetchActorList(
|
||||||
|
Collections.Following,
|
||||||
|
(err, followingActors) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
return this._fetchActorList(
|
||||||
|
Collections.Followers,
|
||||||
|
(err, followerActors) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.followingActors = followingActors;
|
||||||
|
this.followerActors = followerActors;
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
callback => {
|
||||||
|
const v = id => this.getView('main', id);
|
||||||
|
|
||||||
|
const actorListView = v(MciViewIds.main.actorList);
|
||||||
|
const selectedActorInfoView = v(MciViewIds.main.selectedActorInfo);
|
||||||
|
const navMenuView = v(MciViewIds.main.navMenu);
|
||||||
|
|
||||||
|
// We start with following
|
||||||
|
this._switchTo(Collections.Following);
|
||||||
|
|
||||||
|
actorListView.on('index update', index => {
|
||||||
|
const selectedActor = this._getSelectedActorItem(index);
|
||||||
|
this._updateSelectedActorInfo(
|
||||||
|
selectedActorInfoView,
|
||||||
|
selectedActor
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
navMenuView.on('index update', index => {
|
||||||
|
if (0 === index) {
|
||||||
|
this._switchTo(Collections.Following);
|
||||||
|
} else {
|
||||||
|
this._switchTo(Collections.Followers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_switchTo(collectionName) {
|
||||||
|
this.currentCollection = collectionName;
|
||||||
|
const actorListView = this.getView('main', MciViewIds.main.actorList);
|
||||||
|
if (Collections.Following === collectionName) {
|
||||||
|
actorListView.setItems(this.followingActors);
|
||||||
|
} else {
|
||||||
|
actorListView.setItems(this.followerActors);
|
||||||
|
}
|
||||||
|
actorListView.redraw();
|
||||||
|
|
||||||
|
const selectedActor = this._getSelectedActorItem(
|
||||||
|
actorListView.getFocusItemIndex()
|
||||||
|
);
|
||||||
|
const selectedActorInfoView = this.getView(
|
||||||
|
'main',
|
||||||
|
MciViewIds.main.selectedActorInfo
|
||||||
|
);
|
||||||
|
if (selectedActor) {
|
||||||
|
this._updateSelectedActorInfo(selectedActorInfoView, selectedActor);
|
||||||
|
} else {
|
||||||
|
selectedActorInfoView.setText('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSelectedActorItem(index) {
|
||||||
|
if (this.currentCollection === Collections.Following) {
|
||||||
|
return this.followingActors[index];
|
||||||
|
} else {
|
||||||
|
return this.followerActors[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCurrentActorList() {
|
||||||
|
return this.currentCollection === Collections.Following
|
||||||
|
? this.followingActors
|
||||||
|
: this.followerActors;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateSelectedActorInfo(view, actorInfo) {
|
||||||
|
if (actorInfo) {
|
||||||
|
const selectedActorInfoFormat =
|
||||||
|
this.config.selectedActorInfoFormat || '{text}';
|
||||||
|
|
||||||
|
const s = stringFormat(selectedActorInfoFormat, actorInfo);
|
||||||
|
|
||||||
|
if (view instanceof MultiLineEditTextView) {
|
||||||
|
view.setAnsi(pipeToAnsi(s, this.client));
|
||||||
|
} else {
|
||||||
|
view.setText(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCustomViewTextsWithFilter(
|
||||||
|
'main',
|
||||||
|
MciViewIds.main.customRangeStart,
|
||||||
|
this._getCustomInfoFormatObject(actorInfo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_toggleSelectedActorStatus(cb) {
|
||||||
|
const actorListView = this.getView('main', MciViewIds.main.actorList);
|
||||||
|
const selectedActor = this._getSelectedActorItem(
|
||||||
|
actorListView.getFocusItemIndex()
|
||||||
|
);
|
||||||
|
if (selectedActor) {
|
||||||
|
selectedActor.status = !selectedActor.status;
|
||||||
|
selectedActor.statusIndicator = this._getStatusIndicator(
|
||||||
|
selectedActor.status
|
||||||
|
);
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
callback => {
|
||||||
|
if (Collections.Following === this.currentCollection) {
|
||||||
|
return this._followingActorToggled(selectedActor, callback);
|
||||||
|
} else {
|
||||||
|
return this._followerActorToggled(selectedActor, callback);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
this.client.log.error(
|
||||||
|
{ error: err.message, type: this.currentCollection },
|
||||||
|
`Failed to toggle "${this.currentCollection}" status`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: we really need updateItem() call on MenuView
|
||||||
|
actorListView.setItems(this._getCurrentActorList());
|
||||||
|
actorListView.redraw(); // oof
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_followingActorToggled(actorInfo, cb) {
|
||||||
|
// Local user/Actor wants to follow or un-follow
|
||||||
|
const wantsToFollow = actorInfo.status;
|
||||||
|
const actor = this._actorInfoToActor(actorInfo);
|
||||||
|
|
||||||
|
return wantsToFollow
|
||||||
|
? sendFollowRequest(this.client.user, actor, this.webServer, cb)
|
||||||
|
: sendUnfollowRequest(this.client.user, actor, this.webServer, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
_actorInfoToActor(actorInfo) {
|
||||||
|
const actor = cloneDeep(actorInfo);
|
||||||
|
|
||||||
|
// nuke our added properties
|
||||||
|
delete actor.subject;
|
||||||
|
delete actor.text;
|
||||||
|
delete actor.status;
|
||||||
|
delete actor.statusIndicator;
|
||||||
|
|
||||||
|
return actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
_followerActorToggled(actorInfo, cb) {
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCustomInfoFormatObject(actorInfo) {
|
||||||
|
const formatObj = {
|
||||||
|
followingCount: this.followingActors.length,
|
||||||
|
followerCount: this.followerActors.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
const v = f => {
|
||||||
|
return actorInfo ? actorInfo[f] || '' : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(formatObj, {
|
||||||
|
selectedActorId: v('id'),
|
||||||
|
selectedActorSubject: v('subject'),
|
||||||
|
selectedActorType: v('type'),
|
||||||
|
selectedActorName: v('name'),
|
||||||
|
selectedActorSummary: v('summary'),
|
||||||
|
selectedActorPreferredUsername: v('preferredUsername'),
|
||||||
|
selectedActorUrl: v('url'),
|
||||||
|
selectedActorImage: v('image'),
|
||||||
|
selectedActorIcon: v('icon'),
|
||||||
|
selectedActorStatus: actorInfo ? actorInfo.status : false,
|
||||||
|
selectedActorStatusIndicator: v('statusIndicator'),
|
||||||
|
text: v('name'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getStatusIndicator(enabled) {
|
||||||
|
return enabled
|
||||||
|
? this.config.statusIndicatorEnabled || '√'
|
||||||
|
: this.config.statusIndicatorDisabled || 'X';
|
||||||
|
}
|
||||||
|
|
||||||
|
_fetchActorList(collectionName, cb) {
|
||||||
|
const collectionId = Endpoints[collectionName](this.webServer, this.client.user);
|
||||||
|
Collection[collectionName](collectionId, 'all', (err, collection) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collection.orderedItems || collection.orderedItems.length < 1) {
|
||||||
|
return cb(null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusIndicator = this._getStatusIndicator(true);
|
||||||
|
|
||||||
|
async.mapLimit(
|
||||||
|
collection.orderedItems,
|
||||||
|
4,
|
||||||
|
(actorId, nextActorId) => {
|
||||||
|
Actor.fromId(actorId, (err, actor, subject) => {
|
||||||
|
if (err) {
|
||||||
|
this.client.log.warn({ actorId }, 'Failed to retrieve Actor');
|
||||||
|
return nextActorId(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some of our own properties
|
||||||
|
Object.assign(actor, {
|
||||||
|
subject,
|
||||||
|
status: true,
|
||||||
|
statusIndicator,
|
||||||
|
text: actor.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return nextActorId(null, actor);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err, actorsList) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
actorsList = actorsList.filter(f => f); // drop nulls
|
||||||
|
return cb(null, actorsList);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -51,10 +51,7 @@ const EnabledViewGroup = [
|
||||||
exports.getModule = class ActivityPubUserConfig extends MenuModule {
|
exports.getModule = class ActivityPubUserConfig extends MenuModule {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
this.setConfigWithExtraArgs(options);
|
||||||
this.config = Object.assign({}, get(options, 'menuConfig.config'), {
|
|
||||||
extraArgs: options.extraArgs,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.menuMethods = {
|
this.menuMethods = {
|
||||||
mainSubmit: (formData, extraArgs, cb) => {
|
mainSubmit: (formData, extraArgs, cb) => {
|
||||||
|
|
|
@ -3,8 +3,7 @@ const { Errors, ErrorReasons } = require('../enig_error');
|
||||||
const UserProps = require('../user_property');
|
const UserProps = require('../user_property');
|
||||||
const ActivityPubSettings = require('./settings');
|
const ActivityPubSettings = require('./settings');
|
||||||
const { stripAnsiControlCodes } = require('../string_util');
|
const { stripAnsiControlCodes } = require('../string_util');
|
||||||
const { WellKnownRecipientFields, WellKnownActivity } = require('./const');
|
const { WellKnownRecipientFields } = require('./const');
|
||||||
const ActivityPubObject = require('./object');
|
|
||||||
const Log = require('../logger').log;
|
const Log = require('../logger').log;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
|
@ -29,7 +28,6 @@ exports.userNameFromSubject = userNameFromSubject;
|
||||||
exports.userNameToSubject = userNameToSubject;
|
exports.userNameToSubject = userNameToSubject;
|
||||||
exports.extractMessageMetadata = extractMessageMetadata;
|
exports.extractMessageMetadata = extractMessageMetadata;
|
||||||
exports.recipientIdsFromObject = recipientIdsFromObject;
|
exports.recipientIdsFromObject = recipientIdsFromObject;
|
||||||
exports.sendFollowRequest = sendFollowRequest;
|
|
||||||
|
|
||||||
// :TODO: more info in default
|
// :TODO: more info in default
|
||||||
// this profile template is the *default* for both WebFinger
|
// this profile template is the *default* for both WebFinger
|
||||||
|
@ -264,23 +262,3 @@ function recipientIdsFromObject(obj) {
|
||||||
|
|
||||||
return Array.from(new Set(ids));
|
return Array.from(new Set(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendFollowRequest(fromUser, toActor, webServer, cb) {
|
|
||||||
const fromActorId = fromUser.getProperty(UserProps.ActivityPubActorId);
|
|
||||||
if (!fromActorId) {
|
|
||||||
return cb(
|
|
||||||
Errors.MissingProperty(
|
|
||||||
`User missing "${UserProps.ActivityPubActorId}" property`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const followRequest = new ActivityPubObject({
|
|
||||||
id: ActivityPubObject.makeObjectId(webServer, 'follow'),
|
|
||||||
type: WellKnownActivity.Follow,
|
|
||||||
actor: fromActorId,
|
|
||||||
object: toActor.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return followRequest.sendTo(toActor.inbox, fromUser, webServer, cb);
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ const Endpoints = require('../../../activitypub/endpoint');
|
||||||
const {
|
const {
|
||||||
ActivityStreamMediaType,
|
ActivityStreamMediaType,
|
||||||
WellKnownActivity,
|
WellKnownActivity,
|
||||||
|
Collections,
|
||||||
} = require('../../../activitypub/const');
|
} = require('../../../activitypub/const');
|
||||||
const Config = require('../../../config').get;
|
const Config = require('../../../config').get;
|
||||||
const Activity = require('../../../activitypub/activity');
|
const Activity = require('../../../activitypub/activity');
|
||||||
|
@ -64,7 +65,12 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
req,
|
req,
|
||||||
resp,
|
resp,
|
||||||
(req, resp, signature) => {
|
(req, resp, signature) => {
|
||||||
return this._inboxPostHandler(req, resp, signature, 'inbox');
|
return this._inboxPostHandler(
|
||||||
|
req,
|
||||||
|
resp,
|
||||||
|
signature,
|
||||||
|
Collections.Inbox
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -82,7 +88,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
req,
|
req,
|
||||||
resp,
|
resp,
|
||||||
signature,
|
signature,
|
||||||
'sharedInbox'
|
Collections.SharedInbox
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -262,7 +268,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
case WellKnownActivity.Accept:
|
case WellKnownActivity.Accept:
|
||||||
break;
|
return this._inboxAcceptActivity(resp, activity);
|
||||||
|
|
||||||
case WellKnownActivity.Add:
|
case WellKnownActivity.Add:
|
||||||
break;
|
break;
|
||||||
|
@ -292,17 +298,17 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
case WellKnownActivity.Follow:
|
case WellKnownActivity.Follow:
|
||||||
// Follow requests are only allowed directly
|
// Follow requests are only allowed directly
|
||||||
if ('inbox' === inboxType) {
|
if (Collections.Inbox === inboxType) {
|
||||||
return this._inboxFollowActivity(resp, remoteActor, activity);
|
return this._inboxFollowActivity(resp, remoteActor, activity);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WellKnownActivity.Reject:
|
case WellKnownActivity.Reject:
|
||||||
break;
|
return this._inboxRejectActivity(resp, activity);
|
||||||
|
|
||||||
case WellKnownActivity.Undo:
|
case WellKnownActivity.Undo:
|
||||||
// We only Undo from private inboxes
|
// We only Undo from private inboxes
|
||||||
if ('inbox' === inboxType) {
|
if (Collections.Inbox === inboxType) {
|
||||||
// Only Follow Undo's currently supported
|
// Only Follow Undo's currently supported
|
||||||
const type = _.get(activity, 'object.type');
|
const type = _.get(activity, 'object.type');
|
||||||
if (WellKnownActivity.Follow === type) {
|
if (WellKnownActivity.Follow === type) {
|
||||||
|
@ -330,6 +336,21 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_inboxAcceptActivity(resp, activity) {
|
||||||
|
const acceptWhat = _.get(activity, 'object.type');
|
||||||
|
switch (acceptWhat) {
|
||||||
|
case WellKnownActivity.Follow:
|
||||||
|
return this._inboxAcceptFollowActivity(resp, activity);
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.log.warn(
|
||||||
|
{ type: acceptWhat },
|
||||||
|
'Invalid or unsupported "Accept" type'
|
||||||
|
);
|
||||||
|
return this.webServer.notImplemented(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_inboxCreateActivity(resp, activity) {
|
_inboxCreateActivity(resp, activity) {
|
||||||
const createWhat = _.get(activity, 'object.type');
|
const createWhat = _.get(activity, 'object.type');
|
||||||
switch (createWhat) {
|
switch (createWhat) {
|
||||||
|
@ -341,10 +362,23 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
{ type: createWhat },
|
{ type: createWhat },
|
||||||
'Invalid or unsupported "Create" type'
|
'Invalid or unsupported "Create" type'
|
||||||
);
|
);
|
||||||
return this.webServer.resourceNotFound(resp);
|
return this.webServer.notImplemented(resp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_inboxAcceptFollowActivity(resp, activity) {
|
||||||
|
// Currently Accept's to Follow's are really just a formality;
|
||||||
|
// we'll log it, but that's about it for now
|
||||||
|
this.log.info(
|
||||||
|
{
|
||||||
|
remoteActorId: activity.actor,
|
||||||
|
localActorId: _.get(activity, 'object.actor'),
|
||||||
|
},
|
||||||
|
'Follow request Accepted'
|
||||||
|
);
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
|
}
|
||||||
|
|
||||||
_inboxCreateNoteActivity(resp, activity) {
|
_inboxCreateNoteActivity(resp, activity) {
|
||||||
const note = new Note(activity.object);
|
const note = new Note(activity.object);
|
||||||
if (!note.isValid()) {
|
if (!note.isValid()) {
|
||||||
|
@ -380,10 +414,107 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_inboxDeleteActivity(inboxType, signature, resp /*, activity*/) {
|
_inboxRejectFollowActivity(resp, activity) {
|
||||||
// :TODO: Implement me!
|
// A user Rejected our local Actor/user's Follow request;
|
||||||
|
// Update the local Collection to reflect this fact.
|
||||||
|
const remoteActorId = activity.actor;
|
||||||
|
const localActorId = _.get(activity, 'object.actor');
|
||||||
|
|
||||||
|
if (!remoteActorId || !localActorId) {
|
||||||
|
return this.webServer.badRequest(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
userFromActorId(localActorId, (err, localUser) => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.removeOwnedById(
|
||||||
|
Collections.Following,
|
||||||
|
localUser,
|
||||||
|
remoteActorId,
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
this.log.error(
|
||||||
|
{ remoteActorId, localActorId },
|
||||||
|
'Failed removing "following" record'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_inboxDeleteActivity(inboxType, signature, resp, activity) {
|
||||||
|
const objectId = _.get(activity, 'object.id', activity.object);
|
||||||
|
|
||||||
|
this.log.info({ inboxType, objectId }, 'Incoming Delete request');
|
||||||
|
|
||||||
// :TODO: we need to DELETE the existing stored Message object if this is a Note, or associated if this is an Actor
|
// :TODO: we need to DELETE the existing stored Message object if this is a Note, or associated if this is an Actor
|
||||||
// :TODO: delete / invalidate any actor cache if actor
|
// :TODO: delete / invalidate any actor cache if actor
|
||||||
|
Collection.objectsById(objectId, (err, objectsInfo) => {
|
||||||
|
if (err) {
|
||||||
|
this.log.warn({ objectId });
|
||||||
|
// We'll respond accepted so they don't keep trying
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectsInfo.length === 0) {
|
||||||
|
return this.webServer.resourceNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generally we'd have a 1:1 objectId -> object here, but it's
|
||||||
|
// possible for example, that we're being asked to delete an Actor;
|
||||||
|
// If this is the case, they may be following multiple local Actor/users
|
||||||
|
// and we have multiple entries.
|
||||||
|
async.forEachSeries(
|
||||||
|
objectsInfo,
|
||||||
|
(objInfo, nextObjInfo) => {
|
||||||
|
if (objInfo.object) {
|
||||||
|
// Based on the collection we find this entry in,
|
||||||
|
// we may have additional validation or actions
|
||||||
|
switch (objInfo.info.name) {
|
||||||
|
case Collections.Inbox:
|
||||||
|
if (inboxType !== Collections.Inbox) {
|
||||||
|
// :TODO: LOG ME
|
||||||
|
return nextObjInfo(null);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Collections.SharedInbox:
|
||||||
|
if (inboxType !== Collections.SharedInbox) {
|
||||||
|
// :TODO: log me
|
||||||
|
return nextObjInfo(null);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextObjInfo(null);
|
||||||
|
} else {
|
||||||
|
// it's unparsable, so we'll delete it
|
||||||
|
Collection.removeById(objInfo.info.name, objectId, err => {
|
||||||
|
if (err) {
|
||||||
|
this.log.warn(
|
||||||
|
{ objectId, collectionName: objInfo.info.name },
|
||||||
|
'Failed to remove object'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return nextObjInfo(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
// :TODO: log me
|
||||||
|
}
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return this.webServer.accepted(resp);
|
return this.webServer.accepted(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,7 +533,7 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
const activityPubSettings = ActivityPubSettings.fromUser(localUser);
|
const activityPubSettings = ActivityPubSettings.fromUser(localUser);
|
||||||
if (!activityPubSettings.manuallyApproveFollowers) {
|
if (!activityPubSettings.manuallyApproveFollowers) {
|
||||||
this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
|
this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
|
||||||
return this.webServer.ok(resp);
|
return this.webServer.accepted(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// User manually approves requests; add them to their requests collection
|
// User manually approves requests; add them to their requests collection
|
||||||
|
@ -416,12 +547,27 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return this.internalServerError(resp, err);
|
return this.internalServerError(resp, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.webServer.ok(resp);
|
return this.webServer.accepted(resp);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_inboxRejectActivity(resp, activity) {
|
||||||
|
const rejectWhat = _.get(activity, 'object.type');
|
||||||
|
switch (rejectWhat) {
|
||||||
|
case WellKnownActivity.Follow:
|
||||||
|
return this._inboxRejectFollowActivity(resp, activity);
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.log.warn(
|
||||||
|
{ type: rejectWhat },
|
||||||
|
'Invalid or unsupported "Reject" type'
|
||||||
|
);
|
||||||
|
return this.webServer.notImplemented(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_inboxUndoActivity(resp, remoteActor, activity) {
|
_inboxUndoActivity(resp, remoteActor, activity) {
|
||||||
const localActorId = _.get(activity, 'object.object');
|
const localActorId = _.get(activity, 'object.object');
|
||||||
|
|
||||||
|
@ -435,22 +581,27 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
return this.webServer.resourceNotFound(resp);
|
return this.webServer.resourceNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection.removeOwnedById('followers', localUser, remoteActor.id, err => {
|
Collection.removeOwnedById(
|
||||||
if (err) {
|
Collections.Followers,
|
||||||
return this.webServer.internalServerError(resp, err);
|
localUser,
|
||||||
|
remoteActor.id,
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
return this.webServer.internalServerError(resp, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.info(
|
||||||
|
{
|
||||||
|
username: localUser.username,
|
||||||
|
userId: localUser.userId,
|
||||||
|
remoteActorId: remoteActor.id,
|
||||||
|
},
|
||||||
|
'Undo "Follow" (un-follow) success'
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.webServer.accepted(resp);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
this.log.info(
|
|
||||||
{
|
|
||||||
username: localUser.username,
|
|
||||||
userId: localUser.userId,
|
|
||||||
remoteActorId: remoteActor.id,
|
|
||||||
},
|
|
||||||
'Undo "Follow" (un-follow) success'
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.webServer.accepted(resp);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -712,18 +863,18 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
|
||||||
|
|
||||||
_followingGetHandler(req, resp) {
|
_followingGetHandler(req, resp) {
|
||||||
this.log.debug({ url: req.url }, 'Request for "following"');
|
this.log.debug({ url: req.url }, 'Request for "following"');
|
||||||
return this._actorCollectionRequest('following', req, resp);
|
return this._actorCollectionRequest(Collections.Following, req, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
_followersGetHandler(req, resp) {
|
_followersGetHandler(req, resp) {
|
||||||
this.log.debug({ url: req.url }, 'Request for "followers"');
|
this.log.debug({ url: req.url }, 'Request for "followers"');
|
||||||
return this._actorCollectionRequest('followers', req, resp);
|
return this._actorCollectionRequest(Collections.Followers, req, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/
|
// https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/
|
||||||
_outboxGetHandler(req, resp) {
|
_outboxGetHandler(req, resp) {
|
||||||
this.log.debug({ url: req.url }, 'Request for "outbox"');
|
this.log.debug({ url: req.url }, 'Request for "outbox"');
|
||||||
return this._actorCollectionRequest('outbox', req, resp);
|
return this._actorCollectionRequest(Collections.Outbox, req, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
_singlePublicNoteGetHandler(req, resp) {
|
_singlePublicNoteGetHandler(req, resp) {
|
||||||
|
|
|
@ -11,7 +11,6 @@ const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
|
||||||
// deps
|
// deps
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { throws } = require('assert');
|
|
||||||
|
|
||||||
exports.VerticalMenuView = VerticalMenuView;
|
exports.VerticalMenuView = VerticalMenuView;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue