mirror of
https://github.com/CamHenlin/MessagesForMacintosh.git
synced 2024-11-25 13:31:00 +00:00
776 lines
15 KiB
JavaScript
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
|
|
|