Skip to content

Instantly share code, notes, and snippets.

@gauchy
Last active April 24, 2024 03:31
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save gauchy/3866fa059eecd2081ec92bea07a4a29f to your computer and use it in GitHub Desktop.
Save gauchy/3866fa059eecd2081ec92bea07a4a29f to your computer and use it in GitHub Desktop.
Notion to Habitica Sync tool
import requests, json
#Notion's token and databaseId
token = 'XXX'
databaseId = 'XXX'
#Notion's headers
headers = {
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
"Notion-Version": "2021-05-13"
}
#Habitica's headers
headersHabitica = {
"x-api-user": "XXX",
"x-api-key": "XXX"
}
#Completed Status
def completed():
return 'Completed';
#Prefix to the task when created in Habitica
def prefixChar():
return "N2H_"
def readDatabaseOfNotion(databaseId, headers):
readUrl = f"https://api.notion.com/v1/databases/{databaseId}/query"
res = requests.request("POST", readUrl, headers=headers)
data = res.json()
print(res.status_code)
# print(res.text)
with open('./notion.json', 'w', encoding='utf8') as f:
json.dump(data, f, ensure_ascii=False)
def readHabiticaData(headersHabitica):
url = "https://habitica.com/api/v3/tasks/user?type=todos"
res = requests.request("GET", url, headers=headersHabitica)
data = res.json()
print(res.status_code)
# print(res.text)
with open('./habitica.json', 'w', encoding='utf8') as f:
json.dump(data, f, ensure_ascii=False)
def readHabiticaDoneData(headersHabitica):
url = "https://habitica.com/api/v3/tasks/user?type=completedTodos"
res = requests.request("GET", url, headers=headersHabitica)
data = res.json()
print(res.status_code)
# print(res.text)
with open('./habitica_done.json', 'w', encoding='utf8') as f:
json.dump(data, f, ensure_ascii=False)
def createTodoInHabitica(name, headersHabitica):
name = prefixN2H(name)
url = "https://habitica.com/api/v3/tasks/user"
res = requests.post(url, headers=headersHabitica, json={"text": name, "type": "todo", "priority":"2"})
data = res.json()
#print(res.text)
def scoreTaskInHabitica(id):
url = f"https://habitica.com/api/v3/tasks/{id}/score/up"
res = requests.post(url, headers=headersHabitica)
print(res.status_code)
def scoreTaskInNotion(name, headers):
url = f"https://api.notion.com/v1/pages/{name}"
res = requests.patch(url, headers=headers, json={"properties": {"Status" : {"select" :{"name": completed()}}}})
data = res.json()
#print(res.text)
print(res.status_code)
def prefixN2H(taskName):
return prefixChar()+taskName
def isAbsentInHabitica(taskName, habiticaList):
taskName = prefixN2H(taskName)
for i in habiticaList:
if taskName == i['name']:
return False
return True
def getHabiticaList(habitica_file):
lst = []
f = open(habitica_file,encoding="utf8")
data = json.load(f)
for i in data['data']:
name = i['text']
id = i['id']
dict = {'name':name , 'id':id}
lst.append(dict)
f.close()
return lst
def getNotionList(condn):
lst = []
f = open("notion.json")
data = json.load(f)
for i in data['results']:
name = i['properties']['Name']['title'][0]['text']['content']
status = i['properties']['Status']['select']['name']
id = i['id']
if condn(status):
dict = {'name':name , 'id':id}
lst.append(dict)
f.close()
return lst
def notionDoneCondn(status):
return status == completed()
def notionNotDoneCondn(status):
return status != completed()
def getDoneListOfNotion():
return getNotionList(notionDoneCondn)
def getNotDoneListOfNotion():
return getNotionList(notionNotDoneCondn)
def getTaskId(name, list):
for i in list:
if name == i['name']:
return i['id']
def syncNotionToHabitica():
print('==========================')
print('Syncing Notion to Habitica')
print('==========================')
habiticaList = getHabiticaList("habitica.json")
notionDoneList = getDoneListOfNotion()
for task in notionDoneList:
print('Processing completed Notion Task in Habitica ' + task['name'])
name = prefixN2H(task['name'])
habiticaid = getTaskId(name,habiticaList)
if habiticaid is not None:
print('Scoring in Habitica for ' + name + ':' + habiticaid)
scoreTaskInHabitica(habiticaid)
notionNotDoneList = getNotDoneListOfNotion()
for task in notionNotDoneList:
print('Processing Incomplete Notion Task ' + task['name'])
if isAbsentInHabitica(task['name'],habiticaList):
print('Missing in Habitica, Creating '+ task['name'] )
createTodoInHabitica(task['name'], headersHabitica)
def syncHabiticaToNotion():
print('==========================')
print('Syncing Habitica to Notion')
print('==========================')
notionNotDoneList = getNotDoneListOfNotion()
habiticaDoneList = getHabiticaList("habitica_done.json")
for task in notionNotDoneList:
try:
print('Processing Notion task ' + task['name'])
habitica_name = prefixN2H(task['name'])
for i in habiticaDoneList :
if habitica_name == i['name']:
print('Scoring in Notion for ' + habitica_name + ':' + task['id'])
scoreTaskInNotion(task['id'], headers)
except Exception as e:
#print(e)
print('Cannot score : Old or absent Habitica todo ' + task['name'])
def readDB():
print('==========================')
print('Reading data')
print('==========================')
print('Reading Notion Data')
readDatabaseOfNotion(databaseId, headers)
print('Reading Habitica Data')
readHabiticaData(headersHabitica)
print('Reading Habitica Done Data')
readHabiticaDoneData(headersHabitica)
readDB()
syncNotionToHabitica()
syncHabiticaToNotion()
@gauchy
Copy link
Author

gauchy commented Nov 15, 2021

Description:

  1. notion2Habitica.py script takes care of adding 'Todo' in to Habitica whenever a new Task is added to Notion database. (one way sync - task created in Habitica can't be auto-created in Notion)
  2. it checks off the task in Habitica if similar task is marked as Completed in Notion and vice-versa ( two way sync)

Steps needed in Notion -

  1. Create an API integration in Notion - https://www.notion.so/my-integrations. Take note of the 'Internal Integration Token' that will be generated on creating integration
  2. In Notion, select the database that you want to sync with Habitica. Click Share database ( in settings) and choose to Share with API integration that you created.
  3. Take note of the databaseId of the database you are choosing to sync with Habitica.

You will find good documentation on all of the above steps here - https://developers.notion.com/docs/getting-started#share-a-database-with-your-integration

Steps needed in Habitica

  1. Take note of user and api token from you user profile page. Link - https://habitica.fandom.com/wiki/API_Options#API_Token
    Note: Habitica userId is UUID and not same has user handle.

Assumptions in the script
The script makes two assumptions -

  1. There is "Name" 'text' field in the database that can be used to create Todo task in Habitica.
  2. There is "Status" 'select' field that has different statuses of your task for e.g. NotStarted, InProgress, Completed etc. The script just needs 'Completed' status in it's logic.
    Note1: if your database has different field than Name/Status then you can change function getNotionList() and scoreTaskInNotion() to set field names of your choice. Make sure that the field type (for e.g. status is 'select' type in the above script) is set correctly if you are changing field name & type. More can be read about this here - https://developers.notion.com/reference/database
    Note2: if you wish to set a different 'status' that 'Completed' that should be used to determine if a task is done or not then change the function "completed()" to set the final state of your choice.

Setup

  1. Update 'token' variable with notion secret generate while creating API integration
  2. Update 'databaseId' variable of the database of Notion.
  3. Update habitica userid in 'x-api-user' variable
  4. update habitica token in 'x-api-key' variable.

How to run
python notion2Habitica.py

Further integration
Windows task scheduler or Linux CRON can be used to run the script at regular interval say every 1 hour.

@hackbraten68
Copy link

great work mate, will give this a try tonight !

@JeffHCross
Copy link

This was great! One change that I had to make (and I suspect I won't be alone) is in line 95 I had to change the open to f = open("notion.json", encoding='utf-8'). This was to handle the emojis that Notion often has in their default templates (such as Roadmap).

@gauchy
Copy link
Author

gauchy commented Jan 5, 2022

thanks, updated!

@lynchrobo
Copy link

Fantastic! Thanks for making this, it was exactly what I needed.

@Hydrock
Copy link

Hydrock commented May 8, 2022

Thank you very much. Everything works. But what's the point if there is no backward compatibility? We moved the tasks to Habotika, marked them as done, but nothing has been updated in Notion

What's the point of doing this?

@elliotfontaine
Copy link

May I suggest you replace status = i['properties']['Status']['select']['name'] by status = i['properties']['Status']['status']['name'] in line 100 ? I guess the Notion API changed its nomenclature since you wrote this script.

To add to what Hydrock said, do you think it could even be possible to sync back, Habitica2Notion ? I've got no experience with their API.

@gauchy
Copy link
Author

gauchy commented Nov 6, 2022

@elliotfontaine - i see according to the official documentation this is right -> status = i['properties']['Status']['select']['name']

It has been a long time since i update this gist. I will try to create both way syncing between notion and habitica, perhaps within a week from now.

@elliotfontaine
Copy link

elliotfontaine commented Nov 6, 2022

@gauchy Well, it didn't work with 'select', I had to change it to 'status' for the script to work. What led me to the answer was the structure of the json files created by the script.

EDIT: I would be so glad if you managed to make two-way syncing! For me the best way would be to change the name of the new task to sync on both databases, adding your current N2H_ prefix. Then, if you detect one of the two is missing (either by keeping track of the tasks in a json or simply by checking if there is a lonely N2H_ task), you would ✅ the task on both DB.
I have no experience with web services yet, but I'm eager to learn so if you need any help with this little project just ask me :)

@gauchy
Copy link
Author

gauchy commented Nov 7, 2022

@elliotfontaine - did a few changes for two way sync, have a look if it works.
Why status is working for you instead of select is perhaps because the property type you have chosen is of 'status' type not 'select'. See here - https://developers.notion.com/reference/property-object

@callumzhong
Copy link

callumzhong commented Nov 12, 2022

The following is machine translation, English is not my native language.

Great project 👍
two way sync is work, but if the task name is duplicated, there will be problems.

I made some small modifications 62 - 190

  • Notion create new property "HabiticaID" type "text" => Two-way sync key
  • syncNotionToHabitica function check "habitica.json" and "habitica_done.json" for absences
def createTodoInHabitica(name, headersHabitica):
    name = prefixN2H(name)
    url = "https://habitica.com/api/v3/tasks/user"

    res = requests.post(url, headers=headersHabitica, json={"text": name,"type": "todo", "priority":'2'})
    data = res.json()
    # print(res.text)
    return data['data']['id']

def updateNotionInHabiticaID(pageId,habiticaId,headers):
    url = f"https://api.notion.com/v1/pages/{pageId}"
    res = requests.patch(url, headers=headers, json={"properties": {
        "HabiticaID":{"rich_text":[
            {"type":"text","text":{"content":habiticaId}}
        ]}
    }})
    data =res.json()
    print(res.status_code)

def scoreTaskInHabitica(id):
    url = f"https://habitica.com/api/v3/tasks/{id}/score/up"
    res = requests.post(url, headers=headersHabitica)
    print(res.status_code)
    
def scoreTaskInNotion(name, headers):
    url = f"https://api.notion.com/v1/pages/{name}"
    res = requests.patch(url, headers=headers, json={"properties": {"Status" : {"select" :{"name": completed()}}}})
    data = res.json()
    #print(res.text)
    print(res.status_code)

def prefixN2H(taskName):
    return prefixChar()+taskName

def isAbsentInHabitica(habiticaId, habiticaList,habiticaDoneList):
    isAbsent = True
    for i in habiticaList:
        if habiticaId == i['id']:
            isAbsent = False
    for i in habiticaDoneList:
         if habiticaId == i['id']:
            isAbsent = False
    return isAbsent

def getHabiticaList(habitica_file):
    lst = []
    f = open(habitica_file,encoding="utf8")
    data = json.load(f)

    for i in data['data']:
        name = i['text']
        id = i['id']
        dict = {'name':name , 'id':id}
        lst.append(dict)
    f.close()
    return lst

def getNotionList(condn):
    lst = []
    f = open("notion.json")
    data = json.load(f)

    for i in data['results']:
        name = i['properties']['Name']['title'][0]['text']['content']
        status = i['properties']['Status']['select']['name']

        temp = i['properties']['HabiticaID']['rich_text']
        if not temp:
            habiticaId = ''
        else:
            habiticaId = temp[0]['text']['content']
        
        id = i['id']
        if condn(status):
            dict = {'name':name , 'id':id, 'HabiticaID':habiticaId}
            lst.append(dict)
    f.close()
    return lst


def notionDoneCondn(status):
    return status == completed()


def notionNotDoneCondn(status):
    return status != completed()

def getDoneListOfNotion():
    return getNotionList(notionDoneCondn)


def getNotDoneListOfNotion():
    return getNotionList(notionNotDoneCondn)


def getTaskId(habiticaId, list):
    for i in list:
        if habiticaId == i['id']:
            return i['id']


def syncNotionToHabitica():
    print('==========================')
    print('Syncing Notion to Habitica')
    print('==========================')
    habiticaList = getHabiticaList("habitica.json")
    habiticaDoneList = getHabiticaList("habitica_done.json")
    notionDoneList = getDoneListOfNotion()

    for task in notionDoneList:
        print('Processing completed Notion Task in Habitica ' + task['name'])
        name = prefixN2H(task['name'])
        habiticaId = getTaskId(task['HabiticaID'],habiticaList)
        if habiticaId is not None:
            print('Scoring in Habitica for ' + name + ':' + habiticaId)
            scoreTaskInHabitica(habiticaId)


    notionNotDoneList = getNotDoneListOfNotion()

    for task in notionNotDoneList:
        print('Processing Incomplete Notion Task ' + task['name'])
        if isAbsentInHabitica(task['HabiticaID'],habiticaList,habiticaDoneList):
            print('Missing in Habitica, Creating '+ task['name'] )
            habiticaId = createTodoInHabitica(task['name'], headersHabitica)
            print(f'updated HabiticaID in Notion,  '+ task['name'] )
            updateNotionInHabiticaID(task['id'], habiticaId, headers)

def syncHabiticaToNotion():
    print('==========================')
    print('Syncing Habitica to Notion')
    print('==========================')
    notionNotDoneList = getNotDoneListOfNotion()
    habiticaDoneList = getHabiticaList("habitica_done.json")
    for task in notionNotDoneList:
        try:
            print('Processing Notion task ' + task['name'])
            habitica_name = prefixN2H(task['name'])
            habiticaId = task['HabiticaID']
            for i in habiticaDoneList :
                if habiticaId == i['id'] :
                    print('Scoring in Notion for ' + habitica_name + ':' + task['id'])
                    scoreTaskInNotion(task['id'], headers)
            
        except Exception as e:
            #print(e)
            print('Cannot score : Old or absent Habitica todo ' + task['name'])

@vandaref
Copy link

vandaref commented Jan 19, 2024

Hi there, I updated some steps of the tutorial like adding the integration and add few code inside like the difficulty of the task in Habitica depends on the priority in Notion or removing the prefix.
https://github.com/vandaref/from_notion_to_habitica/tree/main
My bad I forgot to make a fork of your work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment