1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8class SocialLib
9{
10	private $relationlib;
11	private $networkType;
12
13	function __construct()
14	{
15		global $prefs;
16		$this->relationlib = TikiLib::lib('relation');
17		$this->networkType = $prefs['social_network_type'] ?: 'follow';
18	}
19
20	function listFriends($user)
21	{
22		return $this->getRelations('follow', $user);
23	}
24
25	function listFollowers($user)
26	{
27		return $this->getRelations('follow.invert', $user);
28	}
29
30	function listIncomingRequests($user)
31	{
32		return $this->getRelations('request.invert', $user);
33	}
34
35	function listOutgoingRequests($user)
36	{
37		return $this->getRelations('request', $user);
38	}
39
40	function addFriend($user, $newFriend)
41	{
42		if ($user == $newFriend) {
43			return false;
44		}
45
46		$userlib = TikiLib::lib('user');
47		if (! $userlib->user_exists($user) || ! $userlib->user_exists($newFriend)) {
48			return false;
49		}
50
51		$tx = TikiDb::get()->begin();
52
53		$hash = $this->createHash($user, $newFriend);
54
55		if ($this->networkType == 'follow') {
56			$this->addRelation('follow', $user, $newFriend);
57			TikiLib::events()->trigger(
58				'tiki.user.follow.add',
59				[
60					'type' => 'user',
61					'object' => $user,
62					'user' => $user,
63					'follow_id' => $newFriend,
64					'aggregate' => $hash,
65				]
66			);
67			TikiLib::events()->trigger(
68				'tiki.user.follow.incoming',
69				[
70					'type' => 'user',
71					'object' => $newFriend,
72					'user' => $newFriend,
73					'follow_id' => $user,
74					'aggregate' => $hash,
75				]
76			);
77		} elseif ($this->networkType == 'follow_approval' || $this->networkType == 'friend') {
78			$request = $this->getRelation('request.invert', $user, $newFriend);
79			$follow = $this->getRelation('follow.invert', $user, $newFriend);
80
81			if ($request || $follow) {
82				// If there was a pending request by the other side (or pre-approved), remove the request
83				// and approve both directions.
84
85				// Re-add or empty-delete are not an issue
86				$this->relationlib->remove_relation($request);
87				$this->addRelation('follow', $user, $newFriend);
88				$this->addRelation('follow.invert', $user, $newFriend);
89
90				$event = ($this->networkType == 'friend') ? 'tiki.user.friend.add' : 'tiki.user.follow.add';
91				TikiLib::events()->trigger(
92					$event,
93					[
94						'type' => 'user',
95						'object' => $user,
96						'user' => $user,
97						'follow_id' => $newFriend,
98						'aggregate' => $hash,
99					]
100				);
101				TikiLib::events()->trigger(
102					$event,
103					[
104						'type' => 'user',
105						'object' => $newFriend,
106						'user' => $newFriend,
107						'follow_id' => $user,
108						'aggregate' => $hash,
109					]
110				);
111			} else {
112				// New request
113				$this->addRelation('request', $user, $newFriend);
114			}
115		}
116
117		$tx->commit();
118
119		require_once('lib/search/refresh-functions.php');
120		refresh_index('user', $user);
121		refresh_index('user', $newFriend);
122
123		return true;
124	}
125
126	function approveFriend($user, $newFriend)
127	{
128		if ($this->networkType != 'follow_approval') {
129			return false;
130		}
131
132		$request = $this->getRelation('request.invert', $user, $newFriend);
133
134		if ($request) {
135			$tx = TikiDb::get()->begin();
136
137			// If there was a pending request by the other side, remove the request
138			// and add them as follower
139			$this->relationlib->remove_relation($request);
140			$this->addRelation('follow.invert', $user, $newFriend);
141
142			TikiLib::events()->trigger(
143				'tiki.user.follow.add',
144				[
145					'type' => 'user',
146					'object' => $newFriend,
147					'user' => $newFriend,
148					'follow_id' => $user,
149				]
150			);
151			TikiLib::events()->trigger(
152				'tiki.user.follow.incoming',
153				[
154					'type' => 'user',
155					'object' => $user,
156					'user' => $user,
157					'follow_id' => $newFriend,
158				]
159			);
160
161			$tx->commit();
162
163			require_once('lib/search/refresh-functions.php');
164			refresh_index('user', $user);
165			refresh_index('user', $newFriend);
166
167			return true;
168		}
169
170		return false;
171	}
172
173	function removeFriend($user, $oldFriend)
174	{
175		$follow = $this->getRelation('follow', $user, $oldFriend);
176		$followInvert = $this->getRelation('follow.invert', $user, $oldFriend);
177		$request = $this->getRelation('request', $user, $oldFriend);
178		$requestInvert = $this->getRelation('request.invert', $user, $oldFriend);
179
180		if ($follow) {
181			$this->relationlib->remove_relation($follow);
182
183			if ($this->networkType == 'friend') {
184				// Friendship breakups are bidirectional, not follow ones
185				$this->relationlib->remove_relation($followInvert);
186			}
187			require_once('lib/search/refresh-functions.php');
188			refresh_index('user', $user);
189			refresh_index('user', $oldFriend);
190			return true;
191		} elseif ($request || $requestInvert) {
192			$this->relationlib->remove_relation($request);
193			$this->relationlib->remove_relation($requestInvert);
194			require_once('lib/search/refresh-functions.php');
195			refresh_index('user', $user);
196			refresh_index('user', $oldFriend);
197			return true;
198		} else {
199			return false;
200		}
201	}
202
203	private function getRelations($type, $from)
204	{
205		$relations = $this->relationlib->get_relations_from('user', $from, 'tiki.friend.' . $type);
206
207		return array_map(
208			function ($relation) {
209				return [
210					'user' => $relation['itemId'],
211				];
212			},
213			$relations
214		);
215	}
216
217	private function addRelation($type, $from, $to)
218	{
219		return $this->relationlib->add_relation('tiki.friend.' . $type, 'user', $from, 'user', $to);
220	}
221
222	private function getRelation($type, $from, $to)
223	{
224		return $this->relationlib->get_relation_id('tiki.friend.' . $type, 'user', $from, 'user', $to);
225	}
226
227	private function createHash($a, $b)
228	{
229		// Hashing needs constant user ordering, so sort
230		if ($a > $b) {
231			$t = $b;
232			$b = $a;
233			$a = $t;
234		}
235
236		return sha1("friendrelation/$a/$b");
237	}
238
239	function addLike($user, $type, $id)
240	{
241		$like = $this->getLike($user, $type, $id);
242
243		if (! $like) {
244			$this->relationlib->add_relation('tiki.social.like', 'user', $user, $type, $id);
245			TikiLib::events()->trigger(
246				'tiki.social.like.add',
247				[
248					'type' => $type,
249					'object' => $id,
250					'user' => $user,
251				]
252			);
253			return true;
254		}
255
256		return false;
257	}
258
259	function removeLike($user, $type, $id)
260	{
261		$like = $this->getLike($user, $type, $id);
262
263		if ($like) {
264			$this->relationlib->remove_relation($like);
265			TikiLib::events()->trigger(
266				'tiki.social.like.remove',
267				[
268					'type' => $type,
269					'object' => $id,
270					'user' => $user,
271				]
272			);
273			return true;
274		}
275
276		return false;
277	}
278
279	function getLikes($type, $id)
280	{
281		$relations = $this->relationlib->get_relations_to($type, $id, 'tiki.social.like');
282
283		return array_map(
284			function ($relation) {
285				return  $relation['itemId'];
286			},
287			$relations
288		);
289	}
290
291	private function getLike($user, $type, $id)
292	{
293		return $this->relationlib->get_relation_id('tiki.social.like', 'user', $user, $type, $id);
294	}
295}
296