Merge pull request #1 from TheOneric/pleroma-emoji-reactions

Support Pleroma’s emoji reactions (+small Masto fix)
This commit is contained in:
Natty 2023-07-22 17:09:56 +02:00 committed by GitHub
commit 2cf6058d06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 81 additions and 6 deletions

View file

@ -42,6 +42,7 @@ async function apiRequest(url, options = null)
* replies: number, * replies: number,
* renotes: number, * renotes: number,
* favorites: number, * favorites: number,
* extra_reacts: boolean,
* instance: string, * instance: string,
* author?: FediUser, * author?: FediUser,
* }} Note * }} Note
@ -109,6 +110,14 @@ class ApiClient {
return client; return client;
} }
let features = apiResponse?.metadata?.features;
if (Array.isArray(features) && features.includes("pleroma_api")) {
const has_emoji_reacts = features.includes("pleroma_emoji_reactions");
const client = new PleromaApiClient(instance, has_emoji_reacts);
instanceTypeCache.set(instance, client);
return client;
}
const client = new MastodonApiClient(instance); const client = new MastodonApiClient(instance);
instanceTypeCache.set(instance, client); instanceTypeCache.set(instance, client);
return client; return client;
@ -144,10 +153,11 @@ class ApiClient {
/** /**
* @param {Note} note * @param {Note} note
* @param {boolean} extra_reacts
* *
* return {Promise<FediUser[] | null>} * return {Promise<FediUser[] | null>}
*/ */
async getFavs(note) { throw new Error("Not implemented"); } async getFavs(note, extra_reacts) { throw new Error("Not implemented"); }
/** /**
* @return string * @return string
@ -193,6 +203,8 @@ class MastodonApiClient extends ApiClient {
replies: note["replies_count"] || 0, replies: note["replies_count"] || 0,
renotes: note["reblogs_count"] || 0, renotes: note["reblogs_count"] || 0,
favorites: note["favourites_count"], favorites: note["favourites_count"],
// Actually a Pleroma/Akkoma thing
extra_reacts: note?.["pleroma"]?.["emoji_reactions"]?.length > 0,
instance: this._instance, instance: this._instance,
author: user author: user
})); }));
@ -223,7 +235,7 @@ class MastodonApiClient extends ApiClient {
return null; return null;
} }
return response["ancestors"].map(note => { return response["descendants"].map(note => {
let handle = parseHandle(note["account"]["acct"], noteIn.instance); let handle = parseHandle(note["account"]["acct"], noteIn.instance);
return { return {
@ -231,6 +243,8 @@ class MastodonApiClient extends ApiClient {
replies: note["replies_count"] || 0, replies: note["replies_count"] || 0,
renotes: note["reblogs_count"] || 0, renotes: note["reblogs_count"] || 0,
favorites: note["favourites_count"], favorites: note["favourites_count"],
// Actually a Pleroma/Akkoma thing
extra_reacts: note?.["pleroma"]?.["emoji_reactions"]?.length > 0,
instance: handle.instance, instance: handle.instance,
author: { author: {
id: note["account"]["id"], id: note["account"]["id"],
@ -243,7 +257,7 @@ class MastodonApiClient extends ApiClient {
}); });
} }
async getFavs(note) { async getFavs(note, extra_reacts) {
const url = `https://${this._instance}/api/v1/statuses/${note.id}/favourited_by`; const url = `https://${this._instance}/api/v1/statuses/${note.id}/favourited_by`;
const response = await apiRequest(url); const response = await apiRequest(url);
@ -265,6 +279,61 @@ class MastodonApiClient extends ApiClient {
} }
} }
class PleromaApiClient extends MastodonApiClient {
/**
* @param {string} instance
* @param {boolean} emoji_reacts
*/
constructor(instance, emoji_reacts) {
super(instance);
this._emoji_reacts = emoji_reacts;
}
async getFavs(note, extra_reacts) {
// Pleroma/Akkoma supports both favs and emoji reacts
// with several emoji reacts per users being possible.
// Coalesce them and count every user only once
let favs = await super.getFavs(note);
if (!this._emoji_reacts || !extra_reacts)
return favs;
/**
* @type {Map<string, FediUser>}
*/
let users = new Map();
if (favs !== null) {
favs.forEach(u => {
users.set(u.id, u);
});
}
const url = `https://${this._instance}/api/v1/pleroma/statuses/${note.id}/reactions`;
const response = await apiRequest(url) ?? [];
for (const reaction of response) {
reaction["accounts"]
.map(account => ({
id: account["id"],
avatar: account["avatar"],
bot: account["bot"],
name: account["display_name"],
handle: parseHandle(account["acct"], note.instance)
}))
.forEach(u => {
if(!users.has(u.id))
users.set(u.id, u);
})
}
return Array.from(users.values());
}
getClientName() {
return "pleroma";
}
}
class MisskeyApiClient extends ApiClient { class MisskeyApiClient extends ApiClient {
/** /**
* @param {string} instance * @param {string} instance
@ -316,6 +385,7 @@ class MisskeyApiClient extends ApiClient {
replies: note["repliesCount"], replies: note["repliesCount"],
renotes: note["renoteCount"], renotes: note["renoteCount"],
favorites: Object.values(note["reactions"]).reduce((a, b) => a + b, 0), favorites: Object.values(note["reactions"]).reduce((a, b) => a + b, 0),
extra_reacts: false,
instance: this._instance, instance: this._instance,
author: user author: user
})); }));
@ -366,6 +436,7 @@ class MisskeyApiClient extends ApiClient {
replies: reply["repliesCount"], replies: reply["repliesCount"],
renotes: reply["renoteCount"], renotes: reply["renoteCount"],
favorites: Object.values(reply["reactions"]).reduce((a, b) => a + b, 0), favorites: Object.values(reply["reactions"]).reduce((a, b) => a + b, 0),
extra_reacts: false,
instance: handle.instance, instance: handle.instance,
author: { author: {
id: reply["user"]["id"], id: reply["user"]["id"],
@ -378,7 +449,7 @@ class MisskeyApiClient extends ApiClient {
}); });
} }
async getFavs(note) { async getFavs(note, extra_reacts) {
const url = `https://${this._instance}/api/notes/reactions`; const url = `https://${this._instance}/api/notes/reactions`;
const response = await apiRequest(url, { const response = await apiRequest(url, {
method: "POST", method: "POST",
@ -455,6 +526,9 @@ async function circleMain() {
case "mastodon": case "mastodon":
client = new MastodonApiClient(selfUser.instance); client = new MastodonApiClient(selfUser.instance);
break; break;
case "pleroma":
client = new PleromaApiClient(selfUser.instance, true);
break;
case "misskey": case "misskey":
client = new MisskeyApiClient(selfUser.instance); client = new MisskeyApiClient(selfUser.instance);
break; break;
@ -524,8 +598,8 @@ async function processNotes(client, connectionList, notes) {
* @param {Note} note * @param {Note} note
*/ */
async function evaluateNote(client, connectionList, note) { async function evaluateNote(client, connectionList, note) {
if (note.favorites > 0) { if (note.favorites > 0 || note.extra_reacts) {
await client.getFavs(note).then(users => { await client.getFavs(note, note.extra_reacts).then(users => {
if (!users) if (!users)
return; return;

View file

@ -21,6 +21,7 @@
<label><input type="radio" name="backend" value="detect" autocomplete="off" checked> Autodetect</label> <label><input type="radio" name="backend" value="detect" autocomplete="off" checked> Autodetect</label>
<label><input type="radio" name="backend" value="mastodon" autocomplete="off"> Mastodon API</label> <label><input type="radio" name="backend" value="mastodon" autocomplete="off"> Mastodon API</label>
<label><input type="radio" name="backend" value="misskey" autocomplete="off"> Misskey API</label> <label><input type="radio" name="backend" value="misskey" autocomplete="off"> Misskey API</label>
<label><input type="radio" name="backend" value="pleroma" autocomplete="off"> Pleroma API</label>
</span> </span>
<br> <br>
<button type="submit" id="generateButton">Generate circle</button> <button type="submit" id="generateButton">Generate circle</button>