├── .gitignore ├── README.md └── googlecontacts.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.dat 2 | *.json 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gcontacts-asterisk 2 | ================== 3 | 4 | Google Contacts to Asterisk Phonebook 5 | ------------------------------------- 6 | 7 | An enhancement/continuation of [this script at Nerdvittles](http://pbxinaflash.com/community/index.php?threads/google-contacts-to-asterisk-phonebook.10943/). 8 | 9 | Edit CLIENT_SECRETS_JSON in googlecontacts.py with the name of your client secrets file from Google Developers Console. 10 | 11 | If running on a remote machine, use `--noauth_local_webserver` for the initial auth flow. 12 | -------------------------------------------------------------------------------- /googlecontacts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Original By: John Baab 3 | # Email: rhpot1991@ubuntu.com 4 | # Updated By: Jay Schulman 5 | # Email: info@jayschulman.com 6 | # Updated Again By: Philip Rosenberg-Watt 7 | # And a little again by: Vicente Monroig (vmonroig@digitaldisseny.com) 8 | # Purpose: syncs contacts from google to asterisk server 9 | # Updates: Updating for Google API v3 with support for Google Apps 10 | # OAuth2 auth flow and tokens 11 | # Requirements: python, gdata python client, asterisk 12 | # 13 | # License: 14 | # 15 | # This Package is free software; you can redistribute it and/or 16 | # modify it under the terms of the GNU General Public 17 | # License as published by the Free Software Foundation; either 18 | # version 3 of the License, or (at your option) any later version. 19 | # 20 | # This package is distributed in the hope that it will be useful, 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 23 | # General Public License for more details. 24 | # 25 | # You should have received a copy of the GNU General Public 26 | # License along with this package; if not, write to the Free Software 27 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 28 | # 29 | # On Debian & Ubuntu systems, a complete copy of the GPL can be found under 30 | # /usr/share/common-licenses/GPL-3, or (at your option) any later version 31 | 32 | import atom,re,sys,os 33 | import json 34 | import gdata.data 35 | import gdata.auth 36 | import gdata.contacts 37 | import gdata.contacts.client 38 | import gdata.contacts.data 39 | import argparse 40 | import unicodedata 41 | from oauth2client import client 42 | from oauth2client import file 43 | from oauth2client import tools 44 | 45 | # Native application Client ID JSON from the Google Developers Console, 46 | # store in the same directory as this script: 47 | CLIENT_SECRETS_JSON = 'client_secret_XXXXXXXXXXX-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy.apps.googleusercontent.com.json' 48 | 49 | 50 | parent_parsers = [tools.argparser] 51 | parser = argparse.ArgumentParser(parents=parent_parsers) 52 | parser.add_argument("--allgroups", help="only works in combination with --group to show members with multiple groups", action="store_true", default=False) 53 | parser.add_argument("--anygroup", help="show members of any user-created group (not My Contacts), OVERRIDES other options", action="store_true", default=False) 54 | parser.add_argument("--asterisk", help="send commands to Asterisk instead of printing to console", action="store_true", default=False) 55 | parser.add_argument("--dbname", help="database tree to use") 56 | parser.add_argument("--delete", help="delete the existing database first", action="store_true", default=False) 57 | parser.add_argument("--group", action="append", help="group name, can use multiple times") 58 | parser.add_argument("--non_interactive", help="abort script if credentials are missing or invalid", action="store_true", default=False) 59 | parser.add_argument("--ascii", help="remove all non-ascii characters from names", action="store_true", default=False) 60 | parser.add_argument("--add_type", help="append ' - type' to name entry", action="store_true", default=False) 61 | args = parser.parse_args() 62 | 63 | 64 | if args.dbname is None: 65 | args.dbname = "cidname" 66 | 67 | phone_map = {2: ["A", "B", "C"], 68 | 3: ["D", "E", "F"], 69 | 4: ["G", "H", "I"], 70 | 5: ["J", "K", "L"], 71 | 6: ["M", "N", "O"], 72 | 7: ["P", "Q", "R", "S"], 73 | 8: ["T", "U", "V"], 74 | 9: ["W", "X", "Y", "Z"]} 75 | 76 | phone_map_one2one = {} 77 | for k, v in phone_map.items(): 78 | for l in v: 79 | phone_map_one2one[l] = str(k) 80 | 81 | 82 | def phone_translate(phone_number): 83 | new_number = "" 84 | for l in phone_number: 85 | if l.upper() in phone_map_one2one.keys(): 86 | new_number += phone_map_one2one[l.upper()] 87 | else: 88 | l = re.sub('[^0-9]', '', l) 89 | new_number += l 90 | if len(new_number) == 11 and new_number[0] == "1": 91 | new_number = new_number[1:] 92 | return new_number 93 | 94 | 95 | def get_auth_token(): 96 | scope = 'https://www.googleapis.com/auth/contacts.readonly' 97 | user_agent = __name__ 98 | client_secrets = os.path.join(os.path.dirname(__file__), CLIENT_SECRETS_JSON) 99 | filename = os.path.splitext(__file__)[0] + '.dat' 100 | 101 | flow = client.flow_from_clientsecrets(client_secrets, scope=scope, message=tools.message_if_missing(client_secrets)) 102 | 103 | storage = file.Storage(filename) 104 | credentials = storage.get() 105 | if credentials is None or credentials.invalid: 106 | if args.non_interactive: 107 | sys.stderr.write('ERROR: Invalid or missing Oauth2 credentials. To reset auth flow manually, run without --non_interactive\n') 108 | sys.exit(1) 109 | else: 110 | credentials = tools.run_flow(flow, storage, args) 111 | 112 | j = json.loads(open(filename).read()) 113 | 114 | return gdata.gauth.OAuth2Token(j['client_id'], j['client_secret'], scope, user_agent, access_token = j['access_token'], refresh_token = j['refresh_token']) 115 | 116 | 117 | def add_to_asterisk(dbname, cid, name): 118 | command = "asterisk -rx \'database put " + dbname + " " + cid + " \"" + name + "\"\'" 119 | if args.asterisk: 120 | os.system(command.encode('utf8')) 121 | else: 122 | print command.encode('utf8') 123 | 124 | def main(): 125 | # Change this if you aren't in the US. If you have more than one country code in your contacts, 126 | # then use an empty string and make sure that each number has a country code. 127 | country_code = "" 128 | 129 | token = get_auth_token() 130 | gd_client = gdata.contacts.client.ContactsClient() 131 | gd_client = token.authorize(gd_client) 132 | qry = gdata.contacts.client.ContactsQuery(max_results=2000) 133 | feed = gd_client.GetContacts(query=qry) 134 | 135 | groups = {} 136 | gq = gd_client.GetGroups() 137 | for e in gq.entry: 138 | striptext = "System Group: " 139 | groupname = e.title.text 140 | if groupname.startswith(striptext): 141 | groupname = groupname[len(striptext):] 142 | groups[e.id.text] = groupname 143 | 144 | # delete all of our contacts before we refetch them, this will allow deletions 145 | if args.delete: 146 | command = "asterisk -rx \'database deltree %s\'" % args.dbname 147 | if args.asterisk: 148 | os.system(command) 149 | else: 150 | print command 151 | 152 | # for each phone number in the contacts 153 | for i, entry in enumerate(feed.entry): 154 | 155 | glist = [] 156 | for grp in entry.group_membership_info: 157 | glist.append(groups[grp.href]) 158 | 159 | name = None 160 | 161 | if entry.organization is not None and entry.organization.name is not None: 162 | name = entry.organization.name.text 163 | 164 | if entry.title.text is not None: 165 | name = entry.title.text 166 | 167 | if entry.nickname is not None: 168 | name = entry.nickname.text 169 | 170 | for r in entry.relation: 171 | if r.label == "CID": 172 | name = r.text 173 | break 174 | 175 | for phone in entry.phone_number: 176 | 177 | if phone.text is None: 178 | sys.stderr.write("ERROR: The following entry has no phone.text value:\n") 179 | sys.stderr.write(str(entry) + "\n") 180 | sys.stderr.write("The script is unable to proceed without a phone number.") 181 | exit(1) 182 | 183 | # Strip out any non numeric characters and convert to UTF-8 184 | # phone.text = re.sub('[^0-9]', '', phone.text) 185 | phone.text = phone.text.encode('utf-8') 186 | phone.text = phone_translate(phone.text) 187 | 188 | # Remove leading digit if it exists, we will add this again later for all numbers 189 | # Only if a country code is defined. 190 | if country_code != "": 191 | phone.text = re.compile('^\+?%s' % country_code, '', phone.text) 192 | 193 | phone.text = country_code + phone.text 194 | 195 | if name is None: 196 | sys.stderr.write("ERROR: The following entry has no way to determine a name:\n") 197 | sys.stderr.write(str(entry) + "\n") 198 | sys.stderr.write("Please fix this entry and re-run the script.\n") 199 | break 200 | 201 | name = name.replace('\'','') 202 | name = name.replace('"','') 203 | 204 | suffix = "" 205 | 206 | if phone.rel is not None: 207 | rel = phone.rel.split("#") 208 | 209 | if args.add_type: 210 | suffix = " - " + rel[-1] 211 | 212 | if args.ascii: 213 | if not isinstance(name, bytes): 214 | name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore') 215 | if not isinstance(suffix, bytes): 216 | suffix = unicodedata.normalize('NFKD', suffix).encode('ascii', 'ignore') 217 | 218 | 219 | if args.anygroup: 220 | if (("My Contacts" in glist and len(glist) > 1) or 221 | ("My Contacts" not in glist and len(glist) > 0)): 222 | add_to_asterisk(args.dbname, phone.text, name + suffix) 223 | else: 224 | if args.group: 225 | if args.allgroups: 226 | if set(args.group).issubset(glist): 227 | add_to_asterisk(args.dbname, phone.text, name + suffix) 228 | else: 229 | for g in args.group: 230 | if g in glist: 231 | add_to_asterisk(args.dbname, phone.text, name + suffix) 232 | else: 233 | add_to_asterisk(args.dbname, phone.text, name + suffix) 234 | 235 | 236 | if __name__ == '__main__': 237 | main() 238 | --------------------------------------------------------------------------------