ao-to-mail

arthur online to email interface

  1. // ==UserScript==
  2. // @name ao-to-mail
  3. // @description arthur online to email interface
  4. // @version 1.0.5
  5. // @author yuze
  6. // @namespace yuze
  7. // @include https://system.arthuronline.co.uk/*
  8. // @include https://mail.google.com/*
  9. // @include https://mail.one.com/*
  10. // @connect arthuronline.co.uk
  11. // @connect ea-api.yuze.now.sh
  12. // @grant GM.getValue
  13. // @grant GM.setValue
  14. // @grant GM.xmlHttpRequest
  15. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
  16. // ==/UserScript==
  17.  
  18. /* eslint-env jquery, greasemonkey */
  19.  
  20. let target = {}
  21.  
  22. window.addEventListener('load', function () {
  23. if (window.location.href.includes('system.arthuronline.co.uk')) {
  24. arthur()
  25. }
  26. if (window.location.href.includes('mail.google.com')) {
  27. init_gmail()
  28. }
  29. if (window.location.href.includes('mail.one.com')) {
  30. init_webmail()
  31. }
  32. }
  33. )
  34.  
  35. function init_gmail() {
  36. console.log('init_gmail')
  37.  
  38. target.email = () => $('.wO.nr textarea')
  39. target.subject = () => $('input[name="subjectbox"]')
  40. target.body = () => $('div[aria-label="Message Body"]')
  41.  
  42. mail()
  43. }
  44.  
  45. function init_webmail() {
  46. console.log('init_webmail')
  47.  
  48. target.email = () => $('#to')
  49. target.subject = () => $('in-place-editor #subject')
  50. target.body = () => $('.rte-frame iframe').contents().find('body')
  51.  
  52. mail()
  53. }
  54.  
  55. async function arthur() {
  56.  
  57. let id = $('.text-logo span').text().toLowerCase()
  58. if (!id) return;
  59.  
  60. init()
  61. function init() {
  62.  
  63. appendCSS()
  64. function appendCSS() {
  65.  
  66. const magicButton = `.magicBtn {
  67. margin-top: 8px;
  68. padding: 5px;
  69. transition: 250ms;
  70. position: absolute;
  71. box-sizing: border-box;
  72. background: #e91e63;
  73. height: 48px;
  74. width: 48px;
  75. border-radius: 6px;
  76. color: white;
  77. user-select: none;
  78. }
  79. .magicBtn:hover {
  80. filter: brightness(125%);
  81. }
  82. .magicBtn:active {
  83. filter: brightness(75%);
  84. }
  85. .magicAnim {
  86. transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1);
  87. padding: 5px 5px 0 5px;
  88. }
  89. .magicFlip {
  90. transform: rotateY(180deg);
  91. }
  92. .magicToast {
  93. padding: 4px;
  94. font-size: 14px;
  95. position: absolute;
  96. color: #e91e63;
  97. font-weight: bold;
  98. top: 50px;
  99. left: 0;
  100. }
  101. .magicSnail{
  102. font-size: 1em;
  103. display: inline-block;
  104. animation: snail 4.75s infinite;
  105. animation-timing-function: linear;
  106. }
  107. @-webkit-keyframes snail {
  108. 0% {
  109. -webkit-transform: translateX(0) rotateY(90deg)
  110. }
  111. 5% {
  112. -webkit-transform: translateX(0) rotateY(0deg)
  113. }
  114. 45% {
  115. -webkit-transform: translateX(100px) rotateY(0deg)
  116. }
  117. 55% {
  118. -webkit-transform: translateX(100px) rotateY(180deg)
  119. }
  120. 95% {
  121. -webkit-transform: translateX(0) rotateY(180deg)
  122. }
  123. 100% {
  124. -webkit-transform: translateX(0) rotateY(90deg)
  125. }
  126. }`
  127.  
  128. const style = ` <style>
  129. ${magicButton}
  130. </style>`
  131.  
  132. $('head').append(style)
  133. }
  134.  
  135. tokenCheck()
  136. async function tokenCheck() {
  137. let token = await GM.getValue(`${id}-ao-token`)
  138. if (!token) {
  139. console.log('No token exists, getting token from DB')
  140. tokenProvider()
  141. }
  142. }
  143.  
  144. checkLocation()
  145.  
  146. }
  147.  
  148. let data = {}
  149. let response;
  150. let savedLoc;
  151.  
  152. $('body').on('click', checkLocation)
  153. $(window).on('focus', checkLocation)
  154.  
  155. async function checkLocation() {
  156. await wait(100)
  157. if (/tenancies\/view\/\d{6}/.test(window.location.href)) {
  158. if (!($('.magicBtn').length)) {
  159. new Promise(function (resolve) {
  160. waitForExistance('.identifier-icon', resolve)
  161. }).then(() => {
  162. magicBtn()
  163. })
  164. }
  165. } else {
  166. $('.magicBtn').remove()
  167. }
  168. }
  169.  
  170. function magicBtn() {
  171. append()
  172. function append() {
  173. const html = `<div class="magicBtn"><div class="magicAnim"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  174. <path fill="currentColor" d="M224 96l16-32 32-16-32-16-16-32-16 32-32 16 32 16 16 32zM80 160l26.66-53.33L160 80l-53.34-26.67L80 0 53.34 53.33 0 80l53.34 26.67L80 160zm352 128l-26.66 53.33L352 368l53.34 26.67L432 448l26.66-53.33L512 368l-53.34-26.67L432 288zm70.62-193.77L417.77 9.38C411.53 3.12 403.34 0 395.15 0c-8.19 0-16.38 3.12-22.63 9.38L9.38 372.52c-12.5 12.5-12.5 32.76 0 45.25l84.85 84.85c6.25 6.25 14.44 9.37 22.62 9.37 8.19 0 16.38-3.12 22.63-9.37l363.14-363.15c12.5-12.48 12.5-32.75 0-45.24zM359.45 203.46l-50.91-50.91 86.6-86.6 50.91 50.91-86.6 86.6z"></path>
  175. </svg></div></div>`
  176.  
  177. $('.identifier-icon').append(html)
  178. $('.magicAnim').on('click', anim)
  179. $('.magicBtn').on('click', getData)
  180. }
  181.  
  182. function anim() {
  183. $(this).toggleClass('magicFlip')
  184. setTimeout(() => $(this).toggleClass('magicFlip'), 512)
  185. }
  186. }
  187.  
  188. async function getData() {
  189. disableAccess('<b style="font-size: 1.5em;">Please wait <div class="magicSnail">🐌</div>&emsp;🥬</b><br>Gathering leafy greens...')
  190.  
  191. // wipe data on entry
  192. GM.setValue('ao-data', '')
  193.  
  194. savedLoc = window.location.href;
  195.  
  196. let extractId = savedLoc.match(/(?!tenancies\/view\/)\d{6}/)[0]
  197. let url = `https://api.arthuronline.co.uk/v2/tenancies/${extractId}`
  198.  
  199. let token = await GM.getValue(`${id}-ao-token`)
  200. let xEntityId = await GM.getValue(`${id}-ao-xEntityId`)
  201.  
  202. GM.xmlHttpRequest({
  203. method: "GET",
  204. url: url,
  205. headers: {
  206. 'Authorization': `Bearer ${token}`,
  207. 'X-EntityID': `${xEntityId}`,
  208. },
  209. onload: function (xhr) {
  210. response = JSON.parse(xhr.responseText);
  211. if (response.error) {
  212. console.log('error')
  213. tokenProvider(true)
  214. } else {
  215. console.log('success')
  216. assignData(response.data)
  217. }
  218. }
  219. })
  220. }
  221.  
  222. async function assignData(response) {
  223.  
  224. data['id'] = id
  225. data['ref'] = response.ref
  226. data['startDate'] = response.start_date
  227. data['property'] = $('.identifier-detail .sub-title a')[0].innerHTML.replace(' - ', ' ').split(',')[0].replace(' Room', ', Room')
  228. data['names'] = []
  229. data['emails'] = []
  230. data['total'] = $('.overdue .number').text()
  231.  
  232. for (let i = 0; i < response.tenants.length; i++) {
  233. data['names'].push((response.tenants[i].first_name + ' ' + response.tenants[i].last_name).replace(/ {2}/g, ' '))
  234. data['emails'].push(response.tenants[i].email)
  235. }
  236. let mode = GM.getValue('mode')
  237.  
  238. if (await mode == 'overdue') {
  239. getArrears()
  240. } else {
  241. saveToLocalStorage()
  242. }
  243. }
  244.  
  245. function getArrears() {
  246. $('.nav.nav-tabs [href^="#tab-transactions"]')[0].click()
  247.  
  248. new Promise(function (resolve) {
  249. waitForExistance('.transactions tbody', resolve)
  250. }).then(() => {
  251. $('#genOverdueBtn')[0].click()
  252. data['arrears'] = $('#genOverdueText')[0].value.split('\n').join('<br>')
  253. $('#genOverdueText').css('display', 'none')
  254. saveToLocalStorage()
  255. returnToSavedLocation()
  256. })
  257. }
  258.  
  259. function returnToSavedLocation() {
  260. if (/tenancies\/view\/\d{6}\/ident:Datatable.{5}$/.test(savedLoc)) {
  261. setTimeout(async () => {
  262. $(`.nav.nav-tabs [href^="#tab-summary"]`)[0].click()
  263. await wait(512)
  264. checkLocation()
  265. }, 1)
  266. } else if (/tenancies\/view\/\d{6}\/ident:Datatable.{5}#tab-.+-/.test(savedLoc)) {
  267. let match = savedLoc.match(/tab-.+(?=-)/)[0]
  268. setTimeout(async () => {
  269. $(`.nav.nav-tabs [href^="#${match}"]`)[0].click()
  270. await wait(512)
  271. checkLocation()
  272. }, 1)
  273. }
  274. }
  275.  
  276. function saveToLocalStorage() {
  277. GM.setValue('ao-data', JSON.stringify(data))
  278.  
  279. data = {}
  280. disableAccess('', true)
  281. }
  282.  
  283. function disableAccess(desc, remove) {
  284.  
  285. if (remove) {
  286. removeDisableAccess()
  287. return;
  288. }
  289. if ($('#disableAccess').length) return;
  290.  
  291. $('body').append(` <div id="disableAccess">
  292. <div id="disableDesc">${desc}</div>
  293. </div>`)
  294.  
  295. $('#disableAccess').hide().fadeIn(618)
  296.  
  297. setTimeout(() => {
  298. if ($('#disableAccess').length) {
  299. $('#disableDesc').append('<br><div class="btn" style="transform: scale(1.75,1.75); margin-top: 48px" id="disableExit">This is taking too long! Get me out of here. 😠</div>')
  300. $('#disableExit').hide().fadeIn(1024)
  301. $('#disableExit').on('click', function () {
  302. removeDisableAccess()
  303. })
  304. }
  305. }, 5500)
  306.  
  307. $('#disableClickCover').on('mousedown keydown', disableAccess)
  308. function disableAccess(e) {
  309. e.preventDefault()
  310. return;
  311. }
  312.  
  313. function removeDisableAccess() {
  314. $('#disableAccess').off('mousedown keydown scroll', disableAccess)
  315. $('#disableAccess').fadeOut(314, function () {
  316. $('#disableAccess').remove()
  317. })
  318. }
  319.  
  320. }
  321.  
  322. function tokenProvider(retry) {
  323.  
  324. GM.xmlHttpRequest({
  325. method: "POST",
  326. url: 'https://ea-api.yuze.now.sh/api/refresh-token',
  327. headers: {
  328. 'Content-Type': 'application/x-www-form-urlencoded'
  329. },
  330. data: `for=${id}_ao`,
  331. onload: function (res) {
  332. let json = (JSON.parse(res.response))
  333. GM.setValue(`${id}-ao-token`, json.token)
  334. GM.setValue(`${id}-ao-xEntityId`, json.xEntityId)
  335. console.log(json.note)
  336.  
  337. if (retry) {
  338. console.log('retrying')
  339. getData()
  340. }
  341. }
  342. })
  343.  
  344. }
  345. }
  346.  
  347. async function mail() {
  348.  
  349. $(window).on('focus', processData)
  350.  
  351. async function processData() {
  352.  
  353. let data = await GM.getValue('ao-data')
  354. let mode = await GM.getValue('mode')
  355.  
  356. let email = target.email()
  357. let subject = target.subject()
  358. let body = target.body()
  359.  
  360. if (!data) return;
  361.  
  362. data = JSON.parse(data)
  363.  
  364. // EMAIL
  365.  
  366. // does email field contain content already?
  367. let replyExisting = $('.oL.aDm span').text()
  368. if (replyExisting.includes('barrons') || replyExisting.includes('mayfields') || replyExisting === '') {
  369.  
  370. email.val(data['emails'].join(', '))
  371. email.trigger('change')
  372.  
  373. }
  374.  
  375. // SUBJECT
  376. subject.val(subject.val() + ` (${data['property']})`)
  377. subject.trigger('change')
  378.  
  379. // BODY --> name
  380. let firstNames = []
  381. for (let i = 0; i < data['names'].length; i++) {
  382. firstNames.push(data['names'][i].split(' ')[0])
  383. }
  384. if (data['names'].length > 2) {
  385. firstNames = firstNames.join(', ').replace(/(,)(?!.+\1)/, ' and')
  386. } else {
  387. firstNames = firstNames.join(' and ')
  388. }
  389.  
  390. // BODY --> due-date
  391. let dueDate = ''
  392. if (await mode == 'overdue') {
  393. dueDate = data['arrears'].split('<br>')
  394.  
  395. for (let i = 0; i <= dueDate.length; i++) {
  396.  
  397. if (!dueDate[i] && i === 0) {
  398. break;
  399. } else if (i == dueDate.length - 1) {
  400. try {
  401. dueDate = dueDate[0].match(/\((\d.+)-/)[1].trim()
  402. break;
  403. } catch (err) {
  404. dueDate = dueDate[0].match(/\((\d.+)\)/)[1].trim()
  405. break;
  406. }
  407.  
  408. } else if (dueDate[i].includes('Outstanding')) {
  409. dueDate = dueDate[i].match(/\((.+)-/)[1].trim()
  410. break;
  411. }
  412. }
  413. GM.setValue('mode', '')
  414. }
  415.  
  416. body.html(
  417. body.html()
  418. .replace('{name}', firstNames)
  419. .replace('{ref}', data['ref'])
  420. .replace('{property}', data['property'])
  421. .replace('{arrears}', data['arrears'])
  422. .replace('{due-date}', dueDate)
  423. .replace('{total}', data['total'])
  424. )
  425.  
  426. if (data['arrears']) {
  427. body.html(body.html().replace(/(Total to be paid: £\d.?\d+\.\d{1,2})/, '<b><u>$1</u></b>'))
  428. }
  429.  
  430. // CONCLUDE
  431. GM.setValue('ao-data', '')
  432. }
  433.  
  434. btnListeners()
  435. async function btnListeners() {
  436.  
  437. await wait(1024)
  438.  
  439. $('#template-overdue').on('click', function () {
  440. GM.setValue('mode', 'overdue')
  441. })
  442.  
  443. }
  444.  
  445. }
  446.  
  447. async function waitForExistance(elem, resolve) {
  448.  
  449. if ($(elem).length) {
  450. resolve()
  451. }
  452.  
  453. let interval;
  454. if (!$(elem).length) {
  455. interval = setInterval(() => checkExistance(), 150)
  456. }
  457. function checkExistance() {
  458. if ($(elem).length) {
  459. clearInterval(interval)
  460. resolve()
  461. }
  462. }
  463. }
  464.  
  465. async function wait(ms) {
  466. return new Promise(resolve => {
  467. setTimeout(() => { resolve() }, ms);
  468. });
  469. }