Merge branch 'mastodon-api-emoji-reactions' into 'develop'
Support proposed Glitch emoji reactions API Closes #10537 See merge request firefish/firefish!10532
This commit is contained in:
commit
8ff1c9b722
13 changed files with 155 additions and 28 deletions
|
@ -32,6 +32,8 @@ export function convertNotification(notification: Entity.Notification) {
|
||||||
notification.id = convertId(notification.id, IdType.MastodonId);
|
notification.id = convertId(notification.id, IdType.MastodonId);
|
||||||
if (notification.status)
|
if (notification.status)
|
||||||
notification.status = convertStatus(notification.status);
|
notification.status = convertStatus(notification.status);
|
||||||
|
if (notification.reaction)
|
||||||
|
notification.reaction = convertReaction(notification.reaction);
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@ export function convertStatus(status: Entity.Status) {
|
||||||
if (status.poll) status.poll = convertPoll(status.poll);
|
if (status.poll) status.poll = convertPoll(status.poll);
|
||||||
if (status.reblog) status.reblog = convertStatus(status.reblog);
|
if (status.reblog) status.reblog = convertStatus(status.reblog);
|
||||||
if (status.quote) status.quote = convertStatus(status.quote);
|
if (status.quote) status.quote = convertStatus(status.quote);
|
||||||
status.emoji_reactions = status.mentions.map(convertReaction);
|
status.reactions = status.reactions.map(convertReaction);
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||||
try {
|
try {
|
||||||
const id = body.in_reply_to_id;
|
const id = body.in_reply_to_id;
|
||||||
const post = await client.getStatus(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);
|
const data = await client.deleteEmojiReaction(id, react);
|
||||||
ctx.body = data.data;
|
ctx.body = data.data;
|
||||||
} catch (e: any) {
|
} 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) => {
|
router.get<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
|
||||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||||
const accessTokens = ctx.headers.authorization;
|
const accessTokens = ctx.headers.authorization;
|
||||||
|
|
|
@ -7,7 +7,7 @@ namespace Entity {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
id: string;
|
id: string;
|
||||||
status?: Status;
|
status?: Status;
|
||||||
emoji?: string;
|
reaction?: Reaction;
|
||||||
type: NotificationType;
|
type: NotificationType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ namespace Entity {
|
||||||
me: boolean;
|
me: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
static_url?: string;
|
||||||
accounts?: Array<Account>;
|
accounts?: Array<Account>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ namespace Entity {
|
||||||
application: Application | null;
|
application: Application | null;
|
||||||
language: string | null;
|
language: string | null;
|
||||||
pinned: boolean | null;
|
pinned: boolean | null;
|
||||||
emoji_reactions: Array<Reaction>;
|
reactions: Array<Reaction>;
|
||||||
quote: Status | null;
|
quote: Status | null;
|
||||||
bookmarked: boolean;
|
bookmarked: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
@ -857,6 +857,21 @@ export interface MegalodonInterface {
|
||||||
* @return Status
|
* @return Status
|
||||||
*/
|
*/
|
||||||
unpinStatus(id: string): Promise<Response<Entity.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
|
// statuses/media
|
||||||
// ======================================
|
// ======================================
|
||||||
|
|
|
@ -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
|
// statuses/media
|
||||||
// ======================================
|
// ======================================
|
||||||
|
|
|
@ -321,7 +321,10 @@ namespace MisskeyAPI {
|
||||||
content: n.text ? this.escapeMFM(n.text) : "",
|
content: n.text ? this.escapeMFM(n.text) : "",
|
||||||
plain_content: n.text ? n.text : null,
|
plain_content: n.text ? n.text : null,
|
||||||
created_at: n.createdAt,
|
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,
|
replies_count: n.repliesCount,
|
||||||
reblogs_count: n.renoteCount,
|
reblogs_count: n.renoteCount,
|
||||||
favourites_count: this.getTotalReactions(n.reactions),
|
favourites_count: this.getTotalReactions(n.reactions),
|
||||||
|
@ -339,28 +342,36 @@ namespace MisskeyAPI {
|
||||||
application: null,
|
application: null,
|
||||||
language: null,
|
language: null,
|
||||||
pinned: 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,
|
bookmarked: false,
|
||||||
quote: n.renote && n.text ? this.note(n.renote, host) : null,
|
quote: n.renote && n.text ? this.note(n.renote, host) : null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
mapReactions = (
|
mapReactions = (
|
||||||
|
emojis: Array<MisskeyEntity.Emoji>,
|
||||||
r: { [key: string]: number },
|
r: { [key: string]: number },
|
||||||
myReaction?: string,
|
myReaction?: string,
|
||||||
): Array<MegalodonEntity.Reaction> => {
|
): 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) => {
|
return Object.keys(r).map((key) => {
|
||||||
if (myReaction && key === myReaction) {
|
// Strip colons from custom emoji reaction names to match emoji shortcodes.
|
||||||
return {
|
const shortcode = key.replaceAll(":", "");
|
||||||
count: r[key],
|
// If this is a custom emoji (vs. a Unicode emoji), find its image URL.
|
||||||
me: true,
|
const url = emojiUrls.get(shortcode);
|
||||||
name: key,
|
// Finally, remove trailing @. from local custom emoji reaction names.
|
||||||
};
|
const name = shortcode.replace("@.", "");
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
count: r[key],
|
count: r[key],
|
||||||
me: false,
|
me: key === myReaction,
|
||||||
name: key,
|
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:
|
case NotificationType.Mention:
|
||||||
return MisskeyNotificationType.Reply;
|
return MisskeyNotificationType.Reply;
|
||||||
case NotificationType.Favourite:
|
case NotificationType.Favourite:
|
||||||
case NotificationType.EmojiReaction:
|
case NotificationType.Reaction:
|
||||||
return MisskeyNotificationType.Reaction;
|
return MisskeyNotificationType.Reaction;
|
||||||
case NotificationType.Reblog:
|
case NotificationType.Reblog:
|
||||||
return MisskeyNotificationType.Renote;
|
return MisskeyNotificationType.Renote;
|
||||||
|
@ -448,7 +459,7 @@ namespace MisskeyAPI {
|
||||||
case MisskeyNotificationType.Quote:
|
case MisskeyNotificationType.Quote:
|
||||||
return NotificationType.Reblog;
|
return NotificationType.Reblog;
|
||||||
case MisskeyNotificationType.Reaction:
|
case MisskeyNotificationType.Reaction:
|
||||||
return NotificationType.EmojiReaction;
|
return NotificationType.Reaction;
|
||||||
case MisskeyNotificationType.PollEnded:
|
case MisskeyNotificationType.PollEnded:
|
||||||
return NotificationType.Poll;
|
return NotificationType.Poll;
|
||||||
case MisskeyNotificationType.ReceiveFollowRequest:
|
case MisskeyNotificationType.ReceiveFollowRequest:
|
||||||
|
@ -496,11 +507,11 @@ namespace MisskeyAPI {
|
||||||
account: this.note(n.note, host).account,
|
account: this.note(n.note, host).account,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
if (n.reaction) {
|
||||||
if (n.reaction) {
|
notification = Object.assign(notification, {
|
||||||
notification = Object.assign(notification, {
|
reaction: this.mapReactions(n.note.emojis, { [n.reaction]: 1 })[0],
|
||||||
emoji: n.reaction,
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
return notification;
|
return notification;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ namespace NotificationType {
|
||||||
export const Favourite: Entity.NotificationType = "favourite";
|
export const Favourite: Entity.NotificationType = "favourite";
|
||||||
export const Reblog: Entity.NotificationType = "reblog";
|
export const Reblog: Entity.NotificationType = "reblog";
|
||||||
export const Mention: Entity.NotificationType = "mention";
|
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 FollowRequest: Entity.NotificationType = "follow_request";
|
||||||
export const Status: Entity.NotificationType = "status";
|
export const Status: Entity.NotificationType = "status";
|
||||||
export const Poll: Entity.NotificationType = "poll";
|
export const Poll: Entity.NotificationType = "poll";
|
||||||
|
|
|
@ -163,7 +163,7 @@ describe('getNotifications', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: reaction,
|
event: reaction,
|
||||||
expected: MegalodonNotificationType.EmojiReaction,
|
expected: MegalodonNotificationType.Reaction,
|
||||||
title: 'reaction'
|
title: 'reaction'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -34,7 +34,7 @@ describe('api_client', () => {
|
||||||
dist: MisskeyNotificationType.Reaction
|
dist: MisskeyNotificationType.Reaction
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: MegalodonNotificationType.EmojiReaction,
|
src: MegalodonNotificationType.Reaction,
|
||||||
dist: MisskeyNotificationType.Reaction
|
dist: MisskeyNotificationType.Reaction
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -80,7 +80,7 @@ describe('api_client', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: MisskeyNotificationType.Reaction,
|
src: MisskeyNotificationType.Reaction,
|
||||||
dist: MegalodonNotificationType.EmojiReaction
|
dist: MegalodonNotificationType.Reaction
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: MisskeyNotificationType.PollEnded,
|
src: MisskeyNotificationType.PollEnded,
|
||||||
|
|
|
@ -54,7 +54,7 @@ const status: Entity.Status = {
|
||||||
} as Entity.Application,
|
} as Entity.Application,
|
||||||
language: null,
|
language: null,
|
||||||
pinned: null,
|
pinned: null,
|
||||||
emoji_reactions: [],
|
reactions: [],
|
||||||
bookmarked: false,
|
bookmarked: false,
|
||||||
quote: null
|
quote: null
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
"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'. */
|
"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. */
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
|
Loading…
Reference in a new issue