bmf 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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