Skip to content

Commit 76ce491

Browse files
committed
WIP: feat: Greenlight 3 import command
1 parent d536e93 commit 76ce491

File tree

4 files changed

+688
-0
lines changed

4 files changed

+688
-0
lines changed
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Enums\RoomLobby;
6+
use App\Enums\RoomUserRole;
7+
use App\Models\Role;
8+
use App\Models\Room;
9+
use App\Models\RoomType;
10+
use App\Models\User;
11+
use App\Settings\GeneralSettings;
12+
use Config;
13+
use DB;
14+
use Hash;
15+
use Illuminate\Console\Command;
16+
use Illuminate\Support\Collection;
17+
use Str;
18+
19+
use function Laravel\Prompts\confirm;
20+
use function Laravel\Prompts\progress;
21+
use function Laravel\Prompts\select;
22+
use function Laravel\Prompts\text;
23+
24+
class ImportGreenlight3Command extends Command
25+
{
26+
protected $signature = 'import:greenlight-v3
27+
{host : ip or hostname of postgres database server}
28+
{port : port of postgres database server}
29+
{database : greenlight database name, see greenlight .env variable DB_NAME}
30+
{username : greenlight database username, see greenlight .env variable DB_USERNAME}
31+
{password : greenlight database password, see greenlight .env variable DB_PASSWORD}';
32+
33+
protected $description = 'Connect to greenlight PostgreSQL database to import users, rooms and shared room accesses';
34+
35+
public function handle()
36+
{
37+
Config::set('database.connections.greenlight', [
38+
'driver' => 'pgsql',
39+
'host' => $this->argument('host'),
40+
'database' => $this->argument('database'),
41+
'username' => $this->argument('username'),
42+
'password' => $this->argument('password'),
43+
'port' => $this->argument('port'),
44+
'charset' => 'utf8',
45+
'prefix' => '',
46+
'prefix_indexes' => true,
47+
'schema' => 'public',
48+
'sslmode' => 'prefer',
49+
]);
50+
51+
$users = DB::connection('greenlight')->table('users')->where('deleted', false)->where('provider', 'greenlight')->get(['id', 'name', 'email', 'external_id', 'password_digest']);
52+
$rooms = DB::connection('greenlight')->table('rooms')->where('deleted', false)->get(['id', 'friendly_id', 'user_id', 'name']);
53+
$sharedAccesses = DB::connection('greenlight')->table('shared_accesses')->get(['room_id', 'user_id']);
54+
55+
// ask user what room type the imported rooms should get
56+
$roomType = select(
57+
label: 'What room type should the rooms be assigned to?',
58+
options: RoomType::pluck('name', 'id'),
59+
scroll: 10
60+
);
61+
62+
// ask user to add prefix to room names
63+
$prefix = text(
64+
label: 'Prefix for room names',
65+
placeholder: 'E.g. (Imported)',
66+
hint: '(Optional).'
67+
);
68+
69+
// ask user what room type the imported rooms should get
70+
$defaultRole = select(
71+
'Please select the default role for new imported non-ldap users',
72+
options: Role::pluck('name', 'id'),
73+
scroll: 10
74+
);
75+
76+
// Start transaction to rollback if import fails or user cancels
77+
DB::beginTransaction();
78+
79+
try {
80+
$userMap = $this->importUsers($users, $defaultRole);
81+
$roomMap = $this->importRooms($rooms, $roomType, $userMap, $prefix);
82+
$this->importSharedAccesses($sharedAccesses, $roomMap, $userMap);
83+
84+
if (confirm('Do you wish to commit the import?')) {
85+
DB::commit();
86+
$this->info('Import completed');
87+
} else {
88+
DB::rollBack();
89+
$this->warn('Import canceled; nothing was imported');
90+
}
91+
} catch (\Exception $e) {
92+
DB::rollBack();
93+
$this->error('Import failed: '.$e->getMessage());
94+
}
95+
}
96+
97+
/**
98+
* Process greenlight user collection and try to import users
99+
*
100+
* @param Collection $users Collection with all users found in the greenlight database
101+
* @param int $defaultRole IDs of the role that should be assigned to new non-ldap users
102+
* @return array Array map of greenlight user ids as key and id of the found/created user as value
103+
*/
104+
protected function importUsers(Collection $users, int $defaultRole): array
105+
{
106+
$this->line('Importing users');
107+
$userMap = [];
108+
109+
$bar = progress(label: 'Importing users', steps: $users->count());
110+
$bar->start();
111+
112+
// counter of user ids that already exists
113+
$existed = 0;
114+
// counter of users that are created
115+
$created = 0;
116+
117+
foreach ($users as $user) {
118+
// check if user with this email exists
119+
$dbUser = User::where('email', $user->email)->first();
120+
if ($dbUser != null) {
121+
// user found, link greenlight user id to id of found user
122+
$existed++;
123+
} else {
124+
// create new user
125+
$dbUser = new User;
126+
$dbUser->authenticator = $user->external_id ? 'oidc' : 'local';
127+
$dbUser->external_id = $user->external_id;
128+
$dbUser->email = $user->email;
129+
// as greenlight doesn't split the name in first and lastname,
130+
// we have to import it as firstname and ask the users or admins to correct it later if desired
131+
$dbUser->firstname = $user->name;
132+
$dbUser->lastname = '';
133+
$dbUser->password = $user->external_id ? Hash::make(Str::random()) : $user->password_digest;
134+
$dbUser->locale = config('app.locale');
135+
$dbUser->timezone = app(GeneralSettings::class)->default_timezone;
136+
$dbUser->save();
137+
138+
if (! $user->external_id) {
139+
$dbUser->roles()->attach($defaultRole);
140+
}
141+
142+
// user was successfully created, link greenlight user id to id of new user
143+
$created++;
144+
}
145+
$userMap[$user->id] = $dbUser->id;
146+
$bar->advance();
147+
}
148+
149+
$bar->finish();
150+
151+
// show import results
152+
$this->line('');
153+
$this->info($created.' created, '.$existed.' skipped (already existed)');
154+
155+
$this->line('');
156+
157+
return $userMap;
158+
}
159+
160+
/**
161+
* Process greenlight room collection and create the rooms if not already existing
162+
*
163+
* @param Collection $rooms Collection with rooms users found in the greenlight database
164+
* @param int $roomType ID of the roomtype the rooms should be assigned to
165+
* @param array $userMap Array map of greenlight user ids as key and id of the found/created user as value
166+
* @param string|null $prefix Prefix to add to room names
167+
* @return array Array map of greenlight room ids as key and id of the created room as value
168+
*/
169+
protected function importRooms(Collection $rooms, int $roomType, array $userMap, ?string $prefix): array
170+
{
171+
$this->line('Importing rooms');
172+
173+
$bar = $this->output->createProgressBar($rooms->count());
174+
$bar->start();
175+
176+
// counter of room ids that already exists
177+
$existed = 0;
178+
// counter of rooms that are created
179+
$created = 0;
180+
// list of rooms that could not be created, e.g. room owner not found
181+
$failed = [];
182+
// array with the key being the greenlight id and value the new object id
183+
$roomMap = [];
184+
185+
// walk through all found greenlight rooms
186+
foreach ($rooms as $room) {
187+
// check if a room with the same id exists
188+
$dbRoom = Room::find($room->friendly_id);
189+
if ($dbRoom != null) {
190+
// if found add counter but not add to room map
191+
// this also prevents adding shared access, as we can't know if this id collision belongs to the same room
192+
// and a shared access import is desired
193+
$existed++;
194+
$bar->advance();
195+
196+
continue;
197+
}
198+
199+
// try to find owner of this room
200+
if (! isset($userMap[$room->user_id])) {
201+
// if owner was not found, eg. missing in the greenlight db or user import failed, don't import room
202+
array_push($failed, [$room->name, $room->friendly_id]);
203+
$bar->advance();
204+
205+
continue;
206+
}
207+
208+
// create room with same id, same name, access code
209+
$dbRoom = new Room;
210+
$dbRoom->id = $room->friendly_id;
211+
$dbRoom->name = Str::limit(($prefix != null ? ($prefix.' ') : '').$room->name, 253); // if prefix given, add prefix separated by a space from the title; truncate after 253 chars to prevent too long room names
212+
$roomOptions = DB::connection('greenlight')->table('room_meeting_options')->join('meeting_options', 'meeting_option_id', '=', 'meeting_options.id')->where('room_id', $room->id)->get(['name', 'value']);
213+
214+
// set room settings
215+
foreach ($roomOptions as $option) {
216+
switch ($option->name) {
217+
case 'glAnyoneCanStart':
218+
$dbRoom->everyone_can_start = $option->value;
219+
break;
220+
case 'glAnyoneJoinAsModerator':
221+
$dbRoom->default_role = $option->value ? RoomUserRole::MODERATOR : RoomUserRole::USER;
222+
break;
223+
case 'glRequireAuthentication':
224+
$dbRoom->allow_guests = ! $option->value;
225+
break;
226+
case 'glViewerAccessCode':
227+
$dbRoom->access_code = $option->value;
228+
break;
229+
case 'guestPolicy':
230+
$dbRoom->lobby = $option->value == 'ASK_MODERATOR' ? RoomLobby::ENABLED : RoomLobby::DISABLED;
231+
break;
232+
case 'muteOnStart':
233+
$dbRoom->mute_on_start = $option->value;
234+
break;
235+
case 'record':
236+
$dbRoom->record = $option->value;
237+
break;
238+
}
239+
}
240+
241+
// associate room with the imported or found user
242+
$dbRoom->owner()->associate($userMap[$room->user_id]);
243+
// set room type to given roomType for this import batch
244+
$dbRoom->roomType()->associate($roomType);
245+
$dbRoom->save();
246+
247+
// increase counter and add room to room map (key = greenlight db id, value = new db id)
248+
$created++;
249+
$roomMap[$room->id] = $room->friendly_id;
250+
$bar->advance();
251+
}
252+
253+
// show import results
254+
$this->line('');
255+
$this->info($created.' created, '.$existed.' skipped (already existed)');
256+
257+
// if any room imports failed, show room name, id and access code
258+
if (count($failed) > 0) {
259+
$this->line('');
260+
261+
$this->error('Room import failed for the following '.count($failed).' rooms, because no room owner was found:');
262+
$this->table(
263+
['Name', 'Friendly ID'],
264+
$failed
265+
);
266+
}
267+
$this->line('');
268+
269+
return $roomMap;
270+
}
271+
272+
/**
273+
* Process greenlight shared room access collection and try to create the room membership for the users and rooms
274+
* Each user get the moderator role, as that is the greenlight equivalent
275+
*
276+
* @param Collection $sharedAccesses Collection of user and room ids for shared room access
277+
* @param array $roomMap Array map of greenlight room ids as key and id of the created room as value
278+
* @param array $userMap Array map of greenlight user ids as key and id of the found/created user as value
279+
*/
280+
protected function importSharedAccesses(Collection $sharedAccesses, array $roomMap, array $userMap)
281+
{
282+
$this->line('Importing shared room accesses');
283+
284+
$bar = $this->output->createProgressBar($sharedAccesses->count());
285+
$bar->start();
286+
287+
// counter of shared accesses that are created
288+
$created = 0;
289+
// counter of shared accesses that could not be created, eg. room or user not imported
290+
$failed = 0;
291+
292+
// walk through all found greenlight shared accesses
293+
foreach ($sharedAccesses as $sharedAccess) {
294+
$room = $sharedAccess->room_id;
295+
$user = $sharedAccess->user_id;
296+
297+
// check if user id and room id are found in the imported rooms
298+
if (! isset($userMap[$user]) || ! isset($roomMap[$room])) {
299+
// one or both are not found
300+
$bar->advance();
301+
$failed++;
302+
303+
continue;
304+
}
305+
306+
// find room object and add user as moderator to the room
307+
$dbRoom = Room::find($roomMap[$room]);
308+
$dbRoom->members()->syncWithoutDetaching([$userMap[$user] => ['role' => RoomUserRole::MODERATOR]]);
309+
$bar->advance();
310+
$created++;
311+
}
312+
313+
// show import result
314+
$this->line('');
315+
$this->info($created.' created, '.$failed.' skipped (user or room not found)');
316+
}
317+
}

0 commit comments

Comments
 (0)