@@ -22,6 +22,7 @@ import {
2222 Article ,
2323 Create ,
2424 CryptographicKey ,
25+ Emoji ,
2526 Follow ,
2627 Image ,
2728 Like as RawLike ,
@@ -40,8 +41,10 @@ import { assert } from "@std/assert/assert";
4041import { assertEquals } from "@std/assert/equals" ;
4142import { assertFalse } from "@std/assert/false" ;
4243import { assertInstanceOf } from "@std/assert/instance-of" ;
44+ import { assertThrows } from "@std/assert/throws" ;
4345import { BotImpl } from "./bot-impl.ts" ;
4446import { parseSemVer } from "./bot.ts" ;
47+ import type { CustomEmoji } from "./emoji.ts" ;
4548import type { FollowRequest } from "./follow.ts" ;
4649import type { Message , MessageClass , SharedMessage } from "./message.ts" ;
4750import type { Like } from "./reaction.ts" ;
@@ -1963,6 +1966,171 @@ Deno.test("BotImpl.fetch()", async () => {
19631966 assertEquals ( response2 . status , 200 ) ;
19641967} ) ;
19651968
1969+ // Test BotImpl.addCustomEmoji() and BotImpl.addCustomEmojis()
1970+ Deno . test ( "BotImpl.addCustomEmoji(), BotImpl.addCustomEmojis()" , async ( t ) => {
1971+ const bot = new BotImpl < void > ( { kv : new MemoryKvStore ( ) , username : "bot" } ) ;
1972+
1973+ await t . step ( "addCustomEmoji()" , ( ) => {
1974+ const emojiData : CustomEmoji = {
1975+ type : "image/png" ,
1976+ url : "https://example.com/emoji.png" ,
1977+ } ;
1978+ const deferredEmoji = bot . addCustomEmoji ( "testEmoji" , emojiData ) ;
1979+ assertEquals ( typeof deferredEmoji , "function" ) ;
1980+ assertEquals ( bot . customEmojis [ "testEmoji" ] , emojiData ) ;
1981+
1982+ // Test invalid name
1983+ assertThrows (
1984+ ( ) => bot . addCustomEmoji ( "invalid name" , emojiData ) ,
1985+ TypeError ,
1986+ "Invalid custom emoji name" ,
1987+ ) ;
1988+
1989+ // Test duplicate name
1990+ assertThrows (
1991+ ( ) => bot . addCustomEmoji ( "testEmoji" , emojiData ) ,
1992+ TypeError ,
1993+ "Duplicate custom emoji name" ,
1994+ ) ;
1995+
1996+ // Test unsupported media type
1997+ assertThrows (
1998+ ( ) =>
1999+ bot . addCustomEmoji ( "invalidType" , {
2000+ // @ts -expect-error: Intended type error for testing runtime check
2001+ type : "text/plain" ,
2002+ url : "https://example.com/emoji.txt" ,
2003+ } ) ,
2004+ TypeError ,
2005+ "Unsupported media type" ,
2006+ ) ;
2007+ } ) ;
2008+
2009+ await t . step ( "addCustomEmojis()" , ( ) => {
2010+ const emojisData = {
2011+ emoji1 : { type : "image/png" , url : "https://example.com/emoji1.png" } ,
2012+ emoji2 : { type : "image/gif" , file : "/path/to/emoji2.gif" } ,
2013+ } as const ;
2014+ const deferredEmojis = bot . addCustomEmojis ( emojisData ) ;
2015+
2016+ assertEquals ( typeof deferredEmojis [ "emoji1" ] , "function" ) ;
2017+ assertEquals ( typeof deferredEmojis [ "emoji2" ] , "function" ) ;
2018+ assertEquals ( bot . customEmojis [ "emoji1" ] , emojisData . emoji1 ) ;
2019+ assertEquals ( bot . customEmojis [ "emoji2" ] , emojisData . emoji2 ) ;
2020+
2021+ // Test duplicate name within the batch
2022+ assertThrows (
2023+ ( ) =>
2024+ bot . addCustomEmojis ( {
2025+ emoji1 : { type : "image/png" , url : "https://example.com/dup1.png" } ,
2026+ } ) ,
2027+ TypeError ,
2028+ "Duplicate custom emoji name: emoji1" ,
2029+ ) ;
2030+ } ) ;
2031+ } ) ;
2032+
2033+ // Test BotImpl.getEmoji()
2034+ Deno . test ( "BotImpl.getEmoji()" , async ( ) => {
2035+ const bot = new BotImpl < void > ( { kv : new MemoryKvStore ( ) , username : "bot" } ) ;
2036+ const ctx = bot . federation . createContext (
2037+ new URL ( "https://example.com" ) ,
2038+ undefined ,
2039+ ) ;
2040+
2041+ // Test with remote URL
2042+ const remoteEmojiData : CustomEmoji = {
2043+ type : "image/png" ,
2044+ url : "https://remote.com/emoji.png" ,
2045+ } ;
2046+ bot . customEmojis [ "remoteEmoji" ] = remoteEmojiData ;
2047+ const remoteEmoji = bot . getEmoji ( ctx , "remoteEmoji" , remoteEmojiData ) ;
2048+ assertInstanceOf ( remoteEmoji , Emoji ) ;
2049+ assertEquals (
2050+ remoteEmoji . id ,
2051+ new URL ( "https://example.com/ap/emoji/remoteEmoji" ) ,
2052+ ) ;
2053+ assertEquals ( remoteEmoji . name , ":remoteEmoji:" ) ;
2054+ const icon = await remoteEmoji . getIcon ( ) ;
2055+ assertInstanceOf ( icon , Image ) ;
2056+ assertEquals ( icon . mediaType , "image/png" ) ;
2057+ assertEquals ( icon . url ?. href , "https://remote.com/emoji.png" ) ;
2058+
2059+ // Test with local file
2060+ const localEmojiData : CustomEmoji = {
2061+ type : "image/gif" ,
2062+ file : "/path/to/local/emoji.gif" ,
2063+ } ;
2064+ bot . customEmojis [ "localEmoji" ] = localEmojiData ;
2065+ const localEmoji = bot . getEmoji ( ctx , "localEmoji" , localEmojiData ) ;
2066+ assertInstanceOf ( localEmoji , Emoji ) ;
2067+ assertEquals (
2068+ localEmoji . id ,
2069+ new URL ( "https://example.com/ap/emoji/localEmoji" ) ,
2070+ ) ;
2071+ assertEquals ( localEmoji . name , ":localEmoji:" ) ;
2072+ const icon2 = await localEmoji . getIcon ( ) ;
2073+ assertInstanceOf ( icon2 , Image ) ;
2074+ assertEquals ( icon2 . mediaType , "image/gif" ) ;
2075+ assertEquals (
2076+ icon2 . url ?. href ,
2077+ "https://example.com/emojis/localEmoji.gif" ,
2078+ ) ;
2079+
2080+ // Test with local file without extension mapping
2081+ const localEmojiDataNoExt : CustomEmoji = {
2082+ type : "image/webp" ,
2083+ file : "/path/to/local/emoji" ,
2084+ } ;
2085+ bot . customEmojis [ "localEmojiNoExt" ] = localEmojiDataNoExt ;
2086+ const localEmojiNoExt = bot . getEmoji (
2087+ ctx ,
2088+ "localEmojiNoExt" ,
2089+ localEmojiDataNoExt ,
2090+ ) ;
2091+ const icon3 = await localEmojiNoExt . getIcon ( ) ;
2092+ assertInstanceOf ( icon3 , Image ) ;
2093+ assertEquals ( icon3 . mediaType , "image/webp" ) ;
2094+ assertEquals (
2095+ icon3 . url ?. href ,
2096+ "https://example.com/emojis/localEmojiNoExt.webp" ,
2097+ ) ;
2098+ } ) ;
2099+
2100+ // Test BotImpl.dispatchEmoji()
2101+ Deno . test ( "BotImpl.dispatchEmoji()" , ( ) => {
2102+ const bot = new BotImpl < void > ( { kv : new MemoryKvStore ( ) , username : "bot" } ) ;
2103+ const ctx = bot . federation . createContext (
2104+ new URL ( "https://example.com" ) ,
2105+ undefined ,
2106+ ) ;
2107+ const emojiData : CustomEmoji = {
2108+ type : "image/png" ,
2109+ url : "https://example.com/emoji.png" ,
2110+ } ;
2111+ bot . customEmojis [ "testEmoji" ] = emojiData ;
2112+
2113+ // Test dispatching an existing emoji
2114+ const emoji = bot . dispatchEmoji ( ctx , { name : "testEmoji" } ) ;
2115+ assertInstanceOf ( emoji , Emoji ) ;
2116+ assertEquals ( emoji . id , new URL ( "https://example.com/ap/emoji/testEmoji" ) ) ;
2117+ assertEquals ( emoji . name , ":testEmoji:" ) ;
2118+
2119+ // Test dispatching a non-existent emoji
2120+ const nonExistent = bot . dispatchEmoji ( ctx , { name : "nonExistent" } ) ;
2121+ assertEquals ( nonExistent , null ) ;
2122+ } ) ;
2123+
2124+ Deno . test ( "BotImpl.getFollowersFirstCursor()" , ( ) => {
2125+ const bot = new BotImpl < void > ( { kv : new MemoryKvStore ( ) , username : "bot" } ) ;
2126+ const ctx = bot . federation . createContext (
2127+ new URL ( "https://example.com" ) ,
2128+ undefined ,
2129+ ) ;
2130+ assertEquals ( bot . getFollowersFirstCursor ( ctx , "non-existent" ) , null ) ;
2131+ assertEquals ( bot . getFollowersFirstCursor ( ctx , "bot" ) , "0" ) ;
2132+ } ) ;
2133+
19662134interface SentActivity {
19672135 recipients : "followers" | Recipient [ ] ;
19682136 activity : Activity ;
0 commit comments