Support proposed Glitch emoji reactions API

Fixes #10537
This commit is contained in:
Vyr Cossont 2023-07-24 14:29:41 -07:00
parent f5a88e7080
commit 5fedfd2599
13 changed files with 155 additions and 28 deletions

View File

@ -32,6 +32,8 @@ export function convertNotification(notification: Entity.Notification) {
notification.id = convertId(notification.id, IdType.MastodonId);
if (notification.status)
notification.status = convertStatus(notification.status);
if (notification.reaction)
notification.reaction = convertReaction(notification.reaction);
return notification;
}
@ -68,7 +70,7 @@ export function convertStatus(status: Entity.Status) {
if (status.poll) status.poll = convertPoll(status.poll);
if (status.reblog) status.reblog = convertStatus(status.reblog);
if (status.quote) status.quote = convertStatus(status.quote);
status.emoji_reactions = status.mentions.map(convertReaction);
status.reactions = status.reactions.map(convertReaction);
return status;
}

View File

@ -48,7 +48,7 @@ export function apiStatusMastodon(router: Router): void {
try {
const id = body.in_reply_to_id;
const post = await client.getStatus(id);
const react = post.data.emoji_reactions.filter((e) => e.me)[0].name;
const react = post.data.reactions.filter((e) => e.me)[0].name;
const data = await client.deleteEmojiReaction(id, react);
ctx.body = data.data;
} catch (e: any) {
@ -367,6 +367,47 @@ export function apiStatusMastodon(router: Router): void {
}
},
);
router.post<{ Params: { id: string; name: string } }>(
"/v1/statuses/:id/react/:name",
async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.reactStatus(
convertId(ctx.params.id, IdType.FirefishId),
ctx.params.name,
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string; name: string } }>(
"/v1/statuses/:id/unreact/:name",
async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unreactStatus(
convertId(ctx.params.id, IdType.FirefishId),
ctx.params.name,
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.get<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;

View File

@ -7,7 +7,7 @@ namespace Entity {
created_at: string;
id: string;
status?: Status;
emoji?: string;
reaction?: Reaction;
type: NotificationType;
};

View File

@ -6,6 +6,7 @@ namespace Entity {
me: boolean;
name: string;
url?: string;
static_url?: string;
accounts?: Array<Account>;
};
}

View File

@ -38,7 +38,7 @@ namespace Entity {
application: Application | null;
language: string | null;
pinned: boolean | null;
emoji_reactions: Array<Reaction>;
reactions: Array<Reaction>;
quote: Status | null;
bookmarked: boolean;
};

View File

@ -857,6 +857,21 @@ export interface MegalodonInterface {
* @return Status
*/
unpinStatus(id: string): Promise<Response<Entity.Status>>;
/**
* POST /api/v1/statuses/:id/react/:name
* @param id The target status id.
* @param name The name of the emoji reaction to add.
* @return Status
*/
reactStatus(id: string, name: string): Promise<Response<Entity.Status>>;
/**
* POST /api/v1/statuses/:id/unreact/:name
*
* @param id The target status id.
* @param name The name of the emoji reaction to remove.
* @return Status
*/
unreactStatus(id: string, name: string): Promise<Response<Entity.Status>>;
// ======================================
// statuses/media
// ======================================

View File

@ -2009,6 +2009,63 @@ export default class Misskey implements MegalodonInterface {
}));
}
/**
* Convert a Unicode emoji or custom emoji name to a Misskey reaction.
* @see Misskey's reaction-lib.ts
*/
private reactionName(name: string): string {
// See: https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji
const isUnicodeEmoji = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu.test(name);
if (isUnicodeEmoji) {
return name;
}
return `:${name}:`;
}
/**
* POST /api/notes/reactions/create
*/
public async reactStatus(id: string, name: string): Promise<Response<Entity.Status>> {
await this.client.post<{}>("/api/notes/reactions/create", {
noteId: id,
reaction: this.reactionName(name),
});
return this.client
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
noteId: id,
})
.then(async (res) => ({
...res,
data: await this.noteWithDetails(
res.data,
this.baseUrlToHost(this.baseUrl),
this.getFreshAccountCache(),
),
}));
}
/**
* POST /api/notes/reactions/delete
*/
public async unreactStatus(id: string, name: string): Promise<Response<Entity.Status>> {
await this.client.post<{}>("/api/notes/reactions/delete", {
noteId: id,
reaction: this.reactionName(name),
});
return this.client
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
noteId: id,
})
.then(async (res) => ({
...res,
data: await this.noteWithDetails(
res.data,
this.baseUrlToHost(this.baseUrl),
this.getFreshAccountCache(),
),
}));
}
// ======================================
// statuses/media
// ======================================

View File

@ -321,7 +321,10 @@ namespace MisskeyAPI {
content: n.text ? this.escapeMFM(n.text) : "",
plain_content: n.text ? n.text : null,
created_at: n.createdAt,
emojis: n.emojis.map((e) => this.emoji(e)),
// Remove reaction emojis with names containing @ from the emojis list.
emojis: n.emojis
.filter((e) => e.name.indexOf("@") === -1)
.map((e) => this.emoji(e)),
replies_count: n.repliesCount,
reblogs_count: n.renoteCount,
favourites_count: this.getTotalReactions(n.reactions),
@ -339,28 +342,36 @@ namespace MisskeyAPI {
application: null,
language: null,
pinned: null,
emoji_reactions: this.mapReactions(n.reactions, n.myReaction),
// Use emojis list to provide URLs for emoji reactions.
reactions: this.mapReactions(n.emojis, n.reactions, n.myReaction),
bookmarked: false,
quote: n.renote && n.text ? this.note(n.renote, host) : null,
};
};
mapReactions = (
emojis: Array<MisskeyEntity.Emoji>,
r: { [key: string]: number },
myReaction?: string,
): Array<MegalodonEntity.Reaction> => {
// Map of emoji shortcodes to image URLs.
const emojiUrls = new Map<string, string>(
emojis.map((e) => [e.name, e.url]),
);
return Object.keys(r).map((key) => {
if (myReaction && key === myReaction) {
return {
count: r[key],
me: true,
name: key,
};
}
// Strip colons from custom emoji reaction names to match emoji shortcodes.
const shortcode = key.replaceAll(":", "");
// If this is a custom emoji (vs. a Unicode emoji), find its image URL.
const url = emojiUrls.get(shortcode);
// Finally, remove trailing @. from local custom emoji reaction names.
const name = shortcode.replace("@.", "");
return {
count: r[key],
me: false,
name: key,
me: key === myReaction,
name,
url,
// We don't actually have a static version of the asset, but clients expect one anyway.
static_url: url,
};
});
};
@ -422,7 +433,7 @@ namespace MisskeyAPI {
case NotificationType.Mention:
return MisskeyNotificationType.Reply;
case NotificationType.Favourite:
case NotificationType.EmojiReaction:
case NotificationType.Reaction:
return MisskeyNotificationType.Reaction;
case NotificationType.Reblog:
return MisskeyNotificationType.Renote;
@ -448,7 +459,7 @@ namespace MisskeyAPI {
case MisskeyNotificationType.Quote:
return NotificationType.Reblog;
case MisskeyNotificationType.Reaction:
return NotificationType.EmojiReaction;
return NotificationType.Reaction;
case MisskeyNotificationType.PollEnded:
return NotificationType.Poll;
case MisskeyNotificationType.ReceiveFollowRequest:
@ -496,11 +507,11 @@ namespace MisskeyAPI {
account: this.note(n.note, host).account,
});
}
}
if (n.reaction) {
notification = Object.assign(notification, {
emoji: n.reaction,
});
if (n.reaction) {
notification = Object.assign(notification, {
reaction: this.mapReactions(n.note.emojis, { [n.reaction]: 1 })[0],
});
}
}
return notification;
};

View File

@ -5,7 +5,7 @@ namespace NotificationType {
export const Favourite: Entity.NotificationType = "favourite";
export const Reblog: Entity.NotificationType = "reblog";
export const Mention: Entity.NotificationType = "mention";
export const EmojiReaction: Entity.NotificationType = "emoji_reaction";
export const Reaction: Entity.NotificationType = "reaction";
export const FollowRequest: Entity.NotificationType = "follow_request";
export const Status: Entity.NotificationType = "status";
export const Poll: Entity.NotificationType = "poll";

View File

@ -163,7 +163,7 @@ describe('getNotifications', () => {
},
{
event: reaction,
expected: MegalodonNotificationType.EmojiReaction,
expected: MegalodonNotificationType.Reaction,
title: 'reaction'
},
{

View File

@ -34,7 +34,7 @@ describe('api_client', () => {
dist: MisskeyNotificationType.Reaction
},
{
src: MegalodonNotificationType.EmojiReaction,
src: MegalodonNotificationType.Reaction,
dist: MisskeyNotificationType.Reaction
},
{
@ -80,7 +80,7 @@ describe('api_client', () => {
},
{
src: MisskeyNotificationType.Reaction,
dist: MegalodonNotificationType.EmojiReaction
dist: MegalodonNotificationType.Reaction
},
{
src: MisskeyNotificationType.PollEnded,

View File

@ -54,7 +54,7 @@ const status: Entity.Status = {
} as Entity.Application,
language: null,
pinned: null,
emoji_reactions: [],
reactions: [],
bookmarked: false,
quote: null
}

View File

@ -3,7 +3,7 @@
/* Basic Options */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["es6", "dom"], /* Specify library files to be included in the compilation. */
"lib": ["es2021", "dom"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */