UNPKG

walletchat-metamask-snap

Version:

Metamask Snap to get WalletChat.fun message notifications directly in the MetaMask browser extension!

369 lines (329 loc) 11.3 kB
import { OnRpcRequestHandler, OnCronjobHandler } from '@metamask/snaps-types'; import { panel, text, heading, divider } from '@metamask/snaps-ui'; const getSnapState = async () => { const state = await snap.request({ method: 'snap_manageState', params: { operation: 'get', }, }); if (state && 'apiKey' in state && typeof state.apiKey === 'string') { return state; } return null; }; const setSnapState = async (apiKey: string | null, address: string | null) => { const state = await getSnapState(); const isDialogOn = state?.isDialogOn || true let unreadCount = state?.unreadCount || 0 if (apiKey == null) { unreadCount = 0 } return snap.request({ method: 'snap_manageState', params: { operation: 'update', newState: { apiKey, address, isDialogOn, unreadCount }, }, }); }; const setSnapStateisDialogOn = async (isDialogOn: boolean) => { const state = await getSnapState(); const apiKey = state?.apiKey as string const address = state?.address as string const unreadCount = state?.unreadCount as number return snap.request({ method: 'snap_manageState', params: { operation: 'update', newState: { apiKey, address, isDialogOn, unreadCount }, }, }); }; const setSnapStateUnreadCount = async (unreadCount: number) => { const state = await getSnapState(); const apiKey = state?.apiKey as string const address = state?.address as string const isDialogOn = state?.isDialogOn as boolean return snap.request({ method: 'snap_manageState', params: { operation: 'update', newState: { apiKey, address, isDialogOn, unreadCount }, }, }); }; //get unread count from WalletChat API const getUnreadCountFromAPI = async (apiKey: string, address: string) => { let retVal = 0 await fetch( ` https://api.v2.walletchat.fun/v1/get_unread_cnt/${address}`, { method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, } ) .then((response) => response.json()) .then((count) => { retVal = count }) .catch((error) => { console.log('🚨[GET][Unread Count] Error:', error) }) return retVal }; //Last unread message is needed just to get TO and FROM address const getLastUnreadMessage = async (apiKey: string, address: string) => { let chatData = '' await fetch( ` https://api.v2.walletchat.fun/v1/get_last_unread/${address}`, { method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, } ) .then((response) => response.json()) .then((chatItem) => { if (chatItem?.message) { chatData = chatItem } }) .catch((error) => { console.log('🚨🚨[GET][Unread Count] Error:', error) }) return chatData } let canRunMutex = true //only one dialog prompt should be running at a time //Cron Jobs run every 30 seconds or so currently, polling for new messages export const onCronjob: OnCronjobHandler = async ({ request }) => { switch (request.method) { case 'fireCronjob': if(!canRunMutex) {return} canRunMutex = false const state = await getSnapState(); const apiKey = state?.apiKey as string const address = state?.address as string const isDialogOn = state?.isDialogOn const unreadCount = state?.unreadCount let newMessages = 0 if (apiKey) { newMessages = await getUnreadCountFromAPI(apiKey, address); } if(newMessages > 0) { // Snaps Dialog Alerts - On by default, user can turn off in WalletChat Web App if (isDialogOn) { const lastUnreadMsg = await getLastUnreadMessage(apiKey, address) let chatHistory = '' //get most recent 6 messages await fetch( ` https://api.v2.walletchat.fun/v1/get_n_chatitems/${lastUnreadMsg.toaddr}/${lastUnreadMsg.fromaddr}/6`, { method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, } ) .then((response) => response.json()) .then((chatData) => { chatHistory = chatData }) .catch((error) => { console.log('🚨🚨[GET][Unread Count] Error:', error) }) //Programmatically format Snaps dialog box with recent chat history let convoBody = [] let prevAddr = '' let hasDivider = false let lastPushWasUsername = false //for last N (currently 6) messages we need to manually format the chat data Object.values(chatHistory).forEach(async val => { lastPushWasUsername = false const from = val?.sender_name || val.fromaddr if(prevAddr.toLowerCase() != val.fromaddr.toLowerCase()) { prevAddr = val.fromaddr lastPushWasUsername = true //just for divider logic below convoBody.push(text(' **' + from + ':** ')) } if(!val.read && !hasDivider) { hasDivider = true //if the previous item in the body is a username header //then put the divider in above the name if(lastPushWasUsername){ convoBody.splice(convoBody.length-1, 0, divider()); } else { convoBody.push(divider()) } } convoBody.push(text(val.message.trim())) if(address.toLowerCase() != val.fromaddr.toLowerCase()) { //only mark unread items from the other party as read if(!val.read) { await fetch( ` https://api.v2.walletchat.fun/v1/update_chatitem/${val.fromaddr}/${val.toaddr}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ fromaddr: val.fromaddr, toaddr: val.toaddr, timestamp: val.timestamp, read: true }), }) //end marking item as read } } }); //end looping through most recent N messages to build chat history for Snaps Dialog Prompt //Show user the Dialog Prompt const diagResponse = await snap.request({ method: 'snap_dialog', params: { type: 'prompt', content: panel(convoBody), placeholder: 'Enter response to message here...', }, }); //If the user responded - post the message to WalletChat if (diagResponse) { console.log("got response: ", diagResponse) const timestamp = new Date() //send the response message to WalletChat API await fetch( ` https://api.v2.walletchat.fun/v1/create_chatitem`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ fromaddr: lastUnreadMsg.toaddr, toaddr: lastUnreadMsg.fromaddr, message: diagResponse, nftid: '0', lit_access_conditions: '', encrypted_sym_lit_key: '', timestamp, read: false, nftaddr: ''}), } ) } } //end of Snaps Dialog Box Notification //only add a new notification in Notifications Tab if unread count has changed if (unreadCount != newMessages) { await setSnapStateUnreadCount(newMessages) const msg = newMessages.toString() + ' unread messages at WalletChat.fun' return snap.request({ method: 'snap_notify', params: { type: 'inApp', message: msg, }, }); } canRunMutex = true } else { return null } default: throw new Error('Method not found.'); } }; /** * Handle incoming JSON-RPC requests, sent through `wallet_invokeSnap`. * * @param args - The request handler args as object. * @param args.request - A validated JSON-RPC request object. * @returns The result of `snap_dialog`. * @throws If the request method is not valid for this snap. */ export const onRpcRequest: OnRpcRequestHandler = async ({ origin, request }) => { if (!origin.endsWith(".walletchat.fun")) { throw new Error('Only WalletChat.fun domains allowed.'); } switch (request.method) { case 'remove_api_key': await setSnapState(null, null); return true; case `set_dialog_on`: const retVal = await setSnapStateisDialogOn(true) return retVal; case `set_dialog_off`: const result = await setSnapStateisDialogOn(false) return result; case 'set_snap_state': if ( (request.params && 'apiKey' in request.params && typeof request.params.apiKey === 'string') && request.params && 'address' in request.params && typeof request.params.address === 'string' ) { await setSnapState(request.params.apiKey, request.params.address); return true; } throw new Error('Must provide params.apiKey.'); case 'get_snap_state': try { const state = await getSnapState(); return state; } catch (error) { return false; } //Currently Unused in full dApp - Reserved for Future Use/Testing case 'is_signed_in': try { const state = await getSnapState(); return Boolean(state?.apiKey); } catch (error) { return false; } //Currently Unused in full dApp - Reserved for Future Use/Testing case 'make_authenticated_request': // eslint-disable-next-line no-case-declarations const state = await getSnapState(); const apiKey = state?.apiKey as string const address = state?.address as string if (apiKey) { return getUnreadCountFromAPI(apiKey, address); } throw new Error('Must SIWE before making request.'); //Currently Unused in full dApp - Reserved for Future Use/Testing case 'inAppNotify': return snap.request({ method: 'snap_notify', params: { type: 'inApp', message: `Message Waiting at WalletChat.fun`, }, }); default: throw new Error('Method not found.'); } };