Explorar o código

Merge pull request #50 from PuercoPop/twitter-plugin

Twitter plugin
Brit Butler %!s(int64=11) %!d(string=hai) anos
pai
achega
7912037f2e
Modificáronse 5 ficheiros con 150 adicións e 0 borrados
  1. 1 0
      coleslaw.asd
  2. 31 0
      docs/plugin-use.md
  3. 108 0
      plugins/twitter.lisp
  4. 9 0
      src/conditions.lisp
  5. 1 0
      src/packages.lisp

+ 1 - 0
coleslaw.asd

@@ -17,6 +17,7 @@
17 17
   :serial t
18 18
   :components ((:file "packages")
19 19
                (:file "util")
20
+               (:file "conditions")
20 21
                (:file "config")
21 22
                (:file "themes")
22 23
                (:file "documents")

+ 31 - 0
docs/plugin-use.md

@@ -72,3 +72,34 @@
72 72
 **Example**: `(import :filepath "/home/redline/redlinernotes-export.timestamp.xml" :output "/home/redlinernotes/blog/")`
73 73
 
74 74
 [config_file]: http://github.com/redline6561/coleslaw/blob/master/examples/single-site.coleslawrc
75
+
76
+## Twitter
77
+
78
+**Description**: This plugin tweets every time a new post is added to your repo. See Setup for an example of how to get your access token & secret.
79
+
80
+**Example**: `(twitter :api-key "<api-key>" :api-secret "<api-seret" :access-token "<access-token>" :access-secret "<access-secret>")`
81
+
82
+**Setup**:
83
+- Create a new [twitter app](https://apps.twitter.com/). Take note of the api key & secret.
84
+
85
+- In the repl do the following:
86
+```lisp
87
+;; Load Chirp
88
+(ql:quickload :chirp)
89
+
90
+;; Use the api key & secret to get a URL where a pin code will be handled to you.
91
+(chirp:initiate-authentication
92
+ :api-key "D1pMCK17gI10bQ6orBPS0w"
93
+ :api-secret "BfkvKNRRMoBPkEtDYAAOPW4s2G9U8Z7u3KAf0dBUA")
94
+;; => "https://api.twitter.com/oauth/authorize?oauth_token=cJIw9MJM5HEtQqZKahkj1cPn3m3kMb0BYEp6qhaRxfk"
95
+
96
+;; Exchange the pin code for an access token and and access secret. Take note
97
+;; of them.
98
+CL-USER> (chirp:complete-authentication "4173325")
99
+;; => "18403733-bXtuum6qbab1O23ltUcwIk2w9NS3RusUFiuum4D3w"
100
+;;    "zDFsFSaLerRz9PEXqhfB0h0FNfUIDgbEe59NIHpRWQbWk"
101
+
102
+;; Finally verify the credentials 
103
+(chirp:account/verify-credentials)
104
+#<CHIRP-OBJECTS:USER PuercoPop #18405433>
105
+```

+ 108 - 0
plugins/twitter.lisp

@@ -0,0 +1,108 @@
1
+(:eval-when (:compile-toplevel :load-toplevel)
2
+  (ql:quickload :chirp))
3
+
4
+(defpackage :coleslaw-twitter
5
+  (:use :cl)
6
+  (:import-from :coleslaw
7
+                :*config*
8
+                :deploy
9
+                :get-updated-files
10
+                :page-url
11
+                :plugin-conf-error)
12
+  (:export #:enable))
13
+
14
+(in-package :coleslaw-twitter)
15
+
16
+(defvar *tweet-format* '(:title "by" :author)
17
+  "Controls what the tweet annoucing the post looks like.")
18
+
19
+(defvar *tweet-format-fn* nil "Function that expects an instance of
20
+coleslaw:post and returns the tweet content.")
21
+
22
+(defvar *tweet-format-dsl-mapping*
23
+  '((:title . coleslaw::post-title)
24
+    (:author . coleslaw::post-author)))
25
+
26
+(define-condition malformed-tweet-format (error)
27
+  ((item :initarg :item :reader item))
28
+  (:report
29
+   (lambda (condition stream)
30
+     (format stream "Malformed tweet format. Can't proccess: ~A"
31
+             (item condition)))))
32
+
33
+(defun compile-tweet-format (tweet-format)
34
+  (multiple-value-bind
35
+        (fmt-ctrl-str accesors) (%compile-tweet-format tweet-format nil nil)
36
+    (let
37
+        ((fmt-ctrl-str (format nil "~{~A~^ ~}" (reverse fmt-ctrl-str)))
38
+         (accesors (reverse accesors)))
39
+      (lambda (post)
40
+        (apply #'format nil fmt-ctrl-str
41
+               (loop
42
+                  :for accesor :in accesors
43
+                  :collect (funcall accesor post)))))))
44
+
45
+(defun %compile-tweet-format (tweet-format fmt-ctrl-str accesors)
46
+  "Transform tweet-format into a format control string and a list of values."
47
+  (if (null tweet-format)
48
+      (values fmt-ctrl-str accesors)
49
+      (let ((next (car tweet-format)))
50
+        (cond
51
+          ((keywordp next)
52
+           (if (assoc next *tweet-format-dsl-mapping*)
53
+               (%compile-tweet-format
54
+                (cdr tweet-format)
55
+                (cons "~A" fmt-ctrl-str)
56
+                (cons (cdr (assoc next *tweet-format-dsl-mapping*))
57
+                      accesors))
58
+               (error 'malformed-tweet-format :item next)))
59
+          ((stringp next)
60
+           (%compile-tweet-format (cdr tweet-format)
61
+                                  (cons next fmt-ctrl-str)
62
+                                  accesors))
63
+          (t (error 'malformed-tweet-format :item next))))))
64
+
65
+(setf *tweet-format-fn* (compile-tweet-format *tweet-format*))
66
+
67
+(defun enable (&key api-key api-secret access-token access-secret tweet-format)
68
+  (if (and api-key api-secret access-token access-secret)
69
+      (setf chirp:*oauth-api-key* api-key
70
+            chirp:*oauth-api-secret* api-secret
71
+            chirp:*oauth-access-token* access-token
72
+            chirp:*oauth-access-secret* access-secret)
73
+      (error 'plugin-conf-error :plugin "twitter"
74
+             :message "Credentials missing."))
75
+
76
+  ;; fallback to chirp for credential erros
77
+  (chirp:account/verify-credentials)
78
+  
79
+  (when tweet-format
80
+    (setf *tweet-format* tweet-format)))
81
+
82
+
83
+(defmethod deploy :after (staging)
84
+  (declare (ignore staging))
85
+  (loop :for (state file) :in (get-updated-files)
86
+     :when (and (string= "A" state) (string= "post" (pathname-type file)))
87
+     :do (tweet-new-post file)))
88
+
89
+(defun tweet-new-post (file)
90
+  "Retrieve most recent post from in memory DB and publish it."
91
+  (let ((post (coleslaw::find-content-by-path file)))
92
+    (chirp:statuses/update (%format-post 0 post))))
93
+
94
+(defun %format-post (offset post)
95
+  "Guarantee that the tweet content is 140 chars at most. The 117 comes from
96
+the spaxe needed for a space and the url."
97
+  (let* ((content-prefix (subseq (render-tweet post) 0 (- 117 offset)))
98
+         (content (format nil "~A ~A/~A" content-prefix
99
+                          (coleslaw::domain *config*)
100
+                          (page-url post)))
101
+         (content-length (chirp:compute-status-length content)))
102
+    (cond
103
+      ((>= 140 content-length) content)
104
+      ((< 140 content-length) (%format-post (1- offset) post)))))
105
+
106
+(defun render-tweet (post)
107
+  "Sans the url, which is a must."
108
+  (funcall *tweet-format-fn* post))

+ 9 - 0
src/conditions.lisp

@@ -0,0 +1,9 @@
1
+(in-package :coleslaw)
2
+
3
+(define-condition plugin-conf-error ()
4
+  ((plugin :initform "Plugin":initarg :plugin :reader plugin)
5
+   (message :initform "" :initarg :message :reader message))
6
+  (:report (lambda (condition stream)
7
+             (format stream "~A: ~A" (plugin condition) (message condition))))
8
+  (:documentation "Condition to signal when the plugin is misconfigured."))
9
+

+ 1 - 0
src/packages.lisp

@@ -17,6 +17,7 @@
17 17
            #:add-injection
18 18
            #:theme-fn
19 19
            #:get-updated-files
20
+           #:plugin-conf-error
20 21
            ;; The Document Protocol
21 22
            #:add-document
22 23
            #:find-all