Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: added JS Docs #20

Merged
merged 1 commit into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions lib/ActivityPub.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,62 @@ const logger = debug('ActivityPub');

/**
* ActivityPubClient - a class for sending and fetching ActivityPub content
* @class
*/
export class ActivityPubClient {
/**
* Constructor for ActivityPubClient
* @constructor
* @param {Object} account - The user account.
*/
constructor(account) {
logger('Initializing ActivityPub client for user:', account);
if (account) {
this.account = account;
}
}

/**
* Setter for actor property
* @param {Object} actor - The actor object.
*/
set actor(actor) {
this._actor = actor;
}

/**
* Getter for actor property
* @returns {Object} The actor object.
*/
get actor() {
return this._actor;
}

/**
* Setter for account property
* @param {Object} account - The user account.
*/
set account(account) {
logger('Setting account:', account);
this._account = account;
this._actor = account?.actor;
}

/**
* Getter for account property
* @returns {Object} The user account.
*/
get account() {
return this._account;
}

/**
* Fetches the Webfinger data for a given username
* @async
* @param {string} username - The username to fetch Webfinger data for.
* @returns {Promise<Object>} The Webfinger data.
* @throws {Error} If Webfinger fetch fails.
*/
async webfinger(username) {
const { targetDomain } = this.getUsernameDomain(username);

Expand All @@ -52,6 +81,13 @@ export class ActivityPubClient {
}
}

/**
* Fetches the actor data for a given user ID
* @async
* @param {string} userId - The user ID to fetch actor data for.
* @returns {Promise<Object>} The actor data.
* @throws {Error} If actor fetch fails.
*/
async fetchActor(userId) {
const actorQuery = await ActivityPub.fetch(userId, {});
if (actorQuery.ok) {
Expand All @@ -76,21 +112,22 @@ export class ActivityPubClient {
const urlFragment = url.pathname + (url.searchParams.toString() ? `?${url.searchParams.toString()}` : '');

const signer = crypto.createSign('sha256');
const d = new Date();
const stringToSign = `(request-target): get ${urlFragment}\nhost: ${url.hostname}\ndate: ${d.toUTCString()}`;
const date = new Date();
const stringToSign = `(request-target): get ${urlFragment}\nhost: ${url.hostname}\ndate: ${date.toUTCString()}`;
signer.update(stringToSign);
signer.end();
const signature = signer.sign(this.account.privateKey);
const signatureB64 = signature.toString('base64');
const header = `keyId="${this.actor.publicKey.id}",headers="(request-target) host date",signature="${signatureB64}"`;
options.headers = {
Date: d.toUTCString(),
Date: date.toUTCString(),
Host: url.hostname,
Accept: 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
Signature: header
};

const controller = new AbortController();
// set timeout for 5s
setTimeout(() => controller.abort(), 5000);
options.signal = controller.signal;

Expand Down Expand Up @@ -118,10 +155,10 @@ export class ActivityPubClient {

const digestHash = crypto.createHash('sha256').update(JSON.stringify(message)).digest('base64');
const signer = crypto.createSign('sha256');
const d = new Date();
const date = new Date();
const stringToSign = `(request-target): post ${inboxFragment}\nhost: ${
url.hostname
}\ndate: ${d.toUTCString()}\ndigest: SHA-256=${digestHash}`;
}\ndate: ${date.toUTCString()}\ndigest: SHA-256=${digestHash}`;
signer.update(stringToSign);
signer.end();
const signature = signer.sign(this.account.privateKey);
Expand All @@ -139,7 +176,7 @@ export class ActivityPubClient {
headers: {
Host: url.hostname,
'Content-type': 'application/activity+json',
Date: d.toUTCString(),
Date: date.toUTCString(),
Digest: `SHA-256=${digestHash}`,
Signature: header
},
Expand Down
111 changes: 106 additions & 5 deletions lib/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import md5 from 'md5';
import { DEFAULT_SETTINGS } from './prefs.js';

import debug from 'debug';

import dotenv from 'dotenv';

const logger = debug('ono:storage');
dotenv.config();

Expand All @@ -29,34 +29,71 @@ const { DOMAIN } = process.env;

export const INDEX = [];
export const CACHE = {};

const cacheMax = 60 * 5 * 1000; // 5 minutes
const cacheMin = 30 * 1000; // 5 minutes
const cacheMin = 30 * 1000; // 30 seconds

/**
* Function to zero-pad a number.
* @param {number} num - The number to zero-pad.
* @returns {string} - The zero-padded number as a string.
*/
const zeroPad = num => {
if (num < 10) {
return `0${num}`;
} else return num;
};

/**
* Checks if an activity belongs to the current user.
* @param {string} activityId - The ID of the activity.
* @returns {boolean} - True if the activity belongs to the current user, false otherwise.
*/
export const isMyPost = activityId => {
return activityId.startsWith(`https://${DOMAIN}/m/`);
};

/**
* Checks if an activity is in the INDEX array.
* @param {string} id - The ID of the activity.
* @returns {boolean} - True if the activity is indexed, false otherwise.
*/
export const isIndexed = id => {
return INDEX.some(p => id === p.id);
};

/**
* Retrieves activity info from the index based on the activity ID.
* @param {string} id - The ID of the activity.
* @returns {Object|boolean} - The activity information if found, otherwise false.
*/
export const fromIndex = id => {
return INDEX.find(p => id === p.id) || false;
};

/**
* Gets user preferences.
* @returns {Object} - User preferences.
*/
export const getPrefs = () => {
return readJSONDictionary(prefsFile, DEFAULT_SETTINGS);
};

/**
* Updates user preferences.
* @param {Object} prefs - The new user preferences.
* @returns {void}
*/
export const updatePrefs = prefs => {
return writeJSONDictionary(prefsFile, prefs);
};

/**
* Adds a failure entry to the index.
* @param {Object} note - The note object.
* @param {string} type - The type of failure (default is 'fail').
* @returns {void}
*/
export const addFailureToIndex = (note, type = 'fail') => {
INDEX.push({
type,
Expand All @@ -65,6 +102,13 @@ export const addFailureToIndex = (note, type = 'fail') => {
status: note.status
});
};

/**
* Adds an activity entry to the index.
* @param {Object} note - The note object.
* @param {string} type - The type of activity (default is 'activity').
* @returns {void}
*/
export const addActivityToIndex = (note, type = 'activity') => {
INDEX.push({
type,
Expand All @@ -74,15 +118,25 @@ export const addActivityToIndex = (note, type = 'activity') => {
inReplyTo: note.inReplyTo
});
};

/**
* Deletes an activity entry from the index.
* @param {string} id - The ID of the activity to be deleted.
* @returns {void}
*/
export const deleteActivityFromIndex = id => {
const n = INDEX.findIndex(idx => idx.id === id);
if (n >= 0) {
INDEX.splice(n, 1);
}
};

/**
* Gets the file name for a given activity ID.
* @param {string} activityId - The ID of the activity.
* @returns {string} - The file name.
*/
export const getFileName = activityId => {
// // find the item in the index
// first check cache!
let meta;
if (CACHE[activityId]) {
Expand All @@ -103,8 +157,12 @@ export const getFileName = activityId => {
return path.resolve(rootPath, folder, `${md5(meta.id)}.json`);
};

/**
* Gets the file name for the likes associated with a given activity ID.
* @param {string} activityId - The ID of the activity.
* @returns {string} - The file name for likes.
*/
export const getLikesFileName = activityId => {
// // find the item in the index
// first check cache!
let meta;
if (CACHE[activityId]) {
Expand All @@ -125,6 +183,11 @@ export const getLikesFileName = activityId => {
return path.resolve(rootPath, folder, `${md5(meta.id)}.likes.json`);
};

/**
* Creates a file name for a given activity.
* @param {Object} activity - The activity object.
* @returns {string} - The file name.
*/
export const createFileName = activity => {
// create a dated subfolder
const datestamp = new Date(activity.published);
Expand All @@ -140,6 +203,10 @@ export const createFileName = activity => {
return path.resolve(rootPath, folder, `${md5(activity.id)}.json`);
};

/**
* Clears expired entries from the cache.
* @returns {void}
*/
const cacheExpire = () => {
const now = new Date().getTime();
for (const key in CACHE) {
Expand All @@ -150,20 +217,28 @@ const cacheExpire = () => {
}
};

/**
* Interval function for the garbage collector to clear expired cache entries.
* @type {number}
*/
const garbageCollector = setInterval(() => {
cacheExpire();
}, cacheMin);

logger('Garbage collector interval', garbageCollector);

/**
* Builds the initial index by reading data from files.
* @returns {Promise<Array<Object>>} - A promise that resolves with the built index.
*/
const buildIndex = () => {
return new Promise((resolve, reject) => {
glob(path.join(pathToFiles, '**/*.json'), async (err, files) => {
if (err) {
console.error(err);
reject(err);
}
// const res = [];

for (const f of files) {
try {
const post = JSON.parse(fs.readFileSync(path.resolve(pathToFiles, f)));
Expand Down Expand Up @@ -198,6 +273,11 @@ const buildIndex = () => {
});
};

/**
* Searches for known users based on a query string.
* @param {string} query - The search query.
* @returns {Promise<Array<Object>>} - A promise that resolves with the search results.
*/
export const searchKnownUsers = async query => {
return new Promise((resolve, reject) => {
glob(path.join(pathToUsers, '**/*.json'), async (err, files) => {
Expand Down Expand Up @@ -227,6 +307,10 @@ export const searchKnownUsers = async query => {
});
};

/**
* Ensures the existence of data folders and default settings.
* @returns {void}
*/
const ensureDataFolder = () => {
if (!fs.existsSync(path.resolve(pathToPosts))) {
logger('mkdir', pathToPosts);
Expand Down Expand Up @@ -260,6 +344,12 @@ const ensureDataFolder = () => {
}
};

/**
* Reads a JSON dictionary from a file path.
* @param {string} path - The path to the JSON file.
* @param {Array} defaultVal - The default value if the file doesn't exist.
* @returns {Array} - The contents of the JSON file.
*/
export const readJSONDictionary = (path, defaultVal = []) => {
const now = new Date().getTime();
if (CACHE[path] && CACHE[path].time > now - cacheMax) {
Expand All @@ -282,11 +372,22 @@ export const readJSONDictionary = (path, defaultVal = []) => {
}
};

/**
* Deletes a JSON dictionary file.
* @param {string} path - The path to the JSON file to be deleted.
* @returns {void}
*/
export const deleteJSONDictionary = path => {
fs.unlinkSync(path);
delete CACHE[path];
};

/**
* Writes a JSON dictionary to a file.
* @param {string} path - The path to the file.
* @param {Object} data - The data to be written.
* @returns {void}
*/
export const writeJSONDictionary = (path, data) => {
const now = new Date().getTime();
logger('write cache', path);
Expand Down
Loading