[READ-ONLY] Mirror of https://github.com/improsocial/impro An extensible Bluesky client for web impro.social
6

Configure Feed

Select the types of activity you want to include in your feed.

Make permission namespacing more generic, add unblock / unmute

Grace Kind (Jul 20, 2026, 8:04 PM -0500) 07e3311a bdda5d72

+220 -224
+27 -35
impro-plugin/main.js
··· 163 163 } 164 164 } 165 165 166 - // Moderation actions on behalf of the signed-in user. Each method requires 167 - // the corresponding scope ("mute", "block", "feedback") to be declared in 168 - // the plugin manifest's `permissions.moderation` array, which the user must 169 - // grant at install time. 170 - class PluginModeration { 171 - muteActor(did, mute = true) { 172 - return hostCall("muteActor", { did, mute }); 166 + class App { 167 + constructor() { 168 + this.currentUser = null; 169 + this.data = new PluginData(); 173 170 } 174 - blockActor(did, block = true) { 175 - return hostCall("blockActor", { did, block }); 171 + on(event, listener) { 172 + addEventListener(event, listener); 173 + } 174 + 175 + refreshFeedFilters(feedURI = null) { 176 + return hostCall("refreshFeedFilters", feedURI); 177 + } 178 + 179 + // Actions on behalf of the signed-in user. Each method requires the 180 + // corresponding scope ("mute", "block", "feedFeedback") to be declared in 181 + // the plugin manifest's `permissions.actions` array, which the user must 182 + // grant at install time. 183 + muteActor(did) { 184 + return hostCall("muteActor", { did, mute: true }); 185 + } 186 + unmuteActor(did) { 187 + return hostCall("muteActor", { did, mute: false }); 188 + } 189 + blockActor(did) { 190 + return hostCall("blockActor", { did, block: true }); 191 + } 192 + unblockActor(did) { 193 + return hostCall("blockActor", { did, block: false }); 176 194 } 177 195 // Acts like the user clicking "Show less like this": sends the requestLess 178 196 // feedback signal to the feed that served the post and collapses the post ··· 182 200 } 183 201 showMoreLikeThis(postUri, feedUri) { 184 202 return hostCall("showMoreLikeThis", { postUri, feedUri }); 185 - } 186 - // Low-level app.bsky.feed.sendInteractions with no UI side effects. event 187 - // must be an allowed app.bsky.feed.defs#interaction value (e.g. 188 - // "app.bsky.feed.defs#interactionSeen") 189 - sendInteraction(postUri, event, feedProxyUrl, { feedContext = null } = {}) { 190 - return hostCall("sendInteraction", { 191 - postUri, 192 - event, 193 - feedProxyUrl, 194 - feedContext, 195 - }); 196 - } 197 - } 198 - 199 - class App { 200 - constructor() { 201 - this.currentUser = null; 202 - this.data = new PluginData(); 203 - this.moderation = new PluginModeration(); 204 - } 205 - on(event, listener) { 206 - addEventListener(event, listener); 207 - } 208 - 209 - refreshFeedFilters(feedURI = null) { 210 - return hostCall("refreshFeedFilters", feedURI); 211 203 } 212 204 } 213 205
+6 -7
src/js/plugins/pluginModal.js
··· 71 71 } 72 72 } 73 73 74 - const MODERATION_ACTION_LABELS = { 74 + const ACTION_LABELS = { 75 75 mute: "Mute and unmute accounts on your behalf", 76 76 block: "Block and unblock accounts on your behalf", 77 - feedback: 77 + feedFeedback: 78 78 'Send feed feedback (e.g. "show fewer/more like this") on your behalf', 79 79 }; 80 80 ··· 93 93 </div> 94 94 `); 95 95 } 96 - const moderationScopes = permissions.moderation ?? []; 97 - if (moderationScopes.length > 0) { 96 + const actionScopes = permissions.actions ?? []; 97 + if (actionScopes.length > 0) { 98 98 sections.push(html` 99 99 <div class="permission-prompt-section"> 100 100 <ul class="permission-prompt-list"> 101 - ${moderationScopes.map( 102 - (scope) => 103 - html`<li>${MODERATION_ACTION_LABELS[scope] ?? scope}</li>`, 101 + ${actionScopes.map( 102 + (scope) => html`<li>${ACTION_LABELS[scope] ?? scope}</li>`, 104 103 )} 105 104 </ul> 106 105 </div>
+12 -12
src/js/plugins/pluginPermissions.js
··· 1 1 import { unique } from "/js/utils.js"; 2 2 3 - const MODERATION_ACTIONS = ["mute", "block", "feedback"]; 3 + const ACTION_SCOPES = ["mute", "block", "feedFeedback"]; 4 4 5 5 export function getPermissionsFromManifest(manifest) { 6 6 return parsePermissions(manifest.permissions ?? {}); ··· 17 17 ); 18 18 if (fetchPatterns.length > 0) parsed.fetch = fetchPatterns; 19 19 } 20 - if (permissions.moderation) { 21 - const moderationArray = Array.isArray(permissions.moderation) 22 - ? permissions.moderation 23 - : [permissions.moderation]; 24 - const moderationScopes = unique( 25 - moderationArray.filter((entry) => MODERATION_ACTIONS.includes(entry)), 20 + if (permissions.actions) { 21 + const actionsArray = Array.isArray(permissions.actions) 22 + ? permissions.actions 23 + : [permissions.actions]; 24 + const actionScopes = unique( 25 + actionsArray.filter((entry) => ACTION_SCOPES.includes(entry)), 26 26 ); 27 - if (moderationScopes.length > 0) parsed.moderation = moderationScopes; 27 + if (actionScopes.length > 0) parsed.actions = actionScopes; 28 28 } 29 29 return parsed; 30 30 } 31 31 32 - // action is one of "mute", "block", "feedback" (the "show fewer/more like 33 - // this" feed-interaction signal) 34 - export function isModerationActionAllowed(action, permissions) { 35 - return (permissions.moderation ?? []).includes(action); 32 + // action is one of "mute", "block", "feedFeedback" (the "show fewer/more 33 + // like this" feed-interaction signal) 34 + export function isActionAllowed(action, permissions) { 35 + return (permissions.actions ?? []).includes(action); 36 36 } 37 37 38 38 export function diffPermissions(current, next) {
+18 -52
src/js/plugins/pluginService.js
··· 21 21 getPermissionsFromManifest, 22 22 diffPermissions, 23 23 isEmptyPermissions, 24 - isModerationActionAllowed, 24 + isActionAllowed, 25 25 } from "/js/plugins/pluginPermissions.js"; 26 26 import { compareVersions, groupBy, isDev, sortBy } from "/js/utils.js"; 27 27 import { ··· 41 41 } 42 42 } 43 43 44 - // The app.bsky.feed.defs#interaction events plugins may send. Mirrors 45 - // social-app's third-party feed policy for now - can expand later if needed 46 - const FEED_INTERACTION_EVENTS = new Set([ 47 - "app.bsky.feed.defs#requestLess", 48 - "app.bsky.feed.defs#requestMore", 49 - "app.bsky.feed.defs#interactionSeen", 50 - "app.bsky.feed.defs#interactionLike", 51 - "app.bsky.feed.defs#interactionRepost", 52 - "app.bsky.feed.defs#interactionReply", 53 - "app.bsky.feed.defs#interactionQuote", 54 - "app.bsky.feed.defs#interactionShare", 55 - ]); 56 44 export const PLUGIN_PREVIEW_QUERY_PARAM = "plugin-preview"; 57 45 58 46 export function arePluginsDisabledByQueryParam() { ··· 450 438 this.pluginBridge.addHostMethod( 451 439 "muteActor", 452 440 async (plugin, { did, mute = true }) => { 453 - this._requireModerationPermission(plugin, "mute"); 441 + this._requireSignedIn(); 442 + this._requireActionPermission(plugin, "mute"); 454 443 requireHostMethodArg("muteActor", "did", did); 455 444 const profile = this._resolveProfileForMutation(did); 456 445 if (mute) await this._dataLayer.mutations.muteProfile(profile); ··· 461 450 this.pluginBridge.addHostMethod( 462 451 "blockActor", 463 452 async (plugin, { did, block = true }) => { 464 - this._requireModerationPermission(plugin, "block"); 453 + this._requireSignedIn(); 454 + this._requireActionPermission(plugin, "block"); 465 455 requireHostMethodArg("blockActor", "did", did); 466 456 const profile = this._resolveProfileForMutation(did); 467 457 if (block) await this._dataLayer.mutations.blockProfile(profile); ··· 472 462 this.pluginBridge.addHostMethod( 473 463 "showLessLikeThis", 474 464 async (plugin, { postUri, feedUri = null }) => { 475 - this._requireModerationPermission(plugin, "feedback"); 465 + this._requireSignedIn(); 466 + this._requireActionPermission(plugin, "feedFeedback"); 476 467 requireHostMethodArg("showLessLikeThis", "postUri", postUri); 477 468 requireHostMethodArg("showLessLikeThis", "feedUri", feedUri); 478 469 const { feedContext, feedProxyUrl } = this._resolveFeedAttribution( ··· 491 482 this.pluginBridge.addHostMethod( 492 483 "showMoreLikeThis", 493 484 async (plugin, { postUri, feedUri = null }) => { 494 - this._requireModerationPermission(plugin, "feedback"); 485 + this._requireSignedIn(); 486 + this._requireActionPermission(plugin, "feedFeedback"); 495 487 requireHostMethodArg("showMoreLikeThis", "postUri", postUri); 496 488 requireHostMethodArg("showMoreLikeThis", "feedUri", feedUri); 497 489 const { feedContext, feedProxyUrl } = this._resolveFeedAttribution( ··· 506 498 ); 507 499 }, 508 500 ); 509 - 510 - this.pluginBridge.addHostMethod( 511 - "sendInteraction", 512 - async ( 513 - plugin, 514 - { postUri, event, feedProxyUrl = null, feedContext = null }, 515 - ) => { 516 - this._requireModerationPermission(plugin, "feedback"); 517 - requireHostMethodArg("sendInteraction", "postUri", postUri); 518 - requireHostMethodArg("sendInteraction", "feedProxyUrl", feedProxyUrl); 519 - if (!FEED_INTERACTION_EVENTS.has(event)) { 520 - throw new Error(`Unsupported feed interaction event "${event}"`); 521 - } 522 - await this._dataLayer.api.sendInteractions( 523 - [ 524 - { 525 - item: postUri, 526 - event, 527 - ...(feedContext != null ? { feedContext } : {}), 528 - }, 529 - ], 530 - feedProxyUrl, 531 - ); 532 - }, 533 - ); 534 501 } 535 502 536 503 _resolveFeedAttribution(postUri, feedUri) { 537 - if (!feedUri || !this._dataLayer) { 538 - return { feedContext: null, feedProxyUrl: null }; 539 - } 540 504 const feed = this._dataLayer.dataStore.$feeds.get(feedUri); 541 505 const feedItem = feed?.feed.find((item) => item.post.uri === postUri); 542 506 const feedGenerator = this._dataLayer.derived.$feedGenerators.get(feedUri); ··· 546 510 }; 547 511 } 548 512 549 - _requireModerationPermission(plugin, action) { 550 - if (!isModerationActionAllowed(action, plugin.permissions)) { 551 - throw new Error( 552 - `"${plugin.pluginId}" does not have "${action}" moderation permission`, 553 - ); 554 - } 513 + _requireSignedIn() { 555 514 if (!this._dataLayer) throw new Error("Not signed in"); 556 515 } 557 516 517 + _requireActionPermission(plugin, action) { 518 + if (!isActionAllowed(action, plugin.permissions)) { 519 + throw new Error( 520 + `"${plugin.pluginId}" does not have "${action}" action permission`, 521 + ); 522 + } 523 + } 524 + 558 525 _resolveProfileForMutation(did) { 559 - if (!this._dataLayer) return { did }; 560 526 return ( 561 527 this._dataLayer.derived.$hydratedDetailedProfiles.get(did) ?? 562 528 this._dataLayer.derived.$hydratedProfiles.get(did) ?? { did }
+34
tests/unit/specs/plugins/pluginPermissions.test.js
··· 5 5 diffPermissions, 6 6 isEmptyPermissions, 7 7 isFetchAllowed, 8 + isActionAllowed, 8 9 } from "/js/plugins/pluginPermissions.js"; 9 10 10 11 describe("parsePermissions", () => { ··· 39 40 }), 40 41 { fetch: ["https://a.com/*", "https://b.com/*"] }, 41 42 ); 43 + }); 44 + 45 + it("parses known action scopes and drops unknown ones", () => { 46 + assert.deepEqual( 47 + parsePermissions({ 48 + actions: ["mute", "block", "feedFeedback", "deleteEverything"], 49 + }), 50 + { actions: ["mute", "block", "feedFeedback"] }, 51 + ); 52 + }); 53 + 54 + it("wraps a string actions value into an array", () => { 55 + assert.deepEqual(parsePermissions({ actions: "mute" }), { 56 + actions: ["mute"], 57 + }); 58 + }); 59 + 60 + it("omits the actions key when no valid scopes remain", () => { 61 + assert.deepEqual(parsePermissions({ actions: [] }), {}); 62 + assert.deepEqual(parsePermissions({ actions: ["feedback"] }), {}); 63 + }); 64 + }); 65 + 66 + describe("isActionAllowed", () => { 67 + it("allows only granted action scopes", () => { 68 + const permissions = { actions: ["mute", "feedFeedback"] }; 69 + assert(isActionAllowed("mute", permissions)); 70 + assert(isActionAllowed("feedFeedback", permissions)); 71 + assert(!isActionAllowed("block", permissions)); 72 + }); 73 + 74 + it("denies everything when the actions key is missing", () => { 75 + assert(!isActionAllowed("mute", {})); 42 76 }); 43 77 }); 44 78
+98 -101
tests/unit/specs/plugins/pluginService.test.js
··· 1446 1446 }); 1447 1447 }); 1448 1448 1449 - describe("feed feedback host methods", () => { 1449 + describe("action host methods", () => { 1450 1450 const feedbackPlugin = { 1451 1451 pluginId: "test-plugin", 1452 - permissions: { moderation: ["feedback"] }, 1452 + permissions: { actions: ["feedFeedback"] }, 1453 1453 }; 1454 1454 const postUri = "at://did:plc:author/app.bsky.feed.post/1"; 1455 1455 const feedUri = "at://did:plc:feedgen/app.bsky.feed.generator/cool-feed"; 1456 1456 1457 - function makeService({ feedItem = null, feedGenerator = null } = {}) { 1457 + function makeService({ 1458 + feedItem = null, 1459 + feedGenerator = null, 1460 + hydratedProfiles = {}, 1461 + } = {}) { 1458 1462 const { provider } = makeProvider(); 1459 1463 const service = new PluginService(provider, null); 1460 - const calls = { showLess: [], showMore: [], sendInteractions: [] }; 1464 + const calls = { 1465 + showLess: [], 1466 + showMore: [], 1467 + mute: [], 1468 + unmute: [], 1469 + block: [], 1470 + unblock: [], 1471 + }; 1461 1472 service.setDataLayer({ 1462 1473 dataStore: { 1463 1474 $feeds: { ··· 1469 1480 $feedGenerators: { 1470 1481 get: (uri) => (uri === feedUri ? feedGenerator : null), 1471 1482 }, 1483 + $hydratedDetailedProfiles: { get: () => null }, 1484 + $hydratedProfiles: { get: (did) => hydratedProfiles[did] ?? null }, 1472 1485 }, 1473 1486 mutations: { 1474 1487 sendShowLessInteraction: async (...args) => calls.showLess.push(args), 1475 1488 sendShowMoreInteraction: async (...args) => calls.showMore.push(args), 1476 - }, 1477 - api: { 1478 - sendInteractions: async (...args) => calls.sendInteractions.push(args), 1489 + muteProfile: async (profile) => calls.mute.push(profile), 1490 + unmuteProfile: async (profile) => calls.unmute.push(profile), 1491 + blockProfile: async (profile) => calls.block.push(profile), 1492 + unblockProfile: async (profile) => calls.unblock.push(profile), 1479 1493 }, 1480 1494 }); 1481 1495 return { service, calls }; ··· 1513 1527 assert.deepEqual(calls.showMore, []); 1514 1528 }); 1515 1529 1516 - it("all three methods reject when postUri is missing", async () => { 1530 + it("both methods reject when postUri is missing", async () => { 1517 1531 const { service, calls } = makeService(); 1518 1532 await assert.rejects( 1519 1533 getHandler(service, "showLessLikeThis")(feedbackPlugin, { feedUri }), ··· 1523 1537 getHandler(service, "showMoreLikeThis")(feedbackPlugin, { feedUri }), 1524 1538 /requires a postUri/, 1525 1539 ); 1526 - await assert.rejects( 1527 - getHandler(service, "sendInteraction")(feedbackPlugin, { 1528 - event: "app.bsky.feed.defs#interactionSeen", 1529 - feedProxyUrl: "did:web:feed.example#bsky_fg", 1530 - }), 1531 - /requires a postUri/, 1532 - ); 1533 1540 assert.deepEqual(calls.showLess, []); 1534 1541 assert.deepEqual(calls.showMore, []); 1535 - assert.deepEqual(calls.sendInteractions, []); 1536 1542 }); 1537 1543 1538 1544 it("muteActor and blockActor reject when did is missing", async () => { 1539 1545 const { service } = makeService(); 1540 1546 await assert.rejects( 1541 1547 getHandler(service, "muteActor")( 1542 - { pluginId: "test-plugin", permissions: { moderation: ["mute"] } }, 1548 + { pluginId: "test-plugin", permissions: { actions: ["mute"] } }, 1543 1549 {}, 1544 1550 ), 1545 1551 /muteActor requires a did/, 1546 1552 ); 1547 1553 await assert.rejects( 1548 1554 getHandler(service, "blockActor")( 1549 - { pluginId: "test-plugin", permissions: { moderation: ["block"] } }, 1555 + { pluginId: "test-plugin", permissions: { actions: ["block"] } }, 1550 1556 {}, 1551 1557 ), 1552 1558 /blockActor requires a did/, ··· 1580 1586 ]); 1581 1587 }); 1582 1588 1583 - it("sendInteraction sends a known event with caller-supplied routing and context", async () => { 1584 - const { service, calls } = makeService(); 1585 - await getHandler(service, "sendInteraction")(feedbackPlugin, { 1586 - postUri, 1587 - event: "app.bsky.feed.defs#interactionSeen", 1588 - feedProxyUrl: "did:web:feed.example#bsky_fg", 1589 - feedContext: "plugin-ctx", 1590 - }); 1591 - assert.deepEqual(calls.sendInteractions, [ 1592 - [ 1593 - [ 1594 - { 1595 - item: postUri, 1596 - event: "app.bsky.feed.defs#interactionSeen", 1597 - feedContext: "plugin-ctx", 1598 - }, 1599 - ], 1600 - "did:web:feed.example#bsky_fg", 1601 - ], 1602 - ]); 1603 - }); 1604 - 1605 - it("sendInteraction omits feedContext when the caller does not supply one", async () => { 1606 - const { service, calls } = makeService(); 1607 - await getHandler(service, "sendInteraction")(feedbackPlugin, { 1608 - postUri, 1609 - event: "app.bsky.feed.defs#interactionShare", 1610 - feedProxyUrl: "did:web:feed.example#bsky_fg", 1611 - }); 1612 - assert.deepEqual(calls.sendInteractions, [ 1613 - [ 1614 - [{ item: postUri, event: "app.bsky.feed.defs#interactionShare" }], 1615 - "did:web:feed.example#bsky_fg", 1616 - ], 1617 - ]); 1618 - }); 1619 - 1620 - it("sendInteraction rejects when feedProxyUrl is missing", async () => { 1621 - const { service, calls } = makeService(); 1622 - await assert.rejects( 1623 - getHandler(service, "sendInteraction")(feedbackPlugin, { 1624 - postUri, 1625 - event: "app.bsky.feed.defs#interactionSeen", 1626 - }), 1627 - /requires a feedProxyUrl/, 1628 - ); 1629 - assert.deepEqual(calls.sendInteractions, []); 1630 - }); 1631 - 1632 - it("sendInteraction rejects unknown and disallowed events", async () => { 1633 - const { service, calls } = makeService(); 1634 - for (const event of [ 1635 - "app.bsky.feed.defs#madeUp", 1636 - "app.bsky.feed.defs#clickthroughItem", 1637 - "app.bsky.feed.defs#clickthroughAuthor", 1638 - "app.bsky.feed.defs#clickthroughReposter", 1639 - "app.bsky.feed.defs#clickthroughEmbed", 1640 - ]) { 1641 - await assert.rejects( 1642 - getHandler(service, "sendInteraction")(feedbackPlugin, { 1643 - postUri, 1644 - event, 1645 - feedProxyUrl: "did:web:feed.example#bsky_fg", 1646 - }), 1647 - /Unsupported feed interaction event/, 1648 - ); 1649 - } 1650 - assert.deepEqual(calls.sendInteractions, []); 1651 - }); 1652 - 1653 - it("all three methods require the feedback moderation permission", async () => { 1589 + it("both methods require the feedFeedback action permission", async () => { 1654 1590 const { service, calls } = makeService(); 1655 1591 const noPermissionPlugin = { 1656 1592 pluginId: "test-plugin", 1657 - permissions: { moderation: ["mute"] }, 1593 + permissions: { actions: ["mute"] }, 1658 1594 }; 1659 - for (const name of [ 1660 - "showLessLikeThis", 1661 - "showMoreLikeThis", 1662 - "sendInteraction", 1663 - ]) { 1595 + for (const name of ["showLessLikeThis", "showMoreLikeThis"]) { 1664 1596 await assert.rejects( 1665 - getHandler(service, name)(noPermissionPlugin, { 1666 - postUri, 1667 - event: "app.bsky.feed.defs#interactionSeen", 1668 - }), 1669 - /"feedback" moderation permission/, 1597 + getHandler(service, name)(noPermissionPlugin, { postUri, feedUri }), 1598 + /"feedFeedback" action permission/, 1670 1599 ); 1671 1600 } 1672 1601 assert.deepEqual(calls.showLess, []); 1673 1602 assert.deepEqual(calls.showMore, []); 1674 - assert.deepEqual(calls.sendInteractions, []); 1603 + }); 1604 + 1605 + it("muteActor routes the mute flag to muteProfile and unmuteProfile", async () => { 1606 + const did = "did:plc:target"; 1607 + const profile = { did, handle: "target.example" }; 1608 + const { service, calls } = makeService({ 1609 + hydratedProfiles: { [did]: profile }, 1610 + }); 1611 + const mutePlugin = { 1612 + pluginId: "test-plugin", 1613 + permissions: { actions: ["mute"] }, 1614 + }; 1615 + await getHandler(service, "muteActor")(mutePlugin, { did, mute: true }); 1616 + await getHandler(service, "muteActor")(mutePlugin, { did, mute: false }); 1617 + assert.deepEqual(calls.mute, [profile]); 1618 + assert.deepEqual(calls.unmute, [profile]); 1619 + }); 1620 + 1621 + it("blockActor routes the block flag to blockProfile and unblockProfile", async () => { 1622 + const did = "did:plc:target"; 1623 + const { service, calls } = makeService(); 1624 + const blockPlugin = { 1625 + pluginId: "test-plugin", 1626 + permissions: { actions: ["block"] }, 1627 + }; 1628 + await getHandler(service, "blockActor")(blockPlugin, { did, block: true }); 1629 + await getHandler(service, "blockActor")(blockPlugin, { did, block: false }); 1630 + assert.deepEqual(calls.block, [{ did }]); 1631 + assert.deepEqual(calls.unblock, [{ did }]); 1632 + }); 1633 + 1634 + it("muteActor and blockActor require their action permissions", async () => { 1635 + const { service, calls } = makeService(); 1636 + const noPermissionPlugin = { 1637 + pluginId: "test-plugin", 1638 + permissions: { actions: ["feedFeedback"] }, 1639 + }; 1640 + const did = "did:plc:target"; 1641 + await assert.rejects( 1642 + getHandler(service, "muteActor")(noPermissionPlugin, { did }), 1643 + /"mute" action permission/, 1644 + ); 1645 + await assert.rejects( 1646 + getHandler(service, "blockActor")(noPermissionPlugin, { did }), 1647 + /"block" action permission/, 1648 + ); 1649 + assert.deepEqual(calls.mute, []); 1650 + assert.deepEqual(calls.block, []); 1651 + }); 1652 + 1653 + it("all action methods reject when signed out", async () => { 1654 + const { provider } = makeProvider(); 1655 + const service = new PluginService(provider, null); 1656 + const allActionsPlugin = { 1657 + pluginId: "test-plugin", 1658 + permissions: { actions: ["mute", "block", "feedFeedback"] }, 1659 + }; 1660 + const argsByMethod = { 1661 + muteActor: { did: "did:plc:target" }, 1662 + blockActor: { did: "did:plc:target" }, 1663 + showLessLikeThis: { postUri, feedUri }, 1664 + showMoreLikeThis: { postUri, feedUri }, 1665 + }; 1666 + for (const [name, args] of Object.entries(argsByMethod)) { 1667 + await assert.rejects( 1668 + getHandler(service, name)(allActionsPlugin, args), 1669 + /Not signed in/, 1670 + ); 1671 + } 1675 1672 }); 1676 1673 }); 1677 1674
+25 -17
tests/unit/specs/plugins/pluginWorker.test.js
··· 335 335 assert.deepEqual(sent.args[0], "at://example/feed"); 336 336 }); 337 337 338 - it("app.moderation feedback methods post hostCalls with postUri and feedUri", async () => { 338 + it("app mute and block methods post hostCalls with the direction flag", async () => { 339 + clearMessages(); 340 + const plugin = new Plugin(); 341 + const did = "did:plc:target"; 342 + 343 + plugin.app.muteActor(did); 344 + assert.deepEqual(lastMessage().method, "muteActor"); 345 + assert.deepEqual(lastMessage().args[0], { did, mute: true }); 346 + 347 + plugin.app.unmuteActor(did); 348 + assert.deepEqual(lastMessage().method, "muteActor"); 349 + assert.deepEqual(lastMessage().args[0], { did, mute: false }); 350 + 351 + plugin.app.blockActor(did); 352 + assert.deepEqual(lastMessage().method, "blockActor"); 353 + assert.deepEqual(lastMessage().args[0], { did, block: true }); 354 + 355 + plugin.app.unblockActor(did); 356 + assert.deepEqual(lastMessage().method, "blockActor"); 357 + assert.deepEqual(lastMessage().args[0], { did, block: false }); 358 + }); 359 + 360 + it("app feedback methods post hostCalls with postUri and feedUri", async () => { 339 361 clearMessages(); 340 362 const plugin = new Plugin(); 341 363 const postUri = "at://example/post/1"; 342 364 const feedUri = "at://example/feed/cool"; 343 365 344 - plugin.app.moderation.showLessLikeThis(postUri, feedUri); 366 + plugin.app.showLessLikeThis(postUri, feedUri); 345 367 assert.deepEqual(lastMessage().method, "showLessLikeThis"); 346 368 assert.deepEqual(lastMessage().args[0], { postUri, feedUri }); 347 369 348 - plugin.app.moderation.showMoreLikeThis(postUri, feedUri); 370 + plugin.app.showMoreLikeThis(postUri, feedUri); 349 371 assert.deepEqual(lastMessage().method, "showMoreLikeThis"); 350 372 assert.deepEqual(lastMessage().args[0], { postUri, feedUri }); 351 - 352 - plugin.app.moderation.sendInteraction( 353 - postUri, 354 - "app.bsky.feed.defs#interactionSeen", 355 - "did:web:feed.example#bsky_fg", 356 - { feedContext: "ctx" }, 357 - ); 358 - assert.deepEqual(lastMessage().method, "sendInteraction"); 359 - assert.deepEqual(lastMessage().args[0], { 360 - postUri, 361 - event: "app.bsky.feed.defs#interactionSeen", 362 - feedProxyUrl: "did:web:feed.example#bsky_fg", 363 - feedContext: "ctx", 364 - }); 365 373 }); 366 374 367 375 it("app.data.getPost posts a hostCall and resolves with the host result", async () => {