aboutsummaryrefslogtreecommitdiff
path: root/libdino/src/service/avatar_manager.vala
blob: b308aa2b096b5488398290659b9e549a17ecdd65 (plain) (blame)
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
using Gdk;
using Gee;
using Qlite;

using Xmpp;
using Dino.Entities;

namespace Dino {

public class AvatarManager : StreamInteractionModule, Object {
    public static ModuleIdentity<AvatarManager> IDENTITY = new ModuleIdentity<AvatarManager>("avatar_manager");
    public string id { get { return IDENTITY.id; } }

    public signal void received_avatar(Jid jid, Account account);

    private enum Source {
        USER_AVATARS,
        VCARD
    }

    private StreamInteractor stream_interactor;
    private Database db;
    private string folder = null;
    private HashMap<Jid, string> user_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
    private HashMap<Jid, string> vcard_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
    private HashMap<string, Pixbuf> cached_pixbuf = new HashMap<string, Pixbuf>();
    private HashMap<string, Gee.List<SourceFuncWrapper>> pending_pixbuf = new HashMap<string, Gee.List<SourceFuncWrapper>>();
    private const int MAX_PIXEL = 192;

    public static void start(StreamInteractor stream_interactor, Database db) {
        AvatarManager m = new AvatarManager(stream_interactor, db);
        stream_interactor.add_module(m);
    }

    private AvatarManager(StreamInteractor stream_interactor, Database db) {
        this.stream_interactor = stream_interactor;
        this.db = db;
        this.folder = Path.build_filename(Dino.get_storage_dir(), "avatars");
        DirUtils.create_with_parents(this.folder, 0700);

        stream_interactor.account_added.connect(on_account_added);
        stream_interactor.module_manager.initialize_account_modules.connect((_, modules) => {
            modules.add(new Xep.UserAvatars.Module());
            modules.add(new Xep.VCard.Module());
        });
    }

    private string? get_avatar_hash(Account account, Jid jid_) {
        Jid jid = jid_;
        if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) {
            jid = jid_.bare_jid;
        }
        if (user_avatars.has_key(jid)) {
            return user_avatars[jid];
        } else if (vcard_avatars.has_key(jid)) {
            return vcard_avatars[jid];
        } else {
            return null;
        }
    }

    public bool has_avatar_cached(Account account, Jid jid) {
        string? hash = get_avatar_hash(account, jid);
        return hash != null && cached_pixbuf.has_key(hash);
    }

    public bool has_avatar(Account account, Jid jid) {
        return get_avatar_hash(account, jid) != null;
    }

    public Pixbuf? get_cached_avatar(Account account, Jid jid_) {
        string? hash = get_avatar_hash(account, jid_);
        if (hash == null) return null;
        if (cached_pixbuf.has_key(hash)) return cached_pixbuf[hash];
        return null;
    }

    public async Pixbuf? get_avatar(Account account, Jid jid_) {
        Jid jid = jid_;
        if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) {
            jid = jid_.bare_jid;
        }

        int source = -1;
        string? hash = null;
        if (user_avatars.has_key(jid)) {
            hash = user_avatars[jid];
            source = 1;
        } else if (vcard_avatars.has_key(jid)) {
            hash = vcard_avatars[jid];
            source = 2;
        }

        if (hash == null) return null;

        if (cached_pixbuf.has_key(hash)) {
            return cached_pixbuf[hash];
        }

        XmppStream? stream = stream_interactor.get_stream(account);
        if (stream == null || !stream.negotiation_complete) return null;

        if (pending_pixbuf.has_key(hash)) {
            pending_pixbuf[hash].add(new SourceFuncWrapper(get_avatar.callback));
            yield;
            return cached_pixbuf[hash];
        }

        pending_pixbuf[hash] = new ArrayList<SourceFuncWrapper>();
        Pixbuf? image = yield get_image(hash);
        if (image != null) {
            cached_pixbuf[hash] = image;
        } else {
            Bytes? bytes = null;
            if (source == 1) {
                bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash);
            } else if (source == 2) {
                bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash);
                if (bytes == null && jid.is_bare()) {
                    db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform();
                }
            }
            if (bytes != null) {
                store_image(hash, bytes);
                image = yield get_image(hash);
            }
            cached_pixbuf[hash] = image;
        }
        foreach (SourceFuncWrapper sfw in pending_pixbuf[hash]) {
            sfw.sfun();
        }
        return image;
    }

    public void publish(Account account, string file) {
        try {
            Pixbuf pixbuf = new Pixbuf.from_file(file);
            if (pixbuf.width >= pixbuf.height && pixbuf.width > MAX_PIXEL) {
                int dest_height = (int) ((float) MAX_PIXEL / pixbuf.width * pixbuf.height);
                pixbuf = pixbuf.scale_simple(MAX_PIXEL, dest_height, InterpType.BILINEAR);
            } else if (pixbuf.height > pixbuf.width && pixbuf.width > MAX_PIXEL) {
                int dest_width = (int) ((float) MAX_PIXEL / pixbuf.height * pixbuf.width);
                pixbuf = pixbuf.scale_simple(dest_width, MAX_PIXEL, InterpType.BILINEAR);
            }
            uint8[] buffer;
            pixbuf.save_to_buffer(out buffer, "png");
            XmppStream stream = stream_interactor.get_stream(account);
            if (stream != null) {
                Xmpp.Xep.UserAvatars.publish_png(stream, buffer, pixbuf.width, pixbuf.height);
            }
        } catch (Error e) {
            warning(e.message);
        }
    }

    private void on_account_added(Account account) {
        stream_interactor.module_manager.get_module(account, Xep.UserAvatars.Module.IDENTITY).received_avatar_hash.connect((stream, jid, id) =>
            on_user_avatar_received.begin(account, jid, id)
        );
        stream_interactor.module_manager.get_module(account, Xep.VCard.Module.IDENTITY).received_avatar_hash.connect((stream, jid, id) =>
            on_vcard_avatar_received.begin(account, jid, id)
        );

        foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) {
            user_avatars[entry.key] = entry.value;
        }
        foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) {

            // FIXME: remove. temporary to remove falsely saved avatars.
            if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(entry.key, account)) {
                db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(entry.key)).perform();
                continue;
            }

            vcard_avatars[entry.key] = entry.value;
        }
    }

    private async void on_user_avatar_received(Account account, Jid jid_, string id) {
        Jid jid = jid_.bare_jid;

        if (!user_avatars.has_key(jid) || user_avatars[jid] != id) {
            user_avatars[jid] = id;
            set_avatar_hash(account, jid, id, Source.USER_AVATARS);
        }
        received_avatar(jid, account);
    }

    private async void on_vcard_avatar_received(Account account, Jid jid_, string id) {
        bool is_gc = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(jid_.bare_jid, account);
        Jid jid = is_gc ? jid_ : jid_.bare_jid;

        if (!vcard_avatars.has_key(jid) || vcard_avatars[jid] != id) {
            vcard_avatars[jid] = id;
            if (jid.is_bare()) { // don't save MUC occupant avatars
                set_avatar_hash(account, jid, id, Source.VCARD);
            }
        }
        received_avatar(jid, account);
    }

    public void set_avatar_hash(Account account, Jid jid, string hash, int type) {
        db.avatar.insert()
            .value(db.avatar.jid_id, db.get_jid_id(jid))
            .value(db.avatar.account_id, account.id)
            .value(db.avatar.hash, hash)
            .value(db.avatar.type_, type)
            .perform();
    }

    public HashMap<Jid, string> get_avatar_hashes(Account account, int type) {
        HashMap<Jid, string> ret = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func);
        foreach (Row row in db.avatar.select({db.avatar.jid_id, db.avatar.hash})
                .with(db.avatar.type_, "=", type)
                .with(db.avatar.account_id, "=", account.id)) {
            ret[db.get_jid_by_id(row[db.avatar.jid_id])] = row[db.avatar.hash];
        }
        return ret;
    }

    public void store_image(string id, Bytes data) {
        File file = File.new_for_path(Path.build_filename(folder, id));
        try {
            if (file.query_exists()) file.delete(); //TODO y?
            DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION));
            fos.write_bytes_async.begin(data);
        } catch (Error e) {
            // Ignore: we failed in storing, so we refuse to display later...
        }
    }

    public bool has_image(string id) {
        File file = File.new_for_path(Path.build_filename(folder, id));
        return file.query_exists();
    }

    public async Pixbuf? get_image(string id) {
        try {
            File file = File.new_for_path(Path.build_filename(folder, id));
            FileInputStream stream = yield file.read_async(Priority.LOW);

            uint8 fbuf[1024];
            size_t size;

            Checksum checksum = new Checksum (ChecksumType.SHA1);
            while ((size = yield stream.read_async(fbuf, Priority.LOW)) > 0) {
                checksum.update(fbuf, size);
            }

            if (checksum.get_string() != id) {
                FileUtils.remove(file.get_path());
            }
            stream.seek(0, SeekType.SET);
            return yield new Pixbuf.from_stream_async(stream, null);
        } catch (Error e) {
            return null;
        }
    }
}

}