heathrow 0.7.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.
- checksums.yaml +7 -0
- data/.gitignore +58 -0
- data/README.md +205 -0
- data/bin/heathrow +42 -0
- data/bin/heathrowd +283 -0
- data/docs/ARCHITECTURE.md +1172 -0
- data/docs/DATABASE_SCHEMA.md +685 -0
- data/docs/DEVELOPMENT_WORKFLOW.md +867 -0
- data/docs/DISCORD_SETUP.md +142 -0
- data/docs/GMAIL_OAUTH_SETUP.md +120 -0
- data/docs/PLUGIN_SYSTEM.md +1370 -0
- data/docs/PROJECT_PLAN.md +1022 -0
- data/docs/README.md +417 -0
- data/docs/REDDIT_SETUP.md +174 -0
- data/docs/REPLY_FORWARD.md +182 -0
- data/docs/WHATSAPP_TELEGRAM_SETUP.md +306 -0
- data/heathrow.gemspec +34 -0
- data/heathrowd.service +21 -0
- data/img/heathrow.svg +95 -0
- data/img/rss_threaded.png +0 -0
- data/img/sources.png +0 -0
- data/lib/heathrow/address_book.rb +42 -0
- data/lib/heathrow/config.rb +332 -0
- data/lib/heathrow/database.rb +731 -0
- data/lib/heathrow/database_new.rb +392 -0
- data/lib/heathrow/event_bus.rb +175 -0
- data/lib/heathrow/logger.rb +122 -0
- data/lib/heathrow/message.rb +176 -0
- data/lib/heathrow/message_composer.rb +399 -0
- data/lib/heathrow/message_organizer.rb +774 -0
- data/lib/heathrow/migrations/001_initial_schema.rb +248 -0
- data/lib/heathrow/notmuch.rb +45 -0
- data/lib/heathrow/oauth2_smtp.rb +254 -0
- data/lib/heathrow/plugin/base.rb +212 -0
- data/lib/heathrow/plugin_manager.rb +141 -0
- data/lib/heathrow/poller.rb +93 -0
- data/lib/heathrow/smtp_sender.rb +204 -0
- data/lib/heathrow/source.rb +39 -0
- data/lib/heathrow/sources/base.rb +74 -0
- data/lib/heathrow/sources/discord.rb +357 -0
- data/lib/heathrow/sources/gmail.rb +294 -0
- data/lib/heathrow/sources/imap.rb +198 -0
- data/lib/heathrow/sources/instagram.rb +307 -0
- data/lib/heathrow/sources/instagram_fetch.py +101 -0
- data/lib/heathrow/sources/instagram_send.py +55 -0
- data/lib/heathrow/sources/instagram_send_marionette.py +104 -0
- data/lib/heathrow/sources/maildir.rb +606 -0
- data/lib/heathrow/sources/messenger.rb +212 -0
- data/lib/heathrow/sources/messenger_fetch.js +297 -0
- data/lib/heathrow/sources/messenger_fetch_marionette.py +138 -0
- data/lib/heathrow/sources/messenger_send.js +32 -0
- data/lib/heathrow/sources/messenger_send.py +100 -0
- data/lib/heathrow/sources/reddit.rb +461 -0
- data/lib/heathrow/sources/rss.rb +299 -0
- data/lib/heathrow/sources/slack.rb +375 -0
- data/lib/heathrow/sources/source_manager.rb +328 -0
- data/lib/heathrow/sources/telegram.rb +498 -0
- data/lib/heathrow/sources/webpage.rb +207 -0
- data/lib/heathrow/sources/weechat.rb +479 -0
- data/lib/heathrow/sources/whatsapp.rb +474 -0
- data/lib/heathrow/ui/application.rb +8098 -0
- data/lib/heathrow/ui/navigation.rb +8 -0
- data/lib/heathrow/ui/panes.rb +8 -0
- data/lib/heathrow/ui/source_wizard.rb +567 -0
- data/lib/heathrow/ui/threaded_view.rb +780 -0
- data/lib/heathrow/ui/views.rb +8 -0
- data/lib/heathrow/version.rb +3 -0
- data/lib/heathrow/wizards/discord_wizard.rb +193 -0
- data/lib/heathrow/wizards/slack_wizard.rb +140 -0
- data/lib/heathrow.rb +55 -0
- metadata +147 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# WeeChat source via Relay protocol (binary)
|
|
5
|
+
# Connects to a running WeeChat instance's relay to read IRC/Slack buffers
|
|
6
|
+
|
|
7
|
+
require 'socket'
|
|
8
|
+
require 'digest'
|
|
9
|
+
require 'json'
|
|
10
|
+
require 'time'
|
|
11
|
+
require 'zlib'
|
|
12
|
+
require_relative 'base'
|
|
13
|
+
|
|
14
|
+
module Heathrow
|
|
15
|
+
module Sources
|
|
16
|
+
class Weechat < Base
|
|
17
|
+
CONFIG_DIR = File.join(Dir.home, '.heathrow', 'cookies')
|
|
18
|
+
CONFIG_FILE = File.join(CONFIG_DIR, 'weechat.json')
|
|
19
|
+
|
|
20
|
+
def initialize(name_or_source, config = nil, db = nil)
|
|
21
|
+
if name_or_source.is_a?(Hash)
|
|
22
|
+
source = name_or_source
|
|
23
|
+
config = source['config']
|
|
24
|
+
config = JSON.parse(config) if config.is_a?(String)
|
|
25
|
+
super(source['name'] || 'WeeChat', config, db)
|
|
26
|
+
else
|
|
27
|
+
super(name_or_source, config, db)
|
|
28
|
+
end
|
|
29
|
+
@host = config['host'] || 'localhost'
|
|
30
|
+
@port = (config['port'] || 8001).to_i
|
|
31
|
+
@password = config['password'] || ''
|
|
32
|
+
@buffer_filter = config['buffer_filter'] # e.g., 'irc.*,python.slack.*'
|
|
33
|
+
@lines_per_buffer = (config['lines_per_buffer'] || 50).to_i
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def sync(source_id)
|
|
37
|
+
sock = connect_and_auth
|
|
38
|
+
return 0 unless sock
|
|
39
|
+
|
|
40
|
+
count = 0
|
|
41
|
+
begin
|
|
42
|
+
# Request buffer list
|
|
43
|
+
sock.write("(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,title,type,local_variables\n")
|
|
44
|
+
buffers_msg = read_and_parse(sock)
|
|
45
|
+
buffer_list = extract_hdata(buffers_msg)
|
|
46
|
+
return 0 if buffer_list.empty?
|
|
47
|
+
|
|
48
|
+
buffer_list.each do |buf|
|
|
49
|
+
full_name = buf['full_name'].to_s
|
|
50
|
+
# Skip non-message buffers
|
|
51
|
+
next if full_name == 'core.weechat'
|
|
52
|
+
next if full_name =~ /^(relay\.|fset\.|script\.|irc\.server\.)/
|
|
53
|
+
|
|
54
|
+
# Apply buffer filter
|
|
55
|
+
if @buffer_filter
|
|
56
|
+
patterns = @buffer_filter.split(',').map(&:strip)
|
|
57
|
+
next unless patterns.any? { |p| File.fnmatch(p, full_name) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
ptr = buf['__path']&.first
|
|
61
|
+
next unless ptr
|
|
62
|
+
|
|
63
|
+
# Fetch recent lines (hdata path: buffer:PTR/own_lines/last_line/data)
|
|
64
|
+
sock.write("(lines_#{ptr}) hdata buffer:#{ptr}/own_lines/last_line(-#{@lines_per_buffer})/data date,prefix,message,tags_array,displayed,highlight,notify_level\n")
|
|
65
|
+
lines_msg = read_and_parse(sock)
|
|
66
|
+
next unless lines_msg
|
|
67
|
+
|
|
68
|
+
lines = extract_hdata(lines_msg)
|
|
69
|
+
lines.each do |line|
|
|
70
|
+
next unless line['displayed'].to_i == 1
|
|
71
|
+
count += process_line(source_id, buf, line)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
ensure
|
|
75
|
+
sock.write("quit\n") rescue nil
|
|
76
|
+
sock.close rescue nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
count
|
|
80
|
+
rescue Errno::ECONNREFUSED
|
|
81
|
+
STDERR.puts "WeeChat relay not available at #{@host}:#{@port}" if ENV['DEBUG']
|
|
82
|
+
0
|
|
83
|
+
rescue => e
|
|
84
|
+
STDERR.puts "WeeChat error: #{e.message}" if ENV['DEBUG']
|
|
85
|
+
STDERR.puts e.backtrace.first(3).join("\n") if ENV['DEBUG']
|
|
86
|
+
0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fetch
|
|
90
|
+
return [] unless enabled?
|
|
91
|
+
source = @db.get_source_by_name(@name)
|
|
92
|
+
return [] unless source
|
|
93
|
+
sync(source['id'])
|
|
94
|
+
update_last_fetch
|
|
95
|
+
[]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_connection
|
|
99
|
+
sock = connect_and_auth
|
|
100
|
+
return { success: false, message: "Auth failed at #{@host}:#{@port}" } unless sock
|
|
101
|
+
|
|
102
|
+
sock.write("(version) info version\n")
|
|
103
|
+
msg = read_and_parse(sock)
|
|
104
|
+
version = extract_info(msg)
|
|
105
|
+
|
|
106
|
+
sock.write("(buflist) hdata buffer:gui_buffers(*) full_name\n")
|
|
107
|
+
buf_msg = read_and_parse(sock)
|
|
108
|
+
buffers = extract_hdata(buf_msg)
|
|
109
|
+
|
|
110
|
+
sock.write("quit\n") rescue nil
|
|
111
|
+
sock.close rescue nil
|
|
112
|
+
|
|
113
|
+
{ success: true, message: "WeeChat #{version} - #{buffers.size} buffers" }
|
|
114
|
+
rescue Errno::ECONNREFUSED
|
|
115
|
+
{ success: false, message: "Cannot connect to #{@host}:#{@port}" }
|
|
116
|
+
rescue => e
|
|
117
|
+
{ success: false, message: "Error: #{e.message}" }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Send a message to a buffer via the relay
|
|
121
|
+
def send_to_buffer(buffer_name, text)
|
|
122
|
+
sock = connect_and_auth
|
|
123
|
+
return { success: false, message: "Auth failed" } unless sock
|
|
124
|
+
|
|
125
|
+
sock.write("input #{buffer_name} #{text}\n")
|
|
126
|
+
sock.write("quit\n") rescue nil
|
|
127
|
+
sock.close rescue nil
|
|
128
|
+
|
|
129
|
+
{ success: true, message: "Sent to #{buffer_name}" }
|
|
130
|
+
rescue => e
|
|
131
|
+
{ success: false, message: "Send failed: #{e.message}" }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Standard send_message interface (used by send_composed_message)
|
|
135
|
+
def send_message(to, subject, body)
|
|
136
|
+
send_to_buffer(to, body)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def connect_and_auth
|
|
142
|
+
sock = TCPSocket.new(@host, @port)
|
|
143
|
+
sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
144
|
+
|
|
145
|
+
# Plain password auth (relay should be behind SSH tunnel or trusted network)
|
|
146
|
+
sock.write("init password=#{@password},compression=off\n")
|
|
147
|
+
|
|
148
|
+
# Verify auth with a ping
|
|
149
|
+
sock.write("(auth_check) ping auth\n")
|
|
150
|
+
response = read_and_parse(sock)
|
|
151
|
+
response ? sock : (sock.close; nil)
|
|
152
|
+
rescue => e
|
|
153
|
+
STDERR.puts "WeeChat connect error: #{e.message}" if ENV['DEBUG']
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# ── Binary protocol reader ──
|
|
158
|
+
|
|
159
|
+
def read_message(sock)
|
|
160
|
+
len_bytes = read_exact(sock, 4)
|
|
161
|
+
return nil unless len_bytes
|
|
162
|
+
|
|
163
|
+
length = len_bytes.unpack1('N')
|
|
164
|
+
return nil if length < 5 || length > 10_000_000
|
|
165
|
+
|
|
166
|
+
data = read_exact(sock, length - 4)
|
|
167
|
+
return nil unless data
|
|
168
|
+
|
|
169
|
+
compression = data[0].unpack1('C')
|
|
170
|
+
payload = data[1..]
|
|
171
|
+
|
|
172
|
+
if compression == 1
|
|
173
|
+
payload = Zlib::Inflate.inflate(payload)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
payload
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def read_exact(sock, n)
|
|
180
|
+
buf = String.new(encoding: 'ASCII-8BIT')
|
|
181
|
+
while buf.size < n
|
|
182
|
+
chunk = sock.read(n - buf.size)
|
|
183
|
+
return nil unless chunk && !chunk.empty?
|
|
184
|
+
buf << chunk
|
|
185
|
+
end
|
|
186
|
+
buf
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def read_and_parse(sock)
|
|
190
|
+
payload = read_message(sock)
|
|
191
|
+
return nil unless payload
|
|
192
|
+
parse_message(payload)
|
|
193
|
+
rescue => e
|
|
194
|
+
STDERR.puts "WeeChat parse error: #{e.message}" if ENV['DEBUG']
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# ── Binary protocol parser ──
|
|
199
|
+
|
|
200
|
+
def parse_message(data)
|
|
201
|
+
pos = 0
|
|
202
|
+
msg = {}
|
|
203
|
+
msg[:id], pos = read_str(data, pos)
|
|
204
|
+
msg[:objects] = []
|
|
205
|
+
|
|
206
|
+
while pos < data.size - 2
|
|
207
|
+
type = data[pos, 3]
|
|
208
|
+
break unless type && type.size == 3
|
|
209
|
+
pos += 3
|
|
210
|
+
obj, pos = read_object(data, pos, type)
|
|
211
|
+
msg[:objects] << { type: type, value: obj }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
msg
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def read_object(data, pos, type)
|
|
218
|
+
case type
|
|
219
|
+
when 'chr' then [data[pos].unpack1('c'), pos + 1]
|
|
220
|
+
when 'int' then read_int(data, pos)
|
|
221
|
+
when 'lon' then read_length_prefixed_ascii(data, pos)
|
|
222
|
+
when 'str', 'buf' then read_str(data, pos)
|
|
223
|
+
when 'ptr' then read_ptr(data, pos)
|
|
224
|
+
when 'tim' then read_length_prefixed_ascii(data, pos)
|
|
225
|
+
when 'htb' then read_hashtable(data, pos)
|
|
226
|
+
when 'hda' then read_hdata(data, pos)
|
|
227
|
+
when 'inf' then read_info(data, pos)
|
|
228
|
+
when 'inl' then read_infolist(data, pos)
|
|
229
|
+
when 'arr' then read_array(data, pos)
|
|
230
|
+
else [nil, data.size]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def read_int(data, pos)
|
|
235
|
+
val = data[pos, 4].unpack1('N')
|
|
236
|
+
val = val - 0x100000000 if val > 0x7FFFFFFF
|
|
237
|
+
[val, pos + 4]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def read_str(data, pos)
|
|
241
|
+
return [nil, pos + 4] if data.size < pos + 4
|
|
242
|
+
len = data[pos, 4].unpack1('N')
|
|
243
|
+
pos += 4
|
|
244
|
+
return [nil, pos] if len >= 0xFFFFFFF0 # NULL
|
|
245
|
+
return ['', pos] if len == 0
|
|
246
|
+
str = data[pos, len]
|
|
247
|
+
str = str.force_encoding('UTF-8') rescue str
|
|
248
|
+
[str, pos + len]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def read_ptr(data, pos)
|
|
252
|
+
len = data[pos].unpack1('C')
|
|
253
|
+
pos += 1
|
|
254
|
+
["0x#{data[pos, len]}", pos + len]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def read_length_prefixed_ascii(data, pos)
|
|
258
|
+
len = data[pos].unpack1('C')
|
|
259
|
+
pos += 1
|
|
260
|
+
[data[pos, len].to_i, pos + len]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def read_hashtable(data, pos)
|
|
264
|
+
key_type = data[pos, 3]; pos += 3
|
|
265
|
+
val_type = data[pos, 3]; pos += 3
|
|
266
|
+
count, pos = read_int(data, pos)
|
|
267
|
+
|
|
268
|
+
hash = {}
|
|
269
|
+
count.times do
|
|
270
|
+
key, pos = read_object(data, pos, key_type)
|
|
271
|
+
val, pos = read_object(data, pos, val_type)
|
|
272
|
+
hash[key.to_s] = val
|
|
273
|
+
end
|
|
274
|
+
[hash, pos]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def read_hdata(data, pos)
|
|
278
|
+
path, pos = read_str(data, pos)
|
|
279
|
+
keys_str, pos = read_str(data, pos)
|
|
280
|
+
count, pos = read_int(data, pos)
|
|
281
|
+
|
|
282
|
+
path_parts = (path || '').split('/')
|
|
283
|
+
keys = (keys_str || '').split(',').map { |k| k.split(':', 2) }
|
|
284
|
+
|
|
285
|
+
items = []
|
|
286
|
+
count.times do
|
|
287
|
+
item = {}
|
|
288
|
+
ptrs = []
|
|
289
|
+
path_parts.size.times do
|
|
290
|
+
ptr, pos = read_object(data, pos, 'ptr')
|
|
291
|
+
ptrs << ptr
|
|
292
|
+
end
|
|
293
|
+
item['__path'] = ptrs
|
|
294
|
+
|
|
295
|
+
keys.each do |name, ktype|
|
|
296
|
+
val, pos = read_object(data, pos, ktype)
|
|
297
|
+
item[name] = val
|
|
298
|
+
end
|
|
299
|
+
items << item
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
[items, pos]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def read_info(data, pos)
|
|
306
|
+
name, pos = read_str(data, pos)
|
|
307
|
+
value, pos = read_str(data, pos)
|
|
308
|
+
[{ name: name, value: value }, pos]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def read_infolist(data, pos)
|
|
312
|
+
name, pos = read_str(data, pos)
|
|
313
|
+
count, pos = read_int(data, pos)
|
|
314
|
+
|
|
315
|
+
items = []
|
|
316
|
+
count.times do
|
|
317
|
+
var_count, pos = read_int(data, pos)
|
|
318
|
+
item = {}
|
|
319
|
+
var_count.times do
|
|
320
|
+
var_name, pos = read_str(data, pos)
|
|
321
|
+
var_type = data[pos, 3]; pos += 3
|
|
322
|
+
var_val, pos = read_object(data, pos, var_type)
|
|
323
|
+
item[var_name.to_s] = var_val
|
|
324
|
+
end
|
|
325
|
+
items << item
|
|
326
|
+
end
|
|
327
|
+
[{ name: name, items: items }, pos]
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def read_array(data, pos)
|
|
331
|
+
elem_type = data[pos, 3]; pos += 3
|
|
332
|
+
count, pos = read_int(data, pos)
|
|
333
|
+
|
|
334
|
+
arr = []
|
|
335
|
+
count.times do
|
|
336
|
+
val, pos = read_object(data, pos, elem_type)
|
|
337
|
+
arr << val
|
|
338
|
+
end
|
|
339
|
+
[arr, pos]
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# ── Helpers ──
|
|
343
|
+
|
|
344
|
+
def extract_hdata(msg)
|
|
345
|
+
return [] unless msg && msg[:objects]
|
|
346
|
+
msg[:objects].each do |obj|
|
|
347
|
+
return obj[:value] if obj[:type] == 'hda' && obj[:value].is_a?(Array)
|
|
348
|
+
end
|
|
349
|
+
[]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def extract_info(msg)
|
|
353
|
+
return nil unless msg && msg[:objects]
|
|
354
|
+
msg[:objects].each do |obj|
|
|
355
|
+
return obj[:value][:value] if obj[:type] == 'inf' && obj[:value].is_a?(Hash)
|
|
356
|
+
end
|
|
357
|
+
nil
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Strip WeeChat color codes from text
|
|
361
|
+
# Format: \x19 followed by color type char and color value, terminated by \x1C or next \x19
|
|
362
|
+
# Color types: F=foreground, B=background, *=bold, etc.
|
|
363
|
+
# Color values: NN (2-digit) or @NNNNN (extended 5-digit)
|
|
364
|
+
def strip_colors(text)
|
|
365
|
+
return '' unless text
|
|
366
|
+
# Remove \x19 + type char + optional @ + digits (color sequences)
|
|
367
|
+
text.gsub(/\x19[FB*_\/|][~@]?\d{0,5}/, '')
|
|
368
|
+
.gsub(/\x19\x1C/, '') # reset sequence
|
|
369
|
+
.gsub(/\x19[^\x19\x1C]*/, '') # catch remaining color codes
|
|
370
|
+
.gsub(/[\x00-\x1f]/, '') # strip all remaining control chars
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def buffer_type(buf)
|
|
374
|
+
full_name = buf['full_name'].to_s
|
|
375
|
+
local_vars = buf['local_variables']
|
|
376
|
+
local_vars = {} unless local_vars.is_a?(Hash)
|
|
377
|
+
type = local_vars['type'] || ''
|
|
378
|
+
|
|
379
|
+
if full_name.start_with?('irc.')
|
|
380
|
+
type == 'private' ? :irc_dm : :irc_channel
|
|
381
|
+
elsif full_name =~ /^python\.slack\./
|
|
382
|
+
if type == 'private'
|
|
383
|
+
:slack_dm
|
|
384
|
+
elsif full_name =~ /&/
|
|
385
|
+
:slack_group
|
|
386
|
+
else
|
|
387
|
+
:slack_channel
|
|
388
|
+
end
|
|
389
|
+
else
|
|
390
|
+
:other
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def process_line(source_id, buf, line)
|
|
395
|
+
prefix = strip_colors(line['prefix'] || '')
|
|
396
|
+
message = strip_colors(line['message'] || '')
|
|
397
|
+
timestamp = line['date'].to_i
|
|
398
|
+
tags = line['tags_array'] || []
|
|
399
|
+
|
|
400
|
+
# Skip system messages (joins, parts, quits, mode changes)
|
|
401
|
+
return 0 if tags.any? { |t| t =~ /irc_join|irc_part|irc_quit|irc_nick|irc_mode|irc_numeric/ }
|
|
402
|
+
return 0 if tags.include?('no_log')
|
|
403
|
+
# Must have a nick tag (real messages) or a non-empty prefix
|
|
404
|
+
return 0 unless tags.any? { |t| t.start_with?('nick_') } || prefix =~ /\w/
|
|
405
|
+
return 0 if message.strip.empty?
|
|
406
|
+
|
|
407
|
+
nick = tags.find { |t| t.start_with?('nick_') }&.sub('nick_', '') || prefix
|
|
408
|
+
full_name = buf['full_name'].to_s
|
|
409
|
+
short_name = buf['short_name'] || full_name.split('.').last
|
|
410
|
+
|
|
411
|
+
ext_id = "weechat_#{Digest::MD5.hexdigest("#{full_name}_#{timestamp}_#{nick}_#{message[0..80]}")}"
|
|
412
|
+
|
|
413
|
+
btype = buffer_type(buf)
|
|
414
|
+
is_dm = (btype == :irc_dm || btype == :slack_dm)
|
|
415
|
+
|
|
416
|
+
platform = case btype
|
|
417
|
+
when :irc_dm, :irc_channel then 'IRC'
|
|
418
|
+
when :slack_dm, :slack_channel, :slack_group then 'Slack'
|
|
419
|
+
else 'WeeChat'
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
channel_name = case btype
|
|
423
|
+
when :irc_channel
|
|
424
|
+
net = full_name.split('.')[1] || 'irc'
|
|
425
|
+
"#{net}/#{short_name}"
|
|
426
|
+
when :irc_dm
|
|
427
|
+
nick
|
|
428
|
+
when :slack_channel, :slack_group
|
|
429
|
+
parts = full_name.split('.')
|
|
430
|
+
ws = parts[2] || 'slack'
|
|
431
|
+
chan = parts[3..].join('.') rescue short_name
|
|
432
|
+
"#{ws}/#{chan}"
|
|
433
|
+
when :slack_dm
|
|
434
|
+
parts = full_name.split('.')
|
|
435
|
+
parts[3..].join('.') rescue nick
|
|
436
|
+
else
|
|
437
|
+
short_name
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
data = {
|
|
441
|
+
source_id: source_id,
|
|
442
|
+
external_id: ext_id,
|
|
443
|
+
sender: nick,
|
|
444
|
+
sender_name: nick,
|
|
445
|
+
recipients: [channel_name],
|
|
446
|
+
subject: message.gsub(/\n/, ' ')[0..200],
|
|
447
|
+
content: message,
|
|
448
|
+
html_content: nil,
|
|
449
|
+
timestamp: timestamp,
|
|
450
|
+
received_at: Time.now.to_i,
|
|
451
|
+
read: false,
|
|
452
|
+
starred: false,
|
|
453
|
+
archived: false,
|
|
454
|
+
labels: [platform],
|
|
455
|
+
attachments: nil,
|
|
456
|
+
metadata: {
|
|
457
|
+
buffer: full_name,
|
|
458
|
+
buffer_short: short_name,
|
|
459
|
+
buffer_type: btype.to_s,
|
|
460
|
+
channel_name: channel_name,
|
|
461
|
+
nick: nick,
|
|
462
|
+
is_dm: is_dm,
|
|
463
|
+
platform: platform.downcase,
|
|
464
|
+
highlight: (line['highlight'].to_i == 1),
|
|
465
|
+
tags: tags
|
|
466
|
+
},
|
|
467
|
+
raw_data: { buffer: full_name, nick: nick }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
begin
|
|
471
|
+
@db.insert_message(data)
|
|
472
|
+
1
|
|
473
|
+
rescue SQLite3::ConstraintException
|
|
474
|
+
0
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|