From 460070e61dd62f870bc855d1a477981e160ca7fa Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 26 Aug 2023 18:49:07 -0600 Subject: [PATCH] Users can now accept a follow request; Deny and remove next --- .../activitypub_social_manager.ans | Bin 2129 -> 2105 bytes art/themes/luciano_blocktronics/theme.hjson | 4 +- core/activitypub/collection.js | 30 ++- core/activitypub/follow_util.js | 57 ++++ core/activitypub/social_manager.js | 252 ++++++++++++++---- core/menu_module.js | 22 +- .../content/web_handlers/activitypub.js | 114 ++------ misc/menu_templates/activitypub.in.hjson | 2 +- 8 files changed, 333 insertions(+), 148 deletions(-) diff --git a/art/themes/luciano_blocktronics/activitypub_social_manager.ans b/art/themes/luciano_blocktronics/activitypub_social_manager.ans index 109fed08885370f7c3ce276e08a6e5c22d76b332..1c1986154c97313e0ff6d47d384db47d3dcbc148 100644 GIT binary patch delta 72 zcmV-O0Js0q5V;VLSCL*B2WM{}Vqs%zlW+zVkNB~4T@Fp;qrlh6VVvl0Q21rJC~ eGaFkmJ2N*sG&XG|NVBm9cL4-AGBwYWEC?l5Mit=z delta 119 zcmdlfa8Y1F_{0tDlLJ^-C&q|yCnx45r{+jU8=L1&tPP%c04$+slp7r23=}lTRj^W! zjyAA1Hp(r@PfyQDRY=Rv$;mI@_-X;GV2H1=bhM$hvAMO0S*|LOH~9jS3(#Qp$@`f~ SCr7hyWic``cs^N#Llpoh@g`9K diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 910583e5..ce31c79e 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -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" } diff --git a/core/activitypub/collection.js b/core/activitypub/collection.js index 8c0a58dc..0324b7a6 100644 --- a/core/activitypub/collection.js +++ b/core/activitypub/collection.js @@ -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); } diff --git a/core/activitypub/follow_util.js b/core/activitypub/follow_util.js index a3058740..396f772c 100644 --- a/core/activitypub/follow_util.js +++ b/core/activitypub/follow_util.js @@ -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); + } + ); +} diff --git a/core/activitypub/social_manager.js b/core/activitypub/social_manager.js index a6be9e0e..0fcec24c 100644 --- a/core/activitypub/social_manager.js +++ b/core/activitypub/social_manager.js @@ -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) { diff --git a/core/menu_module.js b/core/menu_module.js index f6520dde..6639ae89 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -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(); diff --git a/core/servers/content/web_handlers/activitypub.js b/core/servers/content/web_handlers/activitypub.js index c4086e50..412f7a41 100644 --- a/core/servers/content/web_handlers/activitypub.js +++ b/core/servers/content/web_handlers/activitypub.js @@ -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 }, diff --git a/misc/menu_templates/activitypub.in.hjson b/misc/menu_templates/activitypub.in.hjson index 23385341..e3107b8e 100644 --- a/misc/menu_templates/activitypub.in.hjson +++ b/misc/menu_templates/activitypub.in.hjson @@ -209,7 +209,7 @@ MT2: {mode: "preview", acceptsFocus: false, acceptsInput: false} TM3: { focus: true - items: ["following", "followers"] + items: ["following", "followers", "pending requests"] } } actionKeys: [