File: mod_diaspora_contacts.lua

package info (click to toggle)
ruby-diaspora-prosody-config 0.0.7-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 184 kB
  • sloc: ruby: 141; makefile: 3; sh: 2
file content (274 lines) | stat: -rw-r--r-- 8,754 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
-- Prosody module to import diaspora contacts into a users roster.
-- Inspired by mod_auth_sql and mod_groups of the Prosody software.
--
-- As with mod_groups the change is not permanent and thus any changes
-- to the imported contacts will be lost.
--
-- The MIT License (MIT)
--
-- Copyright (c) <2014> <Jonne Haß <me@jhass.eu>>
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.

local log = require "util.logger".init("diaspora_contacts")
local DBI = require "DBI"
local jid, datamanager = require "util.jid", require "util.datamanager"
local jid_prep = jid.prep
local rostermanager = require "core.rostermanager"

local module_host = module:get_host()
local host = prosody.hosts[module_host]

local connection
local params = module:get_option("diaspora_contacts", module:get_option("auth_diaspora", module:get_option("auth_sql", module:get_option("sql"))))

local function test_connection()
  if not connection then return nil; end

  if connection:ping() then
    return true
  else
    module:log("debug", "Database connection closed")
    connection = nil
  end
end

local function set_encoding(conn)
  if params.driver ~= "MySQL" then return; end
  local set_names_query = "SET NAMES '%s';"
  local stmt = assert(conn:prepare("SET NAMES 'utf8mb4';"));
  assert(stmt:execute());
end

local function connect()
  if not test_connection() then
    prosody.unlock_globals()
    local dbh, err = DBI.Connect(
      params.driver, params.database,
      params.username, params.password,
      params.host, params.port
    )
    prosody.lock_globals()
    if not dbh then
      module:log("debug", "Database connection failed: %s", tostring(err))
      return nil, err
    end
    set_encoding(dbh);
    module:log("debug", "Successfully connected to database")
    dbh:autocommit(true) -- don't run in transaction
    connection = dbh
    return connection
  end
end

do -- process options to get a db connection
  assert(params.driver and params.database, "Both the SQL driver and the database need to be specified")

  assert(connect())
end

local function getsql(sql, ...)
  if params.driver == "PostgreSQL" then
    sql = sql:gsub("`", "\"")
  elseif params.driver == "MySQL" then
    sql = sql:gsub(";$", " CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';")
  end

  if not test_connection() then connect(); end

  -- do prepared statement stuff
  local stmt, err = connection:prepare(sql)
  if not stmt and not test_connection() then error("connection failed"); end
  if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
  -- run query
  local ok, err = stmt:execute(...)
  if not ok and not test_connection() then error("connection failed"); end
  if not ok then return nil, err; end

  return stmt;
end

local function get_contacts(username)
  module:log("debug", "loading contacts for %s", username)
  local contacts = {}

  local stmt, err = getsql([[
    SELECT people.diaspora_handle AS jid,
           COALESCE(NULLIF(CONCAT(first_name, ' ', last_name), ' '), people.diaspora_handle) AS name,
           CONCAT(aspects.name, ' (Diaspora)') AS group_name,
           CASE
             WHEN sharing = true  AND receiving = true  THEN 'both'
             WHEN sharing = true  AND receiving = false THEN 'to'
             WHEN sharing = false AND receiving = true  THEN 'from'
             ELSE                                            'none'
           END AS subscription
    FROM contacts
      JOIN people ON people.id = contacts.person_id
      JOIN profiles ON profiles.person_id = people.id
      JOIN users ON users.id = contacts.user_id
      JOIN aspect_memberships ON aspect_memberships.contact_id = contacts.id
      JOIN aspects ON aspects.id = aspect_memberships.aspect_id
    WHERE (receiving = true OR sharing = true)
      AND chat_enabled = true
      AND username = ?
  ]], username)

  if stmt then
    for row in stmt:rows(true) do
      if not contacts[row.jid] then
        contacts[row.jid] = {}
        contacts[row.jid].subscription = row.subscription
        contacts[row.jid].name = row.name
        contacts[row.jid].groups = {}
      end

      contacts[row.jid].groups[row.group_name] = true
    end

    return contacts
  end
end

local function update_roster(roster, contacts, update_action)
  if not contacts then return; end

  for user_jid, contact in pairs(contacts) do
    local updated = false

    if not roster[user_jid] then
      roster[user_jid] = {}
      roster[user_jid].subscription = contact.subscription
      roster[user_jid].name = contact.name
      roster[user_jid].persist = false
      updated = true
    end

    if not roster[user_jid].groups then
      roster[user_jid].groups = {}
    end

    for group in pairs(contact.groups) do
      if not roster[user_jid].groups[group] then
        roster[user_jid].groups[group] = true
        updated = true
      end
    end

    for group in pairs(roster[user_jid].groups) do
      if not contact.groups[group] then
        roster[user_jid].groups[group] = nil
        updated = true
      end
    end

    if updated and update_action then
      update_action(user_jid)
    end
  end

  for user_jid, contact in pairs(roster) do
    if contact.persist == false then
      if not contacts[user_jid] then
        roster[user_jid] = nil

        if update_action then
          update_action(user_jid)
        end
      end
    end
  end
end

function bump_roster_version(roster)
  if roster[false] then
    roster[false].version = (tonumber(roster[false].version) or 0) + 1
  end
end

local function update_roster_contacts(username, host, roster)
  update_roster(roster, get_contacts(username), function (user_jid)
    module:log("debug", "pushing roster update to %s for %s", jid.join(username, host), user_jid)
    bump_roster_version(roster)
    rostermanager.roster_push(username, host, user_jid)
  end)
end

function inject_roster_contacts(event, var2, var3)
  local username = ""
  local host = ""
  local roster = {}
  if type(event) == "table" then
    module:log("debug", "Prosody 0.10 or trunk detected. Use event variable.")
    username = event.username
    host = event.host
    roster = event.roster
  else
    module:log("debug", "Prosody 0.9.x detected, Use old variable style.")
    username = event
    host = var2
    roster = var3
  end
  local fulljid = jid.join(username, host)
  module:log("debug", "injecting contacts for %s", fulljid)
  update_roster(roster, get_contacts(username))

  bump_roster_version(roster)
end


function update_all_rosters()
  module:log("debug", "updating all rosters")

  for username, user in pairs(host.sessions) do
    module:log("debug", "Updating roster for %s", jid.join(username, module_host))
    update_roster_contacts(username, module_host, rostermanager.load_roster(username, module_host))
  end

  return 300
end

function remove_virtual_contacts(username, host, datastore, roster)
  if host == module_host and datastore == "roster" then
    module:log("debug", "removing injected contacts before storing roster of %s", jid.join(username, host))

    local new_roster = {}
    for jid, contact in pairs(roster) do
      if contact.persist ~= false then
        new_roster[jid] = contact
      end
    end
    if roster[false] then
      new_roster[false] = {}
      new_roster[false].version = roster[false].version
    end
    return username, host, datastore, new_roster
  end

  return username, host, datastore, roster
end

function module.load()
  module:hook("roster-load", inject_roster_contacts)
  module:add_timer(300, update_all_rosters)
  datamanager.add_callback(remove_virtual_contacts)
end

function module.unload()
  datamanager.remove_callback(remove_virtual_contacts)
end