Skip to content

Instantly share code, notes, and snippets.

@allandequeiroz
Last active August 5, 2020 20:12
Show Gist options
  • Select an option

  • Save allandequeiroz/2ac0461cf254b30a72a7dd443db0c816 to your computer and use it in GitHub Desktop.

Select an option

Save allandequeiroz/2ac0461cf254b30a72a7dd443db0c816 to your computer and use it in GitHub Desktop.

Revisions

  1. Allan de Queiroz revised this gist Aug 5, 2020. 1 changed file with 79 additions and 25 deletions.
    104 changes: 79 additions & 25 deletions HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -20,6 +20,7 @@ Maintainer: Allan de Queiroz
    */
    // -------------------------------------------------------------------------------

    // -------------------------------------------------------------------------------
    function Activity(tempo) {
    this.tempo = tempo;

    @@ -30,6 +31,9 @@ function Activity(tempo) {
    this.getName = function () {
    return this.name;
    }
    this.getType = function () {
    return this.type;
    }
    this.getTags = function () {
    return this.tags;
    }
    @@ -54,6 +58,9 @@ function Activity(tempo) {
    this.getFormatedDueDate = function () {
    return this.tempo.formatDate(this.dueDate);
    }
    this.getParent = function () {
    return this.parent;
    }

    // SET
    this.setId = function (id) {
    @@ -62,6 +69,9 @@ function Activity(tempo) {
    this.setName = function (name) {
    this.name = name;
    }
    this.setType = function (type) {
    this.type = type;
    }
    this.setTags = function (tags) {
    tags.forEach(function (tag) {
    addTag(tag)
    @@ -79,6 +89,9 @@ function Activity(tempo) {
    this.setDueDate = function (dueDate) {
    this.dueDate = dueDate;
    }
    this.setParent = function (parent) {
    this.parent = parent;
    }

    // MISC
    this.addTag = function (tag) {
    @@ -276,24 +289,35 @@ const HabiticaActivity = (habiticaTag, habiticaRequest, tempo) => {
    let activeTasks = new Map();
    let completedTasks = new Set();

    const computeIfTraceable = (todo, activeTasksLocal) => {
    if (todo["tags"] && todo["tags"].includes(habiticaTag.getDefaultTagId())) {
    let activeActivity = new Activity(tempo);
    activeActivity.setId(todo["id"]);
    activeActivity.setName(todo["text"]);
    activeActivity.setType(todo['type']);
    activeActivity.setCreatedAt(new Date(todo["createdAt"]));
    if (todo["tags"]) {
    todo["tags"].forEach(function (tag) {
    activeActivity.addTag(tag);
    });
    }
    if (todo["date"]) {
    activeActivity.setDueDate(new Date(todo["date"]));
    }
    if (todo['notes']) {
    activeActivity.setNotes(todo["notes"]);
    }
    activeTasksLocal.set(activeActivity.getName(), activeActivity);
    }
    }

    const fetchActivities = () => {
    const activeTasksLocal = new Map();
    habiticaRequest.performGet("https://habitica.com/api/v3/tasks/user?type=todos").forEach(function (todo) {
    if (todo["tags"] && todo["tags"].includes(habiticaTag.getDefaultTagId())) {
    let activeActivity = new Activity(tempo);
    activeActivity.setId(todo["id"]);
    activeActivity.setName(todo["text"]);
    activeActivity.setCreatedAt(new Date(todo["createdAt"]));
    if (todo["tags"]) {
    todo["tags"].forEach(function (tag) {
    activeActivity.addTag(tag);
    });
    }
    if (todo["date"]) {
    activeActivity.setDueDate(new Date(todo["date"]));
    }
    activeTasksLocal.set(activeActivity.getName(), activeActivity);
    }
    computeIfTraceable(todo, activeTasksLocal);
    });
    habiticaRequest.performGet("https://habitica.com/api/v3/tasks/user?type=dailys").forEach(function (todo) {
    computeIfTraceable(todo, activeTasksLocal);
    });
    activeTasks = activeTasksLocal;
    }
    @@ -330,7 +354,7 @@ const HabiticaActivity = (habiticaTag, habiticaRequest, tempo) => {

    // -------------------------------------------------------------------------------

    const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) => {
    const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars, habiticaActive, habiticaCompleted) => {

    let calendarTasks = new Set();
    let calendarEvents = new Set();
    @@ -348,15 +372,17 @@ const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) =>
    if (tempo.getTodayString() === taskActivityDue) {
    let taskActivity = new Activity(tempo);
    taskActivity.setName(task.title);
    taskActivity.setType(taskList.name === "Dailies" ? "daily" : "todo");
    taskActivity.addTag(habiticaTag.getDefaultTagId());
    taskActivity.addTag(habiticaTag.getTagIdByName(taskList.title));
    taskActivity.setNotes(html2Markdown.convert(task.notes));
    taskActivity.setDueDate(new Date(task.due));
    taskActivity.setParent(taskList.id);
    calendarTasksLocal.add(taskActivity);
    if (habiticaCompleted.has(task.title)) {

    if (habiticaCompleted.has(task.title)) {
    task.setStatus("completed");
    Tasks.Tasks.update(task, taskList.id, task.id);
    let result = Tasks.Tasks.update(task, taskList.id, task.id);
    }
    }
    }
    @@ -376,6 +402,7 @@ const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) =>
    for (const event of calendarEvents.entries()) {
    let calendarActivity = new Activity(tempo);
    calendarActivity.setName(event[1].getTitle());
    calendarActivity.setType(calendarName === "Dailies" ? "daily" : "todo");
    calendarActivity.addTag(habiticaTag.getDefaultTagId());
    calendarActivity.addTag(habiticaTag.getTagIdByName(calendarName));
    calendarActivity.setNotes(html2Markdown.convert(event[1].getDescription()));
    @@ -401,19 +428,21 @@ const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) =>

    // -------------------------------------------------------------------------------

    function HabiticaReminders() {

    const observedCalendars = new Set(["Birthdays", "Birthdays External", "Appointments", "Morning", "Work", "Personal", "Gantter", "2020", "After Work"]);
    function SynchronizeActivities() {

    const observedCalendars = new Set(["Birthdays", "Birthdays External", "Appointments", "Work", "Personal", "Dailies", "Gantter", "2020"]);

    const habiticaRequest = HabiticaRequest();
    const habiticaTag = HabiticaTag(habiticaRequest, observedCalendars);
    const html2Markdown = Html2Markdown();
    const tempo = Tempo();
    const habiticaActivity = HabiticaActivity(habiticaTag, habiticaRequest, tempo);
    const googleActivity = GoogleActivity(habiticaTag, html2Markdown, tempo, observedCalendars);

    const habiticaActivity = HabiticaActivity(habiticaTag, habiticaRequest, tempo);
    const habiticaActive = habiticaActivity.getActiveTasks();
    const habiticaCompleted = habiticaActivity.getCompletedTasks();

    const googleActivity = GoogleActivity(habiticaTag, html2Markdown, tempo, observedCalendars, habiticaActive, habiticaCompleted);
    const calendarEvents = googleActivity.getGoogleActivities();

    const visitedEvents = new Map();
    @@ -433,17 +462,36 @@ function HabiticaReminders() {
    "text": activity.getName(),
    "notes": activity.getNotes(),
    "date": activity.getDueDate(),
    "type": "todo",
    "type": activity.getType(),
    "priority": "1.5"
    }

    collectedEvents.add(payload);
    }

    collectedEvents = Array.from(collectedEvents).sort((a, b) => (a.date - b.date));
    collectedEvents = Array.from(collectedEvents).sort((a, b) => (a.date - b.date)).reverse();
    for (let activity of collectedEvents) {
    const notesTasks = new Set()
    if (activity["notes"]) {
    activity["notes"].split("\n").forEach(function (note) {
    if (note.trim().startsWith("*")) {
    const payload = {
    "text": note.trim().replace(/^(\s+)?\*(\s+)?/g, "")
    }
    notesTasks.add(payload)
    activity["notes"] = activity["notes"].replace(note, "")
    }
    });
    activity["notes"] = activity["notes"].trim()
    }

    let taskId = habiticaRequest.performPost("https://habitica.com/api/v3/tasks/user", activity)["id"];

    for (let notesTask of notesTasks) {
    habiticaRequest.performPost("https://habitica.com/api/v3/tasks/" + taskId + "/checklist/", notesTask);
    Utilities.sleep(5 * 1000)
    }

    if (visitedEvents.get(activity.text).getTags()) {
    visitedEvents.get(activity.text).getTags().forEach(function (tagId) {
    habiticaRequest.performPost("https://habitica.com/api/v3/tasks/" + taskId + "/tags/" + tagId);
    @@ -459,3 +507,9 @@ function HabiticaReminders() {
    }
    }
    }

    function CleanUpActivities() {
    const habiticaRequest = HabiticaRequest();
    habiticaRequest.performPost("https://habitica.com/api/v3/tasks/clearCompletedTodos", {});
    }

  2. Allan de Queiroz revised this gist Jul 26, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -356,7 +356,7 @@ const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) =>

    if (habiticaCompleted.has(task.title)) {
    task.setStatus("completed");
    let result = Tasks.Tasks.update(task, taskList.id, task.id);
    Tasks.Tasks.update(task, taskList.id, task.id);
    }
    }
    }
  3. Allan de Queiroz revised this gist Jul 26, 2020. 1 changed file with 5 additions and 0 deletions.
    5 changes: 5 additions & 0 deletions HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -353,6 +353,11 @@ const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) =>
    taskActivity.setNotes(html2Markdown.convert(task.notes));
    taskActivity.setDueDate(new Date(task.due));
    calendarTasksLocal.add(taskActivity);

    if (habiticaCompleted.has(task.title)) {
    task.setStatus("completed");
    let result = Tasks.Tasks.update(task, taskList.id, task.id);
    }
    }
    }
    })
  4. Allan de Queiroz revised this gist Jul 24, 2020. 1 changed file with 17 additions and 10 deletions.
    27 changes: 17 additions & 10 deletions HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -359,7 +359,7 @@ const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) =>
    }
    })
    }
    calendarTasks = new Set(Array.from(calendarTasksLocal).reverse());
    calendarTasks = calendarTasksLocal;
    }

    const getCalendarActivities = () => {
    @@ -379,7 +379,7 @@ const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) =>
    }
    }
    });
    calendarEvents = new Set(Array.from(calendarEventsLocal).reverse());
    calendarEvents = calendarEventsLocal;
    }

    const getGoogleActivities = () => {
    @@ -396,10 +396,9 @@ const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) =>

    // -------------------------------------------------------------------------------


    function HabiticaReminders() {

    const observedCalendars = new Set(["Gantter", "Birthdays", "Appointments", "Work", "..."]);
    const observedCalendars = new Set(["Birthdays", "Birthdays External", "Appointments", "Morning", "Work", "Personal", "Gantter", "2020", "After Work"]);

    const habiticaRequest = HabiticaRequest();
    const habiticaTag = HabiticaTag(habiticaRequest, observedCalendars);
    @@ -411,10 +410,13 @@ function HabiticaReminders() {
    const habiticaActive = habiticaActivity.getActiveTasks();
    const habiticaCompleted = habiticaActivity.getCompletedTasks();
    const calendarEvents = googleActivity.getGoogleActivities();
    const visitedEvents = new Set();

    const visitedEvents = new Map();
    let collectedEvents = new Set();

    for (let activity of calendarEvents) {
    visitedEvents.add(activity.getName());

    visitedEvents.set(activity.getName(), activity);

    if (habiticaActive.has(activity.getName()) ||
    habiticaCompleted.has(activity.getName())) {
    @@ -430,10 +432,15 @@ function HabiticaReminders() {
    "priority": "1.5"
    }

    let taskId = habiticaRequest.performPost("https://habitica.com/api/v3/tasks/user", payload)["id"];
    collectedEvents.add(payload);
    }

    collectedEvents = Array.from(collectedEvents).sort((a, b) => (a.date - b.date));
    for (let activity of collectedEvents) {

    if (activity.getTags()) {
    activity.getTags().forEach(function (tagId) {
    let taskId = habiticaRequest.performPost("https://habitica.com/api/v3/tasks/user", activity)["id"];
    if (visitedEvents.get(activity.text).getTags()) {
    visitedEvents.get(activity.text).getTags().forEach(function (tagId) {
    habiticaRequest.performPost("https://habitica.com/api/v3/tasks/" + taskId + "/tags/" + tagId);
    Utilities.sleep(5 * 1000)
    });
    @@ -446,4 +453,4 @@ function HabiticaReminders() {
    habiticaRequest.performDelete("https://habitica.com/api/v3/tasks/" + activity[1].getId());
    }
    }
    }
    }
  5. Allan de Queiroz revised this gist Jul 22, 2020. 1 changed file with 0 additions and 5 deletions.
    5 changes: 0 additions & 5 deletions HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -416,11 +416,6 @@ function HabiticaReminders() {
    for (let activity of calendarEvents) {
    visitedEvents.add(activity.getName());

    Logger.log(
    '\nactivity -> ' + activity.getName() +
    '\nactive: ' + habiticaActive.has(activity.getName()) +
    '\ncompleted: ' + habiticaCompleted.has(activity.getName()));

    if (habiticaActive.has(activity.getName()) ||
    habiticaCompleted.has(activity.getName())) {

  6. Allan de Queiroz revised this gist Jul 22, 2020. 1 changed file with 417 additions and 165 deletions.
    582 changes: 417 additions & 165 deletions HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -3,24 +3,6 @@ Maintainer: Allan de Queiroz
    ---
    This script aims to provide synchronism between Habitica's inbox Tasks and Google Calendar. It was heavily inspired by Snickersnacker's one.
    The usage of this one is quite similar to the difference that it adds a few more functionalities. This version of the script enables frequent scheduling of synchronization (if you need of course), handling the creation and also removal of tasks if required.
    __Disclaimer:__ All the tasks handled by this script will be tagged with a gCal tag so that the script won't interfere with the manually created ones.
    The script will perform the following validations/actions:
    ## Tasks Creation
    1. The script will check if a gCalendar event is present at the active list of tasks on Habitica. If the task is already there, it will not add a new one. (that's why it is safe to schedule the executions as often as you want).
    2. If there's no task at the active list, the script will look into the complete list, if the script finds the same task there, it validates the date, if the task were completed today it would not create a new one. Assuming that the task was added to Habitica's list, and tackled already.
    1. In the other hand, when looking into the complete tasks, if the script finds a task with the same title but completed in previous days, it assumes that this is a recurring task, so it creates the new one for that day. (to handle recurring tasks).
    2. One observation here about recurring tasks. By design, no new task will be created at the active inbox if there's already one present, doesn't matter if they were created at previous days. The previous day's rule is only taken into consideration when checking the completed tasks inbox.
    ## Tasks Removal
    The script will check the existing tasks at the active inbox and validate against the ones present at gCalendar, if a task managed by the script is present on Habitica but not on gCalendar anymore; this task will be automatically removed from Habitica's active tasks inbox. The idea here is to keep the tasks inbox fully synchronized with gCalendar, if you remove from your calendar, it will be removed from the tasks lists.
    ## Installation
    1. Replace by habId(line 27) and habToken(line 28) with your ones, you can find them [Here](https://habitica.com/user/settings/api)
    @@ -35,168 +17,438 @@ The script will check the existing tasks at the active inbox and validate agains
    * Select type of time based trigger: Minutes timer
    * Select minute interval: Every 5 minutes
    * Failure notification settings: Notify me imeediately
    */
    // -------------------------------------------------------------------------------

    ## References
    function Activity(tempo) {
    this.tempo = tempo;

    * [The original idea](https://habitica.fandom.com/wiki/Thread:48017)
    * [Gist](https://gist.github.com/allandequeiroz/8a369534aaa58e1a2bcf3c3135120d85)
    * [Google Scripts](https://script.google.com/)
    * [Habitica's API personal credentials](https://habitica.com/user/settings/api)
    */
    // GET
    this.getId = function () {
    return this.id;
    }
    this.getName = function () {
    return this.name;
    }
    this.getTags = function () {
    return this.tags;
    }
    this.getNotes = function () {
    return this.notes;
    }
    this.getCreatedAt = function () {
    return this.createdAt;
    }
    this.getFormatedCreatedAt = function () {
    return this.tempo.formatDate(this.createdAt);
    }
    this.getDateCompleted = function () {
    return this.dateCompleted;
    }
    this.getFormatedDateCompleted = function () {
    return this.tempo.formatDate(this.dateCompleted);
    }
    this.getDueDate = function () {
    return this.dueDate;
    }
    this.getFormatedDueDate = function () {
    return this.tempo.formatDate(this.dueDate);
    }

    // Turn the Date into a nice format for comparison purposes
    const formatDate = function(date) {
    var dd = String(date.getDate()).padStart(2, '0');
    var mm = String(date.getMonth() + 1).padStart(2, '0');
    var yyyy = date.getFullYear();
    return yyyy + '/' + mm + '/' + dd;
    }
    // SET
    this.setId = function (id) {
    this.id = id;
    }
    this.setName = function (name) {
    this.name = name;
    }
    this.setTags = function (tags) {
    tags.forEach(function (tag) {
    addTag(tag)
    });
    }
    this.setNotes = function (notes) {
    this.notes = notes;
    }
    this.setCreatedAt = function (createdAt) {
    this.createdAt = createdAt;
    }
    this.setDateCompleted = function (dateCompleted) {
    this.dateCompleted = dateCompleted;
    }
    this.setDueDate = function (dueDate) {
    this.dueDate = dueDate;
    }

    // Basic requirements to fetch the required data
    const habId = "";
    const habToken = "";
    const defaultTag = "gCal";
    const calendarName = "HabiticaReminders";
    const now = new Date();
    const date = formatDate(now);
    const headers = {
    "x-api-user" : habId,
    "x-api-key" : habToken
    }
    const getRequest = {
    "method" : "get",
    "headers" : headers
    }
    const postRequest = {
    "method" : "post",
    "headers" : headers
    // MISC
    this.addTag = function (tag) {
    if (!this.tags) {
    this.tags = [];
    }
    this.tags.push(tag)
    }
    }
    const deleteRequest = {
    "method" : "delete",
    "headers" : headers

    // -------------------------------------------------------------------------------

    const HabiticaRequest = () => {
    const habId = "";
    const habToken = "";

    const headers = {
    "x-api-user": habId,
    "x-api-key": habToken
    }

    const getRequest = {
    "method": "get",
    "headers": headers
    }

    const postRequest = {
    "method": "post",
    "headers": headers
    }

    const deleteRequest = {
    "method": "delete",
    "headers": headers
    }

    const performGet = (url) => {
    return performRequest(url, getRequest);
    }

    const performPost = (url, payload) => {
    const postPayload = postRequest;
    postPayload["payload"] = payload;
    return performRequest(url, postPayload);
    }

    const performDelete = (url) => {
    return performRequest(url, deleteRequest);
    }

    const performRequest = (url, options) => {
    return JSON.parse(UrlFetchApp.fetch(url, options).getContentText())["data"];
    }

    return {
    performGet,
    performPost,
    performDelete
    };
    }

    // Fetch a list of existing tags from Habitica
    const getTagsMaps = function() {
    let tags = JSON.parse(UrlFetchApp.fetch("https://habitica.com/api/v3/tags", getRequest).getContentText())["data"]
    let idName = new Map()
    let nameId = new Map()
    for (i = 0; i < tags.length; i++) {
    idName.set(tags[i]["id"], tags[i]["name"]);
    nameId.set(tags[i]["name"], tags[i]["id"]);
    }
    return {
    "idName" : idName,
    "nameId" : nameId
    };
    // -------------------------------------------------------------------------------

    const HabiticaTag = (habiticaRequest, observedCalendars) => {
    const defaultTag = "gCal";
    const nameIdTags = new Map()
    const idNameTags = new Map()

    const loadTagsMaps = () => {
    habiticaRequest.performGet("https://habitica.com/api/v3/tags").forEach(function (tag) {
    idNameTags.set(tag["id"], tag["name"]);
    nameIdTags.set(tag["name"], tag["id"]);
    });
    }

    const createTagIfAbsent = (tagName) => {
    if (!nameIdTags.has(tagName)) {
    const payload = {
    "name": tagName
    }
    habiticaRequest.performPost("https://habitica.com/api/v3/tags", payload);
    return true;

    } else {
    return false;
    }
    }

    const createTagsIfAbsent = () => {
    createTagIfAbsent(defaultTag);

    for (let calendar of observedCalendars) {
    createTagIfAbsent(calendar);
    }

    const taskLists = Tasks.Tasklists.list();
    if (taskLists.items) {
    taskLists.items.forEach(function (taskList) {
    if (!observedCalendars.has(taskList.title)) {
    createTagIfAbsent(taskList.title);
    }
    })
    }
    loadTagsMaps();
    }

    const getDefaultTag = () => {
    return defaultTag;
    }

    const getDefaultTagId = () => {
    return getTagIdByName(getDefaultTag());
    }

    const getTagIdByName = (tagName) => {
    return nameIdTags.get(tagName);
    }

    const hasTagNamed = (tagName) => {
    return nameIdTags.has(tagName);
    }

    const getTagNameById = (tagId) => {
    return idNameTags.get(tagId);
    }

    const hasTagId = (tagId) => {
    return idNameTags.has(tagId);
    }

    loadTagsMaps();
    createTagsIfAbsent();

    return {
    getDefaultTag,
    getDefaultTagId,
    getTagIdByName,
    hasTagNamed,
    getTagNameById,
    hasTagId
    }
    }

    // Preparing tags for future validations
    const tags = getTagsMaps();
    const nameIdTags = tags["nameId"];
    const idNameTags = tags["idName"];
    // -------------------------------------------------------------------------------

    /*
    Validate if the tag used to indentify the tasks managed by this script
    exists, if not, creates it before moving on
    */
    const createDefaultTagIfAbsent = function() {
    if(!nameIdTags.has(defaultTag)) {
    var params = postRequest;
    params["payload"] = {
    "name" : defaultTag
    }

    UrlFetchApp.fetch("https://habitica.com/api/v3/tags", params).toString()
    }
    const Html2Markdown = () => {
    const convert = function (html) {
    if (html) {
    var params = {
    "method": "post",
    "payload": {
    "html": html
    }
    }
    return UrlFetchApp.fetch("http://fuckyeahmarkdown.com/go/", params).getContentText();
    }
    }

    return {convert};
    }
    createDefaultTagIfAbsent();

    // Fetch a list of open todos from Habitica
    const getTodosMap = function() {
    let todos = JSON.parse(UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/user?type=todos", getRequest).getContentText())["data"]
    let todosMap = new Map()
    for (i = 0; i < todos.length; i++) {
    if(todos[i]["tags"] && todos[i]["tags"].includes(nameIdTags.get(defaultTag))) {
    todosMap.set(todos[i]["text"], todos[i]["id"]);
    }
    }
    return todosMap;

    // -------------------------------------------------------------------------------

    const Tempo = () => {

    const formatDate = (date) => {
    var dd = String(date.getDate()).padStart(2, '0');
    var mm = String(date.getMonth() + 1).padStart(2, '0');
    var yyyy = date.getFullYear();
    return yyyy + '/' + mm + '/' + dd;
    }

    const todayDate = new Date();
    const todayString = formatDate(todayDate);

    const getTodayDate = () => {
    return todayDate;
    }

    const getTodayString = () => {
    return todayString;
    }

    return {
    formatDate,
    getTodayDate,
    getTodayString
    };
    }

    // Fetch a list of closed todos from Habitica
    const getCompletedTodosMap = function() {
    let completedTodos = JSON.parse(UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/user?type=completedTodos", getRequest).getContentText())["data"]
    let completedTodosMap = new Map()
    for (i = 0; i < completedTodos.length; i++) {
    if(completedTodos[i]["tags"] && completedTodos[i]["tags"].includes(nameIdTags.get(defaultTag))) {
    let dateCompleted = new Date(completedTodos[i]["dateCompleted"]);
    completedTodosMap.set(completedTodos[i]["text"], formatDate(dateCompleted));
    }
    }
    return completedTodosMap;
    // -------------------------------------------------------------------------------

    const HabiticaActivity = (habiticaTag, habiticaRequest, tempo) => {

    let activeTasks = new Map();
    let completedTasks = new Set();

    const fetchActivities = () => {
    const activeTasksLocal = new Map();
    habiticaRequest.performGet("https://habitica.com/api/v3/tasks/user?type=todos").forEach(function (todo) {
    if (todo["tags"] && todo["tags"].includes(habiticaTag.getDefaultTagId())) {
    let activeActivity = new Activity(tempo);
    activeActivity.setId(todo["id"]);
    activeActivity.setName(todo["text"]);
    activeActivity.setCreatedAt(new Date(todo["createdAt"]));
    if (todo["tags"]) {
    todo["tags"].forEach(function (tag) {
    activeActivity.addTag(tag);
    });
    }
    if (todo["date"]) {
    activeActivity.setDueDate(new Date(todo["date"]));
    }
    activeTasksLocal.set(activeActivity.getName(), activeActivity);
    }
    });
    activeTasks = activeTasksLocal;
    }

    const fetchCompletedActivities = () => {
    const completedTasksLocal = new Set();
    habiticaRequest.performGet("https://habitica.com/api/v3/tasks/user?type=completedTodos").forEach(function (completedTodo) {
    if (completedTodo["tags"] && completedTodo["tags"].includes(habiticaTag.getDefaultTagId())) {
    let dateCompleted = tempo.formatDate(new Date(completedTodo["dateCompleted"]));
    if (tempo.getTodayString() === dateCompleted) {
    completedTasksLocal.add(completedTodo["text"]);
    }
    }
    });
    completedTasks = completedTasksLocal;
    }

    const getActiveTasks = () => {
    return activeTasks;
    }

    const getCompletedTasks = () => {
    return completedTasks;
    }

    fetchActivities();
    fetchCompletedActivities();

    return {
    getActiveTasks,
    getCompletedTasks
    };
    }

    // Fetch the current calendar events
    const getCalendarEventsSet = function() {
    let calendarEventsSet = new Set()
    let calendarEvents = CalendarApp.getCalendarsByName(calendarName)[0].getEventsForDay(now);
    for (const event of calendarEvents.entries()) {
    let entry = {
    "title" : event[1].getTitle()
    }

    // If there's a description at gCalendar, convert to Markdown
    // before sending to Habitica
    var description = event[1].getDescription();
    if(description) {
    var params = postRequest;
    params["payload"] = {
    "html" : description
    }
    let markdown = UrlFetchApp.fetch("http://fuckyeahmarkdown.com/go/", params).getContentText();
    entry.description = markdown;
    }

    calendarEventsSet.add(entry);
    }
    return calendarEventsSet;
    // -------------------------------------------------------------------------------

    const GoogleActivity = (habiticaTag, html2Markdown, tempo, observedCalendars) => {

    let calendarTasks = new Set();
    let calendarEvents = new Set();

    const getTasksActivities = () => {
    const calendarTasksLocal = new Set()
    const taskLists = Tasks.Tasklists.list();
    if (taskLists.items) {
    taskLists.items.forEach(function (taskList) {
    var tasks = Tasks.Tasks.list(taskList.id);
    if (tasks.items) {
    tasks.items.forEach(function (task) {
    if (task.title) {
    const taskActivityDue = tempo.formatDate(new Date(task.due));
    if (tempo.getTodayString() === taskActivityDue) {
    let taskActivity = new Activity(tempo);
    taskActivity.setName(task.title);
    taskActivity.addTag(habiticaTag.getDefaultTagId());
    taskActivity.addTag(habiticaTag.getTagIdByName(taskList.title));
    taskActivity.setNotes(html2Markdown.convert(task.notes));
    taskActivity.setDueDate(new Date(task.due));
    calendarTasksLocal.add(taskActivity);
    }
    }
    })
    }
    })
    }
    calendarTasks = new Set(Array.from(calendarTasksLocal).reverse());
    }

    const getCalendarActivities = () => {
    const calendarEventsLocal = new Set()
    CalendarApp.getAllCalendars().forEach(function (calendar) {
    const calendarName = calendar.getName();
    if (observedCalendars.has(calendarName)) {
    const calendarEvents = calendar.getEventsForDay(tempo.getTodayDate());
    for (const event of calendarEvents.entries()) {
    let calendarActivity = new Activity(tempo);
    calendarActivity.setName(event[1].getTitle());
    calendarActivity.addTag(habiticaTag.getDefaultTagId());
    calendarActivity.addTag(habiticaTag.getTagIdByName(calendarName));
    calendarActivity.setNotes(html2Markdown.convert(event[1].getDescription()));
    calendarActivity.setDueDate(new Date(event[1].getStartTime()));
    calendarEventsLocal.add(calendarActivity);
    }
    }
    });
    calendarEvents = new Set(Array.from(calendarEventsLocal).reverse());
    }

    const getGoogleActivities = () => {
    return new Set([...calendarTasks, ...calendarEvents])
    }

    getTasksActivities();
    getCalendarActivities();

    return {
    getGoogleActivities
    };
    }

    // Adds or Removes tasks according to the rules described at the begining of the script
    function scheduleToDos() {
    let todosMap = getTodosMap();
    let completedTodosMap = getCompletedTodosMap();
    let calendarEventsSet = getCalendarEventsSet();
    let calendarTitleEventsSet = new Set();

    for (let event of calendarEventsSet) {
    calendarTitleEventsSet.add(event.title);

    if(todosMap.has(event.title) ||
    completedTodosMap.has(event.title) &&
    date === completedTodosMap.get(event.title)) {

    continue;
    }

    var params = postRequest;
    params.muteHttpExceptions = false;
    params["payload"] = {
    "text" : event.title,
    "notes" : event.description,
    "type" : "todo",
    "priority" : "1.5",
    "tags" : nameIdTags.get(defaultTag)
    }
    UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/user", params)
    }

    for (const todo of todosMap.entries()) {
    let key = todo[0];
    let value = todo[1];
    if(!calendarTitleEventsSet.has(key)) {
    UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/" + value, deleteRequest);
    }
    }
    // -------------------------------------------------------------------------------


    function HabiticaReminders() {

    const observedCalendars = new Set(["Gantter", "Birthdays", "Appointments", "Work", "..."]);

    const habiticaRequest = HabiticaRequest();
    const habiticaTag = HabiticaTag(habiticaRequest, observedCalendars);
    const html2Markdown = Html2Markdown();
    const tempo = Tempo();
    const habiticaActivity = HabiticaActivity(habiticaTag, habiticaRequest, tempo);
    const googleActivity = GoogleActivity(habiticaTag, html2Markdown, tempo, observedCalendars);

    const habiticaActive = habiticaActivity.getActiveTasks();
    const habiticaCompleted = habiticaActivity.getCompletedTasks();
    const calendarEvents = googleActivity.getGoogleActivities();
    const visitedEvents = new Set();

    for (let activity of calendarEvents) {
    visitedEvents.add(activity.getName());

    Logger.log(
    '\nactivity -> ' + activity.getName() +
    '\nactive: ' + habiticaActive.has(activity.getName()) +
    '\ncompleted: ' + habiticaCompleted.has(activity.getName()));

    if (habiticaActive.has(activity.getName()) ||
    habiticaCompleted.has(activity.getName())) {

    continue;
    }

    const payload = {
    "text": activity.getName(),
    "notes": activity.getNotes(),
    "date": activity.getDueDate(),
    "type": "todo",
    "priority": "1.5"
    }

    let taskId = habiticaRequest.performPost("https://habitica.com/api/v3/tasks/user", payload)["id"];

    if (activity.getTags()) {
    activity.getTags().forEach(function (tagId) {
    habiticaRequest.performPost("https://habitica.com/api/v3/tasks/" + taskId + "/tags/" + tagId);
    Utilities.sleep(5 * 1000)
    });
    }
    }

    for (const activity of habiticaActive.entries()) {
    if (!visitedEvents.has(activity[1].getName()) &&
    activity[1].getFormatedCreatedAt() === tempo.getTodayString()) {
    habiticaRequest.performDelete("https://habitica.com/api/v3/tasks/" + activity[1].getId());
    }
    }
    }
  7. Allan de Queiroz revised this gist Jul 19, 2020. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -56,6 +56,7 @@ const formatDate = function(date) {
    const habId = "";
    const habToken = "";
    const defaultTag = "gCal";
    const calendarName = "HabiticaReminders";
    const now = new Date();
    const date = formatDate(now);
    const headers = {
    @@ -139,7 +140,7 @@ const getCompletedTodosMap = function() {
    // Fetch the current calendar events
    const getCalendarEventsSet = function() {
    let calendarEventsSet = new Set()
    let calendarEvents = CalendarApp.getCalendarsByName("HabiticaReminders")[0].getEventsForDay(now);
    let calendarEvents = CalendarApp.getCalendarsByName(calendarName)[0].getEventsForDay(now);
    for (const event of calendarEvents.entries()) {
    let entry = {
    "title" : event[1].getTitle()
  8. Allan de Queiroz revised this gist Jul 19, 2020. 1 changed file with 27 additions and 7 deletions.
    34 changes: 27 additions & 7 deletions HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -141,7 +141,23 @@ const getCalendarEventsSet = function() {
    let calendarEventsSet = new Set()
    let calendarEvents = CalendarApp.getCalendarsByName("HabiticaReminders")[0].getEventsForDay(now);
    for (const event of calendarEvents.entries()) {
    calendarEventsSet.add(event[1].getTitle());
    let entry = {
    "title" : event[1].getTitle()
    }

    // If there's a description at gCalendar, convert to Markdown
    // before sending to Habitica
    var description = event[1].getDescription();
    if(description) {
    var params = postRequest;
    params["payload"] = {
    "html" : description
    }
    let markdown = UrlFetchApp.fetch("http://fuckyeahmarkdown.com/go/", params).getContentText();
    entry.description = markdown;
    }

    calendarEventsSet.add(entry);
    }
    return calendarEventsSet;
    }
    @@ -151,19 +167,23 @@ function scheduleToDos() {
    let todosMap = getTodosMap();
    let completedTodosMap = getCompletedTodosMap();
    let calendarEventsSet = getCalendarEventsSet();
    let calendarTitleEventsSet = new Set();

    for (let event of calendarEventsSet) {
    if(todosMap.has(event) ||
    completedTodosMap.has(event) &&
    date === completedTodosMap.get(event)) {
    calendarTitleEventsSet.add(event.title);

    if(todosMap.has(event.title) ||
    completedTodosMap.has(event.title) &&
    date === completedTodosMap.get(event.title)) {

    continue;
    }

    var params = postRequest;
    params.muteHttpExceptions = false;
    params["payload"] = {
    "text" : event,
    "text" : event.title,
    "notes" : event.description,
    "type" : "todo",
    "priority" : "1.5",
    "tags" : nameIdTags.get(defaultTag)
    @@ -174,8 +194,8 @@ function scheduleToDos() {
    for (const todo of todosMap.entries()) {
    let key = todo[0];
    let value = todo[1];
    if(!calendarEventsSet.has(key)) {
    if(!calendarTitleEventsSet.has(key)) {
    UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/" + value, deleteRequest);
    }
    }
    }
    }
  9. Allan de Queiroz created this gist Jul 19, 2020.
    181 changes: 181 additions & 0 deletions HabiticaReminders.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,181 @@
    /*
    Maintainer: Allan de Queiroz
    ---
    This script aims to provide synchronism between Habitica's inbox Tasks and Google Calendar. It was heavily inspired by Snickersnacker's one.
    The usage of this one is quite similar to the difference that it adds a few more functionalities. This version of the script enables frequent scheduling of synchronization (if you need of course), handling the creation and also removal of tasks if required.
    __Disclaimer:__ All the tasks handled by this script will be tagged with a gCal tag so that the script won't interfere with the manually created ones.
    The script will perform the following validations/actions:
    ## Tasks Creation
    1. The script will check if a gCalendar event is present at the active list of tasks on Habitica. If the task is already there, it will not add a new one. (that's why it is safe to schedule the executions as often as you want).
    2. If there's no task at the active list, the script will look into the complete list, if the script finds the same task there, it validates the date, if the task were completed today it would not create a new one. Assuming that the task was added to Habitica's list, and tackled already.
    1. In the other hand, when looking into the complete tasks, if the script finds a task with the same title but completed in previous days, it assumes that this is a recurring task, so it creates the new one for that day. (to handle recurring tasks).
    2. One observation here about recurring tasks. By design, no new task will be created at the active inbox if there's already one present, doesn't matter if they were created at previous days. The previous day's rule is only taken into consideration when checking the completed tasks inbox.
    ## Tasks Removal
    The script will check the existing tasks at the active inbox and validate against the ones present at gCalendar, if a task managed by the script is present on Habitica but not on gCalendar anymore; this task will be automatically removed from Habitica's active tasks inbox. The idea here is to keep the tasks inbox fully synchronized with gCalendar, if you remove from your calendar, it will be removed from the tasks lists.
    ## Installation
    1. Replace by habId(line 27) and habToken(line 28) with your ones, you can find them [Here](https://habitica.com/user/settings/api)
    2. Go to [https://script.google.com/](https://script.google.com/)
    1. Create a new project, give some nice name to it and the script as well, I liked the one from the inspirational script HabiticaReminders.
    2. Paste this script at the large blank area
    3. Edit ( Menu ) -> Current Project's Triggers -> Add Trigger (bottom right):
    # Here is up to you, this are the ones I've chosen
    * Choose which function to run: scheduleToDos
    * Choose which deployment should run: Head
    * Select event source: Time-driven
    * Select type of time based trigger: Minutes timer
    * Select minute interval: Every 5 minutes
    * Failure notification settings: Notify me imeediately
    ## References
    * [The original idea](https://habitica.fandom.com/wiki/Thread:48017)
    * [Gist](https://gist.github.com/allandequeiroz/8a369534aaa58e1a2bcf3c3135120d85)
    * [Google Scripts](https://script.google.com/)
    * [Habitica's API personal credentials](https://habitica.com/user/settings/api)
    */

    // Turn the Date into a nice format for comparison purposes
    const formatDate = function(date) {
    var dd = String(date.getDate()).padStart(2, '0');
    var mm = String(date.getMonth() + 1).padStart(2, '0');
    var yyyy = date.getFullYear();
    return yyyy + '/' + mm + '/' + dd;
    }

    // Basic requirements to fetch the required data
    const habId = "";
    const habToken = "";
    const defaultTag = "gCal";
    const now = new Date();
    const date = formatDate(now);
    const headers = {
    "x-api-user" : habId,
    "x-api-key" : habToken
    }
    const getRequest = {
    "method" : "get",
    "headers" : headers
    }
    const postRequest = {
    "method" : "post",
    "headers" : headers
    }
    const deleteRequest = {
    "method" : "delete",
    "headers" : headers
    }

    // Fetch a list of existing tags from Habitica
    const getTagsMaps = function() {
    let tags = JSON.parse(UrlFetchApp.fetch("https://habitica.com/api/v3/tags", getRequest).getContentText())["data"]
    let idName = new Map()
    let nameId = new Map()
    for (i = 0; i < tags.length; i++) {
    idName.set(tags[i]["id"], tags[i]["name"]);
    nameId.set(tags[i]["name"], tags[i]["id"]);
    }
    return {
    "idName" : idName,
    "nameId" : nameId
    };
    }

    // Preparing tags for future validations
    const tags = getTagsMaps();
    const nameIdTags = tags["nameId"];
    const idNameTags = tags["idName"];

    /*
    Validate if the tag used to indentify the tasks managed by this script
    exists, if not, creates it before moving on
    */
    const createDefaultTagIfAbsent = function() {
    if(!nameIdTags.has(defaultTag)) {
    var params = postRequest;
    params["payload"] = {
    "name" : defaultTag
    }

    UrlFetchApp.fetch("https://habitica.com/api/v3/tags", params).toString()
    }
    }
    createDefaultTagIfAbsent();

    // Fetch a list of open todos from Habitica
    const getTodosMap = function() {
    let todos = JSON.parse(UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/user?type=todos", getRequest).getContentText())["data"]
    let todosMap = new Map()
    for (i = 0; i < todos.length; i++) {
    if(todos[i]["tags"] && todos[i]["tags"].includes(nameIdTags.get(defaultTag))) {
    todosMap.set(todos[i]["text"], todos[i]["id"]);
    }
    }
    return todosMap;
    }

    // Fetch a list of closed todos from Habitica
    const getCompletedTodosMap = function() {
    let completedTodos = JSON.parse(UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/user?type=completedTodos", getRequest).getContentText())["data"]
    let completedTodosMap = new Map()
    for (i = 0; i < completedTodos.length; i++) {
    if(completedTodos[i]["tags"] && completedTodos[i]["tags"].includes(nameIdTags.get(defaultTag))) {
    let dateCompleted = new Date(completedTodos[i]["dateCompleted"]);
    completedTodosMap.set(completedTodos[i]["text"], formatDate(dateCompleted));
    }
    }
    return completedTodosMap;
    }

    // Fetch the current calendar events
    const getCalendarEventsSet = function() {
    let calendarEventsSet = new Set()
    let calendarEvents = CalendarApp.getCalendarsByName("HabiticaReminders")[0].getEventsForDay(now);
    for (const event of calendarEvents.entries()) {
    calendarEventsSet.add(event[1].getTitle());
    }
    return calendarEventsSet;
    }

    // Adds or Removes tasks according to the rules described at the begining of the script
    function scheduleToDos() {
    let todosMap = getTodosMap();
    let completedTodosMap = getCompletedTodosMap();
    let calendarEventsSet = getCalendarEventsSet();

    for (let event of calendarEventsSet) {
    if(todosMap.has(event) ||
    completedTodosMap.has(event) &&
    date === completedTodosMap.get(event)) {

    continue;
    }

    var params = postRequest;
    params.muteHttpExceptions = false;
    params["payload"] = {
    "text" : event,
    "type" : "todo",
    "priority" : "1.5",
    "tags" : nameIdTags.get(defaultTag)
    }
    UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/user", params)
    }

    for (const todo of todosMap.entries()) {
    let key = todo[0];
    let value = todo[1];
    if(!calendarEventsSet.has(key)) {
    UrlFetchApp.fetch("https://habitica.com/api/v3/tasks/" + value, deleteRequest);
    }
    }
    }