ao-to-mail

arthur online to email interface

当前为 2020-02-11 提交的版本,查看 最新版本

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