Users can now accept a follow request; Deny and remove next

This commit is contained in:
Bryan Ashby 2023-08-26 18:49:07 -06:00
parent d5ab53ecad
commit 460070e61d
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
8 changed files with 333 additions and 148 deletions

View File

@ -1321,7 +1321,7 @@
activityPubSocialManager: {
config: {
selectedActorInfoFormat: "|00|15{preferredUsername} |08(|02{name}|08)\n|07following|08: {statusIndicator}\n\n|06{plainTextSummary}"
statusIndicatorEnabled: "|00|10√"
statusFollowing: "|00|10√"
staticIndicatorDisabled: "|00|12X"
}
0: {
@ -1339,7 +1339,7 @@
height: 15
width: 34
}
TM3: {
HM3: {
focusTextStyle: first lower
styleSGR1: "|00|08"
}

View File

@ -73,6 +73,17 @@ module.exports = class Collection extends ActivityPubObject {
);
}
static followRequests(owningUser, page, cb) {
return Collection.ownedOrderedByUser(
Collections.FollowRequests,
owningUser,
true, // private
page,
null, // return full Follow Request Activity
cb
);
}
static outbox(collectionId, page, cb) {
return Collection.publicOrderedById(
Collections.Outbox,
@ -97,16 +108,16 @@ module.exports = class Collection extends ActivityPubObject {
);
}
static addFollowRequest(owningUser, requestingActor, ignoreDupes, cb) {
const collectionId = Endpoints.makeUserUrl(owningUser) + 'follow-requests';
static addFollowRequest(owningUser, requestActivity, cb) {
const collectionId = Endpoints.makeUserUrl(owningUser) + '/follow-requests';
return Collection.addToCollection(
Collections.FollowRequests,
owningUser,
collectionId,
requestingActor.id, // Actor requesting to follow owningUser
requestingActor,
true,
ignoreDupes,
requestActivity.id,
requestActivity,
true, // private
true, // ignoreDupes
cb
);
}
@ -549,7 +560,12 @@ module.exports = class Collection extends ActivityPubObject {
return cb(err);
}
entries = entries || [];
try {
entries = (entries || []).map(e => JSON.parse(e.object_json));
} catch (e) {
Log.error(`Collection "${collectionId}" error: ${e.message}`);
}
if (mapper && entries.length > 0) {
entries = entries.map(mapper);
}

View File

@ -3,9 +3,14 @@ const ActivityPubObject = require('./object');
const UserProps = require('../user_property');
const { Errors } = require('../enig_error');
const Collection = require('./collection');
const Actor = require('./actor');
const Activity = require('./activity');
const async = require('async');
exports.sendFollowRequest = sendFollowRequest;
exports.sendUnfollowRequest = sendUnfollowRequest;
exports.acceptFollowRequest = acceptFollowRequest;
function sendFollowRequest(fromUser, toActor, cb) {
const fromActorId = fromUser.getProperty(UserProps.ActivityPubActorId);
@ -81,3 +86,55 @@ function sendUnfollowRequest(fromUser, toActor, cb) {
}
);
}
function acceptFollowRequest(localUser, remoteActor, requestActivity, cb) {
async.series(
[
callback => {
return Collection.addFollower(
localUser,
remoteActor,
true, // ignore dupes
callback
);
},
callback => {
Actor.fromLocalUser(localUser, (err, localActor) => {
if (err) {
return callback(err);
}
const accept = Activity.makeAccept(localActor.id, requestActivity);
accept.sendTo(remoteActor.inbox, localUser, (err, respBody, res) => {
if (err) {
return callback(Errors.HttpError(err.message, err.code));
}
if (res.statusCode !== 202 && res.statusCode !== 200) {
return callback(
Errors.HttpError(
`Unexpected HTTP status code ${res.statusCode}`
)
);
}
return callback(null);
});
});
},
callback => {
// remove from local requests Collection
return Collection.removeOwnedById(
Collections.FollowRequests,
localUser,
requestActivity.id,
callback
);
},
],
err => {
return cb(err);
}
);
}

View File

@ -7,8 +7,13 @@ 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 {
sendFollowRequest,
sendUnfollowRequest,
acceptFollowRequest,
} = require('./follow_util');
const { Collections } = require('./const');
const EnigAssert = require('../enigma_assert');
// deps
const async = require('async');
@ -42,11 +47,37 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
this.followingActors = [];
this.followerActors = [];
this.followRequests = [];
this.currentCollection = Collections.Following;
this.currentHelpText = '';
this.menuMethods = {
spaceKeyPressed: (formData, extraArgs, cb) => {
return this._toggleSelectedActorStatus(cb);
actorListKeyPressed: (formData, extraArgs, cb) => {
switch (formData.key.name) {
case 'space':
{
if (this.currentCollection === Collections.Following) {
return this._toggleFollowing(cb);
} else if (
this.currentCollection === Collections.FollowRequests
) {
return this._acceptFollowRequest(cb);
}
}
break;
case 'delete':
{
if (this.currentCollection === Collections.Followers) {
return this._removeFollower(cb);
} else if (
this.currentCollection === Collections.FollowRequests
) {
return this._denyFollowRequest(cb);
}
}
break;
}
},
listKeyPressed: (formData, extraArgs, cb) => {
const actorListView = this.getView('main', MciViewIds.main.actorList);
@ -110,32 +141,7 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
);
},
callback => {
this._fetchActorList(
Collections.Following,
(err, followingActors) => {
if (err) {
return callback(err);
}
return this._fetchActorList(
Collections.Followers,
(err, followerActors) => {
if (err) {
return callback(err);
}
const mapper = a => {
a.plainTextSummary = htmlToMessageBody(a.summary);
return a;
};
this.followingActors = followingActors.map(mapper);
this.followerActors = followerActors.map(mapper);
return callback(null);
}
);
}
);
return this._populateActorLists(callback);
},
callback => {
const v = id => this.getView('main', id);
@ -156,11 +162,12 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
});
navMenuView.on('index update', index => {
if (0 === index) {
this._switchTo(Collections.Following);
} else {
this._switchTo(Collections.Followers);
}
const collectionName = [
Collections.Following,
Collections.Followers,
Collections.FollowRequests,
][index];
this._switchTo(collectionName);
});
return callback(null);
@ -175,11 +182,28 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
_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);
let list;
switch (collectionName) {
case Collections.Following:
list = this.followingActors;
this.currentHelpText =
this.config.helpTextFollowing || 'SPC = Toggle Follower';
break;
case Collections.Followers:
list = this.followerActors;
this.currentHelpText =
this.config.helpTextFollowers || 'DEL = Remove Follower';
break;
case Collections.FollowRequests:
list = this.followRequests;
this.currentHelpText =
this.config.helpTextFollowRequests || 'SPC = Accept\r\nDEL = Deny';
break;
}
EnigAssert(list);
actorListView.setItems(list);
actorListView.redraw();
const selectedActor = this._getSelectedActorItem(
@ -193,14 +217,23 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
this._updateSelectedActorInfo(selectedActorInfoView, selectedActor);
} else {
selectedActorInfoView.setText('');
this.updateCustomViewTextsWithFilter(
'main',
MciViewIds.main.customRangeStart,
this._getCustomInfoFormatObject(null),
{ pipeSupport: true }
);
}
}
_getSelectedActorItem(index) {
if (this.currentCollection === Collections.Following) {
return this.followingActors[index];
} else {
return this.followerActors[index];
switch (this.currentCollection) {
case Collections.Following:
return this.followingActors[index];
case Collections.Followers:
return this.followerActors[index];
case Collections.FollowRequests:
return this.followRequests[index];
}
}
@ -231,11 +264,12 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
this.updateCustomViewTextsWithFilter(
'main',
MciViewIds.main.customRangeStart,
this._getCustomInfoFormatObject(actorInfo)
this._getCustomInfoFormatObject(actorInfo),
{ pipeSupport: true }
);
}
_toggleSelectedActorStatus(cb) {
_toggleFollowing(cb) {
const actorListView = this.getView('main', MciViewIds.main.actorList);
const selectedActor = this._getSelectedActorItem(
actorListView.getFocusItemIndex()
@ -274,6 +308,49 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
}
}
_acceptFollowRequest(cb) {
EnigAssert(Collections.FollowRequests === this.currentCollection);
const actorListView = this.getView('main', MciViewIds.main.actorList);
const selectedActor = this._getSelectedActorItem(
actorListView.getFocusItemIndex()
);
if (!selectedActor) {
return cb(null);
}
const request = selectedActor.request;
EnigAssert(request);
acceptFollowRequest(this.client.user, selectedActor, request, err => {
if (err) {
this.client.log.error(
{ error: err.message },
'Failed to fully accept Follow request'
);
}
const followingActor = this.followRequests.splice(
actorListView.getFocusItemIndex(),
1
)[0];
this.followerActors.push(followingActor); // move to followers
this._switchTo(this.currentCollection); // redraw
return cb(err);
});
}
_removeFollower(cb) {
return cb(null);
}
_denyFollowRequest(cb) {
return cb(null);
}
_followingActorToggled(actorInfo, cb) {
// Local user/Actor wants to follow or un-follow
const wantsToFollow = actorInfo.status;
@ -327,6 +404,7 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
selectedActorStatus: actorInfo ? actorInfo.status : false,
selectedActorStatusIndicator: v('statusIndicator'),
text: v('name'),
helpText: this.currentHelpText,
});
return formatObj;
@ -334,8 +412,90 @@ exports.getModule = class activityPubSocialManager extends MenuModule {
_getStatusIndicator(enabled) {
return enabled
? this.config.statusIndicatorEnabled || '√'
: this.config.statusIndicatorDisabled || 'X';
? this.config.statusFollowing || '√'
: this.config.statusNotFollowing || 'X';
}
_populateActorLists(cb) {
async.waterfall(
[
callback => {
return this._fetchActorList(Collections.Following, callback);
},
(following, callback) => {
this._fetchActorList(Collections.Followers, (err, followers) => {
return callback(err, following, followers);
});
},
(following, followers, callback) => {
this._fetchFollowRequestActors((err, followRequests) => {
return callback(err, following, followers, followRequests);
});
},
(following, followers, followRequests, callback) => {
const mapper = a => {
a.plainTextSummary = htmlToMessageBody(a.summary);
return a;
};
this.followingActors = following.map(mapper);
this.followerActors = followers.map(mapper);
this.followRequests = followRequests.map(mapper);
return callback(null);
},
],
err => {
return cb(err);
}
);
}
_fetchFollowRequestActors(cb) {
Collection.followRequests(this.client.user, 'all', (err, collection) => {
if (err) {
return cb(err);
}
if (!collection.orderedItems || collection.orderedItems.length < 1) {
return cb(null, []);
}
const statusIndicator = this._getStatusIndicator(false);
async.mapLimit(
collection.orderedItems,
4,
(request, nextRequest) => {
const actorId = request.actor;
Actor.fromId(actorId, (err, actor, subject) => {
if (err) {
this.client.log.warn({ actorId }, 'Failed to retrieve Actor');
return nextRequest(null, null);
}
// Add some of our own properties
Object.assign(actor, {
subject,
status: false,
statusIndicator,
text: actor.preferredUsername,
request,
});
return nextRequest(null, actor);
});
},
(err, actorsList) => {
if (err) {
return cb(err);
}
actorsList = actorsList.filter(f => f); // drop nulls
return cb(null, actorsList);
}
);
});
}
_fetchActorList(collectionName, cb) {

View File

@ -13,6 +13,7 @@ const MultiLineEditTextView =
const Errors = require('../core/enig_error.js').Errors;
const { getPredefinedMCIValue } = require('../core/predefined_mci.js');
const EnigAssert = require('./enigma_assert');
const { pipeToAnsi } = require('./color_codes.js');
// deps
const async = require('async');
@ -774,10 +775,25 @@ exports.MenuModule = class MenuModule extends PluginModule {
const format = config[view.key];
const text = stringFormat(format, fmtObj);
if (options.appendMultiLine && view instanceof MultiLineEditTextView) {
view.addText(text);
if (view instanceof MultiLineEditTextView) {
if (options.appendMultiLine) {
view.addText(text);
} else {
if (options.pipeSupport) {
const ansi = pipeToAnsi(text, this.client);
if (view.getData() !== ansi) {
view.setAnsi(ansi);
} else {
view.redraw();
}
} else if (view.getData() !== text) {
view.setText(text);
} else {
view.redraw();
}
}
} else {
if (view.getData() != text) {
if (view.getData() !== text) {
view.setText(text);
} else {
view.redraw();

View File

@ -6,6 +6,7 @@ const {
getActorId,
prepareLocalUserAsActor,
} = require('../../../activitypub/util');
const { acceptFollowRequest } = require('../../../activitypub/follow_util');
const SysLog = require('../../../logger').log;
const {
ActivityStreamMediaType,
@ -750,26 +751,36 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
return this.webServer.resourceNotFound(resp);
}
// User accepts any followers automatically
const activityPubSettings = ActivityPubSettings.fromUser(localUser);
if (!activityPubSettings.manuallyApproveFollowers) {
this._recordAcceptedFollowRequest(localUser, remoteActor, activity);
return this.webServer.accepted(resp);
}
// User manually approves requests; add them to their requests collection
Collection.addFollowRequest(
localUser,
remoteActor,
true, // ignore dupes
err => {
const addReq = () => {
// User manually approves requests; add them to their requests collection
// :FIXME: We need to store the Activity and fetch the Actor as needed later;
// when accepting a request, we send back the Activity!
Collection.addFollowRequest(localUser, activity, err => {
if (err) {
return this.internalServerError(resp, err);
}
return this.webServer.accepted(resp);
});
};
// User accepts any followers automatically
const activityPubSettings = ActivityPubSettings.fromUser(localUser);
if (activityPubSettings.manuallyApproveFollowers) {
return addReq();
}
acceptFollowRequest(localUser, remoteActor, activity, err => {
if (err) {
this.log.warn(
{ error: err.message },
'Failed to post Accept. Recording to requests instead.'
);
return addReq();
}
);
return this.webServer.accepted(resp);
});
});
}
@ -1105,81 +1116,6 @@ exports.getModule = class ActivityPubWebHandler extends WebHandlerModule {
});
}
_recordAcceptedFollowRequest(localUser, remoteActor, requestActivity) {
async.series(
[
callback => {
return Collection.addFollower(
localUser,
remoteActor,
true, // ignore dupes
callback
);
},
callback => {
Actor.fromLocalUser(localUser, (err, localActor) => {
if (err) {
this.log.warn(
{ inbox: remoteActor.inbox, error: err.message },
'Failed to load local Actor for "Accept"'
);
return callback(err);
}
const accept = Activity.makeAccept(
localActor.id,
requestActivity
);
accept.sendTo(
remoteActor.inbox,
localUser,
(err, respBody, res) => {
if (err) {
this.log.warn(
{
inbox: remoteActor.inbox,
error: err.message,
},
'Failed POSTing "Accept" to inbox'
);
return callback(null); // just a warning
}
if (res.statusCode !== 202 && res.statusCode !== 200) {
this.log.warn(
{
inbox: remoteActor.inbox,
statusCode: res.statusCode,
},
'Unexpected status code'
);
return callback(null); // just a warning
}
this.log.info(
{ inbox: remoteActor.inbox },
'Remote server received our "Accept" successfully'
);
return callback(null);
}
);
});
},
],
err => {
if (err) {
// :TODO: move this request to the "Request queue" for the user to try later
this.log.error(
{ error: err.message },
'Failed processing Follow request'
);
}
}
);
}
_selfAsActorHandler(localUser, localActor, req, resp) {
this.log.info(
{ username: localUser.username },

View File

@ -209,7 +209,7 @@
MT2: {mode: "preview", acceptsFocus: false, acceptsInput: false}
TM3: {
focus: true
items: ["following", "followers"]
items: ["following", "followers", "pending requests"]
}
}
actionKeys: [