bmf 0.2.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 (40) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.asc +11 -0
  3. data.tar.gz.asc +11 -0
  4. data/bin/bmf +8 -0
  5. data/lib/bmf.rb +70 -0
  6. data/lib/bmf/lib/address_store.rb +45 -0
  7. data/lib/bmf/lib/alert.rb +53 -0
  8. data/lib/bmf/lib/bmf.rb +364 -0
  9. data/lib/bmf/lib/folder.rb +76 -0
  10. data/lib/bmf/lib/message.rb +27 -0
  11. data/lib/bmf/lib/message_store.rb +250 -0
  12. data/lib/bmf/lib/settings.rb +74 -0
  13. data/lib/bmf/lib/thread_status.rb +98 -0
  14. data/lib/bmf/lib/xmlrpc_client.rb +27 -0
  15. data/lib/bmf/public/bitbrowser.css~ +15 -0
  16. data/lib/bmf/public/bitmessageforum.css +50 -0
  17. data/lib/bmf/public/css/bootstrap.min.css +9 -0
  18. data/lib/bmf/public/images/apple-touch-icon-114x114.png +0 -0
  19. data/lib/bmf/public/images/apple-touch-icon-144x144.png +0 -0
  20. data/lib/bmf/public/images/apple-touch-icon-57x57.png +0 -0
  21. data/lib/bmf/public/images/apple-touch-icon-72x72.png +0 -0
  22. data/lib/bmf/public/images/apple-touch-icon.png +0 -0
  23. data/lib/bmf/public/images/favicon.ico +0 -0
  24. data/lib/bmf/public/js/bootstrap.min.js +6 -0
  25. data/lib/bmf/public/js/html5.js +9 -0
  26. data/lib/bmf/public/js/jquery-1.10.1.min.js +6 -0
  27. data/lib/bmf/public/js/messagePoll.js +24 -0
  28. data/lib/bmf/views/addresses.haml +23 -0
  29. data/lib/bmf/views/compose.haml +38 -0
  30. data/lib/bmf/views/couldnt_reach_pybitmessage.haml +24 -0
  31. data/lib/bmf/views/home.haml +12 -0
  32. data/lib/bmf/views/https_quick_start.haml +71 -0
  33. data/lib/bmf/views/identities.haml +32 -0
  34. data/lib/bmf/views/layout.haml +128 -0
  35. data/lib/bmf/views/messages.haml +55 -0
  36. data/lib/bmf/views/settings.haml +18 -0
  37. data/lib/bmf/views/subscriptions.haml +40 -0
  38. data/lib/bmf/views/threads.haml +46 -0
  39. metadata +163 -0
  40. metadata.gz.asc +11 -0
@@ -0,0 +1,76 @@
1
+ require_relative "message_store.rb"
2
+ require_relative "thread_status.rb"
3
+
4
+ class BMF::Folder
5
+ VALID_FOLDERS = %w{chans inbox sent lists}
6
+
7
+ attr_reader :name
8
+
9
+ def initialize folder_name
10
+ raise "Bad folder #{folder_name}" if !VALID_FOLDERS.include?(folder_name)
11
+ @name = folder_name
12
+ @messages = BMF::MessageStore.instance.send(folder_name)
13
+ end
14
+
15
+ def new_messages?
16
+ @messages.each_pair do |address, threads|
17
+ return true if BMF::ThreadStatus.instance.new_messages_for_address?(address, threads.keys)
18
+ end
19
+
20
+ return false
21
+ end
22
+
23
+ def messages opts={}
24
+ msgs = @messages
25
+
26
+ if opts[:sort] == :new
27
+ msgs = msgs.sort{ |a,b| BMF::MessageStore.instance.address_last_updates[a[0]] <=> BMF::MessageStore.instance.address_last_updates[b[0]] }.reverse
28
+ end
29
+
30
+ msgs
31
+ end
32
+
33
+ def threads_for_address address, opts={}
34
+ threads = @messages[address]
35
+
36
+ if threads && opts[:sort] == :new
37
+ threads = threads.sort{ |a,b| BMF::MessageStore.instance.thread_last_updates[address][a[0]] <=> BMF::MessageStore.instance.thread_last_updates[address][b[0]] }.reverse
38
+ end
39
+
40
+ threads
41
+ end
42
+
43
+ def thread_messages address, thread_name, opts={}
44
+ threads = threads_for_address(address)
45
+
46
+ return nil if threads.nil?
47
+
48
+ msgs = threads[thread_name]
49
+ msgs = [] if msgs.nil?
50
+
51
+ if opts[:sort] == :old
52
+ msgs = msgs.sort { |a,b| BMF::Message.time(a) <=> BMF::Message.time(b) }
53
+ end
54
+
55
+ msgs
56
+ end
57
+
58
+ def delete_thread address, thread
59
+ msgs = thread_messages(address, thread)
60
+ return [] if msgs.nil?
61
+
62
+ alerts = []
63
+
64
+ msgs.each do |msg|
65
+ msgid = msg['msgid']
66
+ if msg['_source'] == 'sent'
67
+ alerts << (BMF::XmlrpcClient.instance.trashSentMessage(msgid) + msgid)
68
+ else
69
+ alerts << (BMF::XmlrpcClient.instance.trashMessage(msgid) + msgid)
70
+ end
71
+ end
72
+
73
+ alerts
74
+ end
75
+
76
+ end
@@ -0,0 +1,27 @@
1
+ module BMF::Message
2
+ def self.time(m)
3
+ time = m['receivedTime']
4
+ time = m['lastActionTime'] if time.nil?
5
+ time.to_i
6
+ end
7
+
8
+ def self.sent?(m)
9
+ !m['lastActionTime'].nil?
10
+ end
11
+
12
+ def self.received?(m)
13
+ !m['receivedTime'].nil?
14
+ end
15
+
16
+ def self.sent_or_received m
17
+ if sent?(m)
18
+ :sent
19
+ elsif received?(m)
20
+ :received
21
+ else
22
+ raise "Don't know if #{m.inspect} was sent or received"
23
+ end
24
+ end
25
+
26
+
27
+ end
@@ -0,0 +1,250 @@
1
+ require 'singleton'
2
+ require 'base64'
3
+ require 'json'
4
+ require 'thread'
5
+
6
+ require_relative 'xmlrpc_client.rb'
7
+
8
+ class BMF::MessageStore
9
+ include Singleton
10
+
11
+
12
+
13
+ def initialize
14
+ @messages = {} # messages by msgid
15
+ @address_last_updates = {}
16
+ @thread_last_updates = {}
17
+ @new_messages = 0
18
+ # update
19
+ end
20
+
21
+ def log x
22
+ puts x
23
+ end
24
+
25
+ # attr_reader :messages, :address_last_updates, :thread_last_updates
26
+
27
+ def messages
28
+ Mutex.new.synchronize { @messages.dup.freeze }
29
+ end
30
+
31
+ def address_last_updates
32
+ Mutex.new.synchronize { @address_last_updates.dup.freeze }
33
+ end
34
+
35
+ def thread_last_updates
36
+ Mutex.new.synchronize { @thread_last_updates.dup.freeze }
37
+ end
38
+
39
+ def update_times m
40
+ received_time = BMF::Message.time(m)
41
+ to_address = m["toAddress"]
42
+
43
+ # update channel access time
44
+ @address_last_updates[to_address] ||= 0
45
+ if @address_last_updates[to_address] < received_time
46
+ @address_last_updates[to_address] = received_time
47
+ end
48
+
49
+ subject = m["subject"]
50
+ if subject[0..3] == "Re: "
51
+ subject = subject[4..-1]
52
+ end
53
+
54
+ # update thread access time
55
+ @thread_last_updates[to_address] ||= {}
56
+ @thread_last_updates[to_address][subject] ||= 0
57
+
58
+ if @thread_last_updates[to_address][subject] < received_time
59
+ @thread_last_updates[to_address][subject] = received_time
60
+ end
61
+ end
62
+
63
+ def add_message msgid, m
64
+ m["message"] = Base64.decode64(m["message"]).force_encoding("utf-8")
65
+ m["subject"] = Base64.decode64(m["subject"]).force_encoding("utf-8")
66
+
67
+ m["subject"] = " " if m["subject"] == ""
68
+ m["subject"] = "Re: " if m["subject"] == "Re: "
69
+
70
+ @messages[msgid] = m
71
+
72
+ to_address = m["toAddress"]
73
+
74
+ # Temp hack for lists
75
+ if to_address == "[Broadcast subscribers]"
76
+ hack_mailing_list_name = m["subject"][/\[[^\]]+\]/]
77
+
78
+ hack_mailing_list_name = m["fromAddress"] if hack_mailing_list_name.nil?
79
+
80
+ to_address += " " + hack_mailing_list_name
81
+ m["toAddress"] = to_address
82
+ end
83
+ end
84
+
85
+ def get_full_message mailbox, msgid
86
+ msg_json = BMF::XmlrpcClient.instance.send("get#{mailbox.capitalize}MessageByID", msgid)
87
+ JSON.parse(msg_json)["#{mailbox}Message"][0]
88
+ end
89
+
90
+
91
+ def process_messages new_messages, source="inbox"
92
+ processed_messages = 0
93
+
94
+ new_messages.each do |m|
95
+ msgid = m["msgid"]
96
+
97
+ @new_msgids[msgid] = true
98
+
99
+ if !@messages.has_key?(msgid)
100
+ processed_messages += 1
101
+
102
+ if m["message"] # cliend doesn't support getXMessageIds, so we already have everything
103
+ full_message = m
104
+ else
105
+ full_message = get_full_message(source, msgid)
106
+ end
107
+
108
+
109
+ full_message["_source"] = source
110
+
111
+ add_message msgid, full_message
112
+
113
+ update_times full_message
114
+ end
115
+ end
116
+
117
+ log "Added #{processed_messages} messages..." if processed_messages > 0
118
+
119
+ processed_messages
120
+ end
121
+
122
+ def init_gc
123
+ @new_msgids = {}
124
+ end
125
+
126
+ def do_gc
127
+ deleted_messages = 0
128
+ @messages.keys.each do |old_msgid|
129
+ if !@new_msgids[old_msgid]
130
+ deleted_messages += 1
131
+ @messages.delete(old_msgid)
132
+ end
133
+ end
134
+
135
+ log "Deleted #{deleted_messages}..." if deleted_messages > 0
136
+ end
137
+
138
+ def get_all_message_ids mailbox
139
+ method = "getAll#{mailbox.capitalize}MessageIds"
140
+ msgids = BMF::XmlrpcClient.instance.send(method)
141
+ if BMF::XmlrpcClient.is_error?(msgids) && msgids.downcase.include?("invalid method")
142
+ log "PyBitmessage doesn't support #{method}. Falling back to getAll#{mailbox.capitalize}Messages. Using a version of Pybitmessage that supports #{method} will dramatically increase performance"
143
+ msgids = BMF::XmlrpcClient.instance.send("getAll#{mailbox.capitalize}Messages")
144
+ JSON.parse(msgids)["#{mailbox}Messages"]
145
+ else
146
+ JSON.parse(msgids)["#{mailbox}MessageIds"]
147
+ end
148
+ end
149
+
150
+
151
+ def update
152
+
153
+ inbox_messages = get_all_message_ids "inbox"
154
+ sent_messages = get_all_message_ids "sent"
155
+
156
+ new_messages = 0
157
+
158
+ lock = Mutex.new
159
+ lock.synchronize do
160
+ init_gc
161
+
162
+ new_messages += process_messages(inbox_messages)
163
+ process_messages(sent_messages, "sent")
164
+
165
+ do_gc
166
+ end
167
+
168
+ new_messages
169
+ end
170
+
171
+ def pop_new_message_count
172
+ new = 0
173
+
174
+ Mutex.new.synchronize do
175
+ new += @new_messages
176
+ @new_messages = 0
177
+ end
178
+
179
+ new
180
+ end
181
+
182
+
183
+ def by_recipient sent_or_received=:nil
184
+
185
+ #display messages
186
+
187
+ by_recipient = {}
188
+
189
+ @messages.each do |id, m|
190
+ if sent_or_received
191
+ next if sent_or_received == :sent && !BMF::Message.sent?(m)
192
+ next if sent_or_received == :received && !BMF::Message.received?(m)
193
+ end
194
+
195
+ toAddress = m["toAddress"]
196
+
197
+ subject = m["subject"]
198
+ if subject[0..3] == "Re: "
199
+ subject = subject[4..-1]
200
+ end
201
+
202
+ by_recipient[toAddress] = {} if !by_recipient[toAddress]
203
+ by_recipient[toAddress][subject] = [] if !by_recipient[toAddress][subject]
204
+ by_recipient[m["toAddress"]][subject] << m
205
+ end
206
+
207
+ by_recipient
208
+ end
209
+
210
+ def inbox
211
+ by_recipient(:received).select do |toAddress, messages|
212
+ label = if BMF::AddressStore.instance.addresses.has_key? toAddress
213
+ BMF::AddressStore.instance.addresses[toAddress]['label']
214
+ else
215
+ ""
216
+ end
217
+ not( label.include?("[chan]") || toAddress.include?("[Broadcast subscribers]"))
218
+ end
219
+ end
220
+
221
+ def sent
222
+ by_recipient(:sent).select do |toAddress, messages|
223
+ label = if BMF::AddressStore.instance.addresses.has_key? toAddress
224
+ BMF::AddressStore.instance.addresses[toAddress]['label']
225
+ else
226
+ ""
227
+ end
228
+ not( label.include?("[chan]") || toAddress.include?("[Broadcast subscribers]"))
229
+ end
230
+ end
231
+
232
+ def lists
233
+ by_recipient.select do |toAddress, messages|
234
+ toAddress.include? "[Broadcast subscribers]"
235
+ end
236
+ end
237
+
238
+ def chans
239
+ by_recipient.select do |toAddress, messages|
240
+ label = if BMF::AddressStore.instance.addresses.has_key? toAddress
241
+ BMF::AddressStore.instance.addresses[toAddress]['label']
242
+ else
243
+ ""
244
+ end
245
+ label.include?("[chan]")
246
+ end
247
+ end
248
+
249
+
250
+ end
@@ -0,0 +1,74 @@
1
+ require "singleton"
2
+ require 'yaml'
3
+ require 'fileutils'
4
+
5
+ class BMF::Settings
6
+ include Singleton
7
+
8
+ SETTINGS_AND_DESCRIPTIONS = {
9
+ :server_url => "Url of the BitMessage server to interface with.",
10
+ :sig => "Signature line to attach to messages.",
11
+ :default_send_address => "Default FROM address for messages.",
12
+ :server_interface => "Internet interface to listen on. Set to 0.0.0.0 to open up BMF to the network. WARNING!!! Anyone who can access your IP can read/post/delete/etc.",
13
+ :server_port => "Internet port to listen on.",
14
+ :display_sanitized_html => "Show sanitized HTML minus scripts, css, and any non-inline images.",
15
+ :sync_interval => "Frequency to sync inbox with PyBitmessage, in seconds. Default 60",
16
+ :user => "username for http basic authentication. (You should be using https in conjunction with this!)",
17
+ :password => "password for http basic authentication. (You should be using https in conjunction with this!)",
18
+ :https_server_key_file => "file for https key",
19
+ :https_server_certificate_file => "file for https certificate"
20
+ }
21
+
22
+ DEFAULT_SETTINGS = {"server_url" => 'http://bmf:bmf@localhost:8442/', "display_sanitized_html" => 'no' }
23
+
24
+ VALID_SETTINGS = SETTINGS_AND_DESCRIPTIONS.keys
25
+
26
+ SETTING_DIR = ".bitmessageforum"
27
+ SETTINGS_FILE = "settings.yml"
28
+
29
+ def self.fully_qualified_filename filename
30
+ home_dir = ENV["HOME"] || ENV["HOMEPATH"]
31
+ File.join(home_dir, SETTING_DIR, filename)
32
+ end
33
+
34
+ def initialize
35
+ home_dir = ENV["HOME"] || ENV["HOMEPATH"]
36
+
37
+ setting_dir = File.join(home_dir, SETTING_DIR)
38
+ Dir.mkdir(setting_dir, 0700) if !File.directory? setting_dir
39
+
40
+ settings_filename = BMF::Settings.fully_qualified_filename(SETTINGS_FILE)
41
+
42
+ if !File.exists? (settings_filename)
43
+ puts "No existing settings. Copying defaults into place."
44
+ @settings = DEFAULT_SETTINGS
45
+ persist
46
+ end
47
+
48
+ @settings = YAML.load_file(settings_filename)
49
+ end
50
+
51
+ def update(key, value)
52
+ raise "Bad setting #{key}. Allowed settings #{VALID_SETTINGS.inspect}" if !VALID_SETTINGS.include?(key.to_sym)
53
+ @settings[key] = value
54
+
55
+ BMF::XmlrpcClient.instance.initialize_client if key.to_sym == :server_url
56
+ end
57
+
58
+ def persist
59
+ File.open(BMF::Settings.fully_qualified_filename(SETTINGS_FILE),'w',0600) do |out|
60
+ out.write(@settings.to_yaml)
61
+ end
62
+ end
63
+
64
+ def method_missing(meth, *args)
65
+ if args == [] and VALID_SETTINGS.include?(meth)
66
+ ret = @settings[meth.to_s]
67
+ ret = nil if ret == ""
68
+ return ret
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ end
@@ -0,0 +1,98 @@
1
+ require 'singleton'
2
+ require 'base64'
3
+ require_relative 'message_store.rb'
4
+ require_relative 'settings.rb'
5
+
6
+ class BMF::ThreadStatus
7
+ include Singleton
8
+
9
+ STASH_FILE = BMF::Settings.fully_qualified_filename("thread_status_stash")
10
+
11
+ def initialize
12
+ @thread_last_visited = {}
13
+ load_stash
14
+ end
15
+
16
+ def serialize_stash
17
+ updates = []
18
+ @thread_last_visited.each_pair do |address, threads|
19
+ threads.each_pair do |thread_name, update_time|
20
+ updates << "#{address}:#{Base64.encode64(thread_name).gsub("\n","\\n")}:#{update_time}"
21
+ end
22
+ end
23
+
24
+ updates.join(";")
25
+ end
26
+
27
+ def deserialize_stash serialized_stash
28
+ serialized_stash.split(";").each do |stash_line|
29
+ address, thread, update_time = stash_line.split(':')
30
+ thread = Base64.decode64(thread.gsub("\\n","\n"))
31
+ update_time = update_time.to_i
32
+ thread_visited(address,thread,update_time)
33
+ end
34
+ end
35
+
36
+ def load_stash
37
+ if File.exists? STASH_FILE
38
+ stash = File.open(STASH_FILE).read
39
+ deserialize_stash stash
40
+ end
41
+ rescue Exception => ex # Failure is not an option!
42
+ puts "@" * 80
43
+ puts "Error loading ThreadStatus stash."
44
+ puts "Ignoring so that the app is usable."
45
+ puts "Please report the following information to the project maintainers"
46
+ puts
47
+ puts "Exception: #{ex.message}"
48
+ puts ex.backtrace.join("\n")
49
+ end
50
+
51
+ def persist
52
+ File.open(STASH_FILE,"w",0600) do |f|
53
+ f.write(serialize_stash)
54
+ end
55
+ end
56
+
57
+
58
+ def thread_last_visited(address, thread)
59
+ if @thread_last_visited[address] && @thread_last_visited[address][thread]
60
+ @thread_last_visited[address][thread]
61
+ else
62
+ 0
63
+ end
64
+ end
65
+
66
+ def thread_visited(address, thread, time)
67
+ @thread_last_visited[address] ||= {}
68
+ @thread_last_visited[address][thread] ||= 0
69
+
70
+ if time > @thread_last_visited[address][thread]
71
+ @thread_last_visited[address][thread] = time
72
+ end
73
+
74
+ persist
75
+ end
76
+
77
+ def new_messages?(address, thread)
78
+ if @thread_last_visited[address] && @thread_last_visited[address][thread]
79
+ last_visited_time = @thread_last_visited[address][thread]
80
+ else
81
+ last_visited_time = 0
82
+ end
83
+
84
+ last_message_time = BMF::MessageStore.instance.thread_last_updates[address][thread]
85
+
86
+ raise thread.inspect if last_message_time.nil?
87
+
88
+ last_visited_time < last_message_time
89
+ end
90
+
91
+ def new_messages_for_address?(address, threads)
92
+ return true if !@thread_last_visited[address] # never been updated
93
+
94
+ not threads.detect{ |thread| new_messages?(address, thread)}.nil?
95
+
96
+ end
97
+
98
+ end