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
text/typescript
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.');
}
};