QEverCloud: Evernote SDK для Qt

    Раз уж вы читаете этот пост, то, наверное, знаете, что такое Evernote. Ну и конечно же вы знаете, что такое Qt :)
    И, может быть, вы, как и я, захотели их объединить.



    Если вы пробовали осуществить такое объединение — написать программу на C++/Qt, которая бы обращалась к Evernote Cloud API — то вы уже знаете, что дело это, мягко говоря, нетривиальное. Для C++ создатели Evernote предлагают только сгенерированные компилятором Thrift файлики. От них до чего-то хоть как-то работающего — как до Луны, и, к сожалению, отнюдь не по прямой.

    Я этот путь прошел и по результатам этого памятного путешествия решил, что в этот путь людей посылать — оно не очень гуманно. Так что в конце концов я написал свою библиотеку и выложил ее на GitHub.

    Чем QEverCloud лучше Evernote SDK?

    • Зависит только от Qt. Никаких танцев с бубнами при компиляции.
    • Интерфейс библиотеки насколько возможно избавлен от технических деталей. Пользоваться ей настолько просто, насколько это имхо возможно.
    • Реализована аутентификация через OAuth.
    • Поскольку для реализации клиента HTTP используется QtNetwork, то нет никаких проблем с поддержкой HTTP proxy.


    В качестве примера использования вот простенькая программка, которая создает тестовую заметку в блокноте по умолчанию:

    
    #include <QCoreApplication>
    #include <QTextStream>
    #include <exception>
    #include <QEverCloud.h>
    
    using namespace qevercloud;
    
    
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
        QTextStream cout(stdout);
        try {
            NoteStore* ns = new NoteStore(&a);
    
            // вставьте сюда свой developer token
            // https://www.evernote.com/api/DeveloperToken.action
            ns->setAuthenticationToken("S=s41:U=427a0c:E=14761d37b39:C=1400a224f39:P=1cd:A=en-devtoken:V=2:H=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
    
            // там же узнайте свой NoteStore URL
            ns->setNoteStoreUrl("https://www.evernote.com/shard/xxx/notestore");
    
            Note note;
            note.title = "Тестовая заметка";
            note.content = QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
                    + "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">\n"
                    + "<en-note>"
                    + "<b>Хорошо живет на свете Винни-Пух!</b><br /><br /><p>Хм, похоже, <a href=\"https://github.com/mgsxx/QEverCloud\">QEverCloud</a> таки да работает...</p>"
                    + "</en-note>";
            ns->createNote(note);
        } catch(const std::exception& e) {
            cout << "exception occured: " << e.what() << endl;
            return 1;
        } catch(...) {
            cout << "unknown exception occured!" << endl;
            return 1;
        }
        return 0;
    }
    
    


    Developer tokens весьма удобны для своих личных программок, но для более серьезных программ необходимо токен и NoteStore URL получать через OAuth авторизацию. С помощью QEverCloud это делается легко:

    #include <QEverCloudOAuth.h>
    
    ...
    
    // consumerKey и consumerSecret получайте тут: https://dev.evernote.com/doc/, кнопка GET AN API KEY
    EvernoteOAuthDialog d(consumerKey, consumerSecret, "sandbox.evernote.com");
    d.setWindowTitle("Логин в Evernote");
    if(d.exec() != QDialog::Accepted) {
        QMessageBox::critical(0, "Ошибка", d.oauthError());
    } else {
        QString authenticationToken = d.oauthResult().authenticationToken;
        QString noteStoreUrl = d.oauthResult().noteStoreUrl;
    }
    
    
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 46
    • 0
      Интересно, правда, я пользуюсь Java API.
      Скажите, с помощью вашей библиотеки можно у заметки, у которой установлено поле «updated», сделать его пустым, то есть отображаемым в windows-клиенте как «Updated: none» (как бывает после создания заметки через почту)?
      • 0
        Ха, надо бы попробовать.

        Библиотека может то и ровно то, что позволяет Evernote Cloud API. Ни больше, ни меньше. Если через EDAM API теоретически возможно можно «обнулить» updated, то значит и с помощью QEverCloud можно.
        • 0
          Через Java API у таких заметок дата создания и дата редактирования совпадают. И через этот API нельзя установить поле updated в null, так как метод Note.setUpdated принимает long.
          • +1
            Сейчас вот проверил. 0 работает. Т.е. поле Note.updated должно быть передано со значением 0 в updateNote.
            • +1
              По крайней мере, клиент под Windows такую дату не отображает.
              • 0
                Если через Java API передать 0 в setUpdated, то в качестве времени обновления устанавливается текущее.

                Вообще, я пытаюсь победить баг в Evernote 5.3 для Windows, когда клиент игнорирует дату обновления, заданную через Java API. В то же время клиент для Android и версия 5.0 дату заметок у себя обновляют. Если же поменять дату редактирования через приложение, то она версией 5.3 синхронизируется нормально.
                Вот пытаюсь понять, что мне надо сделать, кроме вызова метода Note.setUpdated, чтобы результат был таким же как изменение даты через клиент.
                Пытаюсь добиться, чтобы дата обновления была равна дате создания, чтобы при сортировке по дате редактирования заметки были в нужном мне порядке.
                • 0
                  На счет хитрого бага несколько раз прочитал, но не уверен, что я понял суть.

                  Чтобы даты заметки были одинаковыми я бы просто задавал created и updated при создании заметки. В QEverCloud это бы выглядело где-то так:

                  quint64 time = QDateTime::currentMSecsSinceEpoch();
                  Note note;
                  note.created = time;
                  note.updated = time;
                  note.title =…
                  note.content =…
                  noteStote->createNote(note);

                  Как это в Java API будет и какие там еще баги подстерегают — этого я уже не скажу…
                  • 0
                    Баг такой:
                    Я через API устанавливаю дату редактирования равной дате создания. На Java это выглядит так:
                    Note note = noteStore.getNote(authToken, guid, false, false, false, false);
                    note.setUpdated(note.getCreated());
                    noteStore.updateNote(authToken, note);
                    

                    После этого выполняю синхронизацию в Evernote for Windows 5.3, а дата обновления у заметки не изменяется, остаётся такой же какой и была. Выполняю синхронизацию в Evernote for Android, там дата обновления становится равной дате создания.
                    Раньше в Windows всё также работало.

                    Но я нашёл решение, клиент для Windows обновляет у себя дату редактирования, если она отличается от даты создания минимум на 1 секунду, то есть работающий код такой:
                    note.setUpdated(note.getCreated() + 1000);
                    
      • 0
        Выглядит местами, как будто либа C++-ная, а не Qt-шная — например, в месте catch(std::exception ...)
        И немного неожиданный конец статьи.
        • +2
          А что, Qt это не С++? ;)
          Если вас это беспокоит, то могу уверить, что кроме std::exception из стандартной библиотеки никиких других классов не используется.

          Неожиданный конец — это без выводов? Меня за это регулярно пеняют. Не люблю воды, а вроде уже сказал все, что хотел. «Кому интересно, качаем и смотрим» — вроде и так подразумевается.
          • +1
            Проблема не в std::exception, а в самом факте синхронного API, это делает невозможным использование библиотеки в реальных сложных проектах.
            • +2
              Далее, в EvernoteOAuthDialog переопределен метод ::exec, только из которого стартует процес авторизации, то есть тоже невозможно использовать классический QDialog::open().
              В QsrandExecutor вообще бага, связанная с криптографией, из-за нее тот же nonce в EvernoteOAuthWebView::authenticate может принимать только 256 различных значений. Для генерации 64-битных чисел лучше использовать, наверное, std::mt19937_64, за счет использования std::random_device seed будет браться не из текущего времени, а из /dev/(u)random.
              • +1
                Спасибо за то, что дали себе труд глянуть исходники. Я приветсвую любую конструктивную критику.

                QDialog::open — да, забыл про него. Видимо потому, что никогда не использую :)
                Пофиксю при случае.

                По поводу «256 возможных значений» не понял. Можете ткнуть носом в конкретную строку? Я такого бага не вижу.

                std::mt19937_64 — это, если я не ошибаюсь, c++11. Библиотека под c++03.

                Пожалуй, надо бы фанктор прикрутить в параметры authenticate для генерации nonce.
                • +1
                  qsrand(QDateTime::currentMSecsSinceEpoch() % static_cast<quint64>(256)*256*256*256);

                  Вначале выполнится деление с остатком на 256, а потом результат домножится на 224. Вообще мне кажется плохим тоном инициализировать qsrand из библиотеки — этим должно заниматься приложение.

                  Библиотека под c++03.

                  А есть причины придерживаться c++03? Сейчас все адекватные компиляторы уже умеют c++11 в достаточной степени.
                  • 0
                    Фейспалм. Фиксю.

                    «Сейчас все адекватные компиляторы уже умеют c++11 в достаточной степени» — это если не поминать MSVC незлым тихим словом.
                    • 0
                      «Сейчас все адекватные компиляторы уже умеют c++11 в достаточной степени» — это если не поминать MSVC незлым тихим словом.

                      Насколько я знаю std::random_device + std::mt19937_64 в 2012 и 2013 студиях быть уже должны.
                      • 0
                        Да, но не в 2010. В 2010 очень много чего нет… и в 2012 тоже много не хватает. Вот какого хрена в 2012 даже сырых (raw) строк нет? Загадка.

                        Увы, но MSVC2010 все еще бывает актуальным. И когда уже оно подохнет…
                    • 0
                      «Вообще мне кажется плохим тоном инициализировать qsrand из библиотеки — этим должно заниматься приложение.»

                      В принципе, я согласен. Лезть исподтишка библиотекой в глобальное состояние — оно действительно некрасиво. Но в данном случае, имхо, это не то чтобы совсем страшно. Если программист вызовет qsrand() где-нибудь в main, то мои закулисые манипуляции потеряют эффект, а если нет — то ему, по видимому, все равно, qrand/rand он не использует и я ему не мешаю.

                      Гораздо неприятнее будет, если программист забудет вызвать qsrand() и в таком виде отдаст свою программу пользователям. Дыра в безопасности, которую моя библиотека некрасивыми средствами пытается закрыть.
                      • +1
                        Не совсем верно, если ваша библиотека подключается в виде динамической библиотеки, то, по крайней мере в linux, она подгрузится только при необходимости и, как следствие, перетрет инициализацию в main, что только ухудшит безопасность.
                        • 0
                          Понятно. Ну, значит нужно выносить наружу.
                • 0
                  Хм… Видимо, Evernote в принципе не предназначен для реальных сложных проектов. Все SDK им предлагаемые — синхронные.

                  Кстати, если серьезно: ничто не мешает мне сделать асинхронный вариант API, немного поменяв генератор кода. Единственный вопрос: как оно, это асинхронное API, дожно выглядеть? У вас похоже, есть ответ на этот вопрос. Не поделитесь ответом?
                  • +1
                    Например сделать по аналогии с QNetworkAccessManager или QtDbus — каждая операция возвращает некоторый MyCoolReply, который по завершению операции пошлет сигнал finished(), на который мы уже можем повеситься и обработать результат как только, так сразу. В QtDBus модуле показан пример реализации асинхронной модели в случае, если разные методы возвращают объекты разных типов, но при этом хочется иметь общий API возвращаемых объектов. Такой подход должен быть не сложен для реализации, но при этом достаточно эффективен.
                    • 0
                      Я думал об этом, но пришел к выводу, что в случае с Thrift вообще и Evernote в частности такая модель не стоит того гембеля, что она привносит. Согласитесь, асинхронность превратит код в лапшу.

                      Thrift по своей идеологии имитирует вызов локальных функций, и Evernote этой идеологии всецело придерживается. Почти всегда перед тем, как выполнять следующий запрос к Evernote, нужно знать результат предыдущего. Как следствие, основное преимущество асинхронной модели — параллелизируемость на уровне отдельных запросов — сводится практически на нет.

                      Имхо проще и в целом так же эффективно будет реализовывать логически связанные последовательности запросов (как то же создание заметки) в виде синхронных вызовов, выполняемых в отдельном потоке. Тогда отдельные последовательности параллелизируются, код удобочитаем, и интерфейс = главный поток «замирать» не будет.

                      Если народ захочет асинхронности, то можно и сделать… но я пока не ощутил в ней насущной необходимости.
                      • +1
                        Я не понимаю никакого бонуса выноса синхронных методов в отдельный поток, с точки зрения API будет хуже, чем event loop-based API, писать сложнее, использовать тоже сложнее.

                        Примерно так можно использовать было бы event-loop based API:
                        NoteStore *ns = ...;
                        Note note = ...;
                        
                        connect(ns->createNote(note), &CreateNote::finished, [] (const EverCloudException &error) {
                            if (error)
                                qWarning() << error.toString();
                        });
                        


                        При этом API будет не сильно отличаться по духу и стилю от самого Qt.
                        • 0
                          «Я не понимаю никакого бонуса выноса синхронных методов в отдельный поток, с точки зрения API будет хуже, чем event loop-based API, писать сложнее, использовать тоже сложнее.»
                          Это субъективно. Моему субъекту, например, все кажется прямо наоборот.

                          Вашему примеру для полноты картины не хватает последовательности вызовов Evernote. В качестве гипотетического случая: три раза createNote, строго один за другим. В синхронном API это выглядит проще некуда:

                          ns->createNote(note);
                          ns->createNote(note);
                          ns->createNote(note);
                          


                          А в асинхронном?
                          • 0
                            Эксперимент есть эксперимент. Наверное, попробую сделать асинхронный вариант API. Я правда пока сомневаюсь, что сам я найду ему применение, но, может, кому-то и пригодится. Время покажет.
                            • 0
                              К слову, а файлик services.* вы же автоматически генерировали, надеюсь? Можете так же на гитхаб выложить скрипты для генерации?
                              • 0
                                Конечно генерил, я же не мазохит ;)

                                Могу выложить, но парсер/генератор писались сугубо под себя и сугубо под мою задачу. Если парсер Thirft еще можно приспособить, то генератор кода заметно нужно будет менять. А комментариев там нет как класс.
              • +1
                Вот смотрю на код примера… вижу try catch при создании заметки, получается, что данный код синхронный?
                • +2
                  Судя по коду библиотеки — да, на каждый запрос создается QEventLoop + ожидание ответа. Не представляю как использовать эту библиотеку в реальных проектах.
                  • 0
                    С тем же успехом можно было сгенерировать код из Thrift файла. После чего переписать транспорт под Qt.
                    • 0
                      Именно так это и было сделано.
                    • 0
                      Не поделитесь, чем конкретно такой подход мешает реализации реальных проектов? Не сочтите за иронию, мне действительно интересно знать ваше мнение.
                      • +1
                        Представим стандартную ситуацию, у пользователя плохой интернет (например, пинги по сервера по 200-300 мс), тогда каждый запрос к API может породить 200-миллисекундную заморозку интерфейса или части интерфейса (зависит от того, при обработке которого события будет спровоцирован запрос к API). Если же в процессе ожидания некоторого запроса мы пошлем еще один запрос (из QEventLoop'а), то первые запрос не завершится, пока не будет обработан следующий вопрос и т.д…

                        Все это суммарно приведет к очень плохому пользовательскому опыту.
                        • 0
                          Т.е. претензии собственно к синхронности, а не к конкретно ее реализации через QEventLoop.
                          • 0
                            «Заморозку» интерфейса можно решить многопоточностью. Распараллеливание на уровне отдельных обращений к Evernote таким образом не получится, но оно по логике самого API в целом неосуществимо.
                            • +1
                              Т.е. претензии собственно к синхронности, а не к конкретно ее реализации через QEventLoop.

                              QEventLoop еще не самый плохой способ, но в больших приложениях даже он становится очень большой проблемой.

                              «Заморозку» интерфейса можно решить многопоточностью.

                              Здесь есть ряд проблем:
                              • Как общаться с тем потоком из гуя? Все равно придется городить асинхронное API, т.е. опять приходим к начальной проблеме
                              • Многопоточность — это накладные расходы, лишняя память, лишние context switch'и. Асинхронное API, основанное на event loop почти всегда быстрее
                              • 0
                                Как общаться с тем потоком из гуя? Все равно придется городить асинхронное API, т.е. опять приходим к начальной проблеме

                                Зато на более высоком уровне, более соответствующем логике конкретной программы. Я не назвал бы это изначальной проблемой. К тому же, общение между потоками в Qt не есть большая проблема. Все готово, сигналы/слоты между потоками работают.

                                Многопоточность — это накладные расходы, лишняя память, лишние context switch'и. Асинхронное API, основанное на event loop почти всегда быстрее.

                                Это верно, но, насколько я понимаю, по сравнению с затратами на сетевые вещи все эти расходы теряют значимость.
                    • 0
                      Хотелось много сказать, но другие пользователи опередили, осталось только:
                      «Для Ubuntu Phone пишется Evernote Client. Попробуйте найти там пару хороших идей».
                      • 0
                        Ссылка на исходники?
                        • +1
                          • –1
                            Хотелось много чего сказать, но я ограничусь одним тезисом: полезно читать перед тем как писать.

                            Прочитайте еще раз мою заметку, гляньте исходники QEverCloud. Надеюсь, вам станет понятно, что целью QEverCloud является заменить предлагаемые Evernote сгенеренные thrift компилятором файлы, которые громко именуются Evernote SDK for C++, а не написать конкретную прикладную программу.

                            В рамках этой цели предлагаемые вам исходники не представляют интереса — там этот самый Evernote SDK for C++ во всю используется, а не заменяется.

                            Все дальнейшее в программе по поводу Evernote мало интересно с точки зрения общего SDK, предназначенного для любых программ с самыми разнообразными задачами и архитектурой. QAbstractItemModel в качестве API к Evernote? Даже не смешно. Как решение задач конкретной программы — почему бы и нет, но как SDK к Evernote (а именно это есть тема заметки) — идея явно не очень здравая.

                            Более того, ваш глубокомысленный тезис о «другие пользователи опередили» имеет неявный подтекст, что предлагаемый мною подход к написанию программ — многопоточность с вынесением обращений к Evernote в отдельные потоки — неверен. Что явно говорит о том, что вы не потрудились ознакомится с темой, перед тем как писать. Ибо в приведенных вами исходниках используется именно этот подход, против которого EuroElessar привел массу разумных и интересных контраргументов, пусть с ними и можно поспорить. Вы уж определитесь, что вы хотите сказать.

                            Так что я ваш тезис о хороших идеях как-то не понял. В приведенных вами исходниках я их не нашел ничего нового и особого интересного.
                            Если вам хочется конструктивной дискуссии — пожалуйста, конкретику в студию. Какие хорошие с вашей точки зрения идеи вы хотели бы видеть в QEverCloud, но не видите? Или может вы хотели бы реализовать хорошую идею с помощью QEverCloud, но не можете из-за каких-то ограничений библиотеки?

                            • +1
                              Это Вы мне говорите «Вы уж определитесь»? Кажется, это у Вас пост начинается с предложения «Хотелось много чего сказать, но я ограничусь одним тезисом: полезно читать перед тем как писать», за которым следует моментальный 6-ти абзацный отказ от своих слов.

                              А теперь по существу: да, может быть я не совсем внимательно прочитал Ваш пост — температура, знаете ли, мешает сосредоточиться. Но даже в полубессознательном состоянии я способен отличить нормальный продукт от поделки. Я сейчас специально открыл два файла (exceptions.cpp и exceptions.h) и мне хватило. Что я там увидел:
                              — Микс строк std::string и QString
                              — Объявления переменных в стиле языка С
                              — Передачу QString по значению
                              — Необернутые в QStringLiteral строковые константы
                              Ну и вообще в целом мне не понравилось использование исключений (Вы либу пишете, которая основывается на Qt'e, и название начинается с «Q», а Вы видели, чтобы в кьюте использовали исключения?) и завязка на виджеты. Это я к тому, что первое впечатление оказалось более или менее правильным. А поскольку оно было не важным, то я и порекомендовал Вам попробовать поискать идеи по реализации в целях улучшения Вашего продукта. Это было искренний и полезный совет, но раз уж Вы так остро реагируете, то и ответ мой получаете в соответствующем стиле.
                              • –1
                                Не вижу смысла продолжать дискуссию с неадекватным собеседником. Я бы резче выразился, но правила хабры запрещают срачи.
                                • +1
                                  Оу, не нашли ничего лучше, чем прибегнуть к оскорблениям? Достойный ответ, что тут скажешь. Больше в соответствующем стиле я отвечать не буду, увольте.
                      • +1
                        Приделал асинхронное API.
                        Пример использования на GitHub.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.