Advertisement
Guest User

Untitled

a guest
May 16th, 2019
228
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*
  2.  * @author Oleg Khalidov <brooth@gmail.com>.
  3.  * -----------------------------------------------
  4.  * Freelance software development:
  5.  * Upwork: https://www.upwork.com/fl/khalidovoleg
  6.  * Freelancer: https://www.freelancer.com/u/brooth
  7.  */
  8. import { Router, Response } from 'express'
  9. import { Pool, Connection, PoolConnection } from 'mysql'
  10. import { Observable, } from 'rxjs'
  11. import fetch from 'node-fetch';
  12. import * as uuid from 'uuid'
  13. import { parseNumber } from 'libphonenumber-js'
  14. import * as microtime from 'microtime'
  15.  
  16. import { SyncContact, ContactPhone, Contact, ContactEmail, GSearchResult } from '../models/domain.models'
  17. import { AuthRequest } from './auth.controller'
  18. import { QueryObservable, GetConnectionObservable } from '../utils/rx.utils';
  19. import { User } from '../models/auth.models';
  20. import { loadContactNotes } from './contact_notes.controller';
  21. import { C } from '../c';
  22. import { googleGeocodeLocation } from './location.controller';
  23. import { loadContactComments } from './contact_comments.controller';
  24. import * as md5 from 'md5';
  25.  
  26. export class ContactsController {
  27.     private db: Pool
  28.  
  29.     constructor(connection: Pool) {
  30.         this.db = connection
  31.         this.lookupPhoneLocation = this.lookupPhoneLocation.bind(this)
  32.     }
  33.  
  34.     router(): Router {
  35.         const router = Router()
  36.         router.get('/', this.get.bind(this))
  37.         router.get('/holders', this.loadHolders.bind(this))
  38.         router.get('/dislike', this.getDisliked.bind(this))
  39.         router.get('/count', this.getContactsCount.bind(this))
  40.         router.get('/common', this.findCommonContacts.bind(this))
  41.         router.post('/', this.add.bind(this))
  42.         router.patch('/', this.sync.bind(this))
  43.         router.patch('/public/:id', this.changePublic.bind(this))
  44.         router.get('/fillHash', this.fillHash.bind(this))
  45.         router.get('/deleteDuplicates', this.deleteDuplicates.bind(this))
  46.         return router
  47.     }
  48.  
  49.     private findCommonContacts(req: AuthRequest, res: Response) {
  50.         console.log('ContactsController.findCommonContacts()')
  51.  
  52.         const ids = req.query.ids.split(',')
  53.         GetConnectionObservable(this.db)
  54.             .concatMap(connection => QueryObservable<Contact[]>(connection,
  55.                 'SELECT c.id, cp.value FROM contacts c' +
  56.                 ' INNER JOIN contact_phones cp on cp.contactId = c.id' +
  57.                 ' WHERE c.id IN (?)',
  58.                 [ids])
  59.                 .do(_ => connection.release())
  60.                 .catch(error => {
  61.                     connection.release();
  62.                     return Observable.throw(error);
  63.                 }))
  64.             .subscribe((data: { id: string, value: string }[]) => {
  65.                 const results: { [key: string]: string[] } = {}
  66.                 for (let i = 0; i < data.length; i++) {
  67.                     const c1 = data[i]
  68.                     for (let j = 0; j < data.length; j++) {
  69.                         const c2 = data[j]
  70.                         if (i == j || c1.id === c2.id)
  71.                             continue
  72.  
  73.                         let common = false
  74.                         if (c1.value === c2.value) {
  75.                             common = true
  76.                         } else {
  77.                             const phone1Parts = c1.value.match(/\d+/g)
  78.                             const phone2Parts = c2.value.match(/\d+/g)
  79.                             if (phone1Parts && phone2Parts) {
  80.                                 const formattedPhone1 = phone1Parts.join('')
  81.                                 const formattedPhone2 = phone2Parts.join('')
  82.                                 if (formattedPhone1 === formattedPhone2)
  83.                                     common = true
  84.                             }
  85.                         }
  86.                         if (common) {
  87.                             if (!results[c1.id])
  88.                                 results[c1.id] = []
  89.                             if (!results[c1.id].includes(c2.id))
  90.                                 results[c1.id].push(c2.id)
  91.                         }
  92.                     }
  93.                 }
  94.                 res.send({ results });
  95.             }, error => {
  96.                 console.error(error)
  97.                 res.status(500).send()
  98.             })
  99.     }
  100.  
  101.     private getContactsCount(req: AuthRequest, res: Response) {
  102.         console.log('ContactsController.getContactsCount()')
  103.  
  104.         GetConnectionObservable(this.db)
  105.             .concatMap(connection => QueryObservable<Contact[]>(connection,
  106.                 'SELECT count(*) as count FROM contacts')
  107.                 .do(_ => connection.release())
  108.                 .catch(error => {
  109.                     connection.release();
  110.                     return Observable.throw(error);
  111.                 }))
  112.             .subscribe(results => {
  113.                 res.send({ result: results[0].count });
  114.             }, error => {
  115.                 console.error(error)
  116.                 res.status(500).send()
  117.             })
  118.     }
  119.  
  120.     private loadHolders(req: AuthRequest, res: Response) {
  121.         console.log('ContactsController.loadHolders()')
  122.  
  123.         GetConnectionObservable(this.db)
  124.             .concatMap(connection => QueryObservable<Contact[]>(connection,
  125.                 'SELECT id, name FROM contacts WHERE holderId IS NULL and id != ?',
  126.                 [req.user.contactId])
  127.                 .do(_ => connection.release())
  128.                 .catch(error => {
  129.                     connection.release();
  130.                     return Observable.throw(error);
  131.                 }))
  132.             .subscribe(results => {
  133.                 res.send({ results });
  134.             }, error => {
  135.                 console.error(error)
  136.                 res.status(500).send()
  137.             })
  138.     }
  139.  
  140.     private changePublic(req: AuthRequest, res: Response) {
  141.         console.log('ContactsController.changePublic()', req.params, req.body)
  142.  
  143.         const updateTs = microtime.now()
  144.         const contactId = req.params.id
  145.         const value = req.body.public === true
  146.         const restrictedGroup = req.body.restrictedTo || []
  147.         const restrict = value && restrictedGroup.length > 0
  148.         const queries = [
  149.             'UPDATE contacts SET public = ?, public_restricted =?, updateTs = ? WHERE id = ?']
  150.         const params = [value, restrict, updateTs, contactId]
  151.         if (restrict) {
  152.             restrictedGroup.forEach((holderId: string) => {
  153.                 queries.push('INSERT INTO public_restrictions VALUES(?, ?, ?)')
  154.                 params.push(uuid.v4(), contactId, holderId)
  155.             })
  156.         } else {
  157.             queries.push('DELETE FROM public_restrictions WHERE contactId = ?')
  158.             params.push(contactId)
  159.         }
  160.  
  161.         GetConnectionObservable(this.db)
  162.             .concatMap(connection => QueryObservable<Contact[]>(connection,
  163.                 queries.join(';'), params)
  164.                 .do(_ => connection.release())
  165.                 .catch(error => {
  166.                     connection.release();
  167.                     return Observable.throw(error);
  168.                 }))
  169.             .subscribe(_ => {
  170.                 res.send({ updateTs });
  171.             }, error => {
  172.                 console.error(error)
  173.                 res.status(500).send()
  174.             })
  175.     }
  176.  
  177.     private getDisliked(req: AuthRequest, res: Response) {
  178.         console.log('ContactsController.getDisliked()')
  179.  
  180.         GetConnectionObservable(this.db)
  181.             .concatMap(connection => QueryObservable<Contact[]>(connection,
  182.                 'SELECT uc.id, uc.localId as lid, uc.name as n, false as lk, h.id as hid, h.name as hn' +
  183.                 ' FROM contacts uc' +
  184.                 '  LEFT JOIN contacts h ON h.id = uc.holderId' +
  185.                 ' WHERE uc.holderId != ? AND uc.deleteTs IS NULL' +
  186.                 ' AND EXISTS(SELECT id FROM contact_likes cl WHERE uc.id = cl.contactId AND cl.value = 0 LIMIT 1)' +
  187.                 ' LIMIT 1000',
  188.                 [req.user.contactId])
  189.                 .concatMap(results =>
  190.                     attachContactDetails(connection, results.map(contact => {
  191.                         contact.p = !!contact.p
  192.                         if (contact.lk != null)
  193.                             contact.lk = !!contact.lk
  194.                         const c = contact as any
  195.                         c.h = {
  196.                             id: c.hid,
  197.                             n: c.hn,
  198.                         }
  199.                         return contact
  200.                     }), null, false, true))
  201.                 .do(_ => connection.release())
  202.                 .catch(error => {
  203.                     connection.release();
  204.                     return Observable.throw(error);
  205.                 }))
  206.             .subscribe(results => {
  207.                 res.send({ results });
  208.             }, error => {
  209.                 console.error(error)
  210.                 res.status(500).send()
  211.             })
  212.     }
  213.  
  214.     private get(req: AuthRequest, res: Response) {
  215.         console.log('ContactsController.get()', req.query)
  216.  
  217.         GetConnectionObservable(this.db)
  218.             .concatMap(connection => (
  219.                 req.query.southEastLatitude
  220.                     ? this.searchInRegion(req, connection)
  221.                         .concatMap(results =>
  222.                             attachContactDetails(connection, results, req.user.contactId, 'if public', false))
  223.                     : req.query.query != null
  224.                         ? this.searchByQuery(req, connection)
  225.                             .concatMap(results =>
  226.                                 attachContactDetails(connection, results, req.user.contactId, 'if public', false))
  227.                         : loadUserContacts(connection, req.user,
  228.                             parseInt(req.query.since), parseInt(req.query.limit)))
  229.                 .do(_ => connection.release())
  230.                 .catch(error => {
  231.                     connection.release();
  232.                     return Observable.throw(error);
  233.                 }))
  234.             .subscribe(results => {
  235.                 res.send({ results });
  236.             }, error => {
  237.                 console.error(error)
  238.                 res.status(500).send()
  239.             })
  240.     }
  241.  
  242.     private searchByQuery(req: AuthRequest, connection: PoolConnection): Observable<Contact[]> {
  243.         console.log('ContactsController.searchByQuery()')
  244.  
  245.         const queries = req.query.query.split('|') as string[]
  246.         return Observable.from(queries)
  247.             .map(query => '%' + query.trim().toLowerCase() + '%')
  248.             .concatMap(q => QueryObservable<Contact[]>(connection,
  249.                 'SELECT uc.id, uc.name as n, cl.value as lk,' +
  250.                 '  COUNT(DISTINCT c.id) as noc,' +
  251.                 '  AVG(cp.latitude) as clt,' +
  252.                 '  AVG(cp.longitude) as cln' +
  253.                 ' FROM contacts uc' +
  254.                 '  LEFT JOIN contact_likes cl ON uc.id = cl.contactId AND cl.userId = ?' +
  255.                 '  LEFT JOIN contacts c ON c.deleteTs IS NULL AND uc.id = c.holderId AND c.public = 0' +
  256.                 '  LEFT JOIN contact_phones cp ON c.id = cp.contactId' +
  257.                 '  LEFT JOIN contact_emails ce ON c.id = ce.contactId' +
  258.                 '  LEFT JOIN contact_notes cn ON c.id = cn.contactId' +
  259.                 '  LEFT JOIN gsearch_results gsr ON gsr.phoneId = cp.id' +
  260.                 ' WHERE uc.id != ?' +
  261.                 '   AND uc.holderId IS NULL' +
  262.                 '   AND (' +
  263.                 '    LOWER(c.name) like ?' +
  264.                 '    OR LOWER(cp.label) like ?' +
  265.                 '    OR cp.value like ?' +
  266.                 '    OR cp.country like ?' +
  267.                 '    OR cp.locality like ?' +
  268.                 '    OR LOWER(ce.label) like ?' +
  269.                 '    OR ce.value like ?' +
  270.                 '    OR LOWER(cn.text) like ?' +
  271.                 '    OR LOWER(gsr.title) like ?' +
  272.                 '    OR LOWER(gsr.description) like ?' +
  273.                 '   )' +
  274.                 ' GROUP BY uc.id, lk',
  275.                 [req.user.id, req.user.contactId,
  276.                     q, q, q, q, q, q, q, q, q, q, q, q])
  277.                 .concatMap(holders =>
  278.                     QueryObservable<Contact[]>(connection,
  279.                         'SELECT DISTINCT c.id, c.name as n, hl.value as lk, h.id as hid,' +
  280.                         '  h.name as hn' +
  281.                         ' FROM contacts c' +
  282.                         '  LEFT JOIN contacts h ON h.id = c.holderId' +
  283.                         '  LEFT JOIN users hu ON hu.contactId = h.id' +
  284.                         '  LEFT JOIN contact_likes hl ON hl.userId = hu.id AND hl.contactId = c.id' +
  285.                         '  LEFT JOIN contact_phones cp ON c.id = cp.contactId' +
  286.                         '  LEFT JOIN contact_emails ce ON c.id = ce.contactId' +
  287.                         '  LEFT JOIN gsearch_results gsr ON gsr.phoneId = cp.id AND c.public = 1' +
  288.                         '  LEFT JOIN contact_comments cc ON c.id = cc.contactId AND cc.authorId = ?' +
  289.                         '  LEFT JOIN public_restrictions pr ON c.id = pr.contactId AND pr.restrictedTo = ?' +
  290.                         ' WHERE c.deleteTs IS NULL' +
  291.                         '   AND (' +
  292.                         '    (c.holderId != ? AND c.public = 1 AND ' +
  293.                         '     (c.public_restricted = 0 OR pr.id IS NOT NULL))' +
  294.                         '    OR (c.holderId IS NULL AND c.id NOT IN (?))' +
  295.                         '   )' +
  296.                         '   AND (' +
  297.                         '    LOWER(c.name) like ?' +
  298.                         '    OR LOWER(cp.label) like ?' +
  299.                         '    OR cp.value like ?' +
  300.                         '    OR cp.country like ?' +
  301.                         '    OR cp.locality like ?' +
  302.                         '    OR LOWER(ce.label) like ?' +
  303.                         '    OR ce.value like ?' +
  304.                         '    OR LOWER(gsr.title) like ?' +
  305.                         '    OR LOWER(gsr.description) like ?' +
  306.                         '    OR LOWER(cc.text) like ?' +
  307.                         '   )',
  308.                         [req.user.contactId, req.user.contactId, req.user.contactId,
  309.                         [req.user.contactId, ...holders.map(h => h.id)],
  310.                             q, q, q, q, q, q, q, q, q, q, q])
  311.                         .map(publics => {
  312.                             publics.forEach((s: any) => {
  313.                                 if (s.hid != null) {
  314.                                     s.h = {
  315.                                         id: s.hid,
  316.                                         n: s.hn,
  317.                                     }
  318.                                 }
  319.                                 if (s.lk != null) s.lk = !!s.lk
  320.                                 delete s.hid
  321.                                 delete s.hn
  322.                                 holders.push(s)
  323.                             })
  324.                             return holders
  325.                         }))
  326.             )
  327.             .reduce((prev, next) => {
  328.                 const results = Array.from(prev)
  329.                 next.forEach(n => {
  330.                     if (prev.findIndex(p => p.id == n.id) == -1)
  331.                         results.push(n)
  332.                 })
  333.                 return results
  334.             })
  335.     }
  336.  
  337.     private searchInRegion(req: AuthRequest, connection: PoolConnection): Observable<Contact[]> {
  338.         console.log('ContactsController.searchInRegion()')
  339.  
  340.         const holdersQuery = [
  341.             'SELECT uc.id, uc.name as n, ucl.value as lk, ucp.country as cn, ucp.locality as lc,',
  342.             '    COUNT(DISTINCT c.id) as noc,',
  343.             '    GROUP_CONCAT(DISTINCT c.id) as cs,' +
  344.             '    AVG(ucp.latitude) as clt,',
  345.             '    AVG(ucp.longitude) as cln',
  346.             ' FROM contacts uc',
  347.             '    LEFT JOIN contact_likes ucl ON uc.id = ucl.contactId AND ucl.userId = ?',
  348.             '    LEFT JOIN contacts c ON c.deleteTs IS NULL AND uc.id = c.holderId AND c.public = 0',
  349.             '    LEFT JOIN contact_phones ucp ON c.id = ucp.contactId']
  350.         const holdersQueryParams = [req.user.id]
  351.  
  352.         const publicsQuery = [
  353.             'SELECT DISTINCT c.id, c.name as n, hl.value as lk, h.id as hid, h.name as hn',
  354.             ' FROM contacts c',
  355.             '  LEFT JOIN contacts h ON h.id = c.holderId' +
  356.             '  LEFT JOIN users hu ON hu.contactId = h.id' +
  357.             '  LEFT JOIN contact_likes hl ON hl.userId = hu.id AND hl.contactId = c.id' +
  358.             '  LEFT JOIN contact_comments cc ON c.id = cc.contactId AND cc.authorId = ?' +
  359.             '  LEFT JOIN public_restrictions pr ON c.id = pr.contactId AND pr.restrictedTo = ?' +
  360.             '  INNER JOIN contact_phones cp ON c.id = cp.contactId' +
  361.             '       AND cp.latitude BETWEEN ? AND ?' +
  362.             '       AND cp.longitude BETWEEN ? AND ?'
  363.         ]
  364.         const publicsQueryParams = [
  365.             req.user.contactId, req.user.contactId,
  366.             req.query.northWestLatitude, req.query.southEastLatitude,
  367.             req.query.southEastLongitude, req.query.northWestLongitude]
  368.  
  369.         if (req.query.query) {
  370.             const commonQueries =
  371.                 ' LEFT JOIN contact_emails ce ON c.id = ce.contactId' +
  372.                 ' LEFT JOIN gsearch_results gsr ON gsr.phoneId = cp.id'
  373.             holdersQuery.push(' LEFT JOIN contact_phones cp ON c.id = cp.contactId')
  374.             holdersQuery.push(commonQueries)
  375.             publicsQuery.push(commonQueries)
  376.         }
  377.  
  378.         if (req.query.holderId) {
  379.             holdersQuery.push(' WHERE uc.id = ?')
  380.             holdersQueryParams.push(req.query.holderId)
  381.             publicsQuery.push(' WHERE c.holderId = ?')
  382.             publicsQueryParams.push(req.query.holderId)
  383.         } else {
  384.             holdersQuery.push(' WHERE uc.id != ?')
  385.             holdersQueryParams.push(req.user.contactId)
  386.             publicsQuery.push(' WHERE c.deleteTs IS NULL')
  387.         }
  388.  
  389.         holdersQuery.push(
  390.             '    AND uc.holderId IS NULL' +
  391.             '    AND ucp.latitude BETWEEN ? AND ?' +
  392.             '    AND ucp.longitude BETWEEN ? AND ?')
  393.         holdersQueryParams.push(
  394.             req.query.northWestLatitude, req.query.southEastLatitude,
  395.             req.query.southEastLongitude, req.query.northWestLongitude)
  396.  
  397.         publicsQuery.push(
  398.             '   AND (' +
  399.             '    (c.holderId != ? AND c.public = 1 AND ' +
  400.             '     (c.public_restricted = 0 OR pr.id IS NOT NULL))' +
  401.             '    OR (c.holderId IS NULL AND c.id != ?)' +
  402.             '   )')
  403.         publicsQueryParams.push(req.user.contactId, req.user.contactId)
  404.  
  405.         if (req.query.query) {
  406.             const queries = req.query.query.split('|') as string[]
  407.             const filters = queries
  408.                 .map(p => '%' + p.toLowerCase() + '%')
  409.                 .map(q => {
  410.                     holdersQueryParams.push(q, q, q, q, q, q, q);
  411.                     publicsQueryParams.push(q, q, q, q, q, q, q, q);
  412.                     return [
  413.                         ' LOWER(c.name) like ? ',
  414.                         ' LOWER(cp.label) like ? ',
  415.                         ' cp.value like ? ',
  416.                         ' LOWER(ce.label) like ? ',
  417.                         ' ce.value like ? ',
  418.                         ' LOWER(gsr.title) like ?',
  419.                         ' LOWER(gsr.description) like ?',
  420.                     ]
  421.                 })
  422.                 .reduce((a1, a2) => a1.concat(a2))
  423.             holdersQuery.push(' AND (' + filters.join(' OR ') + ')')
  424.             publicsQuery.push(' AND (' + filters.join(' OR ') + ' OR LOWER(cc.text) like ?)')
  425.         }
  426.         holdersQuery.push(' GROUP BY lc, cn, uc.id, lk')
  427.         return QueryObservable<Contact[]>(connection, holdersQuery.join(''), holdersQueryParams)
  428.             .do(holders => holders.forEach((h: any) => h.cs = h.cs.split(',')))
  429.             .concatMap(holders =>
  430.                 QueryObservable<Contact[]>(connection, publicsQuery.join(''), publicsQueryParams)
  431.                     .map(publics => {
  432.                         publics.forEach((s: any) => {
  433.                             if (s.hid != null) {
  434.                                 s.h = {
  435.                                     id: s.hid,
  436.                                     n: s.hn,
  437.  
  438.                                 }
  439.                             }
  440.                             if (s.lk != null) s.lk = !!s.lk
  441.                             delete s.hid
  442.                             delete s.hn
  443.                             holders.push(s)
  444.                         })
  445.                         return holders
  446.                     }))
  447.     }
  448.  
  449.     private lookupPhoneLocationByK780(number: string): Observable<Partial<ContactPhone> | null> {
  450.         const uri = `http://api.k780.com/?app=phone.get&phone=${number}&appkey=${C.K780_APP_KEY}&sign=${C.K780_APP_SIGN}&format=json`
  451.         return Observable.from(fetch(uri))
  452.             .concatMap(res => res.json())
  453.             .concatMap(json => {
  454.                 if (json.success !== '1' || !json.result || !json.result.att)
  455.                     return Observable.of(null)
  456.                 return googleGeocodeLocation({ address: json.result.att })
  457.                     .map(location => {
  458.                         if (!location)
  459.                             return null
  460.                         return {
  461.                             c: location.country,
  462.                             cc: location.countryCode,
  463.                             lc: location.locality,
  464.                             lt: location.latitude,
  465.                             ln: location.longitude
  466.                         }
  467.                     })
  468.             })
  469.             .catch(err => {
  470.                 console.log('k780 request failed', err)
  471.                 return Observable.of(null)
  472.             })
  473.     }
  474.  
  475.     private lookupChinaLocalPhoneLocation(connection: Connection, number: string):
  476.         Observable<Partial<ContactPhone> | null> {
  477.         return QueryObservable<any[]>(
  478.             connection,
  479.             'SELECT * FROM phonet WHERE area_code = ? limit 1',
  480.             [number.slice(0, 4)])
  481.             .concatMap(results => results.length ? Observable.of(results) :
  482.                 QueryObservable<any[]>(connection,
  483.                     'SELECT * FROM phonet WHERE area_code = ? limit 1',
  484.                     [number.slice(0, 3)]))
  485.             .concatMap(results => {
  486.                 if (results.length) {
  487.                     const phone: Partial<ContactPhone> = {
  488.                         lc: results[0].province + ', ' + results[0].city,
  489.                         lt: parseFloat(results[0].Lat),
  490.                         ln: parseFloat(results[0].Lon),
  491.                     }
  492.                     // yeah, they exist
  493.                     if (phone.lt! > 90 || phone.ln! > 180 ||
  494.                         phone.lt! < -90 || phone.ln! < -180) {
  495.                         console.log('Invalid coordinates found for phone %s, %d, %d',
  496.                             number, phone.lt, phone.ln)
  497.                         return Observable.of(null);
  498.                     }
  499.                     return Observable.of(phone);
  500.                 }
  501.                 return this.lookupPhoneLocationByK780(number)
  502.             })
  503.     }
  504.  
  505.     private lookupChinaMobilePhoneLocation(connection: Connection,
  506.         fullNumber: string, shortNumber: string): Observable<Partial<ContactPhone> | null> {
  507.         return QueryObservable<any[]>(connection,
  508.             'SELECT * FROM phonet WHERE phone = ?',
  509.             [shortNumber.slice(0, 7)])
  510.             .concatMap(results => {
  511.                 if (results.length) {
  512.                     const phone: Partial<ContactPhone> = {
  513.                         lc: results[0].province + ', ' + results[0].city,
  514.                         lt: parseFloat(results[0].Lat),
  515.                         ln: parseFloat(results[0].Lon),
  516.                     }
  517.                     if (phone.lt! > 90 || phone.ln! > 180 ||
  518.                         phone.lt! < -90 || phone.ln! < -180) {
  519.                         console.log('Invalid coordinates found for phone %s, %d, %d',
  520.                             shortNumber, phone.lt, phone.ln)
  521.                         return Observable.of(null);
  522.                     }
  523.                     return Observable.of(phone);
  524.                 }
  525.                 return this.lookupPhoneLocationByK780('+' + fullNumber)
  526.             })
  527.     }
  528.  
  529.     private lookupInternalPhoneLocation = (connection: Connection, user: User, number: string):
  530.         Observable<Partial<ContactPhone> | null> =>
  531.         QueryObservable<any[]>(connection,
  532.             'SELECT countryCode as cc FROM contact_phones' +
  533.             ' WHERE contactId = ? AND value = ?',
  534.             [user.contactId, user.phone])
  535.             .concatMap(holderPhones => {
  536.                 if (holderPhones.length == 0) {
  537.                     console.error('cannot find user phone, user: %s, contactId: %s, phone: %s',
  538.                         user.id, user.contactId, user.phone)
  539.                     return Observable.of(null);
  540.                 }
  541.                 const holderPhone = holderPhones[0]
  542.                 if (holderPhone.cc == 'CN') {
  543.                     return (number.startsWith('0')
  544.                         ? this.lookupChinaLocalPhoneLocation(connection, number)
  545.                         : this.lookupChinaMobilePhoneLocation(connection, number, number))
  546.                         .map(result => {
  547.                             if (result != null)
  548.                                 return {
  549.                                     ...result,
  550.                                     vl: true,
  551.                                     c: '中国',
  552.                                     cc: holderPhone.cc,
  553.                                 }
  554.  
  555.                             return result
  556.                         })
  557.                 } else {
  558.                     return Observable.of(null);
  559.                 }
  560.             })
  561.  
  562.     private lookupPhoneLocation(connection: Connection, user: User, _phone: ContactPhone):
  563.         Observable<ContactPhone> {
  564.         const phone: ContactPhone = { ..._phone, vl: false }
  565.  
  566.         const numberParts = phone.v.match(/\d+/g)
  567.         if (numberParts == null) {
  568.             console.log('invalid phone number %s', phone.v)
  569.             return Observable.of(phone)
  570.         }
  571.         const formattedPhoneNumber = numberParts.join('')
  572.  
  573.         if (phone.v.startsWith('+')) {
  574.             const data = parseNumber(phone.v, { extended: true });
  575.             if (data.valid === true) {
  576.                 phone.vl = true
  577.                 phone.cc = data.country
  578.                 if (phone.cc == 'CN') {
  579.                     phone.c = '中国';
  580.                     return this.lookupChinaMobilePhoneLocation(connection,
  581.                         formattedPhoneNumber, data.phone as string)
  582.                         .map(result => {
  583.                             if (result != null)
  584.                                 return { ...phone, ...result };
  585.                             return phone
  586.                         })
  587.                 }
  588.             }
  589.             return Observable.of(phone);
  590.         } else {
  591.             return this.lookupInternalPhoneLocation(connection, user, formattedPhoneNumber)
  592.                 .map(data => {
  593.                     if (data != null)
  594.                         return { ...phone, ...data };
  595.                     return phone
  596.                 })
  597.         }
  598.     }
  599.  
  600.     private add(req: AuthRequest, res: Response) {
  601.         console.log('ContactsController.add()')
  602.         const contact = req.body.items as Contact[]
  603.         GetConnectionObservable(this.db)
  604.             .subscribe(
  605.                 connection => {
  606.                     this.saveContact(connection, contact[0], req)
  607.                         .subscribe(results => {
  608.                             res.send({results})
  609.                         })
  610.                 }
  611.             )
  612.     }
  613.  
  614.     private searchContact(v: any, contact: any) : any {
  615.         return v.ps.map(a => a.v).filter(b => contact.ps.map(c => c.v).includes(b)).length
  616.     }
  617.  
  618.     private saveContact(connection: Connection, contact: Contact, req: AuthRequest) {
  619.         return Observable.from(contact.ps)
  620.             .concatMap(phone => this.lookupPhoneLocation(connection, req.user, phone))
  621.             .toArray()
  622.             .map(phones => {
  623.                 const id = uuid.v4()
  624.                 const ts = microtime.now()
  625.                 const result: SyncOperationData = {
  626.                     queries: [],
  627.                     params: [],
  628.                     result: {
  629.                         operation: 'CREATE',
  630.                         contact: {
  631.                             ...contact,
  632.                             id,
  633.                             uts: ts,
  634.                             ps: phones,
  635.                         }
  636.                     }
  637.                 }
  638.                 result.queries.push(
  639.                     'INSERT INTO contacts VALUES (?, ?, ?, null, ?, ?, ?, ?, ?, ?)')
  640.                 result.params.push(id, ts, ts, req.user.contactId,
  641.                     contact.lid, contact.n, contact.p, false, contact.uh)
  642.                 phones.forEach(phone => {
  643.                     phone.id = uuid.v4()
  644.                     result.queries.push('INSERT INTO contact_phones' +
  645.                         ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
  646.                     result.params.push(phone.id, id, phone.l, phone.v,
  647.                         phone.vl, phone.c, phone.cc, phone.lc,
  648.                         toDecimal(phone.lt), toDecimal(phone.ln))
  649.                 })
  650.                 contact.es.forEach(e => {
  651.                     result.queries.push('INSERT INTO contact_emails VALUES (?, ?, ?, ?)')
  652.                     result.params.push(uuid.v4(), id, e.l, e.v)
  653.                 })
  654.                 return result;
  655.             })
  656.             .concatMap(q => QueryObservable(connection, q.queries.join(';'), q.params).mapTo(q.result))
  657.     }
  658.  
  659.     private updateContact(connection: Connection, old: Contact, contact: Contact, req: AuthRequest) {
  660.         return Observable.from(contact.ps)
  661.             .concatMap(phone => this.lookupPhoneLocation(connection, req.user, phone))
  662.             .toArray()
  663.             .map(phones => {
  664.                 const ts = microtime.now()
  665.                 const data: SyncOperationData = {
  666.                     queries: [
  667.                         'DELETE FROM contact_phones WHERE contactId = ?',
  668.                         'DELETE FROM contact_emails WHERE contactId = ?',
  669.                     ],
  670.                     params: [old.id, old.id],
  671.                 }
  672.                 data.queries.push('UPDATE contacts SET updateTs = ?, deleteTs = NULL, name = ?, unique_hash = ?' +
  673.                     ' WHERE id = ?')
  674.                 data.params.push(ts, contact.n, contact.uh, old.id)
  675.                 phones.forEach(p => {
  676.                     p.id = uuid.v4()
  677.                     data.queries.push('INSERT INTO contact_phones' +
  678.                         ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
  679.                     data.params.push(p.id, old.id, p.l, p.v,
  680.                         p.vl, p.c, p.cc, p.lc,
  681.                         toDecimal(p.lt), toDecimal(p.ln))
  682.                 })
  683.                 contact.es.forEach(e => {
  684.                     data.queries.push('INSERT INTO contact_emails VALUES (?, ?, ?, ?)')
  685.                     data.params.push(uuid.v4(), old.id, e.l, e.v)
  686.                 })
  687.                 return data;
  688.             })
  689.             .concatMap(q => QueryObservable(connection,
  690.                 q.queries.join(';'),
  691.                 q.params))
  692.             .subscribe()
  693.     }
  694.  
  695.     private deleteContact(connection: Connection, contact: string) {
  696.         return Observable.of({
  697.             queries: [
  698.                 'DELETE FROM contacts WHERE id = ?',
  699.                 'DELETE FROM contact_notes WHERE contactId = ?',
  700.                 'DELETE FROM contact_phones WHERE contactId = ?',
  701.                 'DELETE FROM contact_emails WHERE contactId = ?'
  702.             ],
  703.             params: [contact, contact, contact, contact],
  704.         })
  705.         .concatMap(q => QueryObservable(connection,
  706.             q.queries.join(';'),
  707.             q.params))
  708.     }
  709.  
  710.     private formatNumber(number: string): string {
  711.         return number
  712.             .trim()
  713.             .replace(/-|\(|\)|( )/g, '')
  714.     }
  715.  
  716.     private formatEmail(email: string): string {
  717.         return email
  718.             .trim()
  719.             .toLowerCase()
  720.     }
  721.  
  722.     private createHash (contact: Contact) : string {
  723.         const phones = contact.ps
  724.             .map(phone => phone.v)
  725.             .map(n => this.formatNumber(n))
  726.             .sort()
  727.             .join('');
  728.         const emails = contact.es
  729.             .map(email => email.v)
  730.             .map(this.formatEmail)
  731.             .sort()
  732.             .join('');
  733.         console.log(`${contact.n}${phones}${emails}`)
  734.         return md5(`${contact.n}${phones}${emails}`)
  735.     }
  736.  
  737.     private sync(req: AuthRequest, res: Response) {
  738.         console.log('ContactsController.sync() ', req.body.items.length)
  739.         const syncResponse : SyncOperationResponse[] = [];
  740.         const items = req.body.items as Contact[];
  741.         const processedContacts : string[] = [];
  742.         GetConnectionObservable(this.db)
  743.             .concatMap(connection => {
  744.                 return QueryObservable<Contact[]>(connection,
  745.                 'SELECT c.id, c.updateTs as uts, c.deleteTs as d, c.localId as lid, name as n,' +
  746.                 '  cl.value as lk, c.public as p, unique_hash as uh' +
  747.                 ' FROM contacts c' +
  748.                 '  LEFT JOIN contact_likes cl ON c.id = cl.contactId AND cl.userId = ?' +
  749.                 ' WHERE holderId = ?' +
  750.                 ' AND deleteTs is null' +
  751.                 ' ORDER BY updateTs',
  752.                 [req.user.id, req.user.contactId])
  753.                 .concatMap(contacts => attachContactDetails(connection, contacts, null, true, true))
  754.                 .concatMap(dbContacts => {
  755.                     dbContacts.forEach(contact => {
  756.                         const isProcessed = processedContacts.includes(contact.uh)
  757.                         if(isProcessed) return
  758.                         processedContacts.push(contact.uh)
  759.                         const existContactIndex = items.findIndex(localContact => (contact.uh === localContact.ouh) || (contact.uh === localContact.uh))
  760.                         if(existContactIndex === -1){
  761.                             syncResponse.push({ operation: 'CREATE', contact: contact } as SyncOperationResponse)
  762.                         } else {
  763.                             if(items[existContactIndex].ouh && items[existContactIndex].uh !== items[existContactIndex].ouh) {
  764.                                 this.updateContact(connection, contact, items[existContactIndex], req)
  765.                                 contact.lk ? items[existContactIndex].lk = contact.lk : null
  766.                                 contact.ns ? items[existContactIndex].ns = contact.ns : null
  767.                                 contact.p ? items[existContactIndex].p = contact.p : null
  768.                                 contact.cc ? items[existContactIndex].cc = contact.cc : null
  769.                                 contact.uts ? items[existContactIndex].uts = contact.uts : null
  770.                                 syncResponse.push({ operation: 'UPDATE', contact: items[existContactIndex] } as SyncOperationResponse)
  771.                                 processedContacts.push(items[existContactIndex].uh, items[existContactIndex].ouh)
  772.                             } else {
  773.                                 contact.lid = items[existContactIndex].lid
  774.                                 // syncResponse.push({ operation: 'SKIP', contact: contact } as SyncOperationResponse)
  775.                             }
  776.                         }
  777.                     })
  778.                     items.filter(contact => !processedContacts.includes(contact.uh))
  779.                         .forEach(contact => {
  780.                             const isProcessed = processedContacts.includes(contact.uh)
  781.                             if(!isProcessed){
  782.                                 this.saveContact(connection, contact, req)
  783.                                     .subscribe(createdContact => console.log('createdContact=> ', createdContact))
  784.                                 // syncResponse.push({ operation: 'LOCAL', contact } as SyncOperationResponse)
  785.                                 processedContacts.push(contact.uh)
  786.                             }
  787.                         })
  788.                         console.log('return')
  789.                    return dbContacts;
  790.                 })
  791.                 .do(_ => connection.release())
  792.                 .catch(error => {
  793.                     connection.release();
  794.                     return Observable.throw(error);
  795.                 })
  796.             })
  797.             .toArray()
  798.             .subscribe(
  799.                 _ => {
  800.                     res.send({ results: syncResponse })
  801.                 },
  802.                 error => {
  803.                     console.error(error)
  804.                     res.status(500).send()
  805.             })
  806.     }
  807.  
  808.     private fillHash(req: AuthRequest, res: Response) {
  809.         console.log('ContactsController.fillHash()')
  810.         const params: any = [];
  811.         const queries: any = [];
  812.         GetConnectionObservable(this.db)
  813.             .concatMap(connection => {
  814.                 return QueryObservable<Contact[]>(connection,
  815.                 'SELECT c.id, c.updateTs as uts, c.deleteTs as d, c.localId as lid, name as n,' +
  816.                 '  cl.value as lk, c.public as p, unique_hash as uh' +
  817.                 ' FROM contacts c' +
  818.                 '  LEFT JOIN contact_likes cl ON c.id = cl.contactId' +
  819.                 ' AND deleteTs is null' +
  820.                 ' ORDER BY updateTs',
  821.                 [])
  822.                 .concatMap(contacts => attachContactDetails(connection, contacts, null, true, true))
  823.                 .concatMap(dbContacts => {
  824.                     dbContacts.map(contact => {
  825.                         queries.push('UPDATE contacts SET unique_hash = ? WHERE id = ?');
  826.                         params.push(this.createHash(contact), contact.id)
  827.                     })
  828.                     console.log(queries.length)
  829.                     Observable.of({
  830.                         queries,
  831.                         params
  832.                     }).concatMap(q => QueryObservable(connection,
  833.                         q.queries.join(';'),
  834.                         q.params))
  835.                     .subscribe()
  836.                    return dbContacts;
  837.                 })
  838.                 .do(_ => connection.release())
  839.                 .catch(error => {
  840.                     connection.release();
  841.                     return Observable.throw(error);
  842.                 })
  843.             })
  844.             .toArray()
  845.             .subscribe(
  846.                 _ => {
  847.                     res.send({ result: 'OK' })
  848.                 },
  849.                 error => {
  850.                     console.error(error)
  851.                     res.status(500).send()
  852.             })
  853.     }
  854.     private deleteDuplicates(req: AuthRequest, res: Response) {
  855.         console.log('ContactsController.deleteDuplicates()')
  856.         const params: any = [];
  857.         const queries: any = [];
  858.         GetConnectionObservable(this.db)
  859.             .concatMap(connection => {
  860.                 return QueryObservable<User[]>(connection, 'SELECT * from users', [])
  861.                     .concatMap(dbUsers => Observable.from(dbUsers)
  862.                         .concatMap(user => QueryObservable<Contact[]>(connection,
  863.                             'SELECT c.id, c.updateTs as uts, c.deleteTs as d, c.localId as lid, name as n,' +
  864.                             '  cl.value as lk, c.public as p, unique_hash as uh' +
  865.                             ' FROM contacts c' +
  866.                             '  LEFT JOIN contact_likes cl ON c.id = cl.contactId' +
  867.                             ' AND deleteTs is null' +
  868.                             ' WHERE holderId = ?' +
  869.                             ' ORDER BY updateTs',
  870.                             [user.contactId])
  871.                             .concatMap(contacts => attachContactDetails(connection, contacts, null, true, true))
  872.                             .concatMap(dbContacts => {
  873.                                 if(dbContacts.length) {
  874.                                     dbContacts.forEach((contact, index) => {
  875.                                         const duplicates = dbContacts.filter(val => val.uh === contact.uh)
  876.                                         if(duplicates.length > 1) {
  877.                                             if(contact.cc && !contact.cc.length) {
  878.                                                 queries.push(
  879.                                                     'DELETE FROM contacts WHERE id = ?',
  880.                                                     'DELETE FROM contact_notes WHERE contactId = ?',
  881.                                                     'DELETE FROM contact_phones WHERE contactId = ?',
  882.                                                     'DELETE FROM contact_emails WHERE contactId = ?'
  883.                                                 );
  884.                                                 params.push(contact.id, contact.id, contact.id, contact.id)
  885.                                             }
  886.                                         }
  887.                                         dbContacts.splice(index, 1)
  888.                                     })
  889.                                 }
  890.                                return dbContacts;
  891.                             })
  892.                         )
  893.                     )
  894.                     .last()
  895.                     .do(_ => {
  896.                         if(queries.length && params.length){
  897.                             Observable.of({ queries, params })
  898.                             .concatMap(q => QueryObservable(connection, q.queries.join(';'), q.params))
  899.                             .subscribe()
  900.                         }
  901.                         connection.release();
  902.                     })
  903.                     .catch(error => {
  904.                         connection.release();
  905.                         return Observable.throw(error);
  906.                    })
  907.             })
  908.             .toArray()
  909.             .subscribe(
  910.                 _ => {
  911.                     res.send({ result: 'OK' })
  912.                 },
  913.                 error => {
  914.                     console.error(error)
  915.                     res.status(500).send()
  916.             })
  917.     }
  918. }
  919. type SyncOperationResponse = {
  920.     operation: string,
  921.     contact: Contact
  922. }
  923.  
  924. type SyncOperationData = {
  925.     queries: string[],
  926.     params: any[],
  927.     result?: any,
  928. }
  929.  
  930. const toDecimal = (num: number | null) =>
  931.     num == null ? null : Math.round(num * 1000000) / 1000000;
  932.  
  933. export const loadContactPhones = (connection: Connection, id: string, includeGsr: boolean) =>
  934.     QueryObservable<ContactPhone[]>(connection,
  935.         'SELECT id, label as l, value as v, valid as vl, country as c,' +
  936.         ' countryCode as cc, locality as lc, latitude as lt, longitude as ln' +
  937.         ' FROM contact_phones WHERE contactId = ?', [id])
  938.         .concatMap(phones => Observable.from(phones)
  939.             .map(phone => {
  940.                 phone.vl = phone.vl === true
  941.                 return phone
  942.             })
  943.             .concatMap(phone => !includeGsr
  944.                 ? Observable.of(phone)
  945.                 : QueryObservable<GSearchResult[]>(connection,
  946.                     'SELECT rank_ as r, uri as u, title as t, description as d' +
  947.                     ' FROM gsearch_results' +
  948.                     ' WHERE phoneId = ?',
  949.                     [phone.id])
  950.                     .map(gsresults => {
  951.                         phone.gsr = gsresults
  952.                         return phone
  953.                     }))
  954.             .toArray())
  955.  
  956. export const loadContactEmails = (connection: Connection, id: string) =>
  957.     QueryObservable<ContactEmail[]>(connection,
  958.         'SELECT label as l, value as v FROM contact_emails WHERE contactId = ?', [id])
  959.  
  960. export const loadUserContacts = (connection: Connection, user: User, since: number, limit: number) =>
  961.     QueryObservable<Contact[]>(connection,
  962.         'SELECT c.id, c.updateTs as uts, c.deleteTs as d, c.localId as lid, name as n,' +
  963.         '  cl.value as lk, c.public as p' +
  964.         ' FROM contacts c' +
  965.         '  LEFT JOIN contact_likes cl ON c.id = cl.contactId AND cl.userId = ?' +
  966.         ' WHERE holderId = ?' +
  967.         '   AND updateTs > ?' +
  968.         ' ORDER BY updateTs' +
  969.         ' LIMIT ?',
  970.         [user.id, user.contactId, since, limit || 1000])
  971.         .concatMap(contacts => Observable.from(contacts)
  972.             .map(contact => {
  973.                 contact.p = !!contact.p
  974.                 contact.d = contact.d != null ? true : undefined
  975.                 return contact
  976.             })
  977.             .toArray())
  978.         .concatMap(contacts => attachContactDetails(connection, contacts, null, true, true))
  979.  
  980. const attachContactDetails = (connection: Connection, contacts: Contact[], authorId: string | null,
  981.     includeGsr: boolean | 'if public', includeNodes: boolean) => Observable.from(contacts)
  982.         .map(contact => {
  983.             if (contact.lk != null)
  984.                 contact.lk = !!contact.lk
  985.             return contact;
  986.         })
  987.         .concatMap(contact => loadContactPhones(connection, contact.id,
  988.             includeGsr === 'if public' && contact.p === true || includeGsr as boolean)
  989.             .map(phones => {
  990.                 contact.ps = phones
  991.                 return contact
  992.             }))
  993.         .concatMap(contact => loadContactEmails(connection, contact.id)
  994.             .map(emails => {
  995.                 contact.es = emails
  996.                 return contact
  997.             }))
  998.         .concatMap(contact => !includeNodes
  999.             ? Observable.of(contact)
  1000.             : loadContactNotes(connection, contact.id)
  1001.                 .map(notes => {
  1002.                     contact.ns = notes
  1003.                     return contact
  1004.                 }))
  1005.         .concatMap(contact => loadContactComments(connection, authorId == null ? '' : authorId, contact.id)
  1006.                 .map(comment => {
  1007.                     contact.cc = comment
  1008.                     return contact
  1009.                 }))
  1010.         .toArray()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement