MessagesForMacintosh/JS/index.js

776 lines
15 KiB
JavaScript

require('cross-fetch/polyfill')
const ApolloClient = require('apollo-boost').ApolloClient;
const InMemoryCache = require('apollo-cache-inmemory').InMemoryCache;
const createHttpLink = require('apollo-link-http').createHttpLink;
const gql = require('graphql-tag')
// TEST_MODE can be turned on or off to prevent communications with the Apollo iMessage Server running on your modern Mac
const TEST_MODE = false
const DEBUG = false
let lastMessageFromSerialPortTime
const defaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore',
},
}
let client
const widthFor12ptFont = [
0,
10,
10,
10,
10,
10,
10,
10,
10,
8,
10,
10,
10,
0,
10,
10,
10,
11,
11,
9,
11,
10,
10,
10,
10,
10,
10,
10,
10,
10,
10,
10,
4,
6,
7,
10,
7,
11,
10,
3,
5,
5,
7,
7,
4,
7,
4,
7,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
4,
4,
6,
8,
6,
8,
11,
8,
8,
8,
8,
7,
7,
8,
8,
6,
7,
9,
7,
12,
9,
8,
8,
8,
8,
7,
6,
8,
8,
12,
8,
8,
8,
5,
7,
5,
8,
8,
6,
8,
8,
7,
8,
8,
6,
8,
8,
4,
6,
8,
4,
12,
8,
8,
8,
8,
6,
7,
6,
8,
8,
12,
8,
8,
8,
5,
5,
5,
8,
8
]
// this is tied to mac_main.c's message window max width
const MAX_WIDTH = 304
const SPACE_WIDTH = widthFor12ptFont[32]
let canStart = false
let hasNewMessages = false
const getNextWordLength = (word) => {
let currentWidth = 0
for (const char of word.split(``)) {
let currentCharWidth = widthFor12ptFont[char.charCodeAt()]
if (isNaN(currentCharWidth)) {
currentCharWidth = 1;
}
currentWidth += currentCharWidth
}
return currentWidth
}
const shortenText = (text) => {
let outputText = ``
let currentWidth = 0
for (const word of text.split(` `)) {
let currentWordWidth = getNextWordLength(word)
if (currentWidth + currentWordWidth + SPACE_WIDTH > MAX_WIDTH) {
outputText = `${outputText}ENDLASTMESSAGE`
currentWidth = 0
// okay, but what if the word itself is greater than max width?
if (currentWordWidth > MAX_WIDTH) {
let splitWordWidth = 0
for (const char of word.split(``)) {
let currentCharWidth = widthFor12ptFont[char.charCodeAt()]
if (isNaN(currentCharWidth)) {
currentCharWidth = 1;
}
if (splitWordWidth + currentCharWidth > MAX_WIDTH) {
outputText = `${outputText}ENDLASTMESSAGE`
splitWordWidth = 0
}
splitWordWidth += currentCharWidth
outputText = `${outputText}${char}`
}
currentWidth += splitWordWidth
continue
}
}
currentWidth += currentWordWidth + SPACE_WIDTH
outputText = `${outputText} ${word}`
}
return outputText
}
const MAX_ROWS = 16
const splitMessages = (messages) => {
let firstMessage = true
if (!messages) {
return `no messages ENDLASTMESSAGE`
}
for (const message of messages) {
if (firstMessage) {
let tempMessageOutput = `${message.chatter}: ${message.text}`
tempMessageOutput = shortenText(tempMessageOutput)
messageOutput = tempMessageOutput
} else {
let tempMessageOutput = `${message.chatter}: ${message.text}`
tempMessageOutput = shortenText(tempMessageOutput)
messageOutput = `${messageOutput}ENDLASTMESSAGE${tempMessageOutput}`
}
firstMessage = false
}
if (messageOutput.split(`ENDLASTMESSAGE`).length > MAX_ROWS) {
messageOutput = messageOutput.split(`ENDLASTMESSAGE`)
let newMessageOutput = []
for (let i = messageOutput.length; i > messageOutput.length - MAX_ROWS; i--) {
newMessageOutput.unshift(messageOutput[i])
}
messageOutput = newMessageOutput.join(`ENDLASTMESSAGE`)
}
lastMessageOutput = messageOutput
return messageOutput
}
const parseChatsToFriendlyNameString = (chats) => {
let friendlyNameStrings = ``
if (!chats) {
return ``
}
for (let chat of chats) {
friendlyNameStrings = `${friendlyNameStrings},${chat.friendlyName.replace(/,/g, '')}`
}
friendlyNameStrings = friendlyNameStrings.substring(1, friendlyNameStrings.length)
return friendlyNameStrings
}
let lastMessageOutput
let TEST_MESSAGES = [
{chatter: `friend 1`, text: `my super fun text message`},
{chatter: `me`, text: `some cool old thing I said earlier`},
{chatter: `friend 2`, text: `this message is not relevant to the conversation! not at all :(`},
{chatter: `friend 1`, text: `i watch star wars in reverse order`},
{chatter: `me`, text: `https://github.com/CamHenlin/MessagesForMacintosh`},
{chatter: `friend 3`, text: `old computers are fun`}
]
const TEST_CHATS = [
{friendlyName: `my group chat 1`, name: `my group chat 1`},
{friendlyName: `friend 1`, name: `friend 1`},
{friendlyName: `friend 4`, name: `friend 4`},
{friendlyName: `boss`, name: `boss`},
{friendlyName: `friend 3`, name: `friend 3`},
{friendlyName: `restaurant`, name: `restaurant`},
{friendlyName: `work`, name: `work`}
]
if (TEST_MODE) {
// setInterval(() => {
// let chatWords = Math.floor(Math.random() * 20) + 1
// let chatMessage = ``
// for (let chatWord = 0; chatWord < chatWords; chatWord++) {
// chatMessage = `${chatMessage} ${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, Math.floor(Math.random() * 16) + 1)}`
// }
// TEST_MESSAGES = TEST_MESSAGES.concat({chatter: `friend 1`, text: chatMessage})
// }, 10000)
}
let storedArgsAndResults = {
getMessages: {
args: {},
output: {}
},
hasNewMessagesInChat: {
args: {},
output: {}
},
getChats: {
args: {},
output: {}
},
getChatCounts: {
args: {},
output: {}
}
}
// this is our private interface, meant to communicate with our GraphQL server and fill caches
// we want everything cached as much as possible to cut down on perceived perf issues on the
// classic Macintosh end
class iMessageGraphClientClass {
async getMessages (chatId, page, fromInterval) {
storedArgsAndResults.getMessages.args = {
chatId,
page
}
if (TEST_MODE) {
let currentLastMessageOutput = `${lastMessageOutput}`
storedArgsAndResults.getMessages.output = splitMessages(TEST_MESSAGES)
if (!hasNewMessages && fromInterval) {
hasNewMessages = currentLastMessageOutput !== storedArgsAndResults.getMessages.output
}
return storedArgsAndResults.getMessages.output
}
if (DEBUG) {
console.log(`get messages for chat ID: ${chatId}`)
}
let result
try {
result = await client.query({
query: gql`query getMessages {
getMessages(chatId: "${chatId}", page: "${page}") {
chatter
text
}
}`
})
} catch (error) {
console.log(`getMessages: error with apollo query`)
console.log(error)
return
}
let messages = result.data.getMessages
let currentLastMessageOutput = `${lastMessageOutput}`
storedArgsAndResults.getMessages.output = splitMessages(messages)
if (!hasNewMessages && fromInterval) {
hasNewMessages = currentLastMessageOutput !== storedArgsAndResults.getMessages.output
if (hasNewMessages) {
console.log(`got new message. previous message was:`)
console.log(currentLastMessageOutput)
console.log(`new message set is:`)
console.log(storedArgsAndResults.getMessages.output)
}
}
return
}
async hasNewMessagesInChat () {
if (!hasNewMessages) {
return `false`
} else {
hasNewMessages = false
}
return `true`
}
async sendMessage (chatId, message) {
if (TEST_MODE) {
TEST_MESSAGES = TEST_MESSAGES.concat({chatter: `me`, text: message})
storedArgsAndResults.getMessages.output = splitMessages(TEST_MESSAGES)
return storedArgsAndResults.getMessages.output
}
let result
try {
message = encodeURIComponent(message)
result = await client.query({
query: gql`query sendMessage {
sendMessage(chatId: "${chatId}", message: "${message}") {
chatter
text
}
}`
})
} catch (error) {
console.log(`sendMessage: error with apollo query`)
console.log(error)
return
}
let messages = result.data.sendMessage
storedArgsAndResults.getMessages.output = splitMessages(messages)
return storedArgsAndResults.getMessages.output
}
async getChats () {
if (DEBUG) {
console.log(`getChats`)
}
if (TEST_MODE) {
storedArgsAndResults.getChats.output = parseChatsToFriendlyNameString(TEST_CHATS)
return
}
let result
try {
result = await client.query({
query: gql`query getChats {
getChats {
name
friendlyName
}
}`
})
} catch (error) {
console.log(`getChats: error with apollo query`)
console.log(error)
return
}
let chats = result.data.getChats
storedArgsAndResults.getChats.output = parseChatsToFriendlyNameString(chats)
if (DEBUG) {
console.log(`getChats complete`)
console.log(storedArgsAndResults.getChats.output)
}
return
}
async getChatCounts () {
if (DEBUG) {
console.log(`getChatCounts`)
}
if (TEST_MODE) {
storedArgsAndResults.getChatCounts.output = parseChatsToFriendlyNameString(TEST_CHATS)
return
}
let result
try {
result = await client.query({
query: gql`query getChatCounts {
getChatCounts {
friendlyName
count
}
}`
})
} catch (error) {
console.log(`getChatCounts: error with apollo query`)
console.log(error)
return
}
let chats = result.data.getChatCounts
if (!chats) {
return
}
let friendlyNameStrings = ``
if (chats.length === 0) {
return
}
for (let chat of chats) {
friendlyNameStrings = `${friendlyNameStrings},${chat.friendlyName.replace(/,/g, '')}:::${chat.count}`
}
// remove trailing comma
friendlyNameStrings = friendlyNameStrings.substring(1, friendlyNameStrings.length)
storedArgsAndResults.getChatCounts.output = friendlyNameStrings
if (DEBUG) {
console.log(`got chat counts`)
console.log(friendlyNameStrings)
}
return
}
setIPAddress (IPAddress) {
console.log(`instantiate apolloclient with uri ${IPAddress}:4000/`)
if (TEST_MODE) {
canStart = true
return `success`
}
try {
client = new ApolloClient({
uri: `${IPAddress}:4000/`,
cache: new InMemoryCache(),
link: new createHttpLink({
uri: `${IPAddress}:4000/`
}),
defaultOptions
});
} catch (err) {
console.log(`error instantiating the ApolloClient`)
console.log(err)
return `failure`
}
console.log(`return success`)
canStart = true
lastMessageFromSerialPortTime = new Date()
return `success`
}
}
let iMessageGraphClient = new iMessageGraphClientClass()
// provide the public interface
class iMessageClient {
constructor () {
canStart = false
// kick off an update interval
const updateInterval = setInterval(async () => {
let intervalDate = new Date().toISOString()
console.log(`${intervalDate}: run interval`)
if (!canStart) {
console.log(`${intervalDate}: can't start yet`)
return
}
if (new Date() - lastMessageFromSerialPortTime > 300000) {
console.log(`${intervalDate}: no serial comms for 300 seconds, unloading interval`)
clearInterval(updateInterval)
return
}
if (DEBUG) {
console.log(`${intervalDate}: running...`)
}
try {
if (Object.keys(storedArgsAndResults.getMessages.args).length > 0) {
console.log(`${intervalDate}: interval: get messages for ${storedArgsAndResults.getMessages.args.chatId}`)
await iMessageGraphClient.getMessages(storedArgsAndResults.getMessages.args.chatId, storedArgsAndResults.getMessages.args.page, true)
}
console.log(`${intervalDate}: interval: getchats`)
await iMessageGraphClient.getChats()
console.log(`${intervalDate}: interval: getchatcounts`)
await iMessageGraphClient.getChatCounts()
} catch (error) {
console.log(`${intervalDate}: caught error when running interval`)
console.log(error)
}
if (DEBUG) {
console.log(`${intervalDate}: complete!`)
}
}, 3000)
}
async getMessages (chatId, page) {
lastMessageFromSerialPortTime = new Date()
console.log(`iMessageClient.getMessages`)
if (storedArgsAndResults.getMessages.args.chatId !== chatId || storedArgsAndResults.getMessages.args.page !== page) {
await iMessageGraphClient.getMessages(chatId, page, false)
}
console.log(`iMessageClient.getMessages, return:`)
console.log(storedArgsAndResults.getMessages.output)
return storedArgsAndResults.getMessages.output
}
async hasNewMessagesInChat (chatId) {
lastMessageFromSerialPortTime = new Date()
console.log(`iMessageClient.hasNewMessagesInChat`)
let returnValue = await iMessageGraphClient.hasNewMessagesInChat(chatId)
console.log(`iMessageClient.hasNewMessagesInChat, return:`)
console.log(returnValue)
return returnValue
}
async sendMessage (chatId, message) {
lastMessageFromSerialPortTime = new Date()
console.log(`iMessageClient.sendMessage(${chatId}, ${message})`)
const messages = await iMessageGraphClient.sendMessage(chatId, message)
return messages
}
async getChats () {
lastMessageFromSerialPortTime = new Date()
console.log(`iMessageClient.getChats`)
if (Object.keys(storedArgsAndResults.getChats.output).length === 0) {
await iMessageGraphClient.getChats()
}
console.log(`iMessageClient.getChats, return:`)
console.log(storedArgsAndResults.getChats.output)
return storedArgsAndResults.getChats.output
}
getChatCounts () {
lastMessageFromSerialPortTime = new Date()
console.log(`iMessageClient.getChatCounts, prestored return:`)
console.log(storedArgsAndResults.getChatCounts.output)
return storedArgsAndResults.getChatCounts.output
}
setIPAddress (IPAddress) {
console.log(`iMessageClient.setIPAddress`)
return iMessageGraphClient.setIPAddress(IPAddress)
}
}
module.exports = iMessageClient