mhc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/COPYRIGHT +28 -0
  6. data/Gemfile +8 -0
  7. data/README.org +209 -0
  8. data/Rakefile +13 -0
  9. data/bin/mhc +312 -0
  10. data/emacs/Cask +25 -0
  11. data/emacs/Makefile +58 -0
  12. data/emacs/mhc-calendar.el +1723 -0
  13. data/emacs/mhc-calfw.el +135 -0
  14. data/emacs/mhc-compat.el +90 -0
  15. data/emacs/mhc-date.el +642 -0
  16. data/emacs/mhc-day.el +149 -0
  17. data/emacs/mhc-db.el +158 -0
  18. data/emacs/mhc-draft.el +211 -0
  19. data/emacs/mhc-e21.el +167 -0
  20. data/emacs/mhc-face.el +236 -0
  21. data/emacs/mhc-file.el +224 -0
  22. data/emacs/mhc-guess.el +648 -0
  23. data/emacs/mhc-header.el +176 -0
  24. data/emacs/mhc-logic.el +563 -0
  25. data/emacs/mhc-message.el +130 -0
  26. data/emacs/mhc-minibuf.el +466 -0
  27. data/emacs/mhc-misc.el +248 -0
  28. data/emacs/mhc-mua.el +260 -0
  29. data/emacs/mhc-parse.el +286 -0
  30. data/emacs/mhc-process.el +35 -0
  31. data/emacs/mhc-ps.el +1174 -0
  32. data/emacs/mhc-record.el +201 -0
  33. data/emacs/mhc-schedule.el +202 -0
  34. data/emacs/mhc-summary.el +763 -0
  35. data/emacs/mhc-sync.el +158 -0
  36. data/emacs/mhc-vars.el +149 -0
  37. data/emacs/mhc.el +1114 -0
  38. data/icons/Anniversary.xbm +6 -0
  39. data/icons/Anniversary.xpm +27 -0
  40. data/icons/Birthday.xbm +6 -0
  41. data/icons/Birthday.xpm +25 -0
  42. data/icons/Business.xbm +6 -0
  43. data/icons/Business.xpm +24 -0
  44. data/icons/CheckBox.xbm +6 -0
  45. data/icons/CheckBox.xpm +24 -0
  46. data/icons/CheckedBox.xbm +6 -0
  47. data/icons/CheckedBox.xpm +25 -0
  48. data/icons/Conflict.xbm +6 -0
  49. data/icons/Conflict.xpm +22 -0
  50. data/icons/Date.xbm +6 -0
  51. data/icons/Date.xpm +29 -0
  52. data/icons/Holiday.xbm +6 -0
  53. data/icons/Holiday.xpm +25 -0
  54. data/icons/Link.xbm +6 -0
  55. data/icons/Link.xpm +25 -0
  56. data/icons/Other.xbm +6 -0
  57. data/icons/Other.xpm +28 -0
  58. data/icons/Party.xbm +6 -0
  59. data/icons/Party.xpm +23 -0
  60. data/icons/Private.xbm +6 -0
  61. data/icons/Private.xpm +26 -0
  62. data/icons/Recurrence.xbm +6 -0
  63. data/icons/Recurrence.xpm +98 -0
  64. data/icons/Vacation.xbm +6 -0
  65. data/icons/Vacation.xpm +26 -0
  66. data/lib/mhc.rb +45 -0
  67. data/lib/mhc/builder.rb +64 -0
  68. data/lib/mhc/caldav.rb +304 -0
  69. data/lib/mhc/calendar.rb +106 -0
  70. data/lib/mhc/command.rb +13 -0
  71. data/lib/mhc/command/cache.rb +14 -0
  72. data/lib/mhc/command/completions.rb +108 -0
  73. data/lib/mhc/command/init.rb +133 -0
  74. data/lib/mhc/command/scan.rb +33 -0
  75. data/lib/mhc/command/sync.rb +22 -0
  76. data/lib/mhc/config.rb +229 -0
  77. data/lib/mhc/converter.rb +330 -0
  78. data/lib/mhc/datastore.rb +164 -0
  79. data/lib/mhc/date_enumerator.rb +274 -0
  80. data/lib/mhc/date_frame.rb +124 -0
  81. data/lib/mhc/date_helper.rb +49 -0
  82. data/lib/mhc/etag.rb +68 -0
  83. data/lib/mhc/event.rb +396 -0
  84. data/lib/mhc/formatter.rb +312 -0
  85. data/lib/mhc/logger.rb +94 -0
  86. data/lib/mhc/modifier.rb +149 -0
  87. data/lib/mhc/occurrence.rb +94 -0
  88. data/lib/mhc/occurrence_enumerator.rb +113 -0
  89. data/lib/mhc/property_value.rb +33 -0
  90. data/lib/mhc/property_value/date.rb +190 -0
  91. data/lib/mhc/property_value/integer.rb +15 -0
  92. data/lib/mhc/property_value/list.rb +41 -0
  93. data/lib/mhc/property_value/period.rb +49 -0
  94. data/lib/mhc/property_value/range.rb +100 -0
  95. data/lib/mhc/property_value/recurrence_condition.rb +272 -0
  96. data/lib/mhc/property_value/text.rb +11 -0
  97. data/lib/mhc/property_value/time.rb +45 -0
  98. data/lib/mhc/query.rb +210 -0
  99. data/lib/mhc/sync.rb +46 -0
  100. data/lib/mhc/sync/driver.rb +108 -0
  101. data/lib/mhc/sync/status.rb +70 -0
  102. data/lib/mhc/sync/status_manager.rb +142 -0
  103. data/lib/mhc/sync/strategy.rb +233 -0
  104. data/lib/mhc/sync/syncinfo.rb +98 -0
  105. data/lib/mhc/templates/config.yml.erb +142 -0
  106. data/lib/mhc/version.rb +4 -0
  107. data/lib/mhc/webdav.rb +319 -0
  108. data/mhc.gemspec +24 -0
  109. data/samples/DOT.mhc-config.yml +116 -0
  110. data/samples/japanese-holidays.mhcc +153 -0
  111. data/samples/mhc-completions.zsh +11 -0
  112. data/spec/mhc_spec.rb +682 -0
  113. data/spec/spec_helper.rb +9 -0
  114. data/xpm/close.xpm +18 -0
  115. data/xpm/delete.xpm +19 -0
  116. data/xpm/exit.xpm +18 -0
  117. data/xpm/month.xpm +18 -0
  118. data/xpm/next.xpm +18 -0
  119. data/xpm/next2.xpm +18 -0
  120. data/xpm/next_year.xpm +18 -0
  121. data/xpm/open.xpm +19 -0
  122. data/xpm/prev.xpm +18 -0
  123. data/xpm/prev2.xpm +18 -0
  124. data/xpm/prev_year.xpm +18 -0
  125. data/xpm/save.xpm +19 -0
  126. data/xpm/today.xpm +18 -0
  127. metadata +214 -0
@@ -0,0 +1,142 @@
1
+ ################################################################
2
+ # config.yml file for MHC created by mhc init command.
3
+ #
4
+ <%
5
+ repository = @config[:topdir] || '~/mhc'
6
+ tzid = @config[:tzid] || 'Unknown'
7
+ -%>
8
+ ---
9
+ ################################################################
10
+ GENERAL:
11
+ ################################################################
12
+
13
+ # TZID defines timezone of all articles.
14
+ # It must be in the form of "Asia/Tokyo".
15
+ # ``mhc init'' is clever enough to set your timezone automatically,
16
+ # but, you may want to change this value.
17
+ # Valid names for TZID is:
18
+ # http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
19
+
20
+ TZID: "<%= @config[:tzid] || 'Unknown' %>"
21
+
22
+ # All MHC articles and status files are placed under this directory:
23
+
24
+ REPOSITORY: "<%= @config[:topdir] || '~/mhc' %>"
25
+
26
+ ################################################################
27
+ SYNC_CHANNELS:
28
+ ################################################################
29
+
30
+ # Each sync channel binds two CALENDARS (see below) for
31
+ # data-synchronization. Currently, STRATEGY allows only "mirror",
32
+ # CALENDAR1 must be a local MHC calendar, and CALENDAR2 must be a
33
+ # Google Calendar. True two-way sync is not yet implemented.
34
+
35
+ - NAME: business
36
+ CALENDAR1: business
37
+ CALENDAR2: google_business
38
+ STRATEGY: mirror
39
+
40
+ - NAME: family
41
+ CALENDAR1: family
42
+ CALENDAR2: google_family
43
+ STRATEGY: mirror
44
+
45
+ ################################################################
46
+ CALENDARS:
47
+ ################################################################
48
+
49
+ ################################################################
50
+ ## MHC local
51
+
52
+ # First entry under CALENDARS: is your default calendar.
53
+ # It must be a local MHC calendar, which has ``TYPE: mhc'' entry.
54
+
55
+ # Master calendar (default)
56
+ - &mhc_default
57
+ NAME: master
58
+ TYPE: mhc
59
+
60
+ # This ``business'' is a sample calendar, which is derived from
61
+ # ``master'' calendar with some filters and modifiers. Filters hide
62
+ # or select articles from master calendar. Modifiers decorate
63
+ # articles by preset decorators: hide_details, hide_description,
64
+ # hide_location, hide_time_range. You can write your original
65
+ # decorators (written in Ruby) in plugins/ directory located at the
66
+ # same place with this file.
67
+
68
+ - <<: *mhc_default
69
+ NAME: business
70
+ FILTER: '!category:private & !subject:"TODO"'
71
+ MODIFIERS:
72
+ - hide_description
73
+ - hide_location
74
+ - replace_subject_by_category
75
+
76
+ # Another sample, supposed to be published to your family's Google
77
+ # Calendar.
78
+ - <<: *mhc_default
79
+ NAME: family
80
+ FILTER: 'category:[birthday holiday private]'
81
+ MODIFIERS:
82
+ - hide_description
83
+
84
+ ################################################################
85
+ ## Google Calendar
86
+
87
+ # Currently, Google Calendar stuffs work only as destinations
88
+ # to publish from local MHC calendars.
89
+ #
90
+ # Since MHC uses standard CalDAV protocol, I believe
91
+ # it works with other CalDAV-based network calendars such as
92
+ # iCloud, but I did not confirmed.
93
+ #
94
+ # Google Calendar URL for CalDAV is in the form of:
95
+ # https://www.google.com/calendar/dav/{calendar_id}/events
96
+ #
97
+ # calendar_id:
98
+ # for primary calendar is just your email address.
99
+ # for others will be in the form of:
100
+ # [long-string-of-characters]@group.calendar.google.com
101
+ #
102
+ # You can get them by clicking the down arrow next to your
103
+ # calendar at calendar.google.com and selecting 'Calendar Settings'.
104
+ # Be sure to use HTTPS in your URL, as an http address will not work.
105
+ #
106
+ # for more details about Google Calendar settings:
107
+ # http://www.google.com/support/calendar/bin/answer.py?answer=99358
108
+ # and click Sunbird section.
109
+ #
110
+ # Fill in the blanks indicated as ***, and invoke the command:
111
+ #
112
+ # % mhc sync {sync_channel_name}
113
+ #
114
+ # {sync_channel_name} must exist in the section of SYNC_CHANNELS:
115
+ #
116
+ # NOTE:
117
+ # Articles between past 90 days and future 90 days in MHC will be
118
+ # published to the target Google Calendar. 90 is hard-coded in
119
+ # lib/mhc/calendar.rb for now. Other articles out of the range in
120
+ # Google Calendar will be DELETED.
121
+ #
122
+ # Before the first try, I recommend you to make a new calendar on your
123
+ # Google Calendar dedicated to MHC. and backup your local spool of
124
+ # MHC.
125
+
126
+ # Displayname: MHC
127
+ - &google_default
128
+ NAME: google_personal
129
+ TYPE: caldav
130
+ USER: "***@gmail.com"
131
+ PASSWORD: "***"
132
+ URL: "https://calendar.google.com/calendar/dav/***@group.calendar.google.com/events/"
133
+
134
+ # Displayname: For Colleagues
135
+ - <<: *google_default
136
+ NAME: google_business
137
+ URL: "https://calendar.google.com/calendar/dav/***@group.calendar.google.com/events/"
138
+
139
+ # Displayname: For Family
140
+ - <<: *google_default
141
+ NAME: google_family
142
+ URL: "https://calendar.google.com/calendar/dav/***@group.calendar.google.com/events/"
@@ -0,0 +1,4 @@
1
+ module Mhc
2
+ VERSION = "1.0.0"
3
+ PRODID = "-//Quickhack.net//MHC #{Mhc::VERSION}//EN"
4
+ end
data/lib/mhc/webdav.rb ADDED
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "net/https"
4
+ require "uri"
5
+ require "rexml/document"
6
+ require "fileutils"
7
+
8
+ module Mhc
9
+ class WebDav
10
+ # WebDAV protocol: RFC4918
11
+ # see http://tools.ietf.org/html/rfc4918
12
+ #
13
+ class Client
14
+ attr_reader :top_directory
15
+
16
+ def initialize(base_url, proxy_host = nil, proxy_port = nil)
17
+ uri = URI.parse(base_url)
18
+ @top_directory = uri.path
19
+ @http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
20
+ @http.use_ssl = true if uri.scheme == "https"
21
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
22
+ end
23
+
24
+ def set_basic_auth(user, password)
25
+ @auth_user, @auth_password = user, password
26
+ return self
27
+ end
28
+
29
+ # 8.1 PROPFIND
30
+ def propfind(path = @top_directory, depth = 1, xml_body = nil)
31
+ req = setup_request(Net::HTTP::Propfind, path)
32
+ req['Depth'] = depth
33
+
34
+ if xml_body
35
+ req.content_type = 'application/xml; charset="utf-8"'
36
+ req.content_length = xml_body.size
37
+ req.body = xml_body
38
+ end
39
+
40
+ res = @http.request(req)
41
+
42
+ if $MHC_DEBUG
43
+ STDERR.print "\n* PROPFIND RESPONSE:\n"
44
+ STDERR.print dump_response(res, true)
45
+ end
46
+
47
+ check_status_code(res, 207) # Multi-Status
48
+
49
+ return res
50
+ end
51
+
52
+ # 8.2 PROPPATCH
53
+ def proppatch
54
+ raise NotImplementedError
55
+ end
56
+
57
+ # 8.3 MKCOL
58
+ def mkcol(path)
59
+ req = setup_request(Net::HTTP::Mkcol, path)
60
+ res = @http.request(req)
61
+ check_status_code(res, 201) # Created
62
+ return res
63
+ end
64
+
65
+ # 8.4 GET
66
+ def get(path)
67
+ req = setup_request(Net::HTTP::Get, path)
68
+ res = @http.request(req)
69
+ check_status_code(res, 200) # OK
70
+ return res
71
+ end
72
+
73
+ # 8.4 HEAD
74
+ def head(path)
75
+ req = setup_request(Net::HTTP::Head, path)
76
+ res = @http.request(req)
77
+ check_status_code(res, 200) # OK
78
+ return res
79
+ end
80
+
81
+ # 8.5 POST
82
+ def post(content, dest_path)
83
+ req = setup_request(Net::HTTP::Post, dest_path)
84
+ req.content_length = content.size
85
+ req.body = content
86
+ res = @http.request(req)
87
+
88
+ check_status_code(res, [201, 204]) # Created or No content
89
+ return res
90
+ end
91
+
92
+ # 8.6 DELETE
93
+ def delete(path, ifmatch = nil)
94
+ req = setup_request(Net::HTTP::Delete, path)
95
+ req['If-Match'] = ifmatch if ifmatch
96
+ res = @http.request(req)
97
+
98
+ check_status_code(res, 204)
99
+ return res
100
+ end
101
+
102
+ # 8.7 PUT
103
+ def put(content, dest_path, ifmatch = nil)
104
+ req = setup_request(Net::HTTP::Put, dest_path)
105
+ req.content_length = content.size
106
+ req['If-Match'] = ifmatch if ifmatch
107
+ req.content_type = "text/calendar; charset=utf-8" # xxx
108
+ req.body = content
109
+ res = @http.request(req)
110
+
111
+ if $MHC_DEBUG
112
+ STDERR.print "\n* PUT RESPONSE:\n"
113
+ STDERR.print dump_response(res)
114
+ STDERR.print "* HEAD RESPONSE:\n"
115
+ STDERR.print dump_response(head(dest_path))
116
+ end
117
+
118
+ check_status_code(res, [201, 204]) # Created or No content
119
+ return res
120
+ end
121
+
122
+ # 8.8 COPY
123
+ def copy(src_path, dest_path)
124
+ req = setup_request(Net::HTTP::Copy, src_path)
125
+ req['Destination'] = dest_path
126
+
127
+ res = @http.request(req)
128
+ check_status_code(res, 204) # No Content
129
+ return res
130
+ end
131
+
132
+ # 8.9 MOVE
133
+ def move(src_path, dest_path)
134
+ req = setup_request(Net::HTTP::Move, src_path)
135
+ req['Destination'] = dest_path
136
+
137
+ res = @http.request(req)
138
+ check_status_code(res, 204) # No Content
139
+ return res
140
+ end
141
+
142
+ # 8.10 LOCK
143
+ def lock(path)
144
+ raise NotImplementedError
145
+ end
146
+
147
+ # 8.11 UNLOCK
148
+ def unlock(path)
149
+ raise NotImplementedError
150
+ end
151
+
152
+ ################################################################
153
+ private
154
+
155
+ def check_status_code(res, required_status)
156
+ unless ([required_status].flatten.map{|c| c.to_s}).member?(res.code)
157
+ header = "Invalid HttpResponse"
158
+ raise Exception.new("#{res.code} #{header} #{res.message} #{res.body}")
159
+ end
160
+ end
161
+
162
+ def setup_request(request, *args)
163
+ req = request.new(*args)
164
+ req.basic_auth @auth_user, @auth_password
165
+
166
+ # XXX: should implement re-connection mechanism for Keep-Alive:
167
+ # http://d.hatena.ne.jp/daftbeats/20080321/1206092975
168
+ req["Connection"] = "Keep-Alive"
169
+
170
+ return req
171
+ end
172
+
173
+ def fetch(uri_str, limit = 10)
174
+ raise StandardError, 'HTTP redirect too deep' if limit == 0
175
+
176
+ response = Net::HTTP.get_response(URI.parse(uri_str))
177
+ case response
178
+ when Net::HTTPSuccess
179
+ response
180
+ when Net::HTTPRedirection
181
+ fetch(response['location'], limit - 1)
182
+ else
183
+ response.value
184
+ end
185
+ end
186
+
187
+ def dump_response(res, include_body = false)
188
+ string = ""
189
+
190
+ res.each do |name, value|
191
+ string += " #{name}: #{value}\n"
192
+ end
193
+ string += res.body + "\n" if include_body
194
+
195
+ return string
196
+ end
197
+
198
+ end # class Client
199
+
200
+ class Cache
201
+ class DirectoryNotFoundError < StandardError
202
+ end
203
+
204
+ def initialize(top_directory)
205
+ set_top_directory(top_directory)
206
+ end
207
+
208
+ def set_top_directory(path)
209
+ raise DirectoryNotFoundError unless File.directory?(path)
210
+ @local_top_pathname = Pathname.new(path)
211
+ return self
212
+ end
213
+
214
+ def set_propfind_cache(path, xml)
215
+ File.open(local_cache_path(path), "w") do |f|
216
+ f.write(xml)
217
+ end
218
+ end
219
+
220
+ def set_basic_auth(user, password)
221
+ # nothing to do
222
+ return self
223
+ end
224
+
225
+ # 8.1 PROPFIND
226
+ def propfind(path, depth = 1, xml_body = nil)
227
+ File.read(local_cache_path(path)) rescue nil
228
+ end
229
+
230
+ # 8.2 PROPPATCH
231
+ def proppatch
232
+ raise NotImplementedError
233
+ end
234
+
235
+ # 8.3 MKCOL
236
+ def mkcol(path)
237
+ File.mkdir(local_path(path))
238
+ end
239
+
240
+ # 8.4 GET
241
+ def get(path)
242
+ File.read(local_path(path))
243
+ end
244
+
245
+ # 8.4 HEAD
246
+ def head(path)
247
+ raise NotImplementedError
248
+ end
249
+
250
+ # 8.5 POST
251
+ def post(content, dest_path)
252
+ raise NotImplementedError
253
+ end
254
+
255
+ # 8.6 DELETE
256
+ def delete(path)
257
+ File.unlink(local_path(path))
258
+ end
259
+
260
+ # 8.7 PUT
261
+ def put(content, dest_path)
262
+ make_directory_or_higher(File.dirname(local_path(dest_path)))
263
+
264
+ File.open(local_path(dest_path), "w") do |f|
265
+ f.write(content)
266
+ end
267
+ end
268
+
269
+ # 8.8 COPY
270
+ def copy(src_path, dest_path)
271
+ raise NotImplementedError
272
+ end
273
+
274
+ # 8.9 MOVE
275
+ def move(src_path, dest_path)
276
+ raise NotImplementedError
277
+ end
278
+
279
+ # 8.10 LOCK
280
+ def lock(path)
281
+ raise NotImplementedError
282
+ end
283
+
284
+ # 8.11 UNLOCK
285
+ def unlock(path)
286
+ raise NotImplementedError
287
+ end
288
+
289
+ private
290
+
291
+ def make_directory_or_higher(directory)
292
+ unless File.directory?(directory)
293
+ parent = File.dirname(directory)
294
+ make_directory_or_higher(parent)
295
+ print "mkdir #{directory}\n"
296
+ return Dir.mkdir(directory)
297
+ end
298
+ end
299
+
300
+ def local_pathname(path)
301
+ pathname = Pathname.new(path)
302
+ raise "path (#{path.to_s})should be absolute." unless pathname.absolute?
303
+ (@local_top_pathname + ("./" + pathname)).cleanpath
304
+ end
305
+
306
+ def local_path(path)
307
+ local_pathname(path).to_s
308
+ end
309
+
310
+ def local_cache_path(path)
311
+ if File.directory?(local_path(path))
312
+ (local_pathname(path) + "propfind-cache.xml").cleanpath.to_s
313
+ else
314
+ local_path(path)
315
+ end
316
+ end
317
+ end # class Cache
318
+ end # class WebDav
319
+ end # module Mhc