From c2d96febbea9de81c39d4d1ee0010aa8fcdba5d7 Mon Sep 17 00:00:00 2001 From: ashariyar Date: Wed, 20 Nov 2024 22:04:11 -0500 Subject: [PATCH 1/9] README improvements --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f8cb20d3..38acb79e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # fedialgo [![Fedialgo Build and Test](https://github.com/pkreissel/fedialgo/actions/workflows/CI.yaml/badge.svg)](https://github.com/pkreissel/fedialgo/actions/workflows/CI.yaml) -Fedialgo is an typescript module, that can be used to produce an algorithmic fediverse experience. This will replace the project "fedifeed" and make it possible to implement the idea into all kinds of other projects. It uses React Native Async Storage, so it should also work in React Native Projects, but havent tested it yet. +Fedialgo is an typescript module, that can be used to produce an algorithmic fediverse experience. This will replace the project "fedifeed" and make it possible to implement the idea into all kinds of other projects. It uses React Native Async Storage, so it should also work in React Native Projects, but havent tested it yet. ## Install directly from github: @@ -25,7 +25,10 @@ Use // @ts-ignore if you run into Typescript warnings (because your project migh ```console npm run build ``` -in fedialgo directory after changes and they will automatically be detected +in `fedialgo` directory after changes and they will automatically be detected + +### Demo App +`fedialgo` is just a node package. You don't use it on its own in the form in this repo; it has to be imported into some other app. Thankfully there's a simple demo app that spins up a webserver, pulls your feed and scores it with the algorithm in this repo, and then presents it to your browser at `http://localhost:3000/` over in [the `foryoufeed` repo](https://github.com/pkreissel/foryoufeed). ## Usage @@ -41,7 +44,7 @@ const api: mastodon.Client = await login({ const currUser = await api.v1.accounts.verifyCredentials() const algo = new TheAlgorithm(api, currUser) const feed = await algo.getFeed() - + ``` ### Adjust Weights: @@ -56,7 +59,7 @@ const newFeed = await algoObj.setWeights(newWeights) ``` ### Learn Weights -You can also let the algorithm learn the weights from the user's behaviour. This is done by passing the scores of the posts to the algorithm. The algorithm will then adjust the weights accordingly. This is quite simple, but still has impact on the feed. For example you could choose to adjust the weight after each click on a post, after a reblog, or after a link click. +You can also let the algorithm learn the weights from the user's behaviour. This is done by passing the scores of the posts to the algorithm. The algorithm will then adjust the weights accordingly. This is quite simple, but still has impact on the feed. For example you could choose to adjust the weight after each click on a post, after a reblog, or after a link click. ```typescript const scores = status.scores @@ -65,5 +68,3 @@ const newWeights = await algoObj.weightAdjust(scores) This is untested early alpha so might be due to massive unannounced changes. - - From f40fa6981148a9e9539f278642b641ab7b598ac1 Mon Sep 17 00:00:00 2001 From: ashariyar Date: Wed, 20 Nov 2024 22:27:35 -0500 Subject: [PATCH 2/9] Fix 'reblogger' typo --- src/scorer/feature/reblogsFeatureScorer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scorer/feature/reblogsFeatureScorer.ts b/src/scorer/feature/reblogsFeatureScorer.ts index 500a1c34..f5d8be88 100644 --- a/src/scorer/feature/reblogsFeatureScorer.ts +++ b/src/scorer/feature/reblogsFeatureScorer.ts @@ -8,7 +8,7 @@ export default class reblogsFeatureScorer extends FeatureScorer { super({ featureGetter: (api: mastodon.rest.Client) => { return FeatureStorage.getTopReblogs(api) }, verboseName: "Reblogs", - description: "Posts that are from your most reblogger users", + description: "Posts that are from accounts you have retooted the most", defaultWeight: 3, }) } @@ -18,4 +18,4 @@ export default class reblogsFeatureScorer extends FeatureScorer { const reblogScore = (status.reblog && status.reblog.account.acct in this.feature) ? this.feature[status.reblog.account.acct] : 0 return authorScore + reblogScore } -} \ No newline at end of file +} From 2c8967c142bfdc9f19d97ea4410ecd75223f5ac3 Mon Sep 17 00:00:00 2001 From: ashariyar Date: Wed, 20 Nov 2024 22:36:55 -0500 Subject: [PATCH 3/9] Better wordings for the algo weighting sliders --- src/features/FeatureStore.ts | 1 + src/feeds/topPostsFeed.ts | 3 +-- src/scorer/feature/favsFeatureScorer.ts | 4 ++-- src/scorer/feature/interactsFeatureScorer.ts | 4 ++-- src/scorer/feature/reblogsFeatureScorer.ts | 2 +- src/scorer/feature/topPostFeatureScorer.ts | 4 ++-- src/scorer/feed/reblogsFeedScorer.ts | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/features/FeatureStore.ts b/src/features/FeatureStore.ts index da866468..f953c755 100644 --- a/src/features/FeatureStore.ts +++ b/src/features/FeatureStore.ts @@ -45,6 +45,7 @@ export default class FeatureStorage extends Storage { } } + // Returns the Mastodon server the user is currently logged in to static async getCoreServer(api: mastodon.rest.Client): Promise { const coreServer: serverFeatureType = await this.get(Key.CORE_SERVER) as serverFeatureType; console.log(coreServer); diff --git a/src/feeds/topPostsFeed.ts b/src/feeds/topPostsFeed.ts index dd56013e..efa350c4 100644 --- a/src/feeds/topPostsFeed.ts +++ b/src/feeds/topPostsFeed.ts @@ -8,7 +8,6 @@ export default async function getTopPostFeed(api: mastodon.rest.Client): Promise const core_servers = await FeatureStore.getCoreServer(api) let results: StatusType[][] = []; - //Get Top Servers const servers = Object.keys(core_servers).sort((a, b) => { return core_servers[b] - core_servers[a] @@ -39,4 +38,4 @@ export default async function getTopPostFeed(api: mastodon.rest.Client): Promise } return status }) -} \ No newline at end of file +} diff --git a/src/scorer/feature/favsFeatureScorer.ts b/src/scorer/feature/favsFeatureScorer.ts index 5eb5ef11..c82fcc26 100644 --- a/src/scorer/feature/favsFeatureScorer.ts +++ b/src/scorer/feature/favsFeatureScorer.ts @@ -9,7 +9,7 @@ export default class favsFeatureScorer extends FeatureScorer { super({ featureGetter: (api: mastodon.rest.Client) => FeatureStorage.getTopFavs(api), verboseName: "Favs", - description: "Posts that are from your most favorited users", + description: "Favor posts from users whose posts you have favorited a lot in the past", defaultWeight: 1, }) } @@ -17,4 +17,4 @@ export default class favsFeatureScorer extends FeatureScorer { async score(_api: mastodon.rest.Client, status: StatusType) { return (status.account.acct in this.feature) ? this.feature[status.account.acct] : 0 } -} \ No newline at end of file +} diff --git a/src/scorer/feature/interactsFeatureScorer.ts b/src/scorer/feature/interactsFeatureScorer.ts index 23809463..0e158346 100644 --- a/src/scorer/feature/interactsFeatureScorer.ts +++ b/src/scorer/feature/interactsFeatureScorer.ts @@ -8,7 +8,7 @@ export default class interactsFeatureScorer extends FeatureScorer { super({ featureGetter: (api: mastodon.rest.Client) => { return FeatureStorage.getTopInteracts(api) }, verboseName: "Interacts", - description: "Posts that are from users, that often interact with your posts", + description: "Favor posts from users that most frequently interact with your posts", defaultWeight: 2, }) } @@ -16,4 +16,4 @@ export default class interactsFeatureScorer extends FeatureScorer { async score(_api: mastodon.rest.Client, status: StatusType) { return (status.account.acct in this.feature) ? this.feature[status.account.acct] : 0 } -} \ No newline at end of file +} diff --git a/src/scorer/feature/reblogsFeatureScorer.ts b/src/scorer/feature/reblogsFeatureScorer.ts index f5d8be88..d95589c7 100644 --- a/src/scorer/feature/reblogsFeatureScorer.ts +++ b/src/scorer/feature/reblogsFeatureScorer.ts @@ -8,7 +8,7 @@ export default class reblogsFeatureScorer extends FeatureScorer { super({ featureGetter: (api: mastodon.rest.Client) => { return FeatureStorage.getTopReblogs(api) }, verboseName: "Reblogs", - description: "Posts that are from accounts you have retooted the most", + description: "Favor posts from accounts you have retooted a lot", defaultWeight: 3, }) } diff --git a/src/scorer/feature/topPostFeatureScorer.ts b/src/scorer/feature/topPostFeatureScorer.ts index 15bb9870..a0c4399e 100644 --- a/src/scorer/feature/topPostFeatureScorer.ts +++ b/src/scorer/feature/topPostFeatureScorer.ts @@ -7,7 +7,7 @@ export default class topPostFeatureScorer extends FeatureScorer { super({ featureGetter: (_api: mastodon.rest.Client) => { return Promise.resolve({}) }, verboseName: "TopPosts", - description: "Posts that are trending on multiple of your most popular instances", + description: "Favor posts that are trending on your Mastodon server", defaultWeight: 1, }) } @@ -15,4 +15,4 @@ export default class topPostFeatureScorer extends FeatureScorer { async score(_api: mastodon.rest.Client, status: StatusType) { return status.topPost ? 1 : 0 } -} \ No newline at end of file +} diff --git a/src/scorer/feed/reblogsFeedScorer.ts b/src/scorer/feed/reblogsFeedScorer.ts index 8f0af36c..8ba0d879 100644 --- a/src/scorer/feed/reblogsFeedScorer.ts +++ b/src/scorer/feed/reblogsFeedScorer.ts @@ -3,7 +3,7 @@ import { StatusType } from "../../types"; export default class reblogsFeedScorer extends FeedScorer { constructor() { - super("reblogsFeed", "More Weight to posts that are reblogged a lot", 6); + super("reblogsFeed", "Favor posts that have been retooted many times", 6); } feedExtractor(feed: StatusType[]) { @@ -26,4 +26,4 @@ export default class reblogsFeedScorer extends FeedScorer { } return features[status.uri] || 0; } -} \ No newline at end of file +} From b8bb25f74eaafa1514a363b7775454edcec7291d Mon Sep 17 00:00:00 2001 From: ashariyar Date: Wed, 20 Nov 2024 22:39:31 -0500 Subject: [PATCH 4/9] Better wordings for the algo weighting sliders --- src/scorer/feed/diversityFeedScorer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scorer/feed/diversityFeedScorer.ts b/src/scorer/feed/diversityFeedScorer.ts index 363e180a..e82cdf3e 100644 --- a/src/scorer/feed/diversityFeedScorer.ts +++ b/src/scorer/feed/diversityFeedScorer.ts @@ -3,7 +3,7 @@ import { StatusType } from "../../types"; export default class diversityFeedScorer extends FeedScorer { constructor() { - super("Diversity", "Downranks posts from users that you have seen a lot of posts from"); + super("Diversity", "Disfavor posts from users that you have seen a lot of posts from already"); } feedExtractor(feed: StatusType[]): Record { @@ -21,4 +21,4 @@ export default class diversityFeedScorer extends FeedScorer { this.features[status.account.acct] = frequ + 1 return frequ + 1 } -} \ No newline at end of file +} From 87aaf55c1e38111efa3a75c84901926df56bdd12 Mon Sep 17 00:00:00 2001 From: ashariyar Date: Wed, 20 Nov 2024 23:31:06 -0500 Subject: [PATCH 5/9] README contributing section --- README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 38acb79e..e7e4295b 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ in `fedialgo` directory after changes and they will automatically be detected ### Demo App `fedialgo` is just a node package. You don't use it on its own in the form in this repo; it has to be imported into some other app. Thankfully there's a simple demo app that spins up a webserver, pulls your feed and scores it with the algorithm in this repo, and then presents it to your browser at `http://localhost:3000/` over in [the `foryoufeed` repo](https://github.com/pkreissel/foryoufeed). -## Usage +## Package Usage -### Basic Feed: +### Weight An Account's Feed: ```typescript import TheAlgorithm from "fedialgo" import { login, mastodon } from "masto"; @@ -44,13 +44,12 @@ const api: mastodon.Client = await login({ const currUser = await api.v1.accounts.verifyCredentials() const algo = new TheAlgorithm(api, currUser) const feed = await algo.getFeed() +``` - ``` - - ### Adjust Weights: +### Adjust Weights: The algorithm uses features and their weights to determine the order of the posts. +You could e.g. show the weights to the user, who can then decide to change them. - You could e.g. show the weights to the user, who can then decide to change them. ```typescript let weights = await algo.getWeights() weights["fav"] = 0.5 // change the weight of the feature "fav" to 0.5 @@ -66,5 +65,10 @@ const scores = status.scores const newWeights = await algoObj.weightAdjust(scores) ``` - +# Contributing +### Developer Setup +Install the dev dependencies with `npm install --dev`. This is untested early alpha so might be due to massive unannounced changes. + +#### Running Tests +`npm run test` From 830895d6204b7a837d1a7b3b495f5e9931b269c5 Mon Sep 17 00:00:00 2001 From: ashariyar Date: Wed, 20 Nov 2024 23:32:19 -0500 Subject: [PATCH 6/9] README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e7e4295b..2f12dd1e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # fedialgo +This is untested early alpha so might be due to massive unannounced changes. [![Fedialgo Build and Test](https://github.com/pkreissel/fedialgo/actions/workflows/CI.yaml/badge.svg)](https://github.com/pkreissel/fedialgo/actions/workflows/CI.yaml) Fedialgo is an typescript module, that can be used to produce an algorithmic fediverse experience. This will replace the project "fedifeed" and make it possible to implement the idea into all kinds of other projects. It uses React Native Async Storage, so it should also work in React Native Projects, but havent tested it yet. @@ -67,8 +68,7 @@ const newWeights = await algoObj.weightAdjust(scores) # Contributing ### Developer Setup -Install the dev dependencies with `npm install --dev`. -This is untested early alpha so might be due to massive unannounced changes. +If necessary install the dev dependencies with `npm install --include=dev`. -#### Running Tests +#### Running Test Suite `npm run test` From c70682491f20f9497bfe7ecc87d444f08d78516c Mon Sep 17 00:00:00 2001 From: ashariyar Date: Wed, 20 Nov 2024 23:34:43 -0500 Subject: [PATCH 7/9] copy --- src/scorer/feature/topPostFeatureScorer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scorer/feature/topPostFeatureScorer.ts b/src/scorer/feature/topPostFeatureScorer.ts index a0c4399e..9140d37b 100644 --- a/src/scorer/feature/topPostFeatureScorer.ts +++ b/src/scorer/feature/topPostFeatureScorer.ts @@ -7,7 +7,7 @@ export default class topPostFeatureScorer extends FeatureScorer { super({ featureGetter: (_api: mastodon.rest.Client) => { return Promise.resolve({}) }, verboseName: "TopPosts", - description: "Favor posts that are trending on your Mastodon server", + description: "Favor posts that are trending in the Fediverse", defaultWeight: 1, }) } From be6e7b3b2fe5d3eb4fa15a53e09b790c326e6881 Mon Sep 17 00:00:00 2001 From: ashariyar Date: Thu, 21 Nov 2024 00:58:48 -0500 Subject: [PATCH 8/9] Add semicolons and comments; improve some whitespace formatting --- src/features/FeatureStore.ts | 7 ++- src/feeds/topPostsFeed.ts | 18 +++---- src/index.ts | 57 ++++++++++++-------- src/scorer/feature/favsFeatureScorer.ts | 13 +++-- src/scorer/feature/interactsFeatureScorer.ts | 12 +++-- src/scorer/feature/reblogsFeatureScorer.ts | 8 +-- src/scorer/feature/topPostFeatureScorer.ts | 6 +-- src/scorer/feed/diversityFeedScorer.ts | 6 +-- src/weights/weightsStore.ts | 10 ++-- 9 files changed, 79 insertions(+), 58 deletions(-) diff --git a/src/features/FeatureStore.ts b/src/features/FeatureStore.ts index f953c755..322595d4 100644 --- a/src/features/FeatureStore.ts +++ b/src/features/FeatureStore.ts @@ -11,6 +11,7 @@ export default class FeatureStorage extends Storage { static async getTopFavs(api: mastodon.rest.Client): Promise { const topFavs: accFeatureType = await this.get(Key.TOP_FAVS) as accFeatureType; console.log(topFavs); + if (topFavs != null && await this.getOpenings() % 10 < 9) { return topFavs; } else { @@ -23,6 +24,7 @@ export default class FeatureStorage extends Storage { static async getTopReblogs(api: mastodon.rest.Client): Promise { const topReblogs: accFeatureType = await this.get(Key.TOP_REBLOGS) as accFeatureType; console.log(topReblogs); + if (topReblogs != null && await this.getOpenings() % 10 < 9) { return topReblogs; } else { @@ -36,6 +38,7 @@ export default class FeatureStorage extends Storage { static async getTopInteracts(api: mastodon.rest.Client): Promise { const topInteracts: accFeatureType = await this.get(Key.TOP_INTERACTS) as accFeatureType; console.log(topInteracts); + if (topInteracts != null && await this.getOpenings() % 10 < 9) { return topInteracts; } else { @@ -49,6 +52,7 @@ export default class FeatureStorage extends Storage { static async getCoreServer(api: mastodon.rest.Client): Promise { const coreServer: serverFeatureType = await this.get(Key.CORE_SERVER) as serverFeatureType; console.log(coreServer); + if (coreServer != null && await this.getOpenings() % 10 != 9) { return coreServer; } else { @@ -58,5 +62,4 @@ export default class FeatureStorage extends Storage { return server; } } - -} +}; diff --git a/src/feeds/topPostsFeed.ts b/src/feeds/topPostsFeed.ts index efa350c4..656a52c8 100644 --- a/src/feeds/topPostsFeed.ts +++ b/src/feeds/topPostsFeed.ts @@ -1,7 +1,7 @@ -import { mastodon } from "masto"; import FeatureStore from "../features/FeatureStore"; -import { StatusType } from "../types"; import Storage from "../Storage"; +import { mastodon } from "masto"; +import { StatusType } from "../types"; import { _transformKeys, mastodonFetch } from "../helpers"; export default async function getTopPostFeed(api: mastodon.rest.Client): Promise { @@ -10,7 +10,7 @@ export default async function getTopPostFeed(api: mastodon.rest.Client): Promise //Get Top Servers const servers = Object.keys(core_servers).sort((a, b) => { - return core_servers[b] - core_servers[a] + return core_servers[b] - core_servers[a]; }).slice(0, 10) if (servers.length === 0) { @@ -26,16 +26,16 @@ export default async function getTopPostFeed(api: mastodon.rest.Client): Promise ).map((status: StatusType) => { status.topPost = true; return status; - }).slice(0, 10) ?? [] + }).slice(0, 10) ?? []; })) - console.log(results) + console.log(results); - const lastOpened = new Date((await Storage.getLastOpened() ?? 0) - 28800000) + const lastOpened = new Date((await Storage.getLastOpened() ?? 0) - 28800000); return results.flat().filter((status: StatusType) => new Date(status.createdAt) > lastOpened).map((status: StatusType) => { - const acct = status.account.acct + const acct = status.account.acct; if (acct && !acct.includes("@")) { - status.account.acct = `${acct}@${status.account.url.split("/")[2]}` + status.account.acct = `${acct}@${status.account.url.split("/")[2]}`; } - return status + return status; }) } diff --git a/src/index.ts b/src/index.ts index 690f73b0..9234728d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,27 +1,27 @@ import { mastodon } from "masto"; import { FeedFetcher, StatusType, weightsType } from "./types"; import { + diversityFeedScorer, favsFeatureScorer, + FeatureScorer, + FeedScorer, interactsFeatureScorer, reblogsFeatureScorer, - diversityFeedScorer, reblogsFeedScorer, - FeatureScorer, - FeedScorer, topPostFeatureScorer } from "./scorer"; -import weightsStore from "./weights/weightsStore"; +import chaosFeatureScorer from "./scorer/feature/chaosFeatureScorer"; import getHomeFeed from "./feeds/homeFeed"; -import topPostsFeed from "./feeds/topPostsFeed"; +import Paginator from "./Paginator"; import Storage from "./Storage"; -import Paginator from "./Paginator" -import chaosFeatureScorer from "./scorer/feature/chaosFeatureScorer"; +import topPostsFeed from "./feeds/topPostsFeed"; +import weightsStore from "./weights/weightsStore"; //import getRecommenderFeed from "./feeds/recommenderFeed"; export default class TheAlgorithm { user: mastodon.v1.Account; fetchers = [getHomeFeed, topPostsFeed]; - featureScorer = [ + featureScorers = [ new favsFeatureScorer(), new reblogsFeatureScorer(), new interactsFeatureScorer(), @@ -48,29 +48,29 @@ export default class TheAlgorithm { feedScorer: Array ) { this.fetchers = fetchers; - this.featureScorer = featureScorer; + this.featureScorers = featureScorer; this.feedScorer = feedScorer; return this.getFeed(); } async getFeed(): Promise { - const { fetchers, featureScorer, feedScorer } = this; + const { fetchers, featureScorers, feedScorer } = this; const response = await Promise.all(fetchers.map(fetcher => fetcher(this.api, this.user))) this.feed = response.flat(); // Load and Prepare Features - await Promise.all(featureScorer.map(scorer => scorer.getFeature(this.api))); + await Promise.all(featureScorers.map(scorer => scorer.getFeature(this.api))); await Promise.all(feedScorer.map(scorer => scorer.setFeed(this.feed))); // Get Score Names - const scoreNames = featureScorer.map(scorer => scorer.getVerboseName()); + const scoreNames = featureScorers.map(scorer => scorer.getVerboseName()); const feedScoreNames = feedScorer.map(scorer => scorer.getVerboseName()); // Score Feed let scoredFeed: StatusType[] = [] for (const status of this.feed) { // Load Scores for each status - const featureScore = await Promise.all(featureScorer.map(scorer => scorer.score(this.api, status))); + const featureScore = await Promise.all(featureScorers.map(scorer => scorer.score(this.api, status))); const feedScore = await Promise.all(feedScorer.map(scorer => scorer.score(status))); // Turn Scores into Weight Objects const featureScoreObj = this._getScoreObj(scoreNames, featureScore); @@ -107,7 +107,7 @@ export default class TheAlgorithm { //Remove duplicates scoredFeed = [...new Map(scoredFeed.map((item: StatusType) => [item["uri"], item])).values()]; - this.feed = scoredFeed + this.feed = scoredFeed; return this.feed; } @@ -128,18 +128,18 @@ export default class TheAlgorithm { } getWeightNames(): string[] { - const scorers = [...this.featureScorer, ...this.feedScorer]; + const scorers = [...this.featureScorers, ...this.feedScorer]; return [...scorers.map(scorer => scorer.getVerboseName())] } async setDefaultWeights(): Promise { //Set Default Weights if they don't exist - const scorers = [...this.featureScorer, ...this.feedScorer]; + const scorers = [...this.featureScorers, ...this.feedScorer]; Promise.all(scorers.map(scorer => weightsStore.defaultFallback(scorer.getVerboseName(), scorer.getDefaultWeight()))) } getWeightDescriptions(): string[] { - const scorers = [...this.featureScorer, ...this.feedScorer]; + const scorers = [...this.featureScorers, ...this.feedScorer]; return [...scorers.map(scorer => scorer.getDescription())] } @@ -171,7 +171,7 @@ export default class TheAlgorithm { } getDescription(verboseName: string): string { - const scorers = [...this.featureScorer, ...this.feedScorer]; + const scorers = [...this.featureScorers, ...this.feedScorer]; const scorer = scorers.find(scorer => scorer.getVerboseName() === verboseName); if (scorer) { return scorer.getDescription(); @@ -179,12 +179,23 @@ export default class TheAlgorithm { return ""; } + //Adjust post weights based on user's chosen slider values async weightAdjust(statusWeights: weightsType, step = 0.001): Promise { - //Adjust Weights based on user interaction if (statusWeights == undefined) return; - const mean = Object.values(statusWeights).filter((value: number) => !isNaN(value)).reduce((accumulator, currentValue) => accumulator + Math.abs(currentValue), 0) / Object.values(statusWeights).length; + + // Compute the total and mean score (AKA 'weight') of all the posts we are weighting + const total = Object.values(statusWeights) + .filter((value: number) => !isNaN(value)) + .reduce((accumulator, currentValue) => accumulator + Math.abs(currentValue), 0); + const mean = total / Object.values(statusWeights).length; + + // Compute the sum and mean of the preferred weighting configured by the user with the weight sliders const currentWeight: weightsType = await this.getWeights() - const currentMean = Object.values(currentWeight).filter((value: number) => !isNaN(value)).reduce((accumulator, currentValue) => accumulator + currentValue, 0) / Object.values(currentWeight).length; + const currentTotal = Object.values(currentWeight) + .filter((value: number) => !isNaN(value)) + .reduce((accumulator, currentValue) => accumulator + currentValue, 0); + const currentMean = currentTotal / Object.values(currentWeight).length; + for (const key in currentWeight) { const reweight = 1 - (Math.abs(statusWeights[key]) / mean) / (currentWeight[key] / currentMean); currentWeight[key] = currentWeight[key] - step * currentWeight[key] * reweight; @@ -194,6 +205,6 @@ export default class TheAlgorithm { } list() { - return new Paginator(this.feed) + return new Paginator(this.feed); } -} \ No newline at end of file +} diff --git a/src/scorer/feature/favsFeatureScorer.ts b/src/scorer/feature/favsFeatureScorer.ts index c82fcc26..d6a21f8c 100644 --- a/src/scorer/feature/favsFeatureScorer.ts +++ b/src/scorer/feature/favsFeatureScorer.ts @@ -1,10 +1,9 @@ -import FeatureScorer from '../FeatureScorer' -import { StatusType } from '../../types' -import { mastodon } from 'masto' -import FeatureStorage from '../../features/FeatureStore' +import FeatureScorer from '../FeatureScorer'; +import FeatureStorage from '../../features/FeatureStore'; +import { mastodon } from 'masto'; +import { StatusType } from '../../types'; export default class favsFeatureScorer extends FeatureScorer { - constructor() { super({ featureGetter: (api: mastodon.rest.Client) => FeatureStorage.getTopFavs(api), @@ -15,6 +14,6 @@ export default class favsFeatureScorer extends FeatureScorer { } async score(_api: mastodon.rest.Client, status: StatusType) { - return (status.account.acct in this.feature) ? this.feature[status.account.acct] : 0 + return (status.account.acct in this.feature) ? this.feature[status.account.acct] : 0; } -} +}; diff --git a/src/scorer/feature/interactsFeatureScorer.ts b/src/scorer/feature/interactsFeatureScorer.ts index 0e158346..f94dc508 100644 --- a/src/scorer/feature/interactsFeatureScorer.ts +++ b/src/scorer/feature/interactsFeatureScorer.ts @@ -1,7 +1,11 @@ +/* + * Gives higher weight to posts from users that have often interacted with your posts. + */ import FeatureScorer from "../FeatureScorer"; -import { StatusType } from "../../types"; -import { mastodon } from "masto"; import FeatureStorage from "../../features/FeatureStore"; +import { mastodon } from "masto"; +import { StatusType } from "../../types"; + export default class interactsFeatureScorer extends FeatureScorer { constructor() { @@ -14,6 +18,6 @@ export default class interactsFeatureScorer extends FeatureScorer { } async score(_api: mastodon.rest.Client, status: StatusType) { - return (status.account.acct in this.feature) ? this.feature[status.account.acct] : 0 + return (status.account.acct in this.feature) ? this.feature[status.account.acct] : 0; } -} +}; diff --git a/src/scorer/feature/reblogsFeatureScorer.ts b/src/scorer/feature/reblogsFeatureScorer.ts index d95589c7..2b434be0 100644 --- a/src/scorer/feature/reblogsFeatureScorer.ts +++ b/src/scorer/feature/reblogsFeatureScorer.ts @@ -14,8 +14,8 @@ export default class reblogsFeatureScorer extends FeatureScorer { } async score(_api: mastodon.rest.Client, status: StatusType) { - const authorScore = (status.account.acct in this.feature) ? this.feature[status.account.acct] : 0 - const reblogScore = (status.reblog && status.reblog.account.acct in this.feature) ? this.feature[status.reblog.account.acct] : 0 - return authorScore + reblogScore + const authorScore = (status.account.acct in this.feature) ? this.feature[status.account.acct] : 0; + const reblogScore = (status.reblog && status.reblog.account.acct in this.feature) ? this.feature[status.reblog.account.acct] : 0; + return authorScore + reblogScore; } -} +}; diff --git a/src/scorer/feature/topPostFeatureScorer.ts b/src/scorer/feature/topPostFeatureScorer.ts index 9140d37b..577ae83e 100644 --- a/src/scorer/feature/topPostFeatureScorer.ts +++ b/src/scorer/feature/topPostFeatureScorer.ts @@ -1,6 +1,6 @@ import FeatureScorer from '../FeatureScorer'; -import { StatusType, } from "../../types"; import { mastodon } from "masto"; +import { StatusType, } from "../../types"; export default class topPostFeatureScorer extends FeatureScorer { constructor() { @@ -13,6 +13,6 @@ export default class topPostFeatureScorer extends FeatureScorer { } async score(_api: mastodon.rest.Client, status: StatusType) { - return status.topPost ? 1 : 0 + return status.topPost ? 1 : 0; } -} +}; diff --git a/src/scorer/feed/diversityFeedScorer.ts b/src/scorer/feed/diversityFeedScorer.ts index e82cdf3e..d10e1066 100644 --- a/src/scorer/feed/diversityFeedScorer.ts +++ b/src/scorer/feed/diversityFeedScorer.ts @@ -17,8 +17,8 @@ export default class diversityFeedScorer extends FeedScorer { async score(status: StatusType) { super.score(status); - const frequ = this.features[status.account.acct] - this.features[status.account.acct] = frequ + 1 - return frequ + 1 + const frequ = this.features[status.account.acct]; + this.features[status.account.acct] = frequ + 1; + return frequ + 1; } } diff --git a/src/weights/weightsStore.ts b/src/weights/weightsStore.ts index 39e82d9d..aabc26d0 100644 --- a/src/weights/weightsStore.ts +++ b/src/weights/weightsStore.ts @@ -1,5 +1,9 @@ -import { weightsType } from "../types"; +/* + * Stores the user's preferred weight for each post scorer. + */ import Storage, { Key } from "../Storage"; +import { weightsType } from "../types"; + export default class weightsStore extends Storage { static async getWeight(verboseName: string) { @@ -18,7 +22,7 @@ export default class weightsStore extends Storage { const weights: weightsType = {} for (const verboseName of verboseNames) { const weight = await this.getWeight(verboseName); - weights[verboseName] = weight[verboseName] + weights[verboseName] = weight[verboseName]; } return weights; } @@ -39,4 +43,4 @@ export default class weightsStore extends Storage { return false; } -} \ No newline at end of file +} From 202fc62327d49d0db21205fb6a8195d9127bc7c2 Mon Sep 17 00:00:00 2001 From: ashariyar Date: Thu, 21 Nov 2024 01:01:09 -0500 Subject: [PATCH 9/9] revert README changes --- README.md | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2f12dd1e..f8cb20d3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # fedialgo -This is untested early alpha so might be due to massive unannounced changes. [![Fedialgo Build and Test](https://github.com/pkreissel/fedialgo/actions/workflows/CI.yaml/badge.svg)](https://github.com/pkreissel/fedialgo/actions/workflows/CI.yaml) -Fedialgo is an typescript module, that can be used to produce an algorithmic fediverse experience. This will replace the project "fedifeed" and make it possible to implement the idea into all kinds of other projects. It uses React Native Async Storage, so it should also work in React Native Projects, but havent tested it yet. +Fedialgo is an typescript module, that can be used to produce an algorithmic fediverse experience. This will replace the project "fedifeed" and make it possible to implement the idea into all kinds of other projects. It uses React Native Async Storage, so it should also work in React Native Projects, but havent tested it yet. ## Install directly from github: @@ -26,14 +25,11 @@ Use // @ts-ignore if you run into Typescript warnings (because your project migh ```console npm run build ``` -in `fedialgo` directory after changes and they will automatically be detected - -### Demo App -`fedialgo` is just a node package. You don't use it on its own in the form in this repo; it has to be imported into some other app. Thankfully there's a simple demo app that spins up a webserver, pulls your feed and scores it with the algorithm in this repo, and then presents it to your browser at `http://localhost:3000/` over in [the `foryoufeed` repo](https://github.com/pkreissel/foryoufeed). +in fedialgo directory after changes and they will automatically be detected -## Package Usage +## Usage -### Weight An Account's Feed: +### Basic Feed: ```typescript import TheAlgorithm from "fedialgo" import { login, mastodon } from "masto"; @@ -45,12 +41,13 @@ const api: mastodon.Client = await login({ const currUser = await api.v1.accounts.verifyCredentials() const algo = new TheAlgorithm(api, currUser) const feed = await algo.getFeed() -``` + + ``` -### Adjust Weights: + ### Adjust Weights: The algorithm uses features and their weights to determine the order of the posts. -You could e.g. show the weights to the user, who can then decide to change them. + You could e.g. show the weights to the user, who can then decide to change them. ```typescript let weights = await algo.getWeights() weights["fav"] = 0.5 // change the weight of the feature "fav" to 0.5 @@ -59,16 +56,14 @@ const newFeed = await algoObj.setWeights(newWeights) ``` ### Learn Weights -You can also let the algorithm learn the weights from the user's behaviour. This is done by passing the scores of the posts to the algorithm. The algorithm will then adjust the weights accordingly. This is quite simple, but still has impact on the feed. For example you could choose to adjust the weight after each click on a post, after a reblog, or after a link click. +You can also let the algorithm learn the weights from the user's behaviour. This is done by passing the scores of the posts to the algorithm. The algorithm will then adjust the weights accordingly. This is quite simple, but still has impact on the feed. For example you could choose to adjust the weight after each click on a post, after a reblog, or after a link click. ```typescript const scores = status.scores const newWeights = await algoObj.weightAdjust(scores) ``` -# Contributing -### Developer Setup -If necessary install the dev dependencies with `npm install --include=dev`. -#### Running Test Suite -`npm run test` +This is untested early alpha so might be due to massive unannounced changes. + +