diff --git a/src/features/FeatureStore.ts b/src/features/FeatureStore.ts index da866468..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 { @@ -45,9 +48,11 @@ 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); + if (coreServer != null && await this.getOpenings() % 10 != 9) { return coreServer; } else { @@ -57,5 +62,4 @@ export default class FeatureStorage extends Storage { return server; } } - -} +}; diff --git a/src/feeds/topPostsFeed.ts b/src/feeds/topPostsFeed.ts index dd56013e..656a52c8 100644 --- a/src/feeds/topPostsFeed.ts +++ b/src/feeds/topPostsFeed.ts @@ -1,17 +1,16 @@ -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 { 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] + return core_servers[b] - core_servers[a]; }).slice(0, 10) if (servers.length === 0) { @@ -27,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; }) -} \ No newline at end of file +} 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 5eb5ef11..d6a21f8c 100644 --- a/src/scorer/feature/favsFeatureScorer.ts +++ b/src/scorer/feature/favsFeatureScorer.ts @@ -1,20 +1,19 @@ -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), 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, }) } 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; } -} \ No newline at end of file +}; diff --git a/src/scorer/feature/interactsFeatureScorer.ts b/src/scorer/feature/interactsFeatureScorer.ts index 23809463..f94dc508 100644 --- a/src/scorer/feature/interactsFeatureScorer.ts +++ b/src/scorer/feature/interactsFeatureScorer.ts @@ -1,19 +1,23 @@ +/* + * 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() { 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, }) } 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; } -} \ No newline at end of file +}; diff --git a/src/scorer/feature/reblogsFeatureScorer.ts b/src/scorer/feature/reblogsFeatureScorer.ts index 500a1c34..2b434be0 100644 --- a/src/scorer/feature/reblogsFeatureScorer.ts +++ b/src/scorer/feature/reblogsFeatureScorer.ts @@ -8,14 +8,14 @@ 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: "Favor posts from accounts you have retooted a lot", defaultWeight: 3, }) } 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; } -} \ No newline at end of file +}; diff --git a/src/scorer/feature/topPostFeatureScorer.ts b/src/scorer/feature/topPostFeatureScorer.ts index 15bb9870..577ae83e 100644 --- a/src/scorer/feature/topPostFeatureScorer.ts +++ b/src/scorer/feature/topPostFeatureScorer.ts @@ -1,18 +1,18 @@ import FeatureScorer from '../FeatureScorer'; -import { StatusType, } from "../../types"; import { mastodon } from "masto"; +import { StatusType, } from "../../types"; export default class topPostFeatureScorer extends FeatureScorer { constructor() { 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 in the Fediverse", defaultWeight: 1, }) } async score(_api: mastodon.rest.Client, status: StatusType) { - return status.topPost ? 1 : 0 + return status.topPost ? 1 : 0; } -} \ No newline at end of file +}; diff --git a/src/scorer/feed/diversityFeedScorer.ts b/src/scorer/feed/diversityFeedScorer.ts index 363e180a..d10e1066 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 { @@ -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; } -} \ 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 +} 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 +}